gsd-pi 2.70.1-dev.3591dcf → 2.70.1-dev.366a872
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/dist/resources/extensions/claude-code-cli/stream-adapter.js +129 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/resources/extensions/gsd/auto-start.js +3 -11
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/guided-flow.js +12 -10
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- 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/required-server-files.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 +12 -12
- 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/2826.dd3dc8bbd3025fa5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +348 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.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 +158 -23
- 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-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
- 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 +58 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +418 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +189 -29
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +145 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/auto-start.ts +3 -13
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/guided-flow.ts +12 -9
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → eMly3KqfZihJJAHQGmxvO}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → eMly3KqfZihJJAHQGmxvO}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { handleAgentEvent } from "../modes/interactive/controllers/chat-controller.js";
|
|
5
|
+
|
|
6
|
+
function makeUsage() {
|
|
7
|
+
return {
|
|
8
|
+
input: 0,
|
|
9
|
+
output: 0,
|
|
10
|
+
cacheRead: 0,
|
|
11
|
+
cacheWrite: 0,
|
|
12
|
+
totalTokens: 0,
|
|
13
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeAssistant(content: any[]) {
|
|
18
|
+
return {
|
|
19
|
+
role: "assistant",
|
|
20
|
+
content,
|
|
21
|
+
api: "anthropic-messages",
|
|
22
|
+
provider: "claude-code",
|
|
23
|
+
model: "claude-sonnet-4",
|
|
24
|
+
usage: makeUsage(),
|
|
25
|
+
stopReason: "stop",
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createHost() {
|
|
31
|
+
const chatContainer = {
|
|
32
|
+
children: [] as any[],
|
|
33
|
+
addChild(component: any) {
|
|
34
|
+
this.children.push(component);
|
|
35
|
+
},
|
|
36
|
+
removeChild(component: any) {
|
|
37
|
+
const idx = this.children.indexOf(component);
|
|
38
|
+
if (idx !== -1) this.children.splice(idx, 1);
|
|
39
|
+
},
|
|
40
|
+
clear() {
|
|
41
|
+
this.children = [];
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const pinnedMessageContainer = {
|
|
46
|
+
children: [] as any[],
|
|
47
|
+
addChild(component: any) {
|
|
48
|
+
this.children.push(component);
|
|
49
|
+
},
|
|
50
|
+
removeChild(component: any) {
|
|
51
|
+
const idx = this.children.indexOf(component);
|
|
52
|
+
if (idx !== -1) this.children.splice(idx, 1);
|
|
53
|
+
},
|
|
54
|
+
clear() {
|
|
55
|
+
this.children = [];
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const host: any = {
|
|
60
|
+
isInitialized: true,
|
|
61
|
+
init: async () => {},
|
|
62
|
+
defaultEditor: { onEscape: undefined },
|
|
63
|
+
editor: {},
|
|
64
|
+
session: { retryAttempt: 0, abortCompaction: () => {}, abortRetry: () => {} },
|
|
65
|
+
ui: { requestRender: () => {}, terminal: { rows: 50 } },
|
|
66
|
+
footer: { invalidate: () => {} },
|
|
67
|
+
keybindings: {},
|
|
68
|
+
statusContainer: { clear: () => {}, addChild: () => {} },
|
|
69
|
+
chatContainer,
|
|
70
|
+
settingsManager: { getTimestampFormat: () => "date-time-iso", getShowImages: () => false },
|
|
71
|
+
pendingTools: new Map(),
|
|
72
|
+
toolOutputExpanded: false,
|
|
73
|
+
hideThinkingBlock: false,
|
|
74
|
+
isBashMode: false,
|
|
75
|
+
defaultWorkingMessage: "Working...",
|
|
76
|
+
compactionQueuedMessages: [],
|
|
77
|
+
editorContainer: {},
|
|
78
|
+
pendingMessagesContainer: { clear: () => {} },
|
|
79
|
+
pinnedMessageContainer,
|
|
80
|
+
addMessageToChat: () => {},
|
|
81
|
+
getMarkdownThemeWithSettings: () => ({}),
|
|
82
|
+
formatWebSearchResult: () => "",
|
|
83
|
+
getRegisteredToolDefinition: () => undefined,
|
|
84
|
+
checkShutdownRequested: async () => {},
|
|
85
|
+
rebuildChatFromMessages: () => {},
|
|
86
|
+
flushCompactionQueue: async () => {},
|
|
87
|
+
showStatus: () => {},
|
|
88
|
+
showError: () => {},
|
|
89
|
+
updatePendingMessagesDisplay: () => {},
|
|
90
|
+
updateTerminalTitle: () => {},
|
|
91
|
+
updateEditorBorderColor: () => {},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return host;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("chat-controller keeps tool output ahead of delayed assistant text for external tool streams", async () => {
|
|
98
|
+
// ToolExecutionComponent uses the global theme singleton.
|
|
99
|
+
// Install a minimal no-op theme implementation for this unit test.
|
|
100
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
101
|
+
fg: (_key: string, text: string) => text,
|
|
102
|
+
bg: (_key: string, text: string) => text,
|
|
103
|
+
bold: (text: string) => text,
|
|
104
|
+
italic: (text: string) => text,
|
|
105
|
+
truncate: (text: string) => text,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const host = createHost();
|
|
109
|
+
const toolId = "mcp-tool-1";
|
|
110
|
+
const toolCall = {
|
|
111
|
+
type: "toolCall",
|
|
112
|
+
id: toolId,
|
|
113
|
+
name: "exec_command",
|
|
114
|
+
arguments: { cmd: "echo hi" },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
118
|
+
|
|
119
|
+
assert.equal(host.streamingComponent, undefined, "assistant component should be deferred at message_start");
|
|
120
|
+
assert.equal(host.chatContainer.children.length, 0, "nothing should render before content arrives");
|
|
121
|
+
|
|
122
|
+
await handleAgentEvent(
|
|
123
|
+
host,
|
|
124
|
+
{
|
|
125
|
+
type: "message_update",
|
|
126
|
+
message: makeAssistant([toolCall]),
|
|
127
|
+
assistantMessageEvent: {
|
|
128
|
+
type: "toolcall_end",
|
|
129
|
+
contentIndex: 0,
|
|
130
|
+
toolCall: {
|
|
131
|
+
...toolCall,
|
|
132
|
+
externalResult: {
|
|
133
|
+
content: [{ type: "text", text: "tool output" }],
|
|
134
|
+
details: {},
|
|
135
|
+
isError: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
partial: makeAssistant([toolCall]),
|
|
139
|
+
},
|
|
140
|
+
} as any,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
assert.equal(host.streamingComponent, undefined, "assistant text container should remain deferred for tool-only updates");
|
|
144
|
+
assert.equal(host.chatContainer.children.length, 1, "tool execution block should render immediately");
|
|
145
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
146
|
+
|
|
147
|
+
// Re-assert required host method before the text-bearing update path.
|
|
148
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
149
|
+
|
|
150
|
+
await handleAgentEvent(
|
|
151
|
+
host,
|
|
152
|
+
{
|
|
153
|
+
type: "message_update",
|
|
154
|
+
message: makeAssistant([toolCall, { type: "text", text: "done" }]),
|
|
155
|
+
assistantMessageEvent: {
|
|
156
|
+
type: "text_delta",
|
|
157
|
+
contentIndex: 1,
|
|
158
|
+
delta: "done",
|
|
159
|
+
partial: makeAssistant([toolCall, { type: "text", text: "done" }]),
|
|
160
|
+
},
|
|
161
|
+
} as any,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
assert.equal(host.chatContainer.children.length, 2, "assistant content should render after existing tool output");
|
|
165
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
166
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("chat-controller keeps serverToolUse output ahead of assistant text when external results arrive", async () => {
|
|
170
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
171
|
+
fg: (_key: string, text: string) => text,
|
|
172
|
+
bg: (_key: string, text: string) => text,
|
|
173
|
+
bold: (text: string) => text,
|
|
174
|
+
italic: (text: string) => text,
|
|
175
|
+
truncate: (text: string) => text,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const host = createHost();
|
|
179
|
+
const toolId = "mcp-secure-1";
|
|
180
|
+
const serverToolUse = {
|
|
181
|
+
type: "serverToolUse",
|
|
182
|
+
id: toolId,
|
|
183
|
+
name: "mcp__gsd-workflow__secure_env_collect",
|
|
184
|
+
input: { projectDir: "/tmp/project", keys: [{ key: "SECURE_PASSWORD" }], destination: "dotenv" },
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
188
|
+
|
|
189
|
+
await handleAgentEvent(
|
|
190
|
+
host,
|
|
191
|
+
{
|
|
192
|
+
type: "message_update",
|
|
193
|
+
message: makeAssistant([serverToolUse]),
|
|
194
|
+
assistantMessageEvent: {
|
|
195
|
+
type: "server_tool_use",
|
|
196
|
+
contentIndex: 0,
|
|
197
|
+
partial: makeAssistant([serverToolUse]),
|
|
198
|
+
},
|
|
199
|
+
} as any,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
assert.equal(host.streamingComponent, undefined, "assistant content should stay deferred while only tool content streams");
|
|
203
|
+
assert.equal(host.chatContainer.children.length, 1, "server tool block should render immediately");
|
|
204
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
205
|
+
|
|
206
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
207
|
+
const resultMessage = makeAssistant([
|
|
208
|
+
{
|
|
209
|
+
...serverToolUse,
|
|
210
|
+
externalResult: {
|
|
211
|
+
content: [{ type: "text", text: "secure_env_collect was cancelled by user." }],
|
|
212
|
+
details: {},
|
|
213
|
+
isError: true,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{ type: "text", text: "The secure password collection was cancelled." },
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
await handleAgentEvent(
|
|
220
|
+
host,
|
|
221
|
+
{
|
|
222
|
+
type: "message_update",
|
|
223
|
+
message: resultMessage,
|
|
224
|
+
assistantMessageEvent: {
|
|
225
|
+
type: "server_tool_use",
|
|
226
|
+
contentIndex: 0,
|
|
227
|
+
partial: resultMessage,
|
|
228
|
+
},
|
|
229
|
+
} as any,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
assert.equal(host.chatContainer.children.length, 2, "assistant text should render after existing server tool output");
|
|
233
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
234
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("chat-controller pins latest assistant text above editor when tool calls are present", async () => {
|
|
238
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
239
|
+
fg: (_key: string, text: string) => text,
|
|
240
|
+
bg: (_key: string, text: string) => text,
|
|
241
|
+
bold: (text: string) => text,
|
|
242
|
+
italic: (text: string) => text,
|
|
243
|
+
truncate: (text: string) => text,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const host = createHost();
|
|
247
|
+
const toolId = "tool-pin-1";
|
|
248
|
+
const toolCall = {
|
|
249
|
+
type: "toolCall",
|
|
250
|
+
id: toolId,
|
|
251
|
+
name: "exec_command",
|
|
252
|
+
arguments: { cmd: "echo hi" },
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
256
|
+
|
|
257
|
+
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should be empty at message_start");
|
|
258
|
+
|
|
259
|
+
// Send a message with text followed by a tool call
|
|
260
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
261
|
+
await handleAgentEvent(
|
|
262
|
+
host,
|
|
263
|
+
{
|
|
264
|
+
type: "message_update",
|
|
265
|
+
message: makeAssistant([
|
|
266
|
+
{ type: "text", text: "Looking at the files now." },
|
|
267
|
+
toolCall,
|
|
268
|
+
]),
|
|
269
|
+
assistantMessageEvent: {
|
|
270
|
+
type: "toolcall_end",
|
|
271
|
+
contentIndex: 1,
|
|
272
|
+
toolCall: {
|
|
273
|
+
...toolCall,
|
|
274
|
+
externalResult: {
|
|
275
|
+
content: [{ type: "text", text: "file contents" }],
|
|
276
|
+
details: {},
|
|
277
|
+
isError: false,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
partial: makeAssistant([{ type: "text", text: "Looking at the files now." }, toolCall]),
|
|
281
|
+
},
|
|
282
|
+
} as any,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Pinned zone should now have a DynamicBorder and a Markdown component
|
|
286
|
+
assert.equal(host.pinnedMessageContainer.children.length, 2, "pinned zone should have border + markdown");
|
|
287
|
+
assert.equal(host.pinnedMessageContainer.children[0]?.constructor?.name, "DynamicBorder");
|
|
288
|
+
assert.equal(host.pinnedMessageContainer.children[1]?.constructor?.name, "Markdown");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("chat-controller clears pinned zone when a new assistant message starts", async () => {
|
|
292
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
293
|
+
fg: (_key: string, text: string) => text,
|
|
294
|
+
bg: (_key: string, text: string) => text,
|
|
295
|
+
bold: (text: string) => text,
|
|
296
|
+
italic: (text: string) => text,
|
|
297
|
+
truncate: (text: string) => text,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const host = createHost();
|
|
301
|
+
const toolCall = {
|
|
302
|
+
type: "toolCall",
|
|
303
|
+
id: "tool-clear-1",
|
|
304
|
+
name: "exec_command",
|
|
305
|
+
arguments: { cmd: "echo hi" },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
309
|
+
|
|
310
|
+
// Populate the pinned zone
|
|
311
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
312
|
+
await handleAgentEvent(
|
|
313
|
+
host,
|
|
314
|
+
{
|
|
315
|
+
type: "message_update",
|
|
316
|
+
message: makeAssistant([{ type: "text", text: "Working on it." }, toolCall]),
|
|
317
|
+
assistantMessageEvent: {
|
|
318
|
+
type: "toolcall_end",
|
|
319
|
+
contentIndex: 1,
|
|
320
|
+
toolCall: {
|
|
321
|
+
...toolCall,
|
|
322
|
+
externalResult: {
|
|
323
|
+
content: [{ type: "text", text: "ok" }],
|
|
324
|
+
details: {},
|
|
325
|
+
isError: false,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
partial: makeAssistant([{ type: "text", text: "Working on it." }, toolCall]),
|
|
329
|
+
},
|
|
330
|
+
} as any,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
assert.ok(host.pinnedMessageContainer.children.length > 0, "pinned zone should be populated");
|
|
334
|
+
|
|
335
|
+
// Start a new assistant message — pinned zone should clear
|
|
336
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
337
|
+
|
|
338
|
+
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should clear on new assistant message");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("chat-controller clears pinned zone when the agent turn ends", async () => {
|
|
342
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
343
|
+
fg: (_key: string, text: string) => text,
|
|
344
|
+
bg: (_key: string, text: string) => text,
|
|
345
|
+
bold: (text: string) => text,
|
|
346
|
+
italic: (text: string) => text,
|
|
347
|
+
truncate: (text: string) => text,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const host = createHost();
|
|
351
|
+
const toolCall = {
|
|
352
|
+
type: "toolCall",
|
|
353
|
+
id: "tool-clear-on-end-1",
|
|
354
|
+
name: "exec_command",
|
|
355
|
+
arguments: { cmd: "echo hi" },
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
359
|
+
|
|
360
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
361
|
+
await handleAgentEvent(
|
|
362
|
+
host,
|
|
363
|
+
{
|
|
364
|
+
type: "message_update",
|
|
365
|
+
message: makeAssistant([{ type: "text", text: "Working on it." }, toolCall]),
|
|
366
|
+
assistantMessageEvent: {
|
|
367
|
+
type: "toolcall_end",
|
|
368
|
+
contentIndex: 1,
|
|
369
|
+
toolCall: {
|
|
370
|
+
...toolCall,
|
|
371
|
+
externalResult: {
|
|
372
|
+
content: [{ type: "text", text: "ok" }],
|
|
373
|
+
details: {},
|
|
374
|
+
isError: false,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
partial: makeAssistant([{ type: "text", text: "Working on it." }, toolCall]),
|
|
378
|
+
},
|
|
379
|
+
} as any,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
assert.ok(host.pinnedMessageContainer.children.length > 0, "pinned zone should be populated before agent_end");
|
|
383
|
+
|
|
384
|
+
await handleAgentEvent(host, { type: "agent_end" } as any);
|
|
385
|
+
|
|
386
|
+
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should clear on agent_end");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("chat-controller does not pin when there are no tool calls", async () => {
|
|
390
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
391
|
+
fg: (_key: string, text: string) => text,
|
|
392
|
+
bg: (_key: string, text: string) => text,
|
|
393
|
+
bold: (text: string) => text,
|
|
394
|
+
italic: (text: string) => text,
|
|
395
|
+
truncate: (text: string) => text,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const host = createHost();
|
|
399
|
+
|
|
400
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
401
|
+
|
|
402
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
403
|
+
await handleAgentEvent(
|
|
404
|
+
host,
|
|
405
|
+
{
|
|
406
|
+
type: "message_update",
|
|
407
|
+
message: makeAssistant([{ type: "text", text: "Just some text, no tools." }]),
|
|
408
|
+
assistantMessageEvent: {
|
|
409
|
+
type: "text_delta",
|
|
410
|
+
contentIndex: 0,
|
|
411
|
+
delta: "Just some text, no tools.",
|
|
412
|
+
partial: makeAssistant([{ type: "text", text: "Just some text, no tools." }]),
|
|
413
|
+
},
|
|
414
|
+
} as any,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should stay empty without tool calls");
|
|
418
|
+
});
|
|
@@ -88,6 +88,8 @@ export interface ExtensionUIDialogOptions {
|
|
|
88
88
|
timeout?: number;
|
|
89
89
|
/** When true, the user can select multiple options. The return type becomes `string[]`. */
|
|
90
90
|
allowMultiple?: boolean;
|
|
91
|
+
/** When true, text input dialogs should hide typed characters if supported by the client surface. */
|
|
92
|
+
secure?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/** Placement for extension widgets. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { Component } from "@gsd/pi-tui";
|
|
1
|
+
import type { Component, TUI } from "@gsd/pi-tui";
|
|
2
|
+
import { visibleWidth } from "@gsd/pi-tui";
|
|
2
3
|
import { theme } from "../theme/theme.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Dynamic border component that adjusts to viewport width.
|
|
7
|
+
* Supports an optional animated spinner in the label area.
|
|
6
8
|
*
|
|
7
9
|
* Note: When used from extensions loaded via jiti, the global `theme` may be undefined
|
|
8
10
|
* because jiti creates a separate module cache. Always pass an explicit color
|
|
@@ -10,11 +12,51 @@ import { theme } from "../theme/theme.js";
|
|
|
10
12
|
*/
|
|
11
13
|
export class DynamicBorder implements Component {
|
|
12
14
|
private color: (str: string) => string;
|
|
15
|
+
private label?: string;
|
|
16
|
+
private spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
17
|
+
private spinnerIndex = 0;
|
|
18
|
+
private spinnerInterval: NodeJS.Timeout | null = null;
|
|
19
|
+
private spinnerColorFn?: (str: string) => string;
|
|
13
20
|
|
|
14
21
|
constructor(color: (str: string) => string = (str) => {
|
|
15
22
|
try { return theme.fg("border", str); } catch { return str; }
|
|
16
|
-
}) {
|
|
23
|
+
}, label?: string) {
|
|
17
24
|
this.color = color;
|
|
25
|
+
this.label = label;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setLabel(label: string | undefined): void {
|
|
29
|
+
this.label = label;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start an animated spinner that prepends to the label.
|
|
34
|
+
* The spinner rotates every 80ms and triggers a re-render via the TUI.
|
|
35
|
+
*/
|
|
36
|
+
startSpinner(ui: TUI, colorFn: (str: string) => string): void {
|
|
37
|
+
this.stopSpinner();
|
|
38
|
+
this.spinnerColorFn = colorFn;
|
|
39
|
+
this.spinnerIndex = 0;
|
|
40
|
+
this.spinnerInterval = setInterval(() => {
|
|
41
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
42
|
+
ui.requestRender();
|
|
43
|
+
}, 80);
|
|
44
|
+
ui.requestRender();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop the spinner animation. The border reverts to a static label.
|
|
49
|
+
*/
|
|
50
|
+
stopSpinner(): void {
|
|
51
|
+
if (this.spinnerInterval) {
|
|
52
|
+
clearInterval(this.spinnerInterval);
|
|
53
|
+
this.spinnerInterval = null;
|
|
54
|
+
}
|
|
55
|
+
this.spinnerColorFn = undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get isSpinning(): boolean {
|
|
59
|
+
return this.spinnerInterval !== null;
|
|
18
60
|
}
|
|
19
61
|
|
|
20
62
|
invalidate(): void {
|
|
@@ -22,6 +64,20 @@ export class DynamicBorder implements Component {
|
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
render(width: number): string[] {
|
|
67
|
+
const spinnerPrefix = this.spinnerInterval && this.spinnerColorFn
|
|
68
|
+
? this.spinnerColorFn(this.spinnerFrames[this.spinnerIndex]) + " "
|
|
69
|
+
: "";
|
|
70
|
+
|
|
71
|
+
if (this.label) {
|
|
72
|
+
const labelText = ` ${spinnerPrefix}${this.label} `;
|
|
73
|
+
const labelVisible = visibleWidth(labelText);
|
|
74
|
+
const leading = "── ";
|
|
75
|
+
const remaining = Math.max(0, width - labelVisible - leading.length);
|
|
76
|
+
const trailing = "─".repeat(Math.max(1, remaining));
|
|
77
|
+
// Color leading and trailing separately so embedded ANSI in the
|
|
78
|
+
// spinner/label doesn't bleed into the trailing dashes.
|
|
79
|
+
return [this.color(leading) + labelText + this.color(trailing)];
|
|
80
|
+
}
|
|
25
81
|
return [this.color("─".repeat(Math.max(1, width)))];
|
|
26
82
|
}
|
|
27
83
|
}
|
|
@@ -11,6 +11,7 @@ import { keyHint } from "./keybinding-hints.js";
|
|
|
11
11
|
export interface ExtensionInputOptions {
|
|
12
12
|
tui?: TUI;
|
|
13
13
|
timeout?: number;
|
|
14
|
+
secure?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export class ExtensionInputComponent extends Container implements Focusable {
|
|
@@ -61,6 +62,7 @@ export class ExtensionInputComponent extends Container implements Focusable {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
this.input = new Input();
|
|
65
|
+
this.input.secure = opts?.secure === true;
|
|
64
66
|
if (placeholder) {
|
|
65
67
|
this.input.placeholder = placeholder;
|
|
66
68
|
}
|