diff --git a/docs/tui-runtime-internals.md b/docs/tui-runtime-internals.md index 20beca245..79df9186d 100644 --- a/docs/tui-runtime-internals.md +++ b/docs/tui-runtime-internals.md @@ -199,18 +199,6 @@ Escape exits inactive mode by clearing editor text and restoring border color; w 2. Stops TUI before suspend. 3. Sends `SIGTSTP` to process group. -### Background mode (`/background` or `/bg`) - -`handleBackgroundCommand()`: - -- Rejects when idle. -- Switches tool UI context to non-interactive (`hasUI=false`) so interactive UI tools fail fast. -- Stops loaders/status line and unsubscribes foreground event handler. -- Subscribes background event handler (primarily waits for `agent_end`). -- Stops TUI and sends `SIGTSTP` (POSIX job control path). - -On `agent_end` in background with no queued work, controller sends completion notification and shuts down. - ## Cancellation paths Primary cancellation inputs: diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f859113b1..5f61b9cac 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -18,6 +18,7 @@ - Changed the interactive controllers (command, MCP, selector, extension-UI, event), debug panels, and the status/error/warning helpers to render chat output through `ctx.present(...)` instead of appending to `chatContainer` and calling `ui.requestRender()` directly; transcript rebuilds dispose live blocks via `ctx.resetTranscript()` so animated blocks' timers stop on reset. - Changed tool-execution block rendering so the container (`ToolExecutionComponent`) is a transparent passthrough — it no longer inserts a top/bottom blank line, adds left/right padding, or paints a state-colored background behind tool output. Tools with substantial body now self-frame with a muted outline and the tool title in the frame's top bar (`edit`/`apply_patch`, `write`, `ask`, `todo`, `github`, `goal`, `inspect_image`, `search_tool_bm25`, `task`), matching the already-framed `bash`/`read`/`eval`/`debug`/`web_search`/`lsp` blocks, while streaming/in-progress and trivial results collapse to a clean status line. The search-family list tools (`find`, `search`, `ast_grep`) and `job` render frameless/minimal; `find`/`search`/`ast_grep` show a magnifier on success instead of a checkmark, and `job` drops its `Job:` label prefix (the per-job rows are self-describing). The `search_tool_bm25`, `github`, and `inspect_image` frames draw with no background fill, and `inspect_image`'s label was shortened to `Inspect`. - Changed the plan-mode active prompt (`prompts/system/plan-mode-active.md`) to make plans decision-complete and cut filler. Added an Objective framing ("another engineer can execute end-to-end without making a single design decision"), a shared "Resolving Unknowns" section (explore discoverable facts before asking; reserve `ask` for non-derivable preferences/tradeoffs with 2–4 options + a recommended default), and a single shared "The Plan" structure (Context / Approach grouped by behavior not file-by-file / ≤5 Critical files / Verification / Assumptions) that replaces the per-branch structure guidance previously duplicated across the iterative and parallel workflows. Added explicit prohibitions on sections that decide nothing (Non-Goals, Out of Scope, Alternatives Considered, Risks/Mitigations boilerplate, Future Work), on enumerating every file/line, and on inventing schema/validation/precedence policy the request never established. +- Changed completion notifications (`completion.notify`) to fire whenever the agent yields its turn, including in the foreground. The `agent_end` notification was previously gated behind background mode (`isBackgrounded`), so an ordinary foreground turn never emitted one; the gate is gone and the desktop toast now fires on every normal turn completion (still skipped for aborted/error turns and when `completion.notify` is `off`). ### Fixed @@ -38,6 +39,10 @@ - Fixed `debug` tool refusing every `dlv` launch on Go modules. The launch handler ran `validateLaunchProgram` before adapter selection and rejected any directory program with `launch program resolves to a directory`, while dlv's default `mode=debug` requires a Go package path (a directory or `.go` source file). Adapter resolution now precedes validation, directory programs prefer adapters that advertise `acceptsDirectoryProgram` before falling back to native extensionless debuggers, the rejection only fires when the resolved adapter does not advertise that flag (set on `dlv` in `dap/defaults.json`), and dlv's `mode` is derived from the program shape — directories and `.go` files launch as `mode=debug`, other files as `mode=exec` — so `omp` can debug both Go packages and pre-built binaries ([#2020](https://github.com/can1357/oh-my-pi/issues/2020)). +### Removed + +- Removed the `/background` (and `/bg`) slash command and the background-mode subsystem it was the sole entry point for — `InteractiveMode.isBackgrounded`, `createBackgroundUiContext`, `handleBackgroundEvent`, and every `isBackgrounded` guard across the input/event/extension-UI controllers and UI helpers. The command suspended the whole process group via `SIGTSTP` (a leftover testing shortcut) instead of detaching the running agent, which is not the expected workflow — use terminal panes or a multiplexer instead. + ## [15.10.0] - 2026-06-06 ### Breaking Changes diff --git a/packages/coding-agent/src/cli/gallery-cli.ts b/packages/coding-agent/src/cli/gallery-cli.ts index 7f52ec956..b145f04f7 100644 --- a/packages/coding-agent/src/cli/gallery-cli.ts +++ b/packages/coding-agent/src/cli/gallery-cli.ts @@ -196,7 +196,10 @@ export async function runGalleryCommand(args: GalleryCommandArgs): Promise const expanded = args.expanded ?? false; const states = args.states && args.states.length > 0 ? args.states : [...GALLERY_STATES]; - const allNames = Object.keys(toolRenderers).sort(); + // Renderer-registry tools plus fixture-only tools (no dedicated renderer, + // e.g. `report_tool_issue` / custom extension tools) so the gallery covers + // the generic fallback + custom-tool branches too. + const allNames = Array.from(new Set([...Object.keys(toolRenderers), ...Object.keys(galleryFixtures)])).sort(); const names = args.tool ? allNames.filter(name => name === args.tool) : allNames; if (args.tool && names.length === 0) { process.stdout.write(`Unknown tool '${args.tool}'. Known tools: ${allNames.join(", ")}\n`); diff --git a/packages/coding-agent/src/cli/gallery-fixtures/misc.ts b/packages/coding-agent/src/cli/gallery-fixtures/misc.ts index 80200a749..4860d01f2 100644 --- a/packages/coding-agent/src/cli/gallery-fixtures/misc.ts +++ b/packages/coding-agent/src/cli/gallery-fixtures/misc.ts @@ -218,4 +218,33 @@ export const miscFixtures: Record = { }, }, }, + + // Built-in tool with no dedicated renderer — exercises the generic fallback + // (`#formatToolExecution`) path so its padded, state-tinted block is QA'd. + report_tool_issue: { + label: "Report Tool Issue", + streamingArgs: { tool: "lsp" }, + args: { + tool: "lsp", + report: "Rename returned no edit for an exported symbol that has 12 references", + }, + result: { content: [{ type: "text", text: "Noted, thanks!" }] }, + errorResult: { + content: [{ type: "text", text: "Could not record the report: issue tracker unreachable" }], + isError: true, + }, + }, + + // Stand-in for a custom/extension tool that ships no renderer — same generic + // fallback path most MCP/extension tools take. + custom: { + label: "Custom Tool", + streamingArgs: { query: "weather" }, + args: { query: "weather in Tokyo", units: "metric" }, + result: { content: [{ type: "text", text: "Tokyo: 22°C, partly cloudy, humidity 64%." }] }, + errorResult: { + content: [{ type: "text", text: "Upstream provider returned 503 Service Unavailable" }], + isError: true, + }, + }, }; diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index bdaeab4ec..3f48c6fe5 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -888,7 +888,6 @@ export class EventController { } sendCompletionNotification(): void { - if (this.ctx.isBackgrounded === false) return; const notify = settings.get("completion.notify"); if (notify === "off") return; @@ -907,15 +906,4 @@ export class EventController { actions: "focus", }); } - - async handleBackgroundEvent(event: AgentSessionEvent): Promise { - if (event.type !== "agent_end") { - return; - } - if (this.ctx.session.queuedMessageCount > 0 || this.ctx.session.isStreaming) { - return; - } - this.sendCompletionNotification(); - await this.ctx.shutdown(); - } } diff --git a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts index 8f09b645b..31d6e7cc0 100644 --- a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts +++ b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts @@ -1,6 +1,5 @@ import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui"; import { Container, Spacer, Text } from "@oh-my-pi/pi-tui"; -import { logger } from "@oh-my-pi/pi-utils"; import { KeybindingsManager } from "../../config/keybindings"; import type { CompactOptions, @@ -326,10 +325,6 @@ export class ExtensionUiController { .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display)) .catch((err: unknown) => { const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`; - if (this.ctx.isBackgrounded) { - logger.error(errorText); - return; - } this.ctx.showError(errorText); }); }, @@ -374,9 +369,6 @@ export class ExtensionUiController { getContextUsage: () => this.ctx.session.getContextUsage(), waitForIdle: () => this.ctx.session.agent.waitForIdle(), reload: async () => { - if (this.ctx.isBackgrounded) { - return; - } await this.ctx.session.reload(); this.ctx.chatContainer.clear(); this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true }); @@ -384,9 +376,6 @@ export class ExtensionUiController { this.ctx.showStatus("Reloaded session"); }, newSession: async options => { - if (this.ctx.isBackgrounded) { - return { cancelled: true }; - } // Stop any loading animation if (this.ctx.loadingAnimation) { this.ctx.loadingAnimation.stop(); @@ -425,9 +414,6 @@ export class ExtensionUiController { return { cancelled: false }; }, branch: async entryId => { - if (this.ctx.isBackgrounded) { - return { cancelled: true }; - } const result = await this.ctx.session.branch(entryId); if (result.cancelled) { return { cancelled: true }; @@ -443,9 +429,6 @@ export class ExtensionUiController { return { cancelled: false }; }, navigateTree: async (targetId, options) => { - if (this.ctx.isBackgrounded) { - return { cancelled: true }; - } const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize }); if (result.cancelled) { return { cancelled: true }; @@ -464,9 +447,6 @@ export class ExtensionUiController { }, compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions), switchSession: async sessionPath => { - if (this.ctx.isBackgrounded) { - return { cancelled: true }; - } this.clearHookWidgets(); const result = await this.ctx.session.switchSession(sessionPath); if (!result) { @@ -482,36 +462,6 @@ export class ExtensionUiController { extensionRunner.initialize(actions, contextActions, commandActions, uiContext); } - createBackgroundUiContext(): ExtensionUIContext { - return { - select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined, - confirm: async (_title: string, _message: string, _dialogOptions) => false, - input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined, - notify: () => {}, - onTerminalInput: () => () => {}, - setStatus: () => {}, - setWorkingMessage: () => {}, - setWidget: () => {}, - setTitle: () => {}, - custom: async () => undefined as never, - setEditorText: () => {}, - pasteToEditor: () => {}, - getEditorText: () => "", - editor: async () => undefined, - get theme() { - return theme; - }, - getAllThemes: () => Promise.resolve([]), - getTheme: () => Promise.resolve(undefined), - setTheme: () => Promise.resolve({ success: false, error: "Background mode" }), - setFooter: () => {}, - setHeader: () => {}, - setEditorComponent: () => {}, - getToolsExpanded: () => false, - setToolsExpanded: () => {}, - }; - } - /** * Emit session event to all extension tools. */ @@ -531,7 +481,7 @@ export class ExtensionUiController { ui: uiContext, getContextUsage: () => this.ctx.session.getContextUsage(), compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions), - hasUI: !this.ctx.isBackgrounded, + hasUI: true, cwd: this.ctx.sessionManager.getCwd(), sessionManager: this.ctx.session.sessionManager, modelRegistry: this.ctx.session.modelRegistry, @@ -557,10 +507,6 @@ export class ExtensionUiController { * Show a tool error in the chat. */ showToolError(toolName: string, error: string): void { - if (this.ctx.isBackgrounded) { - logger.error(`Tool "${toolName}" error: ${error}`); - return; - } const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0); this.ctx.present(errorText); } @@ -569,9 +515,6 @@ export class ExtensionUiController { * Set hook status text in the footer. */ setHookStatus(key: string, text: string | undefined): void { - if (this.ctx.isBackgrounded) { - return; - } this.ctx.statusLine.setHookStatus(key, text); this.ctx.ui.requestRender(); } @@ -862,10 +805,6 @@ export class ExtensionUiController { this.ctx.present(errorText); } async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise { - if (this.ctx.isBackgrounded) { - await this.#compactSession(instructionsOrOptions); - return; - } await this.ctx.executeCompaction(instructionsOrOptions, false); } @@ -890,7 +829,7 @@ export class ExtensionUiController { #applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void { // For non-streaming cases with display=true, update UI // (streaming cases update via message_end event) - if (!this.ctx.isBackgrounded && !wasStreaming && shouldDisplay) { + if (!wasStreaming && shouldDisplay) { this.ctx.rebuildChatFromMessages(); } } diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 253998228..b8ac1c891 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -9,7 +9,6 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete"; import { materializeImageReferenceLinks } from "../../modes/image-references"; import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete"; import type { InteractiveModeContext } from "../../modes/types"; -import type { AgentSessionEvent } from "../../session/agent-session"; import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages"; import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry"; import { isTinyTitleLocalModelKey } from "../../tiny/models"; @@ -41,7 +40,7 @@ export class InputController { constructor(private ctx: InteractiveModeContext) {} #showTinyTitleDownloadProgress(modelKey: string): void { - if (!isTinyTitleLocalModelKey(modelKey) || this.ctx.isBackgrounded) return; + if (!isTinyTitleLocalModelKey(modelKey)) return; const component = new TinyTitleDownloadProgressComponent(modelKey); let added = false; let disposed = false; @@ -291,7 +290,6 @@ export class InputController { // Handle built-in slash commands const slashResult = await executeBuiltinSlashCommand(text, { ctx: this.ctx, - handleBackgroundCommand: () => this.handleBackgroundCommand(), }); if (slashResult === true) { return; @@ -615,67 +613,6 @@ export class InputController { return allQueued.length; } - handleBackgroundCommand(): void { - if (this.ctx.isBackgrounded) { - this.ctx.showStatus("Background mode already enabled"); - return; - } - if (!this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount === 0) { - this.ctx.showWarning("Agent is idle; nothing to background"); - return; - } - if (this.ctx.hasActiveBtw()) { - this.ctx.handleBtwEscape(); - } - if (this.ctx.hasActiveOmfg()) { - this.ctx.handleOmfgEscape(); - } - - this.ctx.isBackgrounded = true; - const backgroundUiContext = this.ctx.createBackgroundUiContext(); - - // Background mode disables interactive UI so tools like ask fail fast. - this.ctx.setToolUIContext(backgroundUiContext, false); - this.ctx.initializeHookRunner(backgroundUiContext, false); - - if (this.ctx.loadingAnimation) { - this.ctx.loadingAnimation.stop(); - this.ctx.loadingAnimation = undefined; - } - if (this.ctx.autoCompactionLoader) { - this.ctx.autoCompactionLoader.stop(); - this.ctx.autoCompactionLoader = undefined; - } - if (this.ctx.retryLoader) { - this.ctx.retryLoader.stop(); - this.ctx.retryLoader = undefined; - } - this.ctx.statusContainer.clear(); - this.ctx.statusLine.dispose(); - - if (this.ctx.unsubscribe) { - this.ctx.unsubscribe(); - } - this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => { - await this.ctx.handleBackgroundEvent(event); - }); - - // Backgrounding keeps the current process to preserve in-flight agent state. - if (this.ctx.isInitialized) { - this.ctx.ui.stop(); - this.ctx.isInitialized = false; - } - - process.stdout.write("Background mode enabled. Run `bg` to continue in background.\n"); - - if (process.platform === "win32" || !process.stdout.isTTY) { - process.stdout.write("Backgrounding requires POSIX job control; continuing in foreground.\n"); - return; - } - - process.kill(0, "SIGTSTP"); - } - async handleImagePaste(): Promise { try { const image = await readImageFromClipboard(); diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index d831e9ba8..ee6840bfe 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -275,7 +275,6 @@ export class InteractiveMode implements InteractiveModeContext { statusLine: StatusLineComponent; isInitialized = false; - isBackgrounded = false; isBashMode = false; toolOutputExpanded = false; todoExpanded = false; @@ -2506,9 +2505,6 @@ export class InteractiveMode implements InteractiveModeContext { initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void { this.#extensionUiController.initializeHookRunner(uiContext, hasUI); } - createBackgroundUiContext(): ExtensionUIContext { - return this.#extensionUiController.createBackgroundUiContext(); - } setEditorComponent( factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined, @@ -2550,11 +2546,6 @@ export class InteractiveMode implements InteractiveModeContext { this.ui.requestRender(); } - // Event handling - async handleBackgroundEvent(event: AgentSessionEvent): Promise { - await this.#eventController.handleBackgroundEvent(event); - } - // UI helpers present(content: Component | readonly Component[]): void { if (Array.isArray(content)) { @@ -2594,7 +2585,6 @@ export class InteractiveMode implements InteractiveModeContext { } showPinnedError(message: string): void { - if (this.isBackgrounded) return; this.errorBannerContainer.clear(); this.errorBannerContainer.addChild(new ErrorBannerComponent(message)); this.ui.requestRender(); @@ -3070,10 +3060,6 @@ export class InteractiveMode implements InteractiveModeContext { this.#inputController.handleDequeue(); } - handleBackgroundCommand(): void { - this.#inputController.handleBackgroundCommand(); - } - handleImagePaste(): Promise { return this.#inputController.handleImagePaste(); } diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index f5c09625c..8eb4c4b3b 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -14,7 +14,7 @@ import type { import type { CompactOptions } from "../extensibility/extensions/types"; import type { MCPManager } from "../mcp"; import type { PlanApprovalDetails } from "../plan-mode/approved-plan"; -import type { AgentSession, AgentSessionEvent } from "../session/agent-session"; +import type { AgentSession } from "../session/agent-session"; import type { HistoryStorage } from "../session/history-storage"; import type { SessionContext, SessionManager } from "../session/session-manager"; import type { ShakeMode } from "../session/shake-types"; @@ -95,7 +95,6 @@ export interface InteractiveModeContext { // State isInitialized: boolean; - isBackgrounded: boolean; isBashMode: boolean; toolOutputExpanded: boolean; todoExpanded: boolean; @@ -149,14 +148,10 @@ export interface InteractiveModeContext { // Extension UI integration setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void; initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void; - createBackgroundUiContext(): ExtensionUIContext; setEditorComponent( factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined, ): void; - // Event handling - handleBackgroundEvent(event: AgentSessionEvent): Promise; - // UI helpers /** * Mount transcript content and repaint once. The single sink for "show this in @@ -293,7 +288,6 @@ export interface InteractiveModeContext { handleCtrlD(): void; handleCtrlZ(): void; handleDequeue(): void; - handleBackgroundCommand(): void; handleImagePaste(): Promise; handleBtwCommand(question: string): Promise; hasActiveBtw(): boolean; diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index 57ecac800..af224fc18 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -915,15 +915,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ runtime.ctx.editor.setText(""); }, }, - { - name: "background", - aliases: ["bg"], - description: "Detach UI and continue running in background", - handleTui: (_command, runtime) => { - runtime.ctx.editor.setText(""); - runtime.handleBackgroundCommand(); - }, - }, { name: "debug", description: "Open debug tools selector", diff --git a/packages/coding-agent/src/slash-commands/types.ts b/packages/coding-agent/src/slash-commands/types.ts index 6725815ba..15412739a 100644 --- a/packages/coding-agent/src/slash-commands/types.ts +++ b/packages/coding-agent/src/slash-commands/types.ts @@ -71,15 +71,13 @@ export interface SlashCommandRuntime { /** * Runtime visible to TUI-only handlers (`handleTui`). Carries the interactive - * mode context plus the background-detach hook. Intentionally narrower than - * `SlashCommandRuntime` so existing callers can keep building it from just - * `{ ctx, handleBackgroundCommand }`; when the TUI dispatcher needs to invoke - * a `handle` (no `handleTui` override), it synthesizes a `SlashCommandRuntime` - * from `ctx`. + * mode context. Intentionally narrower than `SlashCommandRuntime` so existing + * callers can keep building it from just `{ ctx }`; when the TUI dispatcher + * needs to invoke a `handle` (no `handleTui` override), it synthesizes a + * `SlashCommandRuntime` from `ctx`. */ export interface TuiSlashCommandRuntime { ctx: InteractiveModeContext; - handleBackgroundCommand: () => void; } /** Unified slash-command spec consumed by both TUI and ACP dispatchers. */ diff --git a/packages/coding-agent/test/input-controller-skill-queue.test.ts b/packages/coding-agent/test/input-controller-skill-queue.test.ts index 0128b09cd..93dbba9ef 100644 --- a/packages/coding-agent/test/input-controller-skill-queue.test.ts +++ b/packages/coding-agent/test/input-controller-skill-queue.test.ts @@ -95,7 +95,6 @@ function createStubInputControllerContext(opts: { skillCommands: Map(), diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index aae1a575a..f32f13587 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -30,7 +30,6 @@ function createInitialRenderHarness(): { ctx: InteractiveModeContext; helpers: U for (const item of items) ctx.chatContainer.addChild(item); ctx.ui.requestRender(); }, - isBackgrounded: false, sessionManager: { buildSessionContext: () => buildSessionContext([]), getEntries: () => [], @@ -69,7 +68,6 @@ describe("InteractiveMode.showStatus", () => { for (const item of items) ctx.chatContainer.addChild(item); ctx.ui.requestRender(); }, - isBackgrounded: false, lastStatusSpacer: undefined, lastStatusText: undefined, } as unknown as InteractiveModeContext; @@ -95,7 +93,6 @@ describe("InteractiveMode.showStatus", () => { for (const item of items) ctx.chatContainer.addChild(item); ctx.ui.requestRender(); }, - isBackgrounded: false, lastStatusSpacer: undefined, lastStatusText: undefined, } as unknown as InteractiveModeContext; diff --git a/packages/coding-agent/test/loop-limit.test.ts b/packages/coding-agent/test/loop-limit.test.ts index ff2033c19..9e660785c 100644 --- a/packages/coding-agent/test/loop-limit.test.ts +++ b/packages/coding-agent/test/loop-limit.test.ts @@ -16,7 +16,6 @@ describe("/loop slash command", () => { handleLoopCommand, editor: { setText: vi.fn() }, }, - handleBackgroundCommand: vi.fn(), } as unknown as BuiltinSlashCommandRuntime; const result = await executeBuiltinSlashCommand("/loop 10min", runtime); diff --git a/packages/coding-agent/test/modes/controllers/event-controller-abort-guard.test.ts b/packages/coding-agent/test/modes/controllers/event-controller-abort-guard.test.ts index b35db298f..e25eab0a4 100644 --- a/packages/coding-agent/test/modes/controllers/event-controller-abort-guard.test.ts +++ b/packages/coding-agent/test/modes/controllers/event-controller-abort-guard.test.ts @@ -2,12 +2,12 @@ * Regression test for the abort-guard on `EventController.sendCompletionNotification`. * * Bug: a user Ctrl+C on the `ask` tool selector throws `ToolAbortError`, - * the turn ends with `stopReason === "aborted"`, and `handleBackgroundEvent` - * fires `sendCompletionNotification()` unconditionally. The pre-fix code - * then produced a misleading "Task complete" desktop toast for a turn that - * never actually completed. The fix mirrors the `stopReason !== "aborted"` - * pattern already used by `#currentContextTokens`, `#handleMessageEnd`, and - * the retry / TTSR / compaction skip paths in `agent-session.ts`. + * the turn ends with `stopReason === "aborted"`, and `#handleAgentEnd` + * fires `sendCompletionNotification()`. Without a guard this produced a + * misleading "Task complete" desktop toast for a turn that never actually + * completed. The fix mirrors the `stopReason !== "aborted"` pattern already + * used by `#currentContextTokens`, `#handleMessageEnd`, and the + * retry / TTSR / compaction skip paths in `agent-session.ts`. */ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs"; @@ -49,8 +49,6 @@ function makeAssistantMessage(stopReason: StopReason): AssistantMessage { function makeContext(lastMessage: AssistantMessage | undefined): InteractiveModeContext { return { - // sendCompletionNotification only fires when backgrounded. - isBackgrounded: true, sessionManager: { getSessionName: () => "test-session", }, @@ -96,16 +94,6 @@ describe("EventController.sendCompletionNotification — abort guard", () => { expect(spy).toHaveBeenCalledTimes(1); }); - it("honors the existing isBackgrounded gate (no notification when foreground)", () => { - const spy = vi.spyOn(TERMINAL, "sendNotification").mockImplementation(() => {}); - settings.override("completion.notify", "on"); - const ctx = makeContext(makeAssistantMessage("stop")); - (ctx as unknown as { isBackgrounded: boolean }).isBackgrounded = false; - const controller = new EventController(ctx); - controller.sendCompletionNotification(); - expect(spy).toHaveBeenCalledTimes(0); - }); - it("honors the existing completion.notify=off gate", () => { const spy = vi.spyOn(TERMINAL, "sendNotification").mockImplementation(() => {}); settings.override("completion.notify", "off"); diff --git a/packages/coding-agent/test/modes/controllers/event-controller-idle-compaction.test.ts b/packages/coding-agent/test/modes/controllers/event-controller-idle-compaction.test.ts index 48f4e51b1..39b37e38c 100644 --- a/packages/coding-agent/test/modes/controllers/event-controller-idle-compaction.test.ts +++ b/packages/coding-agent/test/modes/controllers/event-controller-idle-compaction.test.ts @@ -48,7 +48,6 @@ describe("EventController idle compaction teardown", () => { const runIdleCompaction = vi.fn(); const context = { isInitialized: true, - isBackgrounded: false, loadingAnimation: undefined, streamingComponent: undefined, streamingMessage: undefined, diff --git a/packages/coding-agent/test/modes/controllers/event-controller-tool-render-mode.test.ts b/packages/coding-agent/test/modes/controllers/event-controller-tool-render-mode.test.ts index 34a320451..ec9e0d444 100644 --- a/packages/coding-agent/test/modes/controllers/event-controller-tool-render-mode.test.ts +++ b/packages/coding-agent/test/modes/controllers/event-controller-tool-render-mode.test.ts @@ -11,7 +11,6 @@ function createContext() { const chatContainer = { addChild: vi.fn(), removeChild: vi.fn() }; const ctx = { isInitialized: true, - isBackgrounded: false, statusLine: { invalidate: vi.fn() }, updateEditorTopBorder: vi.fn(), pendingTools, @@ -19,6 +18,7 @@ function createContext() { hideThinkingBlock: false, editor: { getText: vi.fn(() => "") }, flushPendingModelSwitch: vi.fn(), + sessionManager: { getSessionName: () => undefined }, session: { agent: { state: { messages: [] } }, isCompacting: false, diff --git a/packages/coding-agent/test/slash-commands/btw.test.ts b/packages/coding-agent/test/slash-commands/btw.test.ts index 1638229df..1349fa895 100644 --- a/packages/coding-agent/test/slash-commands/btw.test.ts +++ b/packages/coding-agent/test/slash-commands/btw.test.ts @@ -13,7 +13,6 @@ function createRuntime() { editor: { setText } as unknown as InteractiveModeContext["editor"], handleBtwCommand, } as unknown as InteractiveModeContext, - handleBackgroundCommand: () => {}, }, }; } diff --git a/packages/coding-agent/test/slash-commands/force.test.ts b/packages/coding-agent/test/slash-commands/force.test.ts index 28ce73f31..3c47db41b 100644 --- a/packages/coding-agent/test/slash-commands/force.test.ts +++ b/packages/coding-agent/test/slash-commands/force.test.ts @@ -20,7 +20,6 @@ function createRuntimeHarness(overrides?: { setForcedToolChoice?: (toolName: str return { runtime: { ctx, - handleBackgroundCommand: () => {}, }, setForcedToolChoice, setText, diff --git a/packages/coding-agent/test/slash-commands/fresh.test.ts b/packages/coding-agent/test/slash-commands/fresh.test.ts index d20af962b..270c8fbd6 100644 --- a/packages/coding-agent/test/slash-commands/fresh.test.ts +++ b/packages/coding-agent/test/slash-commands/fresh.test.ts @@ -12,7 +12,6 @@ function createRuntimeHarness(handleFreshCommand: InteractiveModeContext["handle editor: { setText } as unknown as InteractiveModeContext["editor"], handleFreshCommand, } as InteractiveModeContext, - handleBackgroundCommand: () => {}, }, }; } diff --git a/packages/coding-agent/test/slash-commands/login.test.ts b/packages/coding-agent/test/slash-commands/login.test.ts index 083dc0503..7ad589d72 100644 --- a/packages/coding-agent/test/slash-commands/login.test.ts +++ b/packages/coding-agent/test/slash-commands/login.test.ts @@ -4,7 +4,7 @@ import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/typ import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry"; type RuntimeHarness = { - runtime: { ctx: InteractiveModeContext; handleBackgroundCommand: () => void }; + runtime: { ctx: InteractiveModeContext }; getStatus: () => string | undefined; getWarning: () => string | undefined; getSelectorMode: () => "login" | "logout" | undefined; @@ -36,7 +36,6 @@ const createRuntimeHarness = (manualInput: OAuthManualInputManager): RuntimeHarn return { runtime: { ctx, - handleBackgroundCommand: () => {}, }, getStatus: () => statusMessage, getWarning: () => warningMessage, diff --git a/packages/coding-agent/test/slash-commands/omfg.test.ts b/packages/coding-agent/test/slash-commands/omfg.test.ts index 82cbec560..a95bc41f2 100644 --- a/packages/coding-agent/test/slash-commands/omfg.test.ts +++ b/packages/coding-agent/test/slash-commands/omfg.test.ts @@ -13,7 +13,6 @@ function createRuntime() { editor: { setText } as unknown as InteractiveModeContext["editor"], handleOmfgCommand, } as unknown as InteractiveModeContext, - handleBackgroundCommand: () => {}, }, }; } diff --git a/packages/coding-agent/test/slash-commands/plan-history.test.ts b/packages/coding-agent/test/slash-commands/plan-history.test.ts index e4b1dab0c..656be0978 100644 --- a/packages/coding-agent/test/slash-commands/plan-history.test.ts +++ b/packages/coding-agent/test/slash-commands/plan-history.test.ts @@ -30,7 +30,7 @@ function createPlanHarness(opts: { planModeEnabled: boolean; confirmExit: boolea } as unknown as InteractiveModeContext; return { - runtime: { ctx, handleBackgroundCommand: () => {} }, + runtime: { ctx }, state, addToHistory, setText, @@ -53,7 +53,7 @@ function createGoalHarness(opts: { goalModeEnabled: boolean; dropOnCall: boolean } as unknown as InteractiveModeContext; return { - runtime: { ctx, handleBackgroundCommand: () => {} }, + runtime: { ctx }, state, addToHistory, setText, diff --git a/packages/coding-agent/test/slash-commands/retry.test.ts b/packages/coding-agent/test/slash-commands/retry.test.ts index 899031519..bc7486407 100644 --- a/packages/coding-agent/test/slash-commands/retry.test.ts +++ b/packages/coding-agent/test/slash-commands/retry.test.ts @@ -16,7 +16,6 @@ function createRuntime(didRetry: boolean) { editor: { setText } as unknown as InteractiveModeContext["editor"], showStatus, } as unknown as InteractiveModeContext, - handleBackgroundCommand: () => {}, }, }; } diff --git a/packages/coding-agent/test/slash-commands/session.test.ts b/packages/coding-agent/test/slash-commands/session.test.ts index d4662c28a..86c6821d4 100644 --- a/packages/coding-agent/test/slash-commands/session.test.ts +++ b/packages/coding-agent/test/slash-commands/session.test.ts @@ -28,7 +28,6 @@ function createRuntimeHarness(options?: { handleSessionCommand, handleSessionDeleteCommand, } as InteractiveModeContext, - handleBackgroundCommand: () => {}, }, }; } diff --git a/packages/coding-agent/test/slash-commands/shake.test.ts b/packages/coding-agent/test/slash-commands/shake.test.ts index 9edc3beb1..e7b606256 100644 --- a/packages/coding-agent/test/slash-commands/shake.test.ts +++ b/packages/coding-agent/test/slash-commands/shake.test.ts @@ -31,7 +31,6 @@ function tuiRuntime() { handleShakeCommand, showWarning, } as unknown as InteractiveModeContext, - handleBackgroundCommand: vi.fn(), }; return { handleShakeCommand, setText, showWarning, runtime }; } diff --git a/packages/coding-agent/test/slash-commands/switch.test.ts b/packages/coding-agent/test/slash-commands/switch.test.ts index d3d4f5aae..15d0c896e 100644 --- a/packages/coding-agent/test/slash-commands/switch.test.ts +++ b/packages/coding-agent/test/slash-commands/switch.test.ts @@ -5,7 +5,6 @@ import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-comm function createRuntime() { const showModelSelector = vi.fn(); const setText = vi.fn(); - const handleBackgroundCommand = vi.fn(); return { showModelSelector, setText, @@ -13,9 +12,7 @@ function createRuntime() { ctx: { editor: { setText } as unknown as InteractiveModeContext["editor"], showModelSelector, - handleBackgroundCommand, } as unknown as InteractiveModeContext, - handleBackgroundCommand, }, }; }