@oh-my-pi/pi-coding-agent 12.12.3 → 12.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/package.json +8 -7
  3. package/scripts/generate-docs-index.ts +56 -0
  4. package/src/cli/ssh-cli.ts +179 -0
  5. package/src/cli.ts +1 -0
  6. package/src/commands/ssh.ts +60 -0
  7. package/src/commit/prompts/analysis-system.md +1 -3
  8. package/src/config/prompt-templates.ts +20 -5
  9. package/src/config/settings-schema.ts +10 -1
  10. package/src/discovery/builtin.ts +14 -4
  11. package/src/discovery/ssh.ts +26 -19
  12. package/src/extensibility/extensions/types.ts +1 -0
  13. package/src/internal-urls/docs-index.generated.ts +101 -0
  14. package/src/internal-urls/docs-protocol.ts +84 -0
  15. package/src/internal-urls/index.ts +1 -0
  16. package/src/internal-urls/router.ts +1 -1
  17. package/src/modes/controllers/event-controller.ts +20 -0
  18. package/src/modes/controllers/ssh-command-controller.ts +452 -0
  19. package/src/modes/interactive-mode.ts +6 -0
  20. package/src/modes/types.ts +1 -0
  21. package/src/patch/diff.ts +1 -1
  22. package/src/patch/hashline.ts +274 -303
  23. package/src/patch/index.ts +324 -103
  24. package/src/patch/shared.ts +25 -28
  25. package/src/prompts/system/system-prompt.md +14 -2
  26. package/src/prompts/tools/bash.md +1 -1
  27. package/src/prompts/tools/grep.md +12 -8
  28. package/src/prompts/tools/hashline.md +207 -60
  29. package/src/prompts/tools/read.md +3 -3
  30. package/src/sdk.ts +17 -0
  31. package/src/session/agent-session.ts +1 -0
  32. package/src/session/auth-storage.ts +6 -0
  33. package/src/slash-commands/builtin-registry.ts +20 -0
  34. package/src/ssh/config-writer.ts +183 -0
  35. package/src/tools/bash-interactive.ts +47 -7
  36. package/src/tools/fetch.ts +4 -3
  37. package/src/tools/grep.ts +14 -4
  38. package/src/tools/read.ts +2 -2
  39. package/src/tools/ssh.ts +1 -1
  40. package/src/web/search/render.ts +2 -2
@@ -656,7 +656,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
656
656
 
