gsd-pi 2.67.0-dev.5399650 → 2.67.0-dev.6fc2289

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 (151) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
  3. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-start.js +16 -30
  6. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  7. package/dist/resources/extensions/gsd/auto.js +94 -59
  8. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  9. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  11. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  12. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  13. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  14. package/dist/resources/extensions/gsd/doctor.js +8 -4
  15. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  16. package/dist/resources/extensions/gsd/init-wizard.js +15 -12
  17. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  18. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  19. package/dist/resources/extensions/gsd/workflow-mcp.js +64 -24
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  22. package/dist/web/standalone/.next/build-manifest.json +3 -3
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  25. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  52. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  55. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  56. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  57. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  58. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  59. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  60. package/package.json +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  62. package/packages/mcp-server/dist/workflow-tools.js +10 -4
  63. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  64. package/packages/mcp-server/src/workflow-tools.ts +13 -2
  65. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  66. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  67. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  68. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  69. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  70. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  72. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  74. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  76. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  78. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  80. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  101. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  102. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  104. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  105. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  106. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  107. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
  110. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  111. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  112. package/src/resources/extensions/gsd/auto-start.ts +23 -55
  113. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  114. package/src/resources/extensions/gsd/auto.ts +104 -63
  115. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  116. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  117. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  118. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  120. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  121. package/src/resources/extensions/gsd/doctor.ts +9 -5
  122. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  123. package/src/resources/extensions/gsd/init-wizard.ts +17 -11
  124. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  125. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  126. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  127. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  128. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  129. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  130. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  131. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  132. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  133. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  134. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  135. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  136. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  137. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  138. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  139. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  140. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  141. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +178 -17
  142. package/src/resources/extensions/gsd/workflow-mcp.ts +76 -23
  143. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  144. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  145. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  147. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  148. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  149. package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
  150. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → yh2vT27L1E6PChb_C1N_F}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → yh2vT27L1E6PChb_C1N_F}/_ssgManifest.js +0 -0
@@ -1,17 +1,21 @@
1
1
  import { dispatchSlashCommand } from "../slash-command-handlers.js";
2
2
  import type { InteractiveModeStateHost } from "../interactive-mode-state.js";
3
+ import type { ContextualTips } from "../../../core/contextual-tips.js";
3
4
 
4
5
  export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
5
6
  getSlashCommandContext: () => any;
6
7
  handleBashCommand: (command: string, excludeFromContext?: boolean) => Promise<void>;
7
8
  showWarning: (message: string) => void;
8
9
  showError: (message: string) => void;
10
+ showTip: (message: string) => void;
9
11
  updateEditorBorderColor: () => void;
10
12
  isExtensionCommand: (text: string) => boolean;
11
13
  isKnownSlashCommand: (text: string) => boolean;
12
14
  queueCompactionMessage: (text: string, mode: "steer" | "followUp") => void;
13
15
  updatePendingMessagesDisplay: () => void;
14
16
  flushPendingBashComponents: () => void;
17
+ contextualTips: ContextualTips;
18
+ getContextPercent: () => number | undefined;
15
19
  options?: { submitPromptsDirectly?: boolean };
16
20
  }): void {
17
21
  host.defaultEditor.onSubmit = async (text: string) => {
@@ -41,6 +45,10 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
41
45
  host.editor.setText(text);
42
46
  return;
43
47
  }
48
+ // Track included bash commands for double-bang tip
49
+ if (!isExcluded) {
50
+ host.contextualTips.recordBashIncluded();
51
+ }
44
52
  host.editor.addToHistory?.(text);
45
53
  await host.handleBashCommand(command, isExcluded);
46
54
  host.isBashMode = false;
@@ -49,6 +57,17 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
49
57
  }
50
58
  }
51
59
 
