gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.6ddfa43
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 +9 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-start.js +20 -6
- package/dist/resources/extensions/gsd/auto.js +5 -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/crash-recovery.js +51 -0
- 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/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/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- 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 +10 -10
- 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/src/index.ts +4 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +175 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- 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/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +73 -12
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.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 +198 -8
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +92 -17
- 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-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-start.ts +27 -6
- package/src/resources/extensions/gsd/auto.ts +5 -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/crash-recovery.ts +59 -0
- 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/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/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/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/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/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → r6AvNu-aMwn4nwqjHqAfw}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → r6AvNu-aMwn4nwqjHqAfw}/_ssgManifest.js +0 -0
|
@@ -3,8 +3,15 @@ import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-t
|
|
|
3
3
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
4
|
import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
|
|
5
5
|
|
|
6
|
+
export interface ContentRange {
|
|
7
|
+
startIndex: number;
|
|
8
|
+
endIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
|
-
* Component that renders a complete assistant message
|
|
12
|
+
* Component that renders a complete assistant message, or a sub-range of its content[].
|
|
13
|
+
* When `range` is provided, only content[startIndex..endIndex] (inclusive) is rendered.
|
|
14
|
+
* Non-text/thinking blocks within the range are silently skipped.
|
|
8
15
|
*/
|
|
9
16
|
export class AssistantMessageComponent extends Container {
|
|
10
17
|
private contentContainer: Container;
|
|
@@ -12,18 +19,26 @@ export class AssistantMessageComponent extends Container {
|
|
|
12
19
|
private markdownTheme: MarkdownTheme;
|
|
13
20
|
private lastMessage?: AssistantMessage;
|
|
14
21
|
private timestampFormat: TimestampFormat;
|
|
22
|
+
private range?: ContentRange;
|
|
23
|
+
private showMetadata: boolean;
|
|
15
24
|
|
|
16
25
|
constructor(
|
|
17
26
|
message?: AssistantMessage,
|
|
18
27
|
hideThinkingBlock = false,
|
|
19
28
|
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
|
20
29
|
timestampFormat: TimestampFormat = "date-time-iso",
|
|
30
|
+
range?: ContentRange,
|
|
21
31
|
) {
|
|
22
32
|
super();
|
|
23
33
|
|
|
24
34
|
this.hideThinkingBlock = hideThinkingBlock;
|
|
25
35
|
this.markdownTheme = markdownTheme;
|
|
26
36
|
this.timestampFormat = timestampFormat;
|
|
37
|
+
this.range = range;
|
|
38
|
+
// No range = legacy full-message rendering; show metadata by default.
|
|
39
|
+
// Ranged (interleaved) instances start with metadata hidden; chat-controller
|
|
40
|
+
// calls setShowMetadata(true) on the last segment at message_end.
|
|
41
|
+
this.showMetadata = !range;
|
|
27
42
|
|
|
28
43
|
// Container for text/thinking content
|
|
29
44
|
this.contentContainer = new Container();
|
|
@@ -34,6 +49,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
setRange(range: ContentRange | undefined): void {
|
|
53
|
+
this.range = range;
|
|
54
|
+
if (this.lastMessage) {
|
|
55
|
+
this.updateContent(this.lastMessage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setShowMetadata(show: boolean): void {
|
|
60
|
+
this.showMetadata = show;
|
|
61
|
+
if (this.lastMessage) {
|
|
62
|
+
this.updateContent(this.lastMessage);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
override invalidate(): void {
|
|
38
67
|
super.invalidate();
|
|
39
68
|
if (this.lastMessage) {
|
|
@@ -51,7 +80,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
51
80
|
// Clear content container
|
|
52
81
|
this.contentContainer.clear();
|
|
53
82
|
|
|
54
|
-
const
|
|
83
|
+
const start = this.range?.startIndex ?? 0;
|
|
84
|
+
const end = this.range?.endIndex ?? message.content.length - 1;
|
|
85
|
+
const slice = message.content.slice(start, end + 1);
|
|
86
|
+
|
|
87
|
+
const hasVisibleContent = slice.some(
|
|
55
88
|
(c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
56
89
|
);
|
|
57
90
|
|
|
@@ -59,9 +92,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
59
92
|
this.contentContainer.addChild(new Spacer(1));
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
// Render content in order
|
|
63
|
-
for (let i = 0; i <
|
|
64
|
-
const content =
|
|
95
|
+
// Render content in order; non-text/thinking blocks are silently skipped
|
|
96
|
+
for (let i = 0; i < slice.length; i++) {
|
|
97
|
+
const content = slice[i];
|
|
65
98
|
if (content.type === "text" && content.text.trim()) {
|
|
66
99
|
// Assistant text messages with no background - trim the text
|
|
67
100
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
@@ -69,7 +102,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
69
102
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
70
103
|
// Add spacing only when another visible assistant content block follows.
|
|
71
104
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
72
|
-
const hasVisibleContentAfter =
|
|
105
|
+
const hasVisibleContentAfter = slice
|
|
73
106
|
.slice(i + 1)
|
|
74
107
|
.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
75
108
|
|
|
@@ -94,30 +127,33 @@ export class AssistantMessageComponent extends Container {
|
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
// Metadata (errors, timestamp): gated on showMetadata so ranged instances stay clean
|
|
131
|
+
// until chat-controller explicitly enables it on the last segment at message_end.
|
|
132
|
+
if (this.showMetadata) {
|
|
133
|
+
// Check if aborted - show after partial content
|
|
134
|
+
// But only if there are no tool calls (tool execution components will show the error)
|
|
135
|
+
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
|
|
136
|
+
if (!hasToolCalls) {
|
|
137
|
+
if (message.stopReason === "aborted") {
|
|
138
|
+
const abortMessage =
|
|
139
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
140
|
+
? message.errorMessage
|
|
141
|
+
: "Operation aborted";
|
|
142
|
+
if (hasVisibleContent) {
|
|
143
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
144
|
+
}
|
|
145
|
+
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
146
|
+
} else if (message.stopReason === "error") {
|
|
147
|
+
const errorMsg = message.errorMessage || "Unknown error";
|
|
107
148
|
this.contentContainer.addChild(new Spacer(1));
|
|
149
|
+
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
108
150
|
}
|
|
109
|
-
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
110
|
-
} else if (message.stopReason === "error") {
|
|
111
|
-
const errorMsg = message.errorMessage || "Unknown error";
|
|
112
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
113
|
-
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
114
151
|
}
|
|
115
|
-
}
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
if (message.stopReason && message.timestamp) {
|
|
154
|
+
const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
|
|
155
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
|
|
156
|
+
}
|
|
121
157
|
}
|
|
122
158
|
}
|
|
123
159
|
}
|
|
@@ -10,6 +10,13 @@ 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
|
+
// --- Segment walker state (per streaming assistant turn) ---
|
|
14
|
+
type RenderedSegment =
|
|
15
|
+
| { kind: "text-run"; startIndex: number; endIndex: number; component: AssistantMessageComponent }
|
|
16
|
+
| { kind: "tool"; contentIndex: number; component: ToolExecutionComponent };
|
|
17
|
+
|
|
18
|
+
let renderedSegments: RenderedSegment[] = [];
|
|
19
|
+
|
|
13
20
|
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
14
21
|
return message.content.some(
|
|
15
22
|
(c) =>
|
|
@@ -80,6 +87,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
80
87
|
lastProcessedContentIndex = 0;
|
|
81
88
|
lastPinnedText = "";
|
|
82
89
|
hasToolsInTurn = false;
|
|
90
|
+
renderedSegments = [];
|
|
83
91
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
84
92
|
pinnedBorder = undefined;
|
|
85
93
|
pinnedTextComponent = undefined;
|
|
@@ -99,6 +107,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
99
107
|
host.pinnedMessageContainer.clear();
|
|
100
108
|
lastPinnedText = "";
|
|
101
109
|
hasToolsInTurn = false;
|
|
110
|
+
renderedSegments = [];
|
|
102
111
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
103
112
|
pinnedBorder = undefined;
|
|
104
113
|
pinnedTextComponent = undefined;
|
|
@@ -273,24 +282,88 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
// Segment walker: render content blocks in stream order, append-only.
|
|
286
|
+
// Build desired segment plan from content[].
|
|
287
|
+
{
|
|
288
|
+
const blocks = host.streamingMessage.content;
|
|
289
|
+
type DesiredSegment =
|
|
290
|
+
| { kind: "text-run"; startIndex: number; endIndex: number }
|
|
291
|
+
| { kind: "tool"; contentIndex: number; toolId: string };
|
|
292
|
+
const desired: DesiredSegment[] = [];
|
|
293
|
+
let runStart = -1;
|
|
294
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
295
|
+
const b = blocks[i];
|
|
296
|
+
const isText = b.type === "text" || b.type === "thinking";
|
|
297
|
+
const isTool = b.type === "toolCall" || b.type === "serverToolUse";
|
|
298
|
+
if (isText) {
|
|
299
|
+
if (runStart === -1) runStart = i;
|
|
300
|
+
} else {
|
|
301
|
+
if (runStart !== -1) {
|
|
302
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: i - 1 });
|
|
303
|
+
runStart = -1;
|
|
304
|
+
}
|
|
305
|
+
if (isTool) {
|
|
306
|
+
desired.push({ kind: "tool", contentIndex: i, toolId: b.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (runStart !== -1) {
|
|
311
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: blocks.length - 1 });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Append any newly needed segments (never reorder existing ones).
|
|
315
|
+
for (const seg of desired) {
|
|
316
|
+
if (seg.kind === "tool") {
|
|
317
|
+
// Tool segments are already handled above via pendingTools; just
|
|
318
|
+
// register them in renderedSegments if not yet tracked.
|
|
319
|
+
const existing = renderedSegments.find(
|
|
320
|
+
(s) => s.kind === "tool" && s.contentIndex === seg.contentIndex,
|
|
321
|
+
);
|
|
322
|
+
if (!existing) {
|
|
323
|
+
const comp = host.pendingTools.get(seg.toolId);
|
|
324
|
+
if (comp) {
|
|
325
|
+
renderedSegments.push({ kind: "tool", contentIndex: seg.contentIndex, component: comp });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// text-run segment
|
|
330
|
+
const existing = renderedSegments.find(
|
|
331
|
+
(s) => s.kind === "text-run" && s.startIndex === seg.startIndex,
|
|
332
|
+
);
|
|
333
|
+
if (!existing) {
|
|
334
|
+
const comp = new AssistantMessageComponent(
|
|
335
|
+
undefined,
|
|
336
|
+
host.hideThinkingBlock,
|
|
337
|
+
host.getMarkdownThemeWithSettings(),
|
|
338
|
+
host.settingsManager.getTimestampFormat(),
|
|
339
|
+
{ startIndex: seg.startIndex, endIndex: seg.endIndex },
|
|
340
|
+
);
|
|
341
|
+
host.chatContainer.addChild(comp);
|
|
342
|
+
renderedSegments.push({ kind: "text-run", startIndex: seg.startIndex, endIndex: seg.endIndex, component: comp });
|
|
343
|
+
host.streamingComponent = comp;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Update all trailing text-run segments with the latest message so
|
|
349
|
+
// streaming text grows in place.
|
|
350
|
+
for (const seg of renderedSegments) {
|
|
351
|
+
if (seg.kind === "text-run") {
|
|
352
|
+
// Find corresponding desired segment to get current endIndex
|
|
353
|
+
const d = desired.find((ds) => ds.kind === "text-run" && ds.startIndex === seg.startIndex);
|
|
354
|
+
if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) {
|
|
355
|
+
seg.endIndex = d.endIndex;
|
|
356
|
+
seg.component.setRange({ startIndex: seg.startIndex, endIndex: seg.endIndex });
|
|
357
|
+
}
|
|
358
|
+
seg.component.updateContent(host.streamingMessage);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Keep streamingComponent pointing at the last text-run for message_end compatibility.
|
|
363
|
+
const lastTextSeg = [...renderedSegments].reverse().find((s) => s.kind === "text-run");
|
|
364
|
+
if (lastTextSeg && lastTextSeg.kind === "text-run") {
|
|
365
|
+
host.streamingComponent = lastTextSeg.component;
|
|
292
366
|
}
|
|
293
|
-
host.streamingComponent.updateContent(host.streamingMessage);
|
|
294
367
|
}
|
|
295
368
|
|
|
296
369
|
// Update index: fully processed blocks won't need re-scanning.
|
|
@@ -376,6 +449,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
376
449
|
host.chatContainer.addChild(host.streamingComponent);
|
|
377
450
|
}
|
|
378
451
|
if (host.streamingComponent) {
|
|
452
|
+
host.streamingComponent.setShowMetadata(true);
|
|
379
453
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
380
454
|
}
|
|
381
455
|
|
|
@@ -399,6 +473,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
399
473
|
}
|
|
400
474
|
host.streamingComponent = undefined;
|
|
401
475
|
host.streamingMessage = undefined;
|
|
476
|
+
renderedSegments = [];
|
|
402
477
|
// Clear pinned output once the message is finalized in the chat
|
|
403
478
|
// container — prevents duplicate display when the agent continues
|
|
404
479
|
// (e.g. form elicitation) after the assistant message ends.
|
package/pkg/package.json
CHANGED
|
@@ -14,10 +14,11 @@ import type {
|
|
|
14
14
|
Context,
|
|
15
15
|
Model,
|
|
16
16
|
SimpleStreamOptions,
|
|
17
|
+
ThinkingLevel,
|
|
17
18
|
ToolCall,
|
|
18
19
|
} from "@gsd/pi-ai";
|
|
19
20
|
import type { ExtensionUIContext } from "@gsd/pi-coding-agent";
|
|
20
|
-
import { EventStream } from "@gsd/pi-ai";
|
|
21
|
+
import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
|
|
21
22
|
import { execSync } from "node:child_process";
|
|
22
23
|
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
23
24
|
import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
|
|
@@ -600,8 +601,9 @@ export function buildSdkOptions(
|
|
|
600
601
|
modelId: string,
|
|
601
602
|
prompt: string,
|
|
602
603
|
overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" },
|
|
603
|
-
extraOptions: Record<string, unknown> = {},
|
|
604
|
+
extraOptions: Record<string, unknown> & { reasoning?: ThinkingLevel } = {},
|
|
604
605
|
): Record<string, unknown> {
|
|
606
|
+
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
605
607
|
const mcpServers = buildWorkflowMcpServers();
|
|
606
608
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
607
609
|
const disallowedTools = ["AskUserQuestion"];
|
|
@@ -620,6 +622,10 @@ export function buildSdkOptions(
|
|
|
620
622
|
"Bash(pwd)",
|
|
621
623
|
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
622
624
|
];
|
|
625
|
+
const effort =
|
|
626
|
+
reasoning && supportsAdaptiveThinking(modelId)
|
|
627
|
+
? mapThinkingLevelToEffort(reasoning, modelId)
|
|
628
|
+
: undefined;
|
|
623
629
|
return {
|
|
624
630
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
625
631
|
model: modelId,
|
|
@@ -634,7 +640,8 @@ export function buildSdkOptions(
|
|
|
634
640
|
...(allowedTools.length > 0 ? { allowedTools } : {}),
|
|
635
641
|
...(mcpServers ? { mcpServers } : {}),
|
|
636
642
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
637
|
-
...
|
|
643
|
+
...(effort ? { effort } : {}),
|
|
644
|
+
...sdkExtraOptions,
|
|
638
645
|
};
|
|
639
646
|
}
|
|
640
647
|
|
|
@@ -828,11 +835,12 @@ async function pumpSdkMessages(
|
|
|
828
835
|
{ permissionMode },
|
|
829
836
|
typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
|
|
830
837
|
? {
|
|
838
|
+
reasoning: options?.reasoning,
|
|
831
839
|
onElicitation: createClaudeCodeElicitationHandler(
|
|
832
840
|
(options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext,
|
|
833
841
|
),
|
|
834
842
|
}
|
|
835
|
-
: {},
|
|
843
|
+
: { reasoning: options?.reasoning },
|
|
836
844
|
);
|
|
837
845
|
|
|
838
846
|
const queryResult = sdk.query({
|
|
@@ -343,6 +343,26 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
343
343
|
);
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
test("buildSdkOptions maps reasoning to effort for adaptive Claude Code models (#3917)", () => {
|
|
347
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { reasoning: "high" });
|
|
348
|
+
assert.equal(options.effort, "high");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("buildSdkOptions upgrades xhigh reasoning to max for opus 4.6 (#3917)", () => {
|
|
352
|
+
const options = buildSdkOptions("claude-opus-4-6", "test", undefined, { reasoning: "xhigh" });
|
|
353
|
+
assert.equal(options.effort, "max");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("buildSdkOptions omits effort when reasoning is undefined (#3917)", () => {
|
|
357
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test");
|
|
358
|
+
assert.equal("effort" in options, false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("buildSdkOptions omits effort for non-adaptive Claude models (#3917)", () => {
|
|
362
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { reasoning: "high" });
|
|
363
|
+
assert.equal("effort" in options, false);
|
|
364
|
+
});
|
|
365
|
+
|
|
346
366
|
test("buildSdkOptions includes workflow MCP server config when env is set", () => {
|
|
347
367
|
const prev = {
|
|
348
368
|
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
@@ -774,11 +794,12 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
774
794
|
},
|
|
775
795
|
};
|
|
776
796
|
|
|
797
|
+
const secureValue = "ui-collected-value";
|
|
777
798
|
const inputCalls: Array<{ opts?: { secure?: boolean } }> = [];
|
|
778
799
|
const handler = createClaudeCodeElicitationHandler({
|
|
779
800
|
input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
|
|
780
801
|
inputCalls.push({ opts });
|
|
781
|
-
return
|
|
802
|
+
return secureValue;
|
|
782
803
|
},
|
|
783
804
|
} as any);
|
|
784
805
|
assert.ok(handler);
|
|
@@ -787,7 +808,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
787
808
|
assert.deepEqual(result, {
|
|
788
809
|
action: "accept",
|
|
789
810
|
content: {
|
|
790
|
-
TEST_SECURE_FIELD:
|
|
811
|
+
TEST_SECURE_FIELD: secureValue,
|
|
791
812
|
},
|
|
792
813
|
});
|
|
793
814
|
assert.equal(inputCalls.length, 1);
|
|
@@ -15,6 +15,7 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
|
|
|
15
15
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
16
16
|
import { unitPhaseLabel } from "./auto-dashboard.js";
|
|
17
17
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
18
|
+
import { logWarning } from "./workflow-logger.js";
|
|
18
19
|
|
|
19
20
|
export interface ModelSelectionResult {
|
|
20
21
|
/** Routing metadata for metrics recording */
|
|
@@ -25,9 +26,7 @@ export interface ModelSelectionResult {
|
|
|
25
26
|
|
|
26
27
|
export function resolvePreferredModelConfig(
|
|
27
28
|
unitType: string,
|
|
28
|
-
autoModeStartModel: { provider: string; id: string } | null,
|
|
29
|
-
/** When false, only return explicit per-phase model configs — do not
|
|
30
|
-
* synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
|
|
29
|
+
autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
|
|
31
30
|
isAutoMode = true,
|
|
32
31
|
) {
|
|
33
32
|
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
@@ -41,7 +40,7 @@ export function resolvePreferredModelConfig(
|
|
|
41
40
|
if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
|
|
42
41
|
|
|
43
42
|
// Don't synthesize a routing config for flat-rate providers (#3453).
|
|
44
|
-
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider)) return undefined;
|
|
43
|
+
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx)) return undefined;
|
|
45
44
|
|
|
46
45
|
const ceilingModel = routingConfig.tier_models.heavy
|
|
47
46
|
?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
|
|
@@ -68,7 +67,7 @@ export async function selectAndApplyModel(
|
|
|
68
67
|
basePath: string,
|
|
69
68
|
prefs: GSDPreferences | undefined,
|
|
70
69
|
verbose: boolean,
|
|
71
|
-
autoModeStartModel: { provider: string; id: string } | null,
|
|
70
|
+
autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
|
|
72
71
|
retryContext?: { isRetry: boolean; previousTier?: string },
|
|
73
72
|
/** When false (interactive/guided-flow), skip dynamic routing and use the session model.
|
|
74
73
|
* Dynamic routing only applies in auto-mode where cost optimization is expected. (#3962) */
|
|
@@ -79,6 +78,17 @@ export async function selectAndApplyModel(
|
|
|
79
78
|
const effectiveSessionModelOverride = sessionModelOverride === undefined
|
|
80
79
|
? getSessionModelOverride(ctx.sessionManager.getSessionId())
|
|
81
80
|
: (sessionModelOverride ?? undefined);
|
|
81
|
+
// Enrich the start model with a flat-rate context up front so routing
|
|
82
|
+
// synthesis and the dispatch-time guard see the same signals (built-in
|
|
83
|
+
// list + user `flat_rate_providers` preference + externalCli auto-
|
|
84
|
+
// detection). The dispatch-time primary-model check below builds its
|
|
85
|
+
// own per-provider context when it has a resolved primary model.
|
|
86
|
+
if (autoModeStartModel) {
|
|
87
|
+
autoModeStartModel = {
|
|
88
|
+
...autoModeStartModel,
|
|
89
|
+
flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
82
92
|
const modelConfig = effectiveSessionModelOverride
|
|
83
93
|
? undefined
|
|
84
94
|
: resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
|
|
@@ -107,12 +117,16 @@ export async function selectAndApplyModel(
|
|
|
107
117
|
if (routingConfig.enabled) {
|
|
108
118
|
const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
|
|
109
119
|
if (primaryModel) {
|
|
110
|
-
|
|
120
|
+
const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
|
|
121
|
+
if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
|
|
111
122
|
routingConfig.enabled = false;
|
|
112
123
|
}
|
|
113
124
|
} else if (
|
|
114
|
-
(autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
115
|
-
|| (ctx.model?.provider && isFlatRateProvider(
|
|
125
|
+
(autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
126
|
+
|| (ctx.model?.provider && isFlatRateProvider(
|
|
127
|
+
ctx.model.provider,
|
|
128
|
+
buildFlatRateContext(ctx.model.provider, ctx, prefs),
|
|
129
|
+
))
|
|
116
130
|
) {
|
|
117
131
|
// Primary model unresolvable but provider signals indicate flat-rate —
|
|
118
132
|
// disable routing to prevent quality degradation.
|
|
@@ -416,8 +430,68 @@ export function resolveModelId<T extends { id: string; provider: string }>(
|
|
|
416
430
|
* Uses case-insensitive matching with alias support to prevent fail-open on
|
|
417
431
|
* provider naming variations (e.g. "copilot" vs "github-copilot").
|
|
418
432
|
*/
|
|
419
|
-
const
|
|
433
|
+
const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Optional context that lets callers extend flat-rate detection beyond the
|
|
437
|
+
* hard-coded built-in list. Either signal on its own is enough to classify
|
|
438
|
+
* a provider as flat-rate.
|
|
439
|
+
*/
|
|
440
|
+
export interface FlatRateContext {
|
|
441
|
+
/**
|
|
442
|
+
* Auth mode for the specific provider being checked, as returned by
|
|
443
|
+
* `ctx.modelRegistry.getProviderAuthMode(provider)`. Any provider that
|
|
444
|
+
* wraps a local CLI (externalCli) is, by definition, a flat-rate
|
|
445
|
+
* subscription wrapper — every request costs the same regardless of
|
|
446
|
+
* model, so dynamic routing only degrades quality.
|
|
447
|
+
*/
|
|
448
|
+
authMode?: "apiKey" | "oauth" | "externalCli" | "none";
|
|
449
|
+
/**
|
|
450
|
+
* Case-insensitive list of extra provider IDs the user has declared as
|
|
451
|
+
* flat-rate via `preferences.flat_rate_providers`. Used for private
|
|
452
|
+
* subscription-backed proxies and enterprise-gated deployments that the
|
|
453
|
+
* built-in list doesn't know about.
|
|
454
|
+
*/
|
|
455
|
+
userFlatRate?: readonly string[];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function isFlatRateProvider(provider: string, opts?: FlatRateContext): boolean {
|
|
459
|
+
const p = provider.toLowerCase();
|
|
460
|
+
if (BUILTIN_FLAT_RATE.has(p)) return true;
|
|
461
|
+
if (opts?.userFlatRate?.some(id => id.toLowerCase() === p)) return true;
|
|
462
|
+
if (opts?.authMode === "externalCli") return true;
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
420
465
|
|
|
421
|
-
|
|
422
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Build a FlatRateContext for a given provider from live runtime state.
|
|
468
|
+
* Safe to call when ctx or prefs are undefined — missing pieces are
|
|
469
|
+
* treated as "no signal".
|
|
470
|
+
*/
|
|
471
|
+
export function buildFlatRateContext(
|
|
472
|
+
provider: string,
|
|
473
|
+
ctx?: { modelRegistry?: { getProviderAuthMode?: (p: string) => string } },
|
|
474
|
+
prefs?: { flat_rate_providers?: readonly string[] },
|
|
475
|
+
): FlatRateContext {
|
|
476
|
+
let authMode: FlatRateContext["authMode"];
|
|
477
|
+
const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
|
|
478
|
+
if (typeof getAuthMode === "function") {
|
|
479
|
+
try {
|
|
480
|
+
const mode = getAuthMode(provider);
|
|
481
|
+
if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
|
|
482
|
+
authMode = mode;
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
// Registry lookup failure must never break flat-rate detection —
|
|
486
|
+
// fall through with authMode undefined and surface the cause.
|
|
487
|
+
logWarning(
|
|
488
|
+
"dispatch",
|
|
489
|
+
`flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
authMode,
|
|
495
|
+
userFlatRate: prefs?.flat_rate_providers,
|
|
496
|
+
};
|
|
423
497
|
}
|
|
@@ -83,7 +83,11 @@ import { join } from "node:path";
|
|
|
83
83
|
import { sep as pathSep } from "node:path";
|
|
84
84
|
|
|
85
85
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
86
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
isCustomProvider,
|
|
88
|
+
resolveDefaultSessionModel,
|
|
89
|
+
resolveDynamicRoutingConfig,
|
|
90
|
+
} from "./preferences-models.js";
|
|
87
91
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
88
92
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
89
93
|
|
|
@@ -274,8 +278,18 @@ export async function bootstrapAutoSession(
|
|
|
274
278
|
//
|
|
275
279
|
// This preserves #3517 defaults while honoring explicit runtime model
|
|
276
280
|
// selection for subsequent /gsd runs in the same session.
|
|
281
|
+
//
|
|
282
|
+
// Exception (#4122): when the session provider is a custom provider declared
|
|
283
|
+
// in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
|
|
284
|
+
// PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
|
|
285
|
+
// providers, so honoring it would silently reroute auto-mode to a built-in
|
|
286
|
+
// provider the user is not logged into and surface as "Not logged in · Please
|
|
287
|
+
// run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
|
|
277
288
|
const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
|
|
278
|
-
const
|
|
289
|
+
const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
|
|
290
|
+
const preferredModel = sessionProviderIsCustom
|
|
291
|
+
? null
|
|
292
|
+
: resolveDefaultSessionModel(ctx.model?.provider);
|
|
279
293
|
// Validate the preferred model against the live registry + provider auth so
|
|
280
294
|
// an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
|
|
281
295
|
// start-model snapshot. Without this, every subsequent unit would try to
|
|
@@ -811,12 +825,19 @@ export async function bootstrapAutoSession(
|
|
|
811
825
|
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
812
826
|
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
813
827
|
|
|
814
|
-
// Flat-rate providers (e.g. GitHub Copilot, claude-code
|
|
815
|
-
//
|
|
816
|
-
|
|
828
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
|
|
829
|
+
// subscription proxies, externalCli CLIs) suppress routing at dispatch
|
|
830
|
+
// time (#3453) — reflect that in the banner. Thread the same
|
|
831
|
+
// FlatRateContext used by selectAndApplyModel so user-declared
|
|
832
|
+
// flat-rate providers and externalCli auto-detection are respected.
|
|
833
|
+
const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
|
|
834
|
+
const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
817
835
|
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
818
836
|
const effectivelyEnabled = routingConfig.enabled
|
|
819
|
-
&& !(effectiveProvider && isFlatRateProvider(
|
|
837
|
+
&& !(effectiveProvider && isFlatRateProvider(
|
|
838
|
+
effectiveProvider,
|
|
839
|
+
buildFlatRateContext(effectiveProvider, ctx, bannerPrefs),
|
|
840
|
+
));
|
|
820
841
|
|
|
821
842
|
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
822
843
|
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
readCrashLock,
|
|
53
53
|
isLockProcessAlive,
|
|
54
54
|
formatCrashInfo,
|
|
55
|
+
emitCrashRecoveredUnitEnd,
|
|
55
56
|
} from "./crash-recovery.js";
|
|
56
57
|
import {
|
|
57
58
|
acquireSessionLock,
|
|
@@ -1332,6 +1333,10 @@ export async function startAuto(
|
|
|
1332
1333
|
}
|
|
1333
1334
|
|
|
1334
1335
|
if (freshStartAssessment.lock) {
|
|
1336
|
+
// Emit a synthetic unit-end for any unit-start that has no closing event.
|
|
1337
|
+
// This closes the journal gap reported in #3348 where the worker wrote side
|
|
1338
|
+
// effects (SUMMARY.md, DB updates) but died before emitting unit-end.
|
|
1339
|
+
emitCrashRecoveredUnitEnd(base, freshStartAssessment.lock);
|
|
1335
1340
|
clearLock(base);
|
|
1336
1341
|
}
|
|
1337
1342
|
|