feat(coding-agent): removed background mode command and runtime plumbing

- Removed the built-in `background` (`bg`) slash command and its `handleBackgroundCommand` path from the interactive flow.
- Deleted background event subscription and shutdown handling by removing `handleBackgroundEvent` from the event, input, and interactive controllers.
- Simplified `InteractiveModeContext` by dropping background-only fields and helpers such as `isBackgrounded`, background UI context creation, and background event callbacks.
This commit is contained in:
can1357
2026-06-07 05:30:53 +02:00
parent 52f7defeab
commit 741558a6d4
27 changed files with 56 additions and 227 deletions
-12
View File
@@ -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:
+5
View File
@@ -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
+4 -1
View File
@@ -196,7 +196,10 @@ export async function runGalleryCommand(args: GalleryCommandArgs): Promise<void>
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`);
@@ -218,4 +218,33 @@ export const miscFixtures: Record<string, GalleryFixture> = {
},
},
},
// 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,
},
},
};
@@ -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<void> {
if (event.type !== "agent_end") {
return;
}
if (this.ctx.session.queuedMessageCount > 0 || this.ctx.session.isStreaming) {
return;
}
this.sendCompletionNotification();
await this.ctx.shutdown();
}
}
@@ -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<void> {
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();
}
}
@@ -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<boolean> {
try {
const image = await readImageFromClipboard();
@@ -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<void> {
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<boolean> {
return this.#inputController.handleImagePaste();
}
+1 -7
View File
@@ -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<void>;
// 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<boolean>;
handleBtwCommand(question: string): Promise<void>;
hasActiveBtw(): boolean;
@@ -915,15 +915,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
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",
@@ -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. */
@@ -95,7 +95,6 @@ function createStubInputControllerContext(opts: { skillCommands: Map<string, str
isPythonMode: false,
pendingImages: [],
pendingImageLinks: [],
isBackgrounded: false,
loopModeEnabled: false,
compactionQueuedMessages: [],
locallySubmittedUserSignatures: new Set<string>(),
@@ -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;
@@ -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);
@@ -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");
@@ -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,
@@ -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,
@@ -13,7 +13,6 @@ function createRuntime() {
editor: { setText } as unknown as InteractiveModeContext["editor"],
handleBtwCommand,
} as unknown as InteractiveModeContext,
handleBackgroundCommand: () => {},
},
};
}
@@ -20,7 +20,6 @@ function createRuntimeHarness(overrides?: { setForcedToolChoice?: (toolName: str
return {
runtime: {
ctx,
handleBackgroundCommand: () => {},
},
setForcedToolChoice,
setText,
@@ -12,7 +12,6 @@ function createRuntimeHarness(handleFreshCommand: InteractiveModeContext["handle
editor: { setText } as unknown as InteractiveModeContext["editor"],
handleFreshCommand,
} as InteractiveModeContext,
handleBackgroundCommand: () => {},
},
};
}
@@ -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,
@@ -13,7 +13,6 @@ function createRuntime() {
editor: { setText } as unknown as InteractiveModeContext["editor"],
handleOmfgCommand,
} as unknown as InteractiveModeContext,
handleBackgroundCommand: () => {},
},
};
}
@@ -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,
@@ -16,7 +16,6 @@ function createRuntime(didRetry: boolean) {
editor: { setText } as unknown as InteractiveModeContext["editor"],
showStatus,
} as unknown as InteractiveModeContext,
handleBackgroundCommand: () => {},
},
};
}
@@ -28,7 +28,6 @@ function createRuntimeHarness(options?: {
handleSessionCommand,
handleSessionDeleteCommand,
} as InteractiveModeContext,
handleBackgroundCommand: () => {},
},
};
}
@@ -31,7 +31,6 @@ function tuiRuntime() {
handleShakeCommand,
showWarning,
} as unknown as InteractiveModeContext,
handleBackgroundCommand: vi.fn(),
};
return { handleShakeCommand, setText, showWarning, runtime };
}
@@ -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,
},
};
}