Merge PR #3056: feat(coding-agent): add native terminal progress setting (@lederniermagicien)

This commit is contained in:
can1357
2026-06-27 01:39:30 +02:00
6 changed files with 88 additions and 4 deletions
+4
View File
@@ -415,6 +415,10 @@
- Fixed image paste placeholders falling through to terminal hyperlink settings before `Settings.init()`, so early editor rendering falls back to plain text instead of crashing. ([#3064](https://github.com/can1357/oh-my-pi/issues/3064))
- Fixed `omp plugin install github:owner/repo` silently keeping the user on a stale commit when re-run on an already-installed GitHub plugin. `bun install <spec>` respects the existing `bun.lock` pin when the spec is unchanged and never re-resolves the remote ref, so the manager now follows a git re-install with `bun update <name>` to refresh the lockfile pin against the upstream. The install transaction also snapshots `bun.lock` up front and routes feature validation, extension validation, and runtime-config save through one rollback path so a failed install can never leave the rejected commit pinned in the active tree or lockfile. First-time installs are unaffected. ([#3063](https://github.com/can1357/oh-my-pi/issues/3063))
### Added
- Added an Appearance setting for OSC 9;4 native terminal progress indicators during active agent turns and context maintenance.
## [16.1.3] - 2026-06-19
### Changed
@@ -778,6 +778,17 @@ export const SETTINGS_SCHEMA = {
"Maximum number of inline images kept as live terminal graphics (default 8). Older images fall back to a text placeholder via a full redraw once the limit is exceeded. Set to 0 to keep every image (no limit).",
},
"terminal.showProgress": {
type: "boolean",
default: false,
ui: {
tab: "appearance",
group: "Display",
label: "Native Terminal Progress",
description: "Emit OSC 9;4 indeterminate progress while the agent or context maintenance is running",
},
},
"tui.textSizing": {
type: "boolean",
default: false,
@@ -88,6 +88,7 @@ export class EventController {
#toolArgsReveal: ToolArgsRevealController;
#prevHideThinking = false;
#handlers: AgentSessionEventHandlers;
#terminalProgressActive = false;
constructor(private ctx: InteractiveModeContext) {
this.#streamingReveal = new StreamingRevealController({
@@ -155,6 +156,7 @@ export class EventController {
this.#streamingReveal.stop();
this.#toolArgsReveal.stop();
this.#cancelIdleCompaction();
this.#setTerminalProgress(false);
for (const timer of this.#ircExpiryTimers.values()) {
clearTimeout(timer);
}
@@ -269,6 +271,18 @@ export class EventController {
await run(event);
}
#setTerminalProgress(active: boolean): void {
if (active) {
if (this.#terminalProgressActive || this.ctx.settings?.get("terminal.showProgress") !== true) return;
this.ctx.ui.terminal.setProgress(true);
this.#terminalProgressActive = true;
return;
}
if (!this.#terminalProgressActive) return;
this.ctx.ui.terminal.setProgress(false);
this.#terminalProgressActive = false;
}
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
this.#lastIntent = undefined;
this.#readToolCallArgs.clear();
@@ -287,6 +301,7 @@ export class EventController {
this.ctx.statusContainer.clear();
}
this.#cancelIdleCompaction();
this.#setTerminalProgress(true);
this.ctx.ensureLoadingAnimation();
this.ctx.ui.requestRender();
}
@@ -954,6 +969,7 @@ export class EventController {
}
async #finishAgentEnd(): Promise<void> {
this.#setTerminalProgress(false);
this.#streamingReveal.stop();
this.#toolArgsReveal.flushAll();
if (this.ctx.loadingAnimation) {
@@ -1040,6 +1056,7 @@ export class EventController {
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
): Promise<void> {
this.#cancelIdleCompaction();
this.#setTerminalProgress(true);
this.#stopWorkingLoader();
this.ctx.statusContainer.clear();
const reasonText =
@@ -1071,6 +1088,7 @@ export class EventController {
async #handleAutoCompactionEnd(event: Extract<AgentSessionEvent, { type: "auto_compaction_end" }>): Promise<void> {
this.#cancelIdleCompaction();
this.#setTerminalProgress(false);
if (this.ctx.autoCompactionLoader) {
this.ctx.autoCompactionLoader.stop();
this.ctx.autoCompactionLoader = undefined;
@@ -60,6 +60,16 @@ describe("settings layout", () => {
}
});
it("exposes native terminal progress in the appearance settings menu", () => {
const def = getSettingsForTab("appearance").find(def => def.path === "terminal.showProgress");
expect(def).toMatchObject({
type: "boolean",
label: "Native Terminal Progress",
group: "Display",
});
});
it("hides advisor dependent settings when advisor is disabled", () => {
const advisorDependentPaths: SettingPath[] = ["advisor.subagents", "advisor.syncBacklog", "advisor.immuneTurns"];
const advisorDependentPathSet = new Set(advisorDependentPaths);
@@ -24,7 +24,7 @@ interface FakeWorkingLoader {
* kept streaming. The fix tears the working loader down (stop + dereference) so
* the next `agent_start` recreates and re-attaches it.
*/
function createContext() {
function createContext(options: { terminalProgress?: boolean } = {}) {
const streamState = { isStreaming: false };
const children: unknown[] = [];
const statusContainer = {
@@ -41,9 +41,12 @@ function createContext() {
},
};
const workingLoaders: FakeWorkingLoader[] = [];
const setProgress = vi.fn();
const ctx = {
isInitialized: true,
settings: { get: () => false },
settings: {
get: (path: string) => path === "terminal.showProgress" && options.terminalProgress === true,
},
statusLine: { invalidate: vi.fn() },
updateEditorTopBorder: vi.fn(),
pendingTools: new Map<string, unknown>(),
@@ -66,7 +69,7 @@ function createContext() {
showError: vi.fn(),
editor: { getText: () => "" },
sessionManager: { getSessionName: () => "test-session" },
ui: { requestRender: vi.fn(), requestComponentRender: vi.fn() },
ui: { requestRender: vi.fn(), requestComponentRender: vi.fn(), terminal: { setProgress } },
viewSession: { isCompacting: false, getLastAssistantMessage: () => undefined },
session: {
get isStreaming() {
@@ -83,10 +86,11 @@ function createContext() {
ctx.loadingAnimation = working as unknown as typeof ctx.loadingAnimation;
statusContainer.addChild(ctx.loadingAnimation);
});
return { ctx, streamState, statusContainer, workingLoaders };
return { ctx, streamState, statusContainer, workingLoaders, setProgress };
}
const AGENT_START = { type: "agent_start" } as unknown as AgentSessionEvent;
const AGENT_END = { type: "agent_end" } as unknown as AgentSessionEvent;
const COMPACTION_START = {
type: "auto_compaction_start",
reason: "overflow",
@@ -174,4 +178,24 @@ describe("EventController loader recovery after overflow maintenance", () => {
expect(ctx.loadingAnimation).toBeDefined();
expect(statusContainer.children).toContain(ctx.loadingAnimation);
});
it("mirrors agent and auto-compaction activity to OSC 9;4 when enabled", async () => {
const { ctx, setProgress } = createContext({ terminalProgress: true });
const controller = new EventController(ctx);
await controller.handleEvent(AGENT_START);
expect(setProgress).toHaveBeenCalledTimes(1);
expect(setProgress).toHaveBeenLastCalledWith(true);
await controller.handleEvent(COMPACTION_START);
expect(setProgress).toHaveBeenCalledTimes(1);
await controller.handleEvent(COMPACTION_END);
expect(setProgress).toHaveBeenCalledTimes(2);
expect(setProgress).toHaveBeenLastCalledWith(false);
await controller.handleEvent(AGENT_START);
await controller.handleEvent(AGENT_END);
expect(setProgress.mock.calls.map(call => call[0])).toEqual([true, false, true, false]);
});
});
@@ -60,6 +60,12 @@ describe("Settings", () => {
expect(settings.get("tui.maxInlineImages")).toBe(8);
});
it("keeps native terminal progress disabled by default", async () => {
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("terminal.showProgress")).toBe(false);
expect(getDefault("terminal.showProgress")).toBe(false);
});
it("keeps the normal startup splash disabled by default", async () => {
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("startup.showSplash")).toBe(false);
@@ -245,6 +251,17 @@ describe("Settings", () => {
expect((savedSettings.modelRoles as { default?: string } | undefined)?.default).toBe("claude-sonnet");
});
it("persists native terminal progress only after the user changes it", async () => {
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(await readSettings()).toEqual({});
settings.set("terminal.showProgress", true);
await settings.flush();
const savedSettings = await readSettings();
expect(savedSettings.terminal).toEqual({ showProgress: true });
});
it("filters model allow-list and disabled providers by current path prefix", async () => {
const workDir = path.join(projectDir, "work", "service");
const privateDir = path.join(projectDir, "private", "app");