gsd-pi 2.67.0-dev.5399650 → 2.67.0-dev.fe39184
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.
- package/README.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
- package/dist/resources/extensions/gsd/auto-start.js +12 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
- package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
- package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
- package/dist/resources/extensions/gsd/init-wizard.js +15 -12
- package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +64 -24
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-42a66876b763aa26.js} +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +10 -4
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.ts +13 -2
- package/packages/pi-agent-core/dist/agent-loop.js +14 -6
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
- package/packages/pi-agent-core/src/agent-loop.ts +20 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -12
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -15
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
- package/src/resources/extensions/gsd/auto-start.ts +15 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
- package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
- package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
- package/src/resources/extensions/gsd/init-wizard.ts +17 -11
- package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +178 -17
- package/src/resources/extensions/gsd/workflow-mcp.ts +76 -23
- package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
- package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
- /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → gbSATDX4Jt2ufxzUr5nYm}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → gbSATDX4Jt2ufxzUr5nYm}/_ssgManifest.js +0 -0
package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import stripAnsi from "strip-ansi";
|
|
4
|
+
import { ToolExecutionComponent } from "../tool-execution.js";
|
|
5
|
+
import { initTheme } from "../../theme/theme.js";
|
|
6
|
+
|
|
7
|
+
initTheme("dark", false);
|
|
8
|
+
|
|
9
|
+
function renderTool(
|
|
10
|
+
toolName: string,
|
|
11
|
+
args: Record<string, unknown>,
|
|
12
|
+
result?: {
|
|
13
|
+
content: Array<{ type: string; text?: string }>;
|
|
14
|
+
isError: boolean;
|
|
15
|
+
details?: Record<string, unknown>;
|
|
16
|
+
},
|
|
17
|
+
): string {
|
|
18
|
+
const component = new ToolExecutionComponent(
|
|
19
|
+
toolName,
|
|
20
|
+
args,
|
|
21
|
+
{},
|
|
22
|
+
undefined,
|
|
23
|
+
{ requestRender() {} } as any,
|
|
24
|
+
);
|
|
25
|
+
component.setExpanded(true);
|
|
26
|
+
if (result) component.updateResult(result);
|
|
27
|
+
return stripAnsi(component.render(120).join("\n"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("ToolExecutionComponent", () => {
|
|
31
|
+
test("renders capitalized Claude Code Bash tool names with bash output instead of generic args JSON", () => {
|
|
32
|
+
const rendered = renderTool(
|
|
33
|
+
"Bash",
|
|
34
|
+
{ command: "pwd" },
|
|
35
|
+
{ content: [{ type: "text", text: "/tmp/gsd-pr-fix" }], isError: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert.match(rendered, /\$ pwd/);
|
|
39
|
+
assert.match(rendered, /\/tmp\/gsd-pr-fix/);
|
|
40
|
+
assert.doesNotMatch(rendered, /^\{\s*\}$/m);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("renders capitalized Claude Code Read tool names with read output", () => {
|
|
44
|
+
const rendered = renderTool(
|
|
45
|
+
"Read",
|
|
46
|
+
{ path: "/tmp/demo.txt" },
|
|
47
|
+
{ content: [{ type: "text", text: "hello\nworld" }], isError: false },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.match(rendered, /read .*demo\.txt/);
|
|
51
|
+
assert.match(rendered, /hello/);
|
|
52
|
+
assert.match(rendered, /world/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -97,6 +97,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
97
97
|
// When true, this component intentionally renders no lines
|
|
98
98
|
private hideComponent = false;
|
|
99
99
|
|
|
100
|
+
private get normalizedToolName(): string {
|
|
101
|
+
return typeof this.toolName === "string" ? this.toolName.toLowerCase() : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
constructor(
|
|
101
105
|
toolName: string,
|
|
102
106
|
args: any,
|
|
@@ -121,7 +125,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
121
125
|
|
|
122
126
|
// Use contentBox for bash (visual truncation) or custom tools with custom renderers
|
|
123
127
|
// Use contentText for built-in tools (including overrides without custom renderers)
|
|
124
|
-
if (
|
|
128
|
+
if (this.normalizedToolName === "bash" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {
|
|
125
129
|
this.addChild(this.contentBox);
|
|
126
130
|
} else {
|
|
127
131
|
this.addChild(this.contentText);
|
|
@@ -136,7 +140,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
136
140
|
* or the toolDefinition doesn't provide custom renderers.
|
|
137
141
|
*/
|
|
138
142
|
private shouldUseBuiltInRenderer(): boolean {
|
|
139
|
-
const
|
|
143
|
+
const normalizedToolName = this.normalizedToolName;
|
|
144
|
+
const isBuiltInName = normalizedToolName in allTools;
|
|
140
145
|
const hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;
|
|
141
146
|
return isBuiltInName && !hasCustomRenderers;
|
|
142
147
|
}
|
|
@@ -152,7 +157,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
157
|
|
|
153
158
|
updateArgs(args: any): void {
|
|
154
159
|
this.args = args;
|
|
155
|
-
if (this.
|
|
160
|
+
if (this.normalizedToolName === "write" && this.isPartial) {
|
|
156
161
|
this.updateWriteHighlightCacheIncremental();
|
|
157
162
|
}
|
|
158
163
|
this.updateDisplay();
|
|
@@ -308,7 +313,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
308
313
|
): void {
|
|
309
314
|
this.result = result;
|
|
310
315
|
this.isPartial = isPartial;
|
|
311
|
-
if (this.
|
|
316
|
+
if (this.normalizedToolName === "write" && !isPartial) {
|
|
312
317
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
313
318
|
const fileContent = str(this.args?.content);
|
|
314
319
|
if (rawPath !== null && fileContent !== null) {
|
|
@@ -387,7 +392,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
387
392
|
|
|
388
393
|
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
|
389
394
|
if (useBuiltInRenderer) {
|
|
390
|
-
if (this.
|
|
395
|
+
if (this.normalizedToolName === "bash") {
|
|
391
396
|
// Bash uses Box with visual line truncation
|
|
392
397
|
this.contentBox.setBgFn(bgFn);
|
|
393
398
|
this.contentBox.clear();
|
|
@@ -629,8 +634,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
629
634
|
private formatToolExecution(): string {
|
|
630
635
|
let text = "";
|
|
631
636
|
const invalidArg = theme.fg("error", "[invalid arg]");
|
|
637
|
+
const normalizedToolName = this.normalizedToolName;
|
|
632
638
|
|
|
633
|
-
if (
|
|
639
|
+
if (normalizedToolName === "read") {
|
|
634
640
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
635
641
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
636
642
|
const offset = this.args?.offset;
|
|
@@ -692,7 +698,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
692
698
|
}
|
|
693
699
|
}
|
|
694
700
|
}
|
|
695
|
-
} else if (
|
|
701
|
+
} else if (normalizedToolName === "write") {
|
|
696
702
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
697
703
|
const fileContent = str(this.args?.content);
|
|
698
704
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
@@ -751,7 +757,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
751
757
|
text += `\n\n${theme.fg("error", errorText)}`;
|
|
752
758
|
}
|
|
753
759
|
}
|
|
754
|
-
} else if (
|
|
760
|
+
} else if (normalizedToolName === "edit") {
|
|
755
761
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
756
762
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
757
763
|
|
|
@@ -787,7 +793,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
787
793
|
text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
|
|
788
794
|
}
|
|
789
795
|
}
|
|
790
|
-
} else if (
|
|
796
|
+
} else if (normalizedToolName === "ls") {
|
|
791
797
|
const rawPath = str(this.args?.path);
|
|
792
798
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
793
799
|
const limit = this.args?.limit;
|
|
@@ -824,7 +830,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
824
830
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
825
831
|
}
|
|
826
832
|
}
|
|
827
|
-
} else if (
|
|
833
|
+
} else if (normalizedToolName === "find") {
|
|
828
834
|
const pattern = str(this.args?.pattern);
|
|
829
835
|
const rawPath = str(this.args?.path);
|
|
830
836
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
@@ -866,7 +872,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
866
872
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
867
873
|
}
|
|
868
874
|
}
|
|
869
|
-
} else if (
|
|
875
|
+
} else if (normalizedToolName === "grep") {
|
|
870
876
|
const pattern = str(this.args?.pattern);
|
|
871
877
|
const rawPath = str(this.args?.path);
|
|
872
878
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
@@ -916,7 +922,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
916
922
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
917
923
|
}
|
|
918
924
|
}
|
|
919
|
-
} else if (
|
|
925
|
+
} else if (normalizedToolName === "web_search") {
|
|
920
926
|
// Server-side Anthropic web search
|
|
921
927
|
text = theme.fg("toolTitle", theme.bold("web search"));
|
|
922
928
|
|
|
@@ -121,6 +121,27 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
121
121
|
if (host.streamingComponent && event.message.role === "assistant") {
|
|
122
122
|
host.streamingMessage = event.message;
|
|
123
123
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
124
|
+
|
|
125
|
+
// When the stream adapter signals a completed tool call with an
|
|
126
|
+
// external result (from Claude Code SDK), update the pending
|
|
127
|
+
// ToolExecutionComponent immediately so output is visible in
|
|
128
|
+
// real-time instead of waiting for the session to end.
|
|
129
|
+
const innerEvent = event.assistantMessageEvent;
|
|
130
|
+
if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) {
|
|
131
|
+
const tc = innerEvent.toolCall as any;
|
|
132
|
+
const externalResult = tc.externalResult;
|
|
133
|
+
if (externalResult) {
|
|
134
|
+
const component = host.pendingTools.get(tc.id);
|
|
135
|
+
if (component) {
|
|
136
|
+
component.updateResult({
|
|
137
|
+
content: externalResult.content ?? [{ type: "text", text: "" }],
|
|
138
|
+
details: externalResult.details ?? {},
|
|
139
|
+
isError: externalResult.isError ?? false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
const contentBlocks = host.streamingMessage.content;
|
|
125
146
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
126
147
|
const content = contentBlocks[i];
|
|
@@ -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";
|
|
@@ -3413,21 +3412,9 @@ export class InteractiveMode {
|
|
|
3413
3412
|
this.ui.requestRender();
|
|
3414
3413
|
},
|
|
3415
3414
|
async (provider: string) => {
|
|
3415
|
+
// Enter key → auth setup for selected provider (#3579)
|
|
3416
3416
|
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();
|
|
3417
|
+
await this.showLoginDialog(provider);
|
|
3431
3418
|
},
|
|
3432
3419
|
);
|
|
3433
3420
|
return { component, focus: component };
|
|
@@ -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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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",
|