Merge PR #3056: feat(coding-agent): add native terminal progress setting (@lederniermagicien)
This commit is contained in:
@@ -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);
|
||||
|
||||
+28
-4
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user