60
+ // Evaluate contextual tips before sending to agent
61
+ const tip = host.contextualTips.evaluate({
62
+ input: text,
63
+ isStreaming: host.session.isStreaming,
64
+ thinkingLevel: host.session.thinkingLevel,
65
+ contextPercent: host.getContextPercent(),
66
+ });
67
+ if (tip) {
68
+ host.showTip(tip);
69
+ }
70
+
52
71
  if (host.session.isCompacting) {
53
72
  if (host.isExtensionCommand(text)) {
54
73
  host.editor.addToHistory?.(text);
@@ -82,7 +82,6 @@ import { LoginDialogComponent } from "./components/login-dialog.js";
82
82
  import { ModelSelectorComponent, providerDisplayName } from "./components/model-selector.js";
83
83
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
84
84
  import { ProviderManagerComponent } from "./components/provider-manager.js";
85
- import { getProviderSetupAction } from "./provider-auth-setup.js";
86
85
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
87
86
  import { SessionSelectorComponent } from "./components/session-selector.js";
88
87
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
@@ -91,6 +90,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
91
90
  import { TreeSelectorComponent } from "./components/tree-selector.js";
92
91
  import { UserMessageComponent } from "./components/user-message.js";
93
92
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
93
+ import { ContextualTips } from "../../core/contextual-tips.js";
94
94
  import { type SlashCommandContext, dispatchSlashCommand, getAppKeyDisplay } from "./slash-command-handlers.js";
95
95
  import { handleAgentEvent } from "./controllers/chat-controller.js";
96
96
  import { createExtensionUIContext as buildExtensionUIContext } from "./controllers/extension-ui-controller.js";
@@ -215,6 +215,9 @@ export class InteractiveMode {
215
215
  // Track if editor is in bash mode (text starts with !)
216
216
  private isBashMode = false;
217
217
 
218
+ // Contextual tips — session-scoped, non-intrusive hints
219
+ private contextualTips = new ContextualTips();
220
+
218
221
  // Track current bash execution component
219
222
  private bashComponent: BashExecutionComponent | undefined = undefined;
220
223
 
@@ -2594,6 +2597,16 @@ export class InteractiveMode {
2594
2597
  this.ui.requestRender();
2595
2598
  }
2596
2599
 
2600
+ showTip(message: string): void {
2601
+ this.chatContainer.addChild(new Spacer(1));
2602
+ this.chatContainer.addChild(new Text(theme.fg("dim", `💡 ${message}`), 1, 0));
2603
+ this.ui.requestRender();
2604
+ }
2605
+
2606
+ getContextPercent(): number | undefined {
2607
+ return this.session.getContextUsage()?.percent ?? undefined;
2608
+ }
2609
+
2597
2610
  showNewVersionNotification(newVersion: string): void {
2598
2611
  const action = theme.fg("accent", getUpdateInstruction("@gsd/pi-coding-agent"));
2599
2612
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
@@ -3413,21 +3426,9 @@ export class InteractiveMode {
3413
3426
  this.ui.requestRender();
3414
3427
  },
3415
3428
  async (provider: string) => {
3429
+ // Enter key → auth setup for selected provider (#3579)
3416
3430
  done();
3417
-
3418
- const action = getProviderSetupAction({
3419
- provider,
3420
- authMode: this.session.modelRegistry.getProviderAuthMode(provider),
3421
- hasAuth: this.session.modelRegistry.authStorage.hasAuth(provider),
3422
- });
3423
-
3424
- if (action.kind === "oauth-login") {
3425
- await this.showLoginDialog(provider);
3426
- return;
3427
- }
3428
-
3429
- this.showStatus(action.message);
3430
- this.ui.requestRender();
3431
+ await this.showLoginDialog(provider);
3431
3432
  },
3432
3433
  );
3433
3434
  return { component, focus: component };
@@ -3692,6 +3693,9 @@ export class InteractiveMode {
3692
3693
  this.streamingMessage = undefined;
3693
3694
  this.pendingTools.clear();
3694
3695
 
3696
+ // Reset contextual tips for the new session
3697
+ this.contextualTips.reset();
3698
+
3695
3699
  this.chatContainer.addChild(new Spacer(1));
3696
3700
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
3697
3701
  this.ui.requestRender();
@@ -14,6 +14,7 @@ import type {
14
14
  Context,
15
15
  Model,
16
16
  SimpleStreamOptions,
17
+ ToolCall,
17
18
  } from "@gsd/pi-ai";
18
19
  import { EventStream } from "@gsd/pi-ai";
19
20
  import { execSync } from "node:child_process";
@@ -24,8 +25,26 @@ import type {
24
25
  SDKMessage,
25
26
  SDKPartialAssistantMessage,
26
27
  SDKResultMessage,
28
+ SDKUserMessage,
27
29
  } from "./sdk-types.js";
28
30
 
31
+ export interface ExternalToolResultContentBlock {
32
+ type: string;
33
+ text?: string;
34
+ data?: string;
35
+ mimeType?: string;
36
+ }
37
+
38
+ export interface ExternalToolResultPayload {
39
+ content: ExternalToolResultContentBlock[];
40
+ details?: Record<string, unknown>;
41
+ isError: boolean;
42
+ }
43
+
44
+ type ToolCallWithExternalResult = ToolCall & {
45
+ externalResult?: ExternalToolResultPayload;
46
+ };
47
+
29
48
  // ---------------------------------------------------------------------------
30
49
  // Stream factory
31
50
  // ---------------------------------------------------------------------------
@@ -153,89 +172,6 @@ export function makeStreamExhaustedErrorMessage(model: string, lastTextContent:
153
172
  return message;
154
173
  }
155
174
 
156
- /**
157
- * Claude Code executes its own internal tool loop inside the SDK call. The
158
- * streamed and final assistant messages should therefore contain only
159
- * user-facing content (text/thinking), not replayable tool blocks that GSD
160
- * would render again.
161
- */
162
- function isUserFacingClaudeCodeBlock(block: AssistantMessage["content"][number]): boolean {
163
- return block.type === "text" || block.type === "thinking";
164
- }
165
-
166
- function filterUserFacingClaudeCodeContent(
167
- blocks: AssistantMessage["content"],
168
- ): AssistantMessage["content"] {
169
- return blocks.filter(isUserFacingClaudeCodeBlock);
170
- }
171
-
172
- function remapClaudeCodeContentIndex(
173
- blocks: AssistantMessage["content"],
174
- contentIndex: number,
175
- ): number {
176
- let visibleCount = 0;
177
- for (let i = 0; i <= contentIndex && i < blocks.length; i++) {
178
- if (isUserFacingClaudeCodeBlock(blocks[i]!)) visibleCount++;
179
- }
180
- return Math.max(0, visibleCount - 1);
181
- }
182
-
183
- function sanitizeClaudeCodePartial(
184
- partial: AssistantMessage,
185
- ): AssistantMessage {
186
- return {
187
- ...partial,
188
- content: filterUserFacingClaudeCodeContent(partial.content),
189
- };
190
- }
191
-
192
- export function sanitizeClaudeCodeStreamingEvent(
193
- event: AssistantMessageEvent,
194
- ): AssistantMessageEvent | null {
195
- switch (event.type) {
196
- case "toolcall_start":
197
- case "toolcall_delta":
198
- case "toolcall_end":
199
- case "server_tool_use":
200
- case "web_search_result":
201
- return null;
202
- case "text_start":
203
- case "text_delta":
204
- case "text_end":
205
- case "thinking_start":
206
- case "thinking_delta":
207
- case "thinking_end":
208
- return {
209
- ...event,
210
- contentIndex: remapClaudeCodeContentIndex(event.partial.content, event.contentIndex),
211
- partial: sanitizeClaudeCodePartial(event.partial),
212
- };
213
- default:
214
- return event;
215
- }
216
- }
217
-
218
- export function buildFinalClaudeCodeContent(
219
- blocks: AssistantMessage["content"],
220
- lastThinkingContent: string,
221
- lastTextContent: string,
222
- resultText?: string,
223
- ): AssistantMessage["content"] {
224
- const finalContent = filterUserFacingClaudeCodeContent(blocks);
225
- if (finalContent.length > 0) return finalContent;
226
-
227
- if (lastThinkingContent) {
228
- finalContent.push({ type: "thinking", thinking: lastThinkingContent });
229
- }
230
- if (lastTextContent) {
231
- finalContent.push({ type: "text", text: lastTextContent });
232
- }
233
- if (finalContent.length === 0 && resultText) {
234
- finalContent.push({ type: "text", text: resultText });
235
- }
236
- return finalContent;
237
- }
238
-
239
175
  // ---------------------------------------------------------------------------
240
176
  // SDK options builder
241
177
  // ---------------------------------------------------------------------------
@@ -263,6 +199,110 @@ export function buildSdkOptions(modelId: string, prompt: string): Record<string,
263
199
  };
264
200
  }
265
201
 
202
+ function normalizeToolResultContent(content: unknown): ExternalToolResultContentBlock[] {
203
+ if (typeof content === "string") {
204
+ return [{ type: "text", text: content }];
205
+ }
206
+
207
+ if (!Array.isArray(content)) {
208
+ if (content == null) return [{ type: "text", text: "" }];
209
+ return [{ type: "text", text: JSON.stringify(content) }];
210
+ }
211
+
212
+ const blocks: ExternalToolResultContentBlock[] = [];
213
+
214
+ for (const item of content) {
215
+ if (typeof item === "string") {
216
+ blocks.push({ type: "text", text: item });
217
+ continue;
218
+ }
219
+ if (!item || typeof item !== "object") {
220
+ blocks.push({ type: "text", text: String(item) });
221
+ continue;
222
+ }
223
+
224
+ const block = item as Record<string, unknown>;
225
+ if (block.type === "text") {
226
+ blocks.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
227
+ continue;
228
+ }
229
+ if (
230
+ block.type === "image"
231
+ && typeof block.data === "string"
232
+ && typeof block.mimeType === "string"
233
+ ) {
234
+ blocks.push({ type: "image", data: block.data, mimeType: block.mimeType });
235
+ continue;
236
+ }
237
+
238
+ blocks.push({ type: "text", text: JSON.stringify(block) });
239
+ }
240
+
241
+ return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
242
+ }
243
+
244
+ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): Array<{
245
+ toolUseId: string;
246
+ result: ExternalToolResultPayload;
247
+ }> {
248
+ const extracted: Array<{ toolUseId: string; result: ExternalToolResultPayload }> = [];
249
+ const seen = new Set<string>();
250
+ const rawMessage = message.message as Record<string, unknown> | null | undefined;
251
+ const content = Array.isArray(rawMessage?.content) ? rawMessage.content : [];
252
+
253
+ for (const item of content) {
254
+ if (!item || typeof item !== "object") continue;
255
+ const block = item as Record<string, unknown>;
256
+ const type = typeof block.type === "string" ? block.type : "";
257
+ if (type !== "tool_result" && type !== "mcp_tool_result") continue;
258
+
259
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
260
+ if (!toolUseId || seen.has(toolUseId)) continue;
261
+ seen.add(toolUseId);
262
+
263
+ extracted.push({
264
+ toolUseId,
265
+ result: {
266
+ content: normalizeToolResultContent(block.content),
267
+ details: {},
268
+ isError: block.is_error === true,
269
+ },
270
+ });
271
+ }
272
+
273
+ if (extracted.length === 0) {
274
+ const fallback = message.tool_use_result;
275
+ if (fallback && typeof fallback === "object") {
276
+ const toolResult = fallback as Record<string, unknown>;
277
+ const toolUseId = typeof toolResult.tool_use_id === "string" ? toolResult.tool_use_id : "";
278
+ if (toolUseId) {
279
+ extracted.push({
280
+ toolUseId,
281
+ result: {
282
+ content: normalizeToolResultContent(toolResult.content),
283
+ details: {},
284
+ isError: toolResult.is_error === true,
285
+ },
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ return extracted;
292
+ }
293
+
294
+ function attachExternalResultsToToolCalls(
295
+ toolCalls: AssistantMessage["content"],
296
+ toolResultsById: ReadonlyMap<string, ExternalToolResultPayload>,
297
+ ): void {
298
+ for (const block of toolCalls) {
299
+ if (block.type !== "toolCall") continue;
300
+ const externalResult = toolResultsById.get(block.id);
301
+ if (!externalResult) continue;
302
+ (block as ToolCallWithExternalResult).externalResult = externalResult;
303
+ }
304
+ }
305
+
266
306
  // ---------------------------------------------------------------------------
267
307
  // streamSimple implementation
268
308
  // ---------------------------------------------------------------------------
@@ -297,6 +337,10 @@ async function pumpSdkMessages(
297
337
  /** Track the last text content seen across all assistant turns for the final message. */
298
338
  let lastTextContent = "";
299
339
  let lastThinkingContent = "";
340
+ /** Collect tool calls from intermediate SDK turns for tool_execution events. */
341
+ const intermediateToolCalls: AssistantMessage["content"] = [];
342
+ /** Preserve real external tool results from Claude Code's synthetic user messages. */
343
+ const toolResultsById = new Map<string, ExternalToolResultPayload>();
300
344
 
301
345
  try {
302
346
  // Dynamic import — the SDK is an optional dependency.
@@ -365,10 +409,9 @@ async function pumpSdkMessages(
365
409
  if (!builder) break;
366
410
 
367
411
  const assistantEvent = builder.handleEvent(event);
368
- const sanitizedEvent = assistantEvent
369
- ? sanitizeClaudeCodeStreamingEvent(assistantEvent)
370
- : null;
371
- if (sanitizedEvent) stream.push(sanitizedEvent);
412
+ if (assistantEvent) {
413
+ stream.push(assistantEvent);
414
+ }
372
415
  break;
373
416
  }
374
417
 
@@ -396,9 +439,39 @@ async function pumpSdkMessages(
396
439
  lastTextContent = block.text;
397
440
  } else if (block.type === "thinking" && block.thinking) {
398
441
  lastThinkingContent = block.thinking;
442
+ } else if (block.type === "toolCall") {
443
+ // Collect tool calls for externalToolExecution rendering
444
+ intermediateToolCalls.push(block);
399
445
  }
400
446
  }
401
447
  }
448
+
449
+ // Extract tool results from the SDK's synthetic user message
450
+ // and attach to corresponding tool call blocks immediately.
451
+ for (const { toolUseId, result } of extractToolResultsFromSdkUserMessage(msg as SDKUserMessage)) {
452
+ toolResultsById.set(toolUseId, result);
453
+ }
454
+ attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
455
+
456
+ // Push a synthetic toolcall_end for each tool call from this turn
457
+ // so the TUI can render tool results in real-time during the SDK
458
+ // session instead of waiting until the entire session completes.
459
+ if (builder) {
460
+ for (const block of builder.message.content) {
461
+ if (block.type !== "toolCall") continue;
462
+ const extResult = (block as ToolCallWithExternalResult).externalResult;
463
+ if (!extResult) continue;
464
+ // Push a toolcall_end with result attached so the chat-controller
465
+ // can call updateResult on the pending ToolExecutionComponent.
466
+ stream.push({
467
+ type: "toolcall_end",
468
+ contentIndex: builder.message.content.indexOf(block),
469
+ toolCall: block,
470
+ partial: builder.message,
471
+ });
472
+ }
473
+ }
474
+
402
475
  builder = null;
403
476
  break;
404
477
  }
@@ -406,12 +479,36 @@ async function pumpSdkMessages(
406
479
  // -- Result (terminal) --
407
480
  case "result": {
408
481
  const result = msg as SDKResultMessage;
409
- const finalContent = buildFinalClaudeCodeContent(
410
- builder?.message.content ?? [],
411
- lastThinkingContent,
412
- lastTextContent,
413
- result.subtype === "success" ? result.result : undefined,
414
- );
482
+
483
+ // Build final message. Include intermediate tool calls so the
484
+ // agent loop's externalToolExecution path emits tool_execution
485
+ // events for proper TUI rendering, followed by the text response.
486
+ const finalContent: AssistantMessage["content"] = [];
487
+
488
+ // Add tool calls from intermediate turns first (renders above text)
489
+ attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
490
+ finalContent.push(...intermediateToolCalls);
491
+
492
+ // Add text/thinking from the last turn
493
+ if (builder && builder.message.content.length > 0) {
494
+ for (const block of builder.message.content) {
495
+ if (block.type === "text" || block.type === "thinking") {
496
+ finalContent.push(block);
497
+ }
498
+ }
499
+ } else {
500
+ if (lastThinkingContent) {
501
+ finalContent.push({ type: "thinking", thinking: lastThinkingContent });
502
+ }
503
+ if (lastTextContent) {
504
+ finalContent.push({ type: "text", text: lastTextContent });
505
+ }
506
+ }
507
+
508
+ // Fallback: use the SDK's result text if we have no content
509
+ if (finalContent.length === 0 && result.subtype === "success" && result.result) {
510
+ finalContent.push({ type: "text", text: result.result });
511
+ }
415
512
 
416
513
  const finalMessage: AssistantMessage = {
417
514
  role: "assistant",