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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user