657
657
  fileLoadPromises.push(
658
658
  loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, level, {
659
- extensions: ["json", "md"],
659
+ extensions: ["json", "md", "ts", "js", "sh", "bash", "py"],
660
660
  transform: (name, content, path, source) => {
661
661
  if (name.endsWith(".json")) {
662
662
  const data = parseJSON<{ name?: string; description?: string }>(content);
@@ -668,11 +668,21 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
668
668
  _source: source,
669
669
  };
670
670
  }
671
- const { frontmatter } = parseFrontmatter(content, { source: path });
671
+ if (name.endsWith(".md")) {
672
+ const { frontmatter } = parseFrontmatter(content, { source: path });
673
+ return {
674
+ name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
675
+ path,
676
+ description: frontmatter.description as string | undefined,
677
+ level,
678
+ _source: source,
679
+ };
680
+ }
681
+ // Executable tool files (.ts, .js, .sh, .bash, .py)
682
+ const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
672
683
  return {
673
- name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
684
+ name: toolName,
674
685
  path,
675
- description: frontmatter.description as string | undefined,
676
686
  level,
677
687
  _source: source,
678
688
  };
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * SSH JSON Provider
3
3
  *
4
- * Discovers SSH hosts from ssh.json or .ssh.json in the project root.
5
- * Priority: 5 (low, project-level only)
4
+ * Discovers SSH hosts from managed omp config paths and legacy root ssh.json files.
5
+ * Priority: 5 (low, project/user config discovery)
6
6
  */
7
7
  import * as path from "node:path";
8
+ import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs";
8
9
  import { registerProvider } from "../capability";
9
10
  import { readFile } from "../capability/fs";
10
11
  import { type SSHHost, sshCapability } from "../capability/ssh";
@@ -90,38 +91,39 @@ function normalizeHost(
90
91
  };
91
92
  }
92
93
 
93
- async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadResult<SSHHost>> {
94
+ async function loadSshJsonFile(
95
+ ctx: LoadContext,
96
+ filePath: string,
97
+ level: "user" | "project",
98
+ ): Promise<LoadResult<SSHHost>> {
94
99
  const items: SSHHost[] = [];
95
100
  const warnings: string[] = [];
96
-
97
- const content = await readFile(path);
101
+ const content = await readFile(filePath);
98
102
  if (content === null) {
99
103
  return { items, warnings };
100
104
  }
101
-
102
105
  const parsed = parseJSON<SSHConfigFile>(content);
103
106
  if (!parsed) {
104
- warnings.push(`Failed to parse JSON in ${path}`);
107
+ warnings.push(`Failed to parse JSON in ${filePath}`);
105
108
  return { items, warnings };
106
109
  }
107
-
108
110
  const config = expandEnvVarsDeep(parsed);
109
111
  if (!config.hosts || typeof config.hosts !== "object") {
110
- warnings.push(`Missing hosts in ${path}`);
112
+ warnings.push(`Missing hosts in ${filePath}`);
111
113
  return { items, warnings };
112
114
  }
113
115
 
114
- const source = createSourceMeta(PROVIDER_ID, path, "project");
116
+ const source = createSourceMeta(PROVIDER_ID, filePath, level);
115
117
  for (const [name, rawHost] of Object.entries(config.hosts)) {
116
118
  if (!name.trim()) {
117
- warnings.push(`Invalid SSH host name in ${path}`);
119
+ warnings.push(`Invalid SSH host name in ${filePath}`);
118
120
  continue;
119
121
  }
120
122
  if (!rawHost || typeof rawHost !== "object") {
121
- warnings.push(`Invalid host entry in ${path}: ${name}`);
123
+ warnings.push(`Invalid host entry in ${filePath}: ${name}`);
122
124
  continue;
123
125
  }
124
- const host = normalizeHost(name, rawHost, source, _ctx.home, warnings);
126
+ const host = normalizeHost(name, rawHost, source, ctx.home, warnings);
125
127
  if (host) items.push(host);
126
128
  }
127
129
 
@@ -130,14 +132,19 @@ async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadRes
130
132
  warnings: warnings.length > 0 ? warnings : undefined,
131
133
  };
132
134
  }
133
-
134
135
  async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
135
- const filenames = ["ssh.json", ".ssh.json"];
136
- const results = await Promise.all(filenames.map(filename => loadSshJsonFile(ctx, path.join(ctx.cwd, filename))));
137
-
136
+ const candidateSources: Array<{ path: string; level: "user" | "project" }> = [
137
+ { path: getSSHConfigPath("project", ctx.cwd), level: "project" },
138
+ { path: getSSHConfigPath("user", ctx.cwd), level: "user" },
139
+ { path: path.join(ctx.cwd, "ssh.json"), level: "project" },
140
+ { path: path.join(ctx.cwd, ".ssh.json"), level: "project" },
141
+ ];
142
+ const uniqueSources = candidateSources.filter(
143
+ (source, index, arr) => arr.findIndex(candidate => candidate.path === source.path) === index,
144
+ );
145
+ const results = await Promise.all(uniqueSources.map(source => loadSshJsonFile(ctx, source.path, source.level)));
138
146
  const allItems = results.flatMap(r => r.items);
139
147
  const allWarnings = results.flatMap(r => r.warnings ?? []);
140
-
141
148
  return {
142
149
  items: allItems,
143
150
  warnings: allWarnings.length > 0 ? allWarnings : undefined,
@@ -147,7 +154,7 @@ async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
147
154
  registerProvider(sshCapability.id, {
148
155
  id: PROVIDER_ID,
149
156
  displayName: DISPLAY_NAME,
150
- description: "Load SSH hosts from ssh.json or .ssh.json in the project root",
157
+ description: "Load SSH hosts from managed omp paths and legacy ssh.json/.ssh.json files",
151
158
  priority: 5,
152
159
  load,
153
160
  });
@@ -503,6 +503,7 @@ export interface ToolExecutionStartEvent {
503
503
  toolCallId: string;
504
504
  toolName: string;
505
505
  args: unknown;
506
+ intent?: string;
506
507
  }
507
508
 
508
509
  /** Fired during tool execution with partial/streaming output */
@@ -0,0 +1,101 @@
1
+ // Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
2
+
3
+ import bashToolRuntimeMd from "../../../../docs/bash-tool-runtime.md" with { type: "text" };
4
+ import blobArtifactArchitectureMd from "../../../../docs/blob-artifact-architecture.md" with { type: "text" };
5
+ import compactionMd from "../../../../docs/compaction.md" with { type: "text" };
6
+ import configUsageMd from "../../../../docs/config-usage.md" with { type: "text" };
7
+ import customToolsMd from "../../../../docs/custom-tools.md" with { type: "text" };
8
+ import environmentVariablesMd from "../../../../docs/environment-variables.md" with { type: "text" };
9
+ import extensionLoadingMd from "../../../../docs/extension-loading.md" with { type: "text" };
10
+ import extensionsMd from "../../../../docs/extensions.md" with { type: "text" };
11
+ import fsScanCacheArchitectureMd from "../../../../docs/fs-scan-cache-architecture.md" with { type: "text" };
12
+ import geminiManifestExtensionsMd from "../../../../docs/gemini-manifest-extensions.md" with { type: "text" };
13
+ import handoffGenerationPipelineMd from "../../../../docs/handoff-generation-pipeline.md" with { type: "text" };
14
+ import hooksMd from "../../../../docs/hooks.md" with { type: "text" };
15
+ import mcpProtocolTransportsMd from "../../../../docs/mcp-protocol-transports.md" with { type: "text" };
16
+ import mcpRuntimeLifecycleMd from "../../../../docs/mcp-runtime-lifecycle.md" with { type: "text" };
17
+ import mcpServerToolAuthoringMd from "../../../../docs/mcp-server-tool-authoring.md" with { type: "text" };
18
+ import modelsMd from "../../../../docs/models.md" with { type: "text" };
19
+ import nativesAddonLoaderRuntimeMd from "../../../../docs/natives-addon-loader-runtime.md" with { type: "text" };
20
+ import nativesArchitectureMd from "../../../../docs/natives-architecture.md" with { type: "text" };
21
+ import nativesBindingContractMd from "../../../../docs/natives-binding-contract.md" with { type: "text" };
22
+ import nativesBuildReleaseDebuggingMd from "../../../../docs/natives-build-release-debugging.md" with { type: "text" };
23
+ import nativesMediaSystemUtilsMd from "../../../../docs/natives-media-system-utils.md" with { type: "text" };
24
+ import nativesRustTaskCancellationMd from "../../../../docs/natives-rust-task-cancellation.md" with { type: "text" };
25
+ import nativesShellPtyProcessMd from "../../../../docs/natives-shell-pty-process.md" with { type: "text" };
26
+ import nativesTextSearchPipelineMd from "../../../../docs/natives-text-search-pipeline.md" with { type: "text" };
27
+ import nonCompactionRetryPolicyMd from "../../../../docs/non-compaction-retry-policy.md" with { type: "text" };
28
+ import notebookToolRuntimeMd from "../../../../docs/notebook-tool-runtime.md" with { type: "text" };
29
+ import pluginManagerInstallerPlumbingMd from "../../../../docs/plugin-manager-installer-plumbing.md" with { type: "text" };
30
+ import portingFromPiMonoMd from "../../../../docs/porting-from-pi-mono.md" with { type: "text" };
31
+ import portingToNativesMd from "../../../../docs/porting-to-natives.md" with { type: "text" };
32
+ import providerStreamingInternalsMd from "../../../../docs/provider-streaming-internals.md" with { type: "text" };
33
+ import pythonReplMd from "../../../../docs/python-repl.md" with { type: "text" };
34
+ import rpcMd from "../../../../docs/rpc.md" with { type: "text" };
35
+ import rulebookMatchingPipelineMd from "../../../../docs/rulebook-matching-pipeline.md" with { type: "text" };
36
+ import sdkMd from "../../../../docs/sdk.md" with { type: "text" };
37
+ import secretsMd from "../../../../docs/secrets.md" with { type: "text" };
38
+ import sessionMd from "../../../../docs/session.md" with { type: "text" };
39
+ import sessionOperationsExportShareForkResumeMd from "../../../../docs/session-operations-export-share-fork-resume.md" with { type: "text" };
40
+ import sessionSwitchingAndRecentListingMd from "../../../../docs/session-switching-and-recent-listing.md" with { type: "text" };
41
+ import sessionTreePlanMd from "../../../../docs/session-tree-plan.md" with { type: "text" };
42
+ import skillsMd from "../../../../docs/skills.md" with { type: "text" };
43
+ import slashCommandInternalsMd from "../../../../docs/slash-command-internals.md" with { type: "text" };
44
+ import taskAgentDiscoveryMd from "../../../../docs/task-agent-discovery.md" with { type: "text" };
45
+ import themeMd from "../../../../docs/theme.md" with { type: "text" };
46
+ import treeMd from "../../../../docs/tree.md" with { type: "text" };
47
+ import ttsrInjectionLifecycleMd from "../../../../docs/ttsr-injection-lifecycle.md" with { type: "text" };
48
+ import tuiMd from "../../../../docs/tui.md" with { type: "text" };
49
+ import tuiRuntimeInternalsMd from "../../../../docs/tui-runtime-internals.md" with { type: "text" };
50
+
51
+ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
52
+ "bash-tool-runtime.md": bashToolRuntimeMd,
53
+ "blob-artifact-architecture.md": blobArtifactArchitectureMd,
54
+ "compaction.md": compactionMd,
55
+ "config-usage.md": configUsageMd,
56
+ "custom-tools.md": customToolsMd,
57
+ "environment-variables.md": environmentVariablesMd,
58
+ "extension-loading.md": extensionLoadingMd,
59
+ "extensions.md": extensionsMd,
60
+ "fs-scan-cache-architecture.md": fsScanCacheArchitectureMd,
61
+ "gemini-manifest-extensions.md": geminiManifestExtensionsMd,
62
+ "handoff-generation-pipeline.md": handoffGenerationPipelineMd,
63
+ "hooks.md": hooksMd,
64
+ "mcp-protocol-transports.md": mcpProtocolTransportsMd,
65
+ "mcp-runtime-lifecycle.md": mcpRuntimeLifecycleMd,
66
+ "mcp-server-tool-authoring.md": mcpServerToolAuthoringMd,
67
+ "models.md": modelsMd,
68
+ "natives-addon-loader-runtime.md": nativesAddonLoaderRuntimeMd,
69
+ "natives-architecture.md": nativesArchitectureMd,
70
+ "natives-binding-contract.md": nativesBindingContractMd,
71
+ "natives-build-release-debugging.md": nativesBuildReleaseDebuggingMd,
72
+ "natives-media-system-utils.md": nativesMediaSystemUtilsMd,
73
+ "natives-rust-task-cancellation.md": nativesRustTaskCancellationMd,
74
+ "natives-shell-pty-process.md": nativesShellPtyProcessMd,
75
+ "natives-text-search-pipeline.md": nativesTextSearchPipelineMd,
76
+ "non-compaction-retry-policy.md": nonCompactionRetryPolicyMd,
77
+ "notebook-tool-runtime.md": notebookToolRuntimeMd,
78
+ "plugin-manager-installer-plumbing.md": pluginManagerInstallerPlumbingMd,
79
+ "porting-from-pi-mono.md": portingFromPiMonoMd,
80
+ "porting-to-natives.md": portingToNativesMd,
81
+ "provider-streaming-internals.md": providerStreamingInternalsMd,
82
+ "python-repl.md": pythonReplMd,
83
+ "rpc.md": rpcMd,
84
+ "rulebook-matching-pipeline.md": rulebookMatchingPipelineMd,
85
+ "sdk.md": sdkMd,
86
+ "secrets.md": secretsMd,
87
+ "session.md": sessionMd,
88
+ "session-operations-export-share-fork-resume.md": sessionOperationsExportShareForkResumeMd,
89
+ "session-switching-and-recent-listing.md": sessionSwitchingAndRecentListingMd,
90
+ "session-tree-plan.md": sessionTreePlanMd,
91
+ "skills.md": skillsMd,
92
+ "slash-command-internals.md": slashCommandInternalsMd,
93
+ "task-agent-discovery.md": taskAgentDiscoveryMd,
94
+ "theme.md": themeMd,
95
+ "tree.md": treeMd,
96
+ "ttsr-injection-lifecycle.md": ttsrInjectionLifecycleMd,
97
+ "tui.md": tuiMd,
98
+ "tui-runtime-internals.md": tuiRuntimeInternalsMd,
99
+ };
100
+
101
+ export const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort();
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Protocol handler for docs:// URLs.
3
+ *
4
+ * Serves statically embedded documentation files bundled at build time.
5
+ *
6
+ * URL forms:
7
+ * - docs:// - Lists all available documentation files
8
+ * - docs://<file>.md - Reads a specific documentation file
9
+ */
10
+ import * as path from "node:path";
11
+ import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
12
+ import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
13
+
14
+ /**
15
+ * Handler for docs:// URLs.
16
+ *
17
+ * Resolves documentation file names to their content, or lists available docs.
18
+ */
19
+ export class DocsProtocolHandler implements ProtocolHandler {
20
+ readonly scheme = "docs";
21
+
22
+ async resolve(url: InternalUrl): Promise<InternalResource> {
23
+ // Extract filename from host + path
24
+ const host = url.rawHost || url.hostname;
25
+ const pathname = url.rawPathname ?? url.pathname;
26
+ const filename = host ? (pathname && pathname !== "/" ? host + pathname : host) : "";
27
+
28
+ if (!filename) {
29
+ return this.#listDocs(url);
30
+ }
31
+
32
+ return this.#readDoc(filename, url);
33
+ }
34
+
35
+ async #listDocs(url: InternalUrl): Promise<InternalResource> {
36
+ if (EMBEDDED_DOC_FILENAMES.length === 0) {
37
+ throw new Error("No documentation files found");
38
+ }
39
+
40
+ const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](docs://${f})`).join("\n");
41
+ const content = `# Documentation\n\n${EMBEDDED_DOC_FILENAMES.length} files available:\n\n${listing}\n`;
42
+
43
+ return {
44
+ url: url.href,
45
+ content,
46
+ contentType: "text/markdown",
47
+ size: Buffer.byteLength(content, "utf-8"),
48
+ sourcePath: "docs://",
49
+ };
50
+ }
51
+
52
+ async #readDoc(filename: string, url: InternalUrl): Promise<InternalResource> {
53
+ // Validate: no traversal, no absolute paths
54
+ if (path.isAbsolute(filename)) {
55
+ throw new Error("Absolute paths are not allowed in docs:// URLs");
56
+ }
57
+
58
+ const normalized = path.posix.normalize(filename.replaceAll("\\", "/"));
59
+ if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
60
+ throw new Error("Path traversal (..) is not allowed in docs:// URLs");
61
+ }
62
+
63
+ const content = EMBEDDED_DOCS[normalized];
64
+ if (content === undefined) {
65
+ const lookup = normalized.replace(/\.md$/, "");
66
+ const suggestions = EMBEDDED_DOC_FILENAMES.filter(
67
+ f => f.includes(lookup) || lookup.includes(f.replace(/\.md$/, "")),
68
+ ).slice(0, 5);
69
+ const suffix =
70
+ suggestions.length > 0
71
+ ? `\nDid you mean: ${suggestions.join(", ")}`
72
+ : "\nUse docs:// to list available files.";
73
+ throw new Error(`Documentation file not found: ${filename}${suffix}`);
74
+ }
75
+
76
+ return {
77
+ url: url.href,
78
+ content,
79
+ contentType: "text/markdown",
80
+ size: Buffer.byteLength(content, "utf-8"),
81
+ sourcePath: `docs://${normalized}`,
82
+ };
83
+ }
84
+ }
@@ -22,6 +22,7 @@
22
22
 
23
23
  export { AgentProtocolHandler, type AgentProtocolOptions } from "./agent-protocol";
24
24
  export { ArtifactProtocolHandler, type ArtifactProtocolOptions } from "./artifact-protocol";
25
+ export { DocsProtocolHandler } from "./docs-protocol";
25
26
  export { applyQuery, parseQuery, pathToQuery } from "./json-query";
26
27
  export { MemoryProtocolHandler, type MemoryProtocolOptions, resolveMemoryUrlToPath } from "./memory-protocol";
27
28
  export { PlanProtocolHandler, type PlanProtocolOptions, resolvePlanUrlToPath } from "./plan-protocol";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://).
2
+ * Internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://, docs://).
3
3
  */
4
4
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
5
5
 
@@ -1,3 +1,4 @@
1
+ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
1
2
  import { Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
2
3
  import { settings } from "../../config/settings";
3
4
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -14,6 +15,7 @@ export class EventController {
14
15
  #lastReadGroup: ReadToolGroupComponent | undefined = undefined;
15
16
  #lastThinkingCount = 0;
16
17
  #renderedCustomMessages = new Set<string>();
18
+ #lastIntent: string | undefined = undefined;
17
19
 
18
20
  constructor(private ctx: InteractiveModeContext) {}
19
21
 
@@ -32,6 +34,13 @@ export class EventController {
32
34
  return this.#lastReadGroup;
33
35
  }
34
36
 
37
+ #updateWorkingMessageFromIntent(intent: string | undefined): void {
38
+ const trimmed = intent?.trim();
39
+ if (!trimmed || trimmed === this.#lastIntent) return;
40
+ this.#lastIntent = trimmed;
41
+ this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
42
+ }
43
+
35
44
  subscribeToAgent(): void {
36
45
  this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
37
46
  await this.handleEvent(event);
@@ -48,6 +57,7 @@ export class EventController {
48
57
 
49
58
  switch (event.type) {
50
59
  case "agent_start":
60
+ this.#lastIntent = undefined;
51
61
  if (this.ctx.retryEscapeHandler) {
52
62
  this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
53
63
  this.ctx.retryEscapeHandler = undefined;
@@ -155,6 +165,15 @@ export class EventController {
155
165
  }
156
166
  }
157
167
  }
168
+
169
+ // Update working message with intent from streamed tool arguments
170
+ for (const content of this.ctx.streamingMessage.content) {
171
+ if (content.type !== "toolCall") continue;
172
+ const args = content.arguments;
173
+ if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
174
+ this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
175
+ }
176
+
158
177
  this.ctx.ui.requestRender();
159
178
  }
160
179
  break;
@@ -196,6 +215,7 @@ export class EventController {
196
215
  break;
197
216
 
198
217
  case "tool_execution_start": {
218
+ this.#updateWorkingMessageFromIntent(event.intent);
199
219
  if (!this.ctx.pendingTools.has(event.toolCallId)) {
200
220
  if (event.toolName === "read") {
201
221
  const group = this.#getReadGroup();