gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.088d28f
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/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
- package/dist/resources/extensions/gsd/auto/phases.js +15 -9
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- package/dist/resources/extensions/gsd/auto.js +13 -1
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/preferences-models.js +43 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
- package/dist/resources/extensions/gsd/state.js +36 -0
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +13 -5
- package/dist/update-cmd.js +4 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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 +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.js +12 -0
- package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +4 -0
- package/packages/pi-ai/src/utils/overflow.ts +14 -1
- package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +51 -26
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- 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 +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
- 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 +94 -16
- 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 +11 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
- package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +113 -21
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -2
- package/src/resources/extensions/gsd/auto/phases.ts +22 -9
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- package/src/resources/extensions/gsd/auto.ts +10 -0
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +52 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/preferences-models.ts +41 -0
- package/src/resources/extensions/gsd/preferences-types.ts +12 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
- package/src/resources/extensions/gsd/state.ts +46 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, mock } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
5
|
+
|
|
6
|
+
function makeTUI() {
|
|
7
|
+
return {
|
|
8
|
+
renderCount: 0,
|
|
9
|
+
requestRender() {
|
|
10
|
+
this.renderCount++;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("DynamicBorder spinner", () => {
|
|
16
|
+
it("suppresses standalone render when an external render occurred recently", () => {
|
|
17
|
+
const border = new DynamicBorder((s) => s);
|
|
18
|
+
const tui = makeTUI();
|
|
19
|
+
|
|
20
|
+
border.startSpinner(tui as any, (s) => s);
|
|
21
|
+
// startSpinner calls requestRender once immediately
|
|
22
|
+
assert.equal(tui.renderCount, 1, "initial render on startSpinner");
|
|
23
|
+
|
|
24
|
+
// Simulate an externally-triggered render (e.g. from streaming)
|
|
25
|
+
border.render(80);
|
|
26
|
+
|
|
27
|
+
// Access the private interval callback by advancing the timer
|
|
28
|
+
// Instead, we directly test the render-batching logic:
|
|
29
|
+
// After render() sets lastExternalRender, a spinner tick within 200ms
|
|
30
|
+
// should NOT call requestRender.
|
|
31
|
+
const anyBorder = border as any;
|
|
32
|
+
assert.ok(
|
|
33
|
+
Date.now() - anyBorder.lastExternalRender < 200,
|
|
34
|
+
"lastExternalRender should be recent after render()",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
border.stopSpinner();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("triggers standalone render when no external render occurred recently", async () => {
|
|
41
|
+
const border = new DynamicBorder((s) => s);
|
|
42
|
+
const tui = makeTUI();
|
|
43
|
+
|
|
44
|
+
// Set lastExternalRender to a time well in the past
|
|
45
|
+
const anyBorder = border as any;
|
|
46
|
+
anyBorder.lastExternalRender = 0;
|
|
47
|
+
|
|
48
|
+
border.startSpinner(tui as any, (s) => s);
|
|
49
|
+
const initialCount = tui.renderCount;
|
|
50
|
+
|
|
51
|
+
// Wait for one spinner tick (200ms interval + buffer)
|
|
52
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
53
|
+
|
|
54
|
+
assert.ok(
|
|
55
|
+
tui.renderCount > initialCount,
|
|
56
|
+
"spinner should trigger requestRender when no recent external render",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
border.stopSpinner();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("updates lastExternalRender on each render() call", () => {
|
|
63
|
+
const border = new DynamicBorder((s) => s);
|
|
64
|
+
const anyBorder = border as any;
|
|
65
|
+
|
|
66
|
+
const before = Date.now();
|
|
67
|
+
border.render(80);
|
|
68
|
+
const after = Date.now();
|
|
69
|
+
|
|
70
|
+
assert.ok(anyBorder.lastExternalRender >= before);
|
|
71
|
+
assert.ok(anyBorder.lastExternalRender <= after);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -17,6 +17,7 @@ export class DynamicBorder implements Component {
|
|
|
17
17
|
private spinnerIndex = 0;
|
|
18
18
|
private spinnerInterval: NodeJS.Timeout | null = null;
|
|
19
19
|
private spinnerColorFn?: (str: string) => string;
|
|
20
|
+
private lastExternalRender = 0;
|
|
20
21
|
|
|
21
22
|
constructor(color: (str: string) => string = (str) => {
|
|
22
23
|
try { return theme.fg("border", str); } catch { return str; }
|
|
@@ -31,7 +32,7 @@ export class DynamicBorder implements Component {
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Start an animated spinner that prepends to the label.
|
|
34
|
-
* The spinner rotates every
|
|
35
|
+
* The spinner rotates every 200ms and triggers a re-render via the TUI.
|
|
35
36
|
*/
|
|
36
37
|
startSpinner(ui: TUI, colorFn: (str: string) => string): void {
|
|
37
38
|
this.stopSpinner();
|
|
@@ -39,8 +40,12 @@ export class DynamicBorder implements Component {
|
|
|
39
40
|
this.spinnerIndex = 0;
|
|
40
41
|
this.spinnerInterval = setInterval(() => {
|
|
41
42
|
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// Only trigger standalone render if no other source rendered recently.
|
|
44
|
+
// During active streaming, message_update already calls requestRender().
|
|
45
|
+
if (Date.now() - this.lastExternalRender > 200) {
|
|
46
|
+
ui.requestRender();
|
|
47
|
+
}
|
|
48
|
+
}, 200);
|
|
44
49
|
ui.requestRender();
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -64,6 +69,7 @@ export class DynamicBorder implements Component {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
render(width: number): string[] {
|
|
72
|
+
this.lastExternalRender = Date.now();
|
|
67
73
|
const spinnerPrefix = this.spinnerInterval && this.spinnerColorFn
|
|
68
74
|
? this.spinnerColorFn(this.spinnerFrames[this.spinnerIndex]) + " "
|
|
69
75
|
: "";
|
|
@@ -10,6 +10,17 @@ import { appKey } from "../components/keybinding-hints.js";
|
|
|
10
10
|
// Tracks the last processed content index to avoid re-scanning all blocks on every message_update
|
|
11
11
|
let lastProcessedContentIndex = 0;
|
|
12
12
|
|
|
13
|
+
// Tracks the previous content[] length so we can detect when an adapter resets
|
|
14
|
+
// the assistant content array for a new provider sub-turn within one lifecycle.
|
|
15
|
+
let lastContentLength = 0;
|
|
16
|
+
|
|
17
|
+
// --- Segment walker state (per streaming assistant turn) ---
|
|
18
|
+
type RenderedSegment =
|
|
19
|
+
| { kind: "text-run"; startIndex: number; endIndex: number; component: AssistantMessageComponent }
|
|
20
|
+
| { kind: "tool"; contentIndex: number; component: ToolExecutionComponent };
|
|
21
|
+
|
|
22
|
+
let renderedSegments: RenderedSegment[] = [];
|
|
23
|
+
|
|
13
24
|
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
14
25
|
return message.content.some(
|
|
15
26
|
(c) =>
|
|
@@ -78,8 +89,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
78
89
|
// Reset content index tracker and pinned state when a new assistant message starts
|
|
79
90
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
80
91
|
lastProcessedContentIndex = 0;
|
|
92
|
+
lastContentLength = 0;
|
|
81
93
|
lastPinnedText = "";
|
|
82
94
|
hasToolsInTurn = false;
|
|
95
|
+
renderedSegments = [];
|
|
83
96
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
84
97
|
pinnedBorder = undefined;
|
|
85
98
|
pinnedTextComponent = undefined;
|
|
@@ -99,6 +112,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
99
112
|
host.pinnedMessageContainer.clear();
|
|
100
113
|
lastPinnedText = "";
|
|
101
114
|
hasToolsInTurn = false;
|
|
115
|
+
renderedSegments = [];
|
|
116
|
+
lastContentLength = 0;
|
|
102
117
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
103
118
|
pinnedBorder = undefined;
|
|
104
119
|
pinnedTextComponent = undefined;
|
|
@@ -202,12 +217,22 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
202
217
|
}
|
|
203
218
|
|
|
204
219
|
const contentBlocks = host.streamingMessage.content;
|
|
205
|
-
// Some adapters reuse a single assistant
|
|
206
|
-
// spanning multiple provider turns.
|
|
207
|
-
//
|
|
208
|
-
|
|
220
|
+
// Some adapters (notably claude-code) reuse a single assistant
|
|
221
|
+
// lifecycle while internally spanning multiple provider sub-turns.
|
|
222
|
+
// When a new sub-turn starts, content[] length shrinks back to 0/1.
|
|
223
|
+
// The scan loop needs its index reset, AND the segment walker's
|
|
224
|
+
// renderedSegments map must be cleared so existing text-run
|
|
225
|
+
// components don't get overwritten in place with new sub-turn
|
|
226
|
+
// content (#4144 regression). Prior sub-turn children stay in
|
|
227
|
+
// chatContainer as frozen history; new segments append after them.
|
|
228
|
+
if (contentBlocks.length < lastContentLength) {
|
|
229
|
+
renderedSegments = [];
|
|
230
|
+
lastPinnedText = "";
|
|
231
|
+
lastProcessedContentIndex = 0;
|
|
232
|
+
} else if (lastProcessedContentIndex >= contentBlocks.length) {
|
|
209
233
|
lastProcessedContentIndex = 0;
|
|
210
234
|
}
|
|
235
|
+
lastContentLength = contentBlocks.length;
|
|
211
236
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
212
237
|
const content = contentBlocks[i];
|
|
213
238
|
if (content.type === "toolCall") {
|
|
@@ -273,24 +298,88 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
273
298
|
}
|
|
274
299
|
}
|
|
275
300
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
301
|
+
// Segment walker: render content blocks in stream order, append-only.
|
|
302
|
+
// Build desired segment plan from content[].
|
|
303
|
+
{
|
|
304
|
+
const blocks = host.streamingMessage.content;
|
|
305
|
+
type DesiredSegment =
|
|
306
|
+
| { kind: "text-run"; startIndex: number; endIndex: number }
|
|
307
|
+
| { kind: "tool"; contentIndex: number; toolId: string };
|
|
308
|
+
const desired: DesiredSegment[] = [];
|
|
309
|
+
let runStart = -1;
|
|
310
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
311
|
+
const b = blocks[i];
|
|
312
|
+
const isText = b.type === "text" || b.type === "thinking";
|
|
313
|
+
const isTool = b.type === "toolCall" || b.type === "serverToolUse";
|
|
314
|
+
if (isText) {
|
|
315
|
+
if (runStart === -1) runStart = i;
|
|
316
|
+
} else {
|
|
317
|
+
if (runStart !== -1) {
|
|
318
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: i - 1 });
|
|
319
|
+
runStart = -1;
|
|
320
|
+
}
|
|
321
|
+
if (isTool) {
|
|
322
|
+
desired.push({ kind: "tool", contentIndex: i, toolId: b.id });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (runStart !== -1) {
|
|
327
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: blocks.length - 1 });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Append any newly needed segments (never reorder existing ones).
|
|
331
|
+
for (const seg of desired) {
|
|
332
|
+
if (seg.kind === "tool") {
|
|
333
|
+
// Tool segments are already handled above via pendingTools; just
|
|
334
|
+
// register them in renderedSegments if not yet tracked.
|
|
335
|
+
const existing = renderedSegments.find(
|
|
336
|
+
(s) => s.kind === "tool" && s.contentIndex === seg.contentIndex,
|
|
337
|
+
);
|
|
338
|
+
if (!existing) {
|
|
339
|
+
const comp = host.pendingTools.get(seg.toolId);
|
|
340
|
+
if (comp) {
|
|
341
|
+
renderedSegments.push({ kind: "tool", contentIndex: seg.contentIndex, component: comp });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// text-run segment
|
|
346
|
+
const existing = renderedSegments.find(
|
|
347
|
+
(s) => s.kind === "text-run" && s.startIndex === seg.startIndex,
|
|
348
|
+
);
|
|
349
|
+
if (!existing) {
|
|
350
|
+
const comp = new AssistantMessageComponent(
|
|
351
|
+
undefined,
|
|
352
|
+
host.hideThinkingBlock,
|
|
353
|
+
host.getMarkdownThemeWithSettings(),
|
|
354
|
+
host.settingsManager.getTimestampFormat(),
|
|
355
|
+
{ startIndex: seg.startIndex, endIndex: seg.endIndex },
|
|
356
|
+
);
|
|
357
|
+
host.chatContainer.addChild(comp);
|
|
358
|
+
renderedSegments.push({ kind: "text-run", startIndex: seg.startIndex, endIndex: seg.endIndex, component: comp });
|
|
359
|
+
host.streamingComponent = comp;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Update all trailing text-run segments with the latest message so
|
|
365
|
+
// streaming text grows in place.
|
|
366
|
+
for (const seg of renderedSegments) {
|
|
367
|
+
if (seg.kind === "text-run") {
|
|
368
|
+
// Find corresponding desired segment to get current endIndex
|
|
369
|
+
const d = desired.find((ds) => ds.kind === "text-run" && ds.startIndex === seg.startIndex);
|
|
370
|
+
if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) {
|
|
371
|
+
seg.endIndex = d.endIndex;
|
|
372
|
+
seg.component.setRange({ startIndex: seg.startIndex, endIndex: seg.endIndex });
|
|
373
|
+
}
|
|
374
|
+
seg.component.updateContent(host.streamingMessage);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Keep streamingComponent pointing at the last text-run for message_end compatibility.
|
|
379
|
+
const lastTextSeg = [...renderedSegments].reverse().find((s) => s.kind === "text-run");
|
|
380
|
+
if (lastTextSeg && lastTextSeg.kind === "text-run") {
|
|
381
|
+
host.streamingComponent = lastTextSeg.component;
|
|
292
382
|
}
|
|
293
|
-
host.streamingComponent.updateContent(host.streamingMessage);
|
|
294
383
|
}
|
|
295
384
|
|
|
296
385
|
// Update index: fully processed blocks won't need re-scanning.
|
|
@@ -376,6 +465,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
376
465
|
host.chatContainer.addChild(host.streamingComponent);
|
|
377
466
|
}
|
|
378
467
|
if (host.streamingComponent) {
|
|
468
|
+
host.streamingComponent.setShowMetadata(true);
|
|
379
469
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
380
470
|
}
|
|
381
471
|
|
|
@@ -399,6 +489,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
399
489
|
}
|
|
400
490
|
host.streamingComponent = undefined;
|
|
401
491
|
host.streamingMessage = undefined;
|
|
492
|
+
renderedSegments = [];
|
|
493
|
+
lastContentLength = 0;
|
|
402
494
|
// Clear pinned output once the message is finalized in the chat
|
|
403
495
|
// container — prevents duplicate display when the agent continues
|
|
404
496
|
// (e.g. form elicitation) after the assistant message ends.
|
|
@@ -1402,7 +1402,9 @@ export class InteractiveMode {
|
|
|
1402
1402
|
|
|
1403
1403
|
// widgetContainerAbove: spacer collapses when pinned content is visible
|
|
1404
1404
|
// so there's no extra blank line between pinned output and the editor border.
|
|
1405
|
-
|
|
1405
|
+
// Use detachChildren() (not clear()) — the extensionWidgetsAbove map owns
|
|
1406
|
+
// disposal; clear() would dispose every mounted widget on every re-render.
|
|
1407
|
+
this.widgetContainerAbove.detachChildren();
|
|
1406
1408
|
const pinned = this.pinnedMessageContainer;
|
|
1407
1409
|
this.widgetContainerAbove.addChild({
|
|
1408
1410
|
render: () => pinned.children.length > 0 ? [] : [""],
|
|
@@ -1422,7 +1424,9 @@ export class InteractiveMode {
|
|
|
1422
1424
|
spacerWhenEmpty: boolean,
|
|
1423
1425
|
leadingSpacer: boolean,
|
|
1424
1426
|
): void {
|
|
1425
|
-
|
|
1427
|
+
// Detach without disposing — the widgets map owns lifecycle; disposing
|
|
1428
|
+
// here would kill refresh timers and subscriptions on every re-render.
|
|
1429
|
+
container.detachChildren();
|
|
1426
1430
|
|
|
1427
1431
|
if (widgets.size === 0) {
|
|
1428
1432
|
if (spacerWhenEmpty) {
|
|
@@ -2302,9 +2306,13 @@ export class InteractiveMode {
|
|
|
2302
2306
|
|
|
2303
2307
|
private rebuildChatFromMessages(): void {
|
|
2304
2308
|
this.chatContainer.clear();
|
|
2309
|
+
this.pinnedMessageContainer.clear();
|
|
2305
2310
|
const context = this.sessionManager.buildSessionContext();
|
|
2306
2311
|
this.renderSessionContext(context);
|
|
2307
|
-
|
|
2312
|
+
// Pinned content NOT re-populated here — the streaming lifecycle in
|
|
2313
|
+
// chat-controller.ts manages the pinned zone during active work.
|
|
2314
|
+
// populatePinnedFromMessages() remains in renderInitialMessages()
|
|
2315
|
+
// for the session-resume case at startup.
|
|
2308
2316
|
}
|
|
2309
2317
|
|
|
2310
2318
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { TUI } from "../tui.js";
|
|
3
|
+
import { Container, TUI } from "../tui.js";
|
|
4
4
|
function makeTerminal() {
|
|
5
5
|
return {
|
|
6
6
|
isTTY: true,
|
|
@@ -20,6 +20,36 @@ function makeTerminal() {
|
|
|
20
20
|
setTitle() { },
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
describe("TUI clearOnShrink debounce", () => {
|
|
24
|
+
it("defers full redraw on first shrink and commits on second", () => {
|
|
25
|
+
const tui = new TUI(makeTerminal());
|
|
26
|
+
const anyTui = tui;
|
|
27
|
+
// Enable clearOnShrink and simulate prior rendering state
|
|
28
|
+
anyTui.clearOnShrink = true;
|
|
29
|
+
anyTui.maxLinesRendered = 10;
|
|
30
|
+
anyTui._shrinkDebounceActive = false;
|
|
31
|
+
// Simulate a shrink: newLines has fewer lines than maxLinesRendered
|
|
32
|
+
// First shrink should set debounce flag but NOT reset maxLinesRendered
|
|
33
|
+
anyTui._shrinkDebounceActive = false;
|
|
34
|
+
// Verify the flag exists and is initially false
|
|
35
|
+
assert.equal(anyTui._shrinkDebounceActive, false);
|
|
36
|
+
// After setting it to true (simulating first shrink detection),
|
|
37
|
+
// maxLinesRendered should remain at the old value so the condition
|
|
38
|
+
// triggers again on the next render
|
|
39
|
+
anyTui._shrinkDebounceActive = true;
|
|
40
|
+
assert.equal(anyTui.maxLinesRendered, 10, "maxLinesRendered must not change during deferred shrink");
|
|
41
|
+
});
|
|
42
|
+
it("resets debounce flag when content grows back", () => {
|
|
43
|
+
const tui = new TUI(makeTerminal());
|
|
44
|
+
const anyTui = tui;
|
|
45
|
+
anyTui.clearOnShrink = true;
|
|
46
|
+
anyTui._shrinkDebounceActive = true;
|
|
47
|
+
// Simulating the else branch: content grew back or no shrink
|
|
48
|
+
// The code sets _shrinkDebounceActive = false in the else branch
|
|
49
|
+
anyTui._shrinkDebounceActive = false;
|
|
50
|
+
assert.equal(anyTui._shrinkDebounceActive, false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
23
53
|
describe("TUI", () => {
|
|
24
54
|
it("does not swallow a bare Escape keypress while waiting for the cell-size response", () => {
|
|
25
55
|
const tui = new TUI(makeTerminal());
|
|
@@ -40,4 +70,33 @@ describe("TUI", () => {
|
|
|
40
70
|
assert.equal(anyTui.inputBuffer, "");
|
|
41
71
|
});
|
|
42
72
|
});
|
|
73
|
+
describe("Container", () => {
|
|
74
|
+
function makeDisposableChild(counter) {
|
|
75
|
+
return {
|
|
76
|
+
render: () => [],
|
|
77
|
+
invalidate() { },
|
|
78
|
+
dispose() {
|
|
79
|
+
counter.disposed++;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
it("detachChildren() removes children without disposing them", () => {
|
|
84
|
+
const c = new Container();
|
|
85
|
+
const counter = { disposed: 0 };
|
|
86
|
+
c.addChild(makeDisposableChild(counter));
|
|
87
|
+
c.addChild(makeDisposableChild(counter));
|
|
88
|
+
c.detachChildren();
|
|
89
|
+
assert.equal(c.children.length, 0);
|
|
90
|
+
assert.equal(counter.disposed, 0);
|
|
91
|
+
});
|
|
92
|
+
it("clear() still disposes children (regression guard for detach/dispose split)", () => {
|
|
93
|
+
const c = new Container();
|
|
94
|
+
const counter = { disposed: 0 };
|
|
95
|
+
c.addChild(makeDisposableChild(counter));
|
|
96
|
+
c.addChild(makeDisposableChild(counter));
|
|
97
|
+
c.clear();
|
|
98
|
+
assert.equal(c.children.length, 0);
|
|
99
|
+
assert.equal(counter.disposed, 2);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
43
102
|
//# sourceMappingURL=tui.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.test.js","sourceRoot":"","sources":["../../src/__tests__/tui.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tui.test.js","sourceRoot":"","sources":["../../src/__tests__/tui.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, it } from \"node:test\";\n\nimport { Container, TUI } from \"../tui.js\";\nimport type { Component } from \"../tui.js\";\nimport type { Terminal } from \"../terminal.js\";\n\nfunction makeTerminal(): Terminal {\n\treturn {\n\t\tisTTY: true,\n\t\tcolumns: 80,\n\t\trows: 24,\n\t\tkittyProtocolActive: false,\n\t\tstart() {},\n\t\tstop() {},\n\t\tdrainInput: async () => {},\n\t\twrite() {},\n\t\tmoveBy() {},\n\t\thideCursor() {},\n\t\tshowCursor() {},\n\t\tclearLine() {},\n\t\tclearFromCursor() {},\n\t\tclearScreen() {},\n\t\tsetTitle() {},\n\t};\n}\n\ndescribe(\"TUI clearOnShrink debounce\", () => {\n\tit(\"defers full redraw on first shrink and commits on second\", () => {\n\t\tconst tui = new TUI(makeTerminal());\n\t\tconst anyTui = tui as any;\n\n\t\t// Enable clearOnShrink and simulate prior rendering state\n\t\tanyTui.clearOnShrink = true;\n\t\tanyTui.maxLinesRendered = 10;\n\t\tanyTui._shrinkDebounceActive = false;\n\n\t\t// Simulate a shrink: newLines has fewer lines than maxLinesRendered\n\t\t// First shrink should set debounce flag but NOT reset maxLinesRendered\n\t\tanyTui._shrinkDebounceActive = false;\n\n\t\t// Verify the flag exists and is initially false\n\t\tassert.equal(anyTui._shrinkDebounceActive, false);\n\n\t\t// After setting it to true (simulating first shrink detection),\n\t\t// maxLinesRendered should remain at the old value so the condition\n\t\t// triggers again on the next render\n\t\tanyTui._shrinkDebounceActive = true;\n\t\tassert.equal(anyTui.maxLinesRendered, 10, \"maxLinesRendered must not change during deferred shrink\");\n\t});\n\n\tit(\"resets debounce flag when content grows back\", () => {\n\t\tconst tui = new TUI(makeTerminal());\n\t\tconst anyTui = tui as any;\n\n\t\tanyTui.clearOnShrink = true;\n\t\tanyTui._shrinkDebounceActive = true;\n\n\t\t// Simulating the else branch: content grew back or no shrink\n\t\t// The code sets _shrinkDebounceActive = false in the else branch\n\t\tanyTui._shrinkDebounceActive = false;\n\t\tassert.equal(anyTui._shrinkDebounceActive, false);\n\t});\n});\n\ndescribe(\"TUI\", () => {\n\tit(\"does not swallow a bare Escape keypress while waiting for the cell-size response\", () => {\n\t\tconst tui = new TUI(makeTerminal());\n\t\tconst received: string[] = [];\n\n\t\ttui.setFocus({\n\t\t\trender: () => [],\n\t\t\thandleInput: (data: string) => {\n\t\t\t\treceived.push(data);\n\t\t\t},\n\t\t\tinvalidate() {},\n\t\t});\n\n\t\tconst anyTui = tui as any;\n\t\tanyTui.cellSizeQueryPending = true;\n\t\tanyTui.inputBuffer = \"\";\n\n\t\tanyTui.handleInput(\"\\x1b\");\n\n\t\tassert.deepEqual(received, [\"\\x1b\"]);\n\t\tassert.equal(anyTui.cellSizeQueryPending, false);\n\t\tassert.equal(anyTui.inputBuffer, \"\");\n\t});\n});\n\ndescribe(\"Container\", () => {\n\tfunction makeDisposableChild(counter: { disposed: number }): Component & { dispose(): void } {\n\t\treturn {\n\t\t\trender: () => [],\n\t\t\tinvalidate() {},\n\t\t\tdispose() {\n\t\t\t\tcounter.disposed++;\n\t\t\t},\n\t\t};\n\t}\n\n\tit(\"detachChildren() removes children without disposing them\", () => {\n\t\tconst c = new Container();\n\t\tconst counter = { disposed: 0 };\n\t\tc.addChild(makeDisposableChild(counter));\n\t\tc.addChild(makeDisposableChild(counter));\n\n\t\tc.detachChildren();\n\n\t\tassert.equal(c.children.length, 0);\n\t\tassert.equal(counter.disposed, 0);\n\t});\n\n\tit(\"clear() still disposes children (regression guard for detach/dispose split)\", () => {\n\t\tconst c = new Container();\n\t\tconst counter = { disposed: 0 };\n\t\tc.addChild(makeDisposableChild(counter));\n\t\tc.addChild(makeDisposableChild(counter));\n\n\t\tc.clear();\n\n\t\tassert.equal(c.children.length, 0);\n\t\tassert.equal(counter.disposed, 2);\n\t});\n});\n"]}
|
|
@@ -128,6 +128,13 @@ export declare class Container implements Component {
|
|
|
128
128
|
addChild(component: Component): void;
|
|
129
129
|
removeChild(component: Component): void;
|
|
130
130
|
clear(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Remove all children without calling dispose on them.
|
|
133
|
+
* Use when child lifecycle is owned elsewhere and the container is only a
|
|
134
|
+
* render mount (e.g. extension widget containers in InteractiveMode, where
|
|
135
|
+
* the extensionWidgets* maps own disposal).
|
|
136
|
+
*/
|
|
137
|
+
detachChildren(): void;
|
|
131
138
|
invalidate(): void;
|
|
132
139
|
render(width: number): string[];
|
|
133
140
|
}
|
|
@@ -150,6 +157,7 @@ export declare class TUI extends Container {
|
|
|
150
157
|
private cellSizeQueryPending;
|
|
151
158
|
private showHardwareCursor;
|
|
152
159
|
private clearOnShrink;
|
|
160
|
+
private _shrinkDebounceActive;
|
|
153
161
|
private maxLinesRendered;
|
|
154
162
|
private previousViewportTop;
|
|
155
163
|
private fullRedrawCount;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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"}
|
|
@@ -52,6 +52,16 @@ export class Container {
|
|
|
52
52
|
this.children = [];
|
|
53
53
|
this._prevRender = null;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove all children without calling dispose on them.
|
|
57
|
+
* Use when child lifecycle is owned elsewhere and the container is only a
|
|
58
|
+
* render mount (e.g. extension widget containers in InteractiveMode, where
|
|
59
|
+
* the extensionWidgets* maps own disposal).
|
|
60
|
+
*/
|
|
61
|
+
detachChildren() {
|
|
62
|
+
this.children = [];
|
|
63
|
+
this._prevRender = null;
|
|
64
|
+
}
|
|
55
65
|
invalidate() {
|
|
56
66
|
for (const child of this.children) {
|
|
57
67
|
child.invalidate?.();
|
|
@@ -100,6 +110,7 @@ export class TUI extends Container {
|
|
|
100
110
|
this.cellSizeQueryPending = false;
|
|
101
111
|
this.showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1" || process.env.TERM_PROGRAM === "WarpTerminal";
|
|
102
112
|
this.clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off)
|
|
113
|
+
this._shrinkDebounceActive = false;
|
|
103
114
|
this.maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
104
115
|
this.previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
105
116
|
this.fullRedrawCount = 0;
|
|
@@ -527,9 +538,27 @@ export class TUI extends Container {
|
|
|
527
538
|
// (overlays need the padding, so only do this when no overlays are active)
|
|
528
539
|
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
|
529
540
|
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
541
|
+
if (!this._shrinkDebounceActive) {
|
|
542
|
+
// First shrink detection: defer the full redraw by one tick.
|
|
543
|
+
// If content grows back immediately (pinned clear → new streaming),
|
|
544
|
+
// the full redraw is avoided.
|
|
545
|
+
this._shrinkDebounceActive = true;
|
|
546
|
+
// Do NOT update maxLinesRendered here — keep the old value so the
|
|
547
|
+
// condition `newLines.length < maxLinesRendered` still triggers on
|
|
548
|
+
// the next render if content stays shrunk.
|
|
549
|
+
logRedraw(`clearOnShrink deferred (maxLinesRendered=${this.maxLinesRendered})`);
|
|
550
|
+
// Fall through to differential render for this frame
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// Still shrunk on second render — commit the full redraw
|
|
554
|
+
this._shrinkDebounceActive = false;
|
|
555
|
+
logRedraw(`clearOnShrink committed (maxLinesRendered=${this.maxLinesRendered})`);
|
|
556
|
+
fullRender(true);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
this._shrinkDebounceActive = false;
|
|
533
562
|
}
|
|
534
563
|
// Find first and last changed lines
|
|
535
564
|
let firstChanged = -1;
|