diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cdd5a21f5..881907071 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -33,6 +33,9 @@ - Removed the `xdev` `--tools` token; the `xd://` device system now mounts only via the `tools.xdev` setting (the `--tools ...,xdev` opt-in and `HIDDEN_TOOLS.xdev` are gone). ### Fixed +- Fixed prewalk never switching after a successful todo call; the todo gate now opens at the completed turn boundary without allowing the todo call itself to trigger the handoff. +- Fixed live pending tool headers retaining a stale spinner frame by rebuilding their cached display on each spinner tick. +- Fixed task and eval tool descriptions omitting the configured default or allowed subagent names under restricted spawn policies. - Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). - Fixed `--tools` silently dropping hidden tool names (`xdev`, `yield`, ...); hidden built-ins are now addressable per the `hidden` tool contract. diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 3910fd9b9..afb7edf88 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -624,6 +624,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac const frameCount = theme.spinnerFrames.length; this.#spinnerFrame = sharedSpinnerFrame(frameCount, now); this.#renderState.spinnerFrame = this.#spinnerFrame; + this.#updateDisplay(); // Component-scoped: a spinner tick only changes this tool block, so // the TUI reuses every other root subtree instead of walking the // whole tree (issue #4377). diff --git a/packages/coding-agent/src/prompts/tools/eval.md b/packages/coding-agent/src/prompts/tools/eval.md index 8ba73935c..270ac4a5d 100644 --- a/packages/coding-agent/src/prompts/tools/eval.md +++ b/packages/coding-agent/src/prompts/tools/eval.md @@ -18,7 +18,7 @@ env(key?=None, value?=None) → str | None | dict output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | dict | list[dict] tool.(args) → unknown completion(prompt, model?="default"|"smol"|"slow", system?=None, schema?=None) → str | dict -{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, schema?=None, handle?=False) → str | dict{{#if spawnAllowedAgentsText}} Allowed: {{spawnAllowedAgentsText}}.{{/if}} +{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, schema?=None, handle?=False) → str | dict{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}. omit it to use `{{spawnDefaultAgent}}`.{{/if}} {{#if js}} JS: agent(prompt, { agent, schema, handle }).{{/if}} {{/if}} parallel(thunks) → list pipeline(items, ...stages) → list diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index d41cf71e3..53c9ffe8d 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -4,7 +4,8 @@ Execution does not block — you receive an ID immediately; the result delivers Agents marked BLOCKING run inline — results return in this call; non-blocking items in the same batch still spawn as background jobs.{{/if}}{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch. Execution blocks until all work finishes.{{else}}Run ONE subagent synchronously. Execution blocks until work finishes.{{/if}}{{/if}} # Task Design -- **Agent typing:** Pick each item's `agent` type. Read-only research MUST use `agent: "scout"` (faster model). Use default worker only when no specialist fits. +- **Agent typing:** Pick each item's `agent` type. Read-only research MUST use `agent: "scout"` (faster model). Use the general-purpose worker (`{{defaultAgent}}`) only when no specialist fits. +{{#if allowedAgentsText}}Current spawn policy allows: {{allowedAgentsText}}.{{/if}} - **No overhead:** Each `task` MUST instruct its agent to skip formatters, linters, and project-wide test suites. Run those once at the end. - **One-pass:** Prefer agents that investigate AND edit in one pass; spin a read-only scout only when affected files are genuinely unknown. diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index fac12a450..ee344ea3c 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -1738,6 +1738,8 @@ export class AgentSession { #prewalk: Prewalk | undefined; /** True once the plan nudge has been queued; scrubbed from context at the switch. */ #prewalkPlanInjected = false; + /** A text-only turn receives one continuation; tool progress re-arms it. */ + #prewalkContinuationInjected = false; /** True once any successful `todo` call landed — opens the prewalk * trigger gate: the switch fires at the first edit/write AFTER the todo * list exists (sessions without an ACTIVE todo tool skip the gate). */ @@ -2239,7 +2241,10 @@ export class AgentSession { // silently killing production SWE-bench runs before any code was ever // written. Force one more turn only in that specific, self-created // hazard window. - if (this.#prewalkPlanInjected && context.toolResults.length === 0) { + if (context.toolResults.length > 0) { + this.#prewalkContinuationInjected = false; + } else if (this.#prewalkPlanInjected && !this.#prewalkContinuationInjected) { + this.#prewalkContinuationInjected = true; this.agent.steer({ role: "custom", customType: PREWALK_CONTINUE_MESSAGE_TYPE, @@ -2262,6 +2267,9 @@ export class AgentSession { const action = todoGateOpen ? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName]) : undefined; + if (context.toolResults.some(result => result.toolName === "todo" && !result.isError)) { + this.#prewalkTodoSeen = true; + } if (!action) { if (!this.#prewalkPlanInjected) { this.#prewalkPlanInjected = true; diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index 2e787c297..95fce8002 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -202,6 +202,7 @@ function renderDescription( agents: renderedAgents, spawningDisabled, defaultAgent: spawnPolicy.defaultAgent, + allowedAgentsText: spawnPolicy.allowedPromptText, isolationEnabled, batchEnabled, asyncEnabled, diff --git a/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts b/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts index 81f348bd1..d84dca517 100644 --- a/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts +++ b/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts @@ -20,6 +20,8 @@ describe("ToolExecutionComponent live preview spinners", () => { it("animates the eval pending cell while the call is live", () => { vi.useFakeTimers(); + let now = 0; + vi.spyOn(performance, "now").mockImplementation(() => now); const requestRender = vi.fn(); const requestComponentRender = vi.fn(); const component = new ToolExecutionComponent( @@ -33,6 +35,7 @@ describe("ToolExecutionComponent live preview spinners", () => { try { const firstFrame = stripVTControlCharacters(component.render(80).join("\n")); + now = 120; vi.advanceTimersByTime(120); const secondFrame = stripVTControlCharacters(component.render(80).join("\n")); @@ -48,6 +51,8 @@ describe("ToolExecutionComponent live preview spinners", () => { it("animates a shell pending header while the call is live", () => { vi.useFakeTimers(); + let now = 0; + vi.spyOn(performance, "now").mockImplementation(() => now); const requestRender = vi.fn(); const requestComponentRender = vi.fn(); const component = new ToolExecutionComponent( @@ -61,6 +66,7 @@ describe("ToolExecutionComponent live preview spinners", () => { try { const firstFrame = stripVTControlCharacters(component.render(80).join("\n")); + now = 120; vi.advanceTimersByTime(120); const secondFrame = stripVTControlCharacters(component.render(80).join("\n"));