@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.0
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/CHANGELOG.md +115 -7
- package/dist/cli.js +5996 -5945
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/models-config-schema.ts +13 -0
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/ssh.ts +8 -1
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -11,6 +11,7 @@ export const CATCHUP_FRAMES = 8;
|
|
|
11
11
|
type AssistantContentBlock = AssistantMessage["content"][number];
|
|
12
12
|
type DisplayThinkingContentBlock = Extract<AssistantContentBlock, { type: "thinking" }> & { rawThinking?: string };
|
|
13
13
|
type StreamingRevealComponent = Pick<AssistantMessageComponent, "updateContent">;
|
|
14
|
+
type GraphemeSlicer = (index: number, text: string, units: number) => string;
|
|
14
15
|
|
|
15
16
|
type StreamingRevealControllerOptions = {
|
|
16
17
|
getSmoothStreaming(): boolean;
|
|
@@ -44,12 +45,29 @@ function countGraphemesFrom(text: string, start: number): { count: number; tailS
|
|
|
44
45
|
}
|
|
45
46
|
return { count, tailStart };
|
|
46
47
|
}
|
|
48
|
+
/** Segment `text` from code-unit offset `start`, walking up to `clusters`
|
|
49
|
+
* graphemes. Returns the code-unit END of the final cluster walked, its START
|
|
50
|
+
* (`lastStart`), and how many clusters were found (`count` may be less than
|
|
51
|
+
* `clusters` if the suffix is shorter than requested). */
|
|
52
|
+
function segmentFrom(text: string, start: number, clusters: number): { end: number; lastStart: number; count: number } {
|
|
53
|
+
let count = 0;
|
|
54
|
+
let lastStart = start;
|
|
55
|
+
let end = start;
|
|
56
|
+
for (const seg of getSegmenter().segment(start === 0 ? text : text.slice(start))) {
|
|
57
|
+
count += 1;
|
|
58
|
+
lastStart = start + seg.index;
|
|
59
|
+
end = start + seg.index + seg.segment.length;
|
|
60
|
+
if (count >= clusters) break;
|
|
61
|
+
}
|
|
62
|
+
return { end, lastStart, count };
|
|
63
|
+
}
|
|
47
64
|
|
|
48
65
|
/** Memoizes per-block grapheme counts across reveal ticks. Streaming blocks only
|
|
49
66
|
* grow by appending, and an append can only alter the final grapheme cluster of
|
|
50
67
|
* the previous text, so only the suffix from that cluster needs re-segmenting. */
|
|
51
|
-
class BlockUnitCounter {
|
|
68
|
+
export class BlockUnitCounter {
|
|
52
69
|
#entries = new Map<number, { text: string; count: number; tailStart: number }>();
|
|
70
|
+
#sliceEntries = new Map<number, { text: string; units: number; end: number; lastStart: number }>();
|
|
53
71
|
|
|
54
72
|
count(index: number, text: string): number {
|
|
55
73
|
const entry = this.#entries.get(index);
|
|
@@ -69,6 +87,29 @@ class BlockUnitCounter {
|
|
|
69
87
|
|
|
70
88
|
reset(): void {
|
|
71
89
|
this.#entries.clear();
|
|
90
|
+
this.#sliceEntries.clear();
|
|
91
|
+
}
|
|
92
|
+
/** Slice `text` to its first `units` graphemes. Memoized across reveal ticks:
|
|
93
|
+
* streaming blocks grow only by appending and the reveal target advances
|
|
94
|
+
* monotonically, so a previously sliced prefix is reused and only the suffix
|
|
95
|
+
* from the boundary cluster is re-segmented. Only an exact (text, units) hit
|
|
96
|
+
* skips segmentation entirely — an append can extend the boundary cluster, so
|
|
97
|
+
* the incremental path still re-segments from that cluster's start. */
|
|
98
|
+
slice(index: number, text: string, units: number): string {
|
|
99
|
+
if (units <= 0 || text.length === 0) return "";
|
|
100
|
+
const entry = this.#sliceEntries.get(index);
|
|
101
|
+
if (entry !== undefined && entry.text === text && entry.units === units) {
|
|
102
|
+
return entry.end >= text.length ? text : text.slice(0, entry.end);
|
|
103
|
+
}
|
|
104
|
+
if (entry !== undefined && (entry.text === text || text.startsWith(entry.text)) && units >= entry.units) {
|
|
105
|
+
const extra = units - entry.units + 1;
|
|
106
|
+
const seg = segmentFrom(text, entry.lastStart, extra);
|
|
107
|
+
this.#sliceEntries.set(index, { text, units, end: seg.end, lastStart: seg.lastStart });
|
|
108
|
+
return seg.end >= text.length ? text : text.slice(0, seg.end);
|
|
109
|
+
}
|
|
110
|
+
const seg = segmentFrom(text, 0, units);
|
|
111
|
+
this.#sliceEntries.set(index, { text, units, end: seg.end, lastStart: seg.lastStart });
|
|
112
|
+
return seg.end >= text.length ? text : text.slice(0, seg.end);
|
|
72
113
|
}
|
|
73
114
|
}
|
|
74
115
|
|
|
@@ -104,20 +145,24 @@ function revealTextBlock(
|
|
|
104
145
|
block: Extract<AssistantContentBlock, { type: "text" }>,
|
|
105
146
|
remaining: number,
|
|
106
147
|
units: number,
|
|
148
|
+
index: number,
|
|
149
|
+
sliceOf: GraphemeSlicer,
|
|
107
150
|
): AssistantContentBlock {
|
|
108
151
|
if (remaining <= 0) return block.text.length === 0 ? block : { ...block, text: "" };
|
|
109
152
|
if (remaining >= units) return block;
|
|
110
|
-
return { ...block, text:
|
|
153
|
+
return { ...block, text: sliceOf(index, block.text, remaining) };
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
function revealThinkingBlock(
|
|
114
157
|
block: Extract<AssistantContentBlock, { type: "thinking" }>,
|
|
115
158
|
remaining: number,
|
|
116
159
|
units: number,
|
|
160
|
+
index: number,
|
|
161
|
+
sliceOf: GraphemeSlicer,
|
|
117
162
|
): AssistantContentBlock {
|
|
118
163
|
if (remaining <= 0) return block.thinking.length === 0 ? block : { ...block, thinking: "" };
|
|
119
164
|
if (remaining >= units) return block;
|
|
120
|
-
return { ...block, thinking:
|
|
165
|
+
return { ...block, thinking: sliceOf(index, block.thinking, remaining) };
|
|
121
166
|
}
|
|
122
167
|
|
|
123
168
|
export function buildDisplayMessage(
|
|
@@ -126,6 +171,7 @@ export function buildDisplayMessage(
|
|
|
126
171
|
hideThinking: boolean,
|
|
127
172
|
proseOnly = true,
|
|
128
173
|
countOf: (index: number, text: string) => number = (_index, text) => countGraphemes(text),
|
|
174
|
+
sliceOf: GraphemeSlicer = (_index, text, units) => sliceGraphemes(text, units),
|
|
129
175
|
): AssistantMessage {
|
|
130
176
|
let remaining = Math.max(0, Math.floor(revealed));
|
|
131
177
|
const content: AssistantContentBlock[] = [];
|
|
@@ -133,7 +179,7 @@ export function buildDisplayMessage(
|
|
|
133
179
|
const block = target.content[i]!;
|
|
134
180
|
if (block.type === "text") {
|
|
135
181
|
const units = countOf(i, block.text);
|
|
136
|
-
content.push(revealTextBlock(block, remaining, units));
|
|
182
|
+
content.push(revealTextBlock(block, remaining, units, i, sliceOf));
|
|
137
183
|
remaining = Math.max(0, remaining - units);
|
|
138
184
|
} else if (block.type === "thinking" && !hideThinking) {
|
|
139
185
|
const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
|
|
@@ -144,7 +190,7 @@ export function buildDisplayMessage(
|
|
|
144
190
|
thinking: formatted,
|
|
145
191
|
rawThinking: block.thinking,
|
|
146
192
|
};
|
|
147
|
-
content.push(revealThinkingBlock(displayBlock, remaining, units));
|
|
193
|
+
content.push(revealThinkingBlock(displayBlock, remaining, units, i, sliceOf));
|
|
148
194
|
remaining = Math.max(0, remaining - units);
|
|
149
195
|
} else {
|
|
150
196
|
content.push(block);
|
|
@@ -174,6 +220,8 @@ export class StreamingRevealController {
|
|
|
174
220
|
#smoothStreaming = true;
|
|
175
221
|
readonly #unitCounter = new BlockUnitCounter();
|
|
176
222
|
readonly #countOf = (index: number, text: string): number => this.#unitCounter.count(index, text);
|
|
223
|
+
readonly #sliceOf = (index: number, text: string, units: number): string =>
|
|
224
|
+
this.#unitCounter.slice(index, text, units);
|
|
177
225
|
|
|
178
226
|
constructor(options: StreamingRevealControllerOptions) {
|
|
179
227
|
this.#getSmoothStreaming = options.getSmoothStreaming;
|
|
@@ -181,6 +229,16 @@ export class StreamingRevealController {
|
|
|
181
229
|
this.#getProseOnlyThinking = options.getProseOnlyThinking;
|
|
182
230
|
this.#requestRender = options.requestRender;
|
|
183
231
|
}
|
|
232
|
+
#build(target: AssistantMessage, revealed: number): AssistantMessage {
|
|
233
|
+
return buildDisplayMessage(
|
|
234
|
+
target,
|
|
235
|
+
revealed,
|
|
236
|
+
this.#hideThinkingBlock,
|
|
237
|
+
this.#proseOnlyThinking,
|
|
238
|
+
this.#countOf,
|
|
239
|
+
this.#sliceOf,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
184
242
|
|
|
185
243
|
begin(component: StreamingRevealComponent, message: AssistantMessage): void {
|
|
186
244
|
this.stop();
|
|
@@ -192,10 +250,7 @@ export class StreamingRevealController {
|
|
|
192
250
|
this.#smoothStreaming = this.#getSmoothStreaming();
|
|
193
251
|
if (!this.#smoothStreaming) {
|
|
194
252
|
const total = this.#visibleUnits(message);
|
|
195
|
-
component.updateContent(
|
|
196
|
-
buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
|
|
197
|
-
{ transient: true },
|
|
198
|
-
);
|
|
253
|
+
component.updateContent(this.#build(message, total), { transient: true });
|
|
199
254
|
return;
|
|
200
255
|
}
|
|
201
256
|
const total = this.#visibleUnits(message);
|
|
@@ -203,18 +258,9 @@ export class StreamingRevealController {
|
|
|
203
258
|
// A tool call is a transcript-order boundary: finish any leading
|
|
204
259
|
// assistant text before EventController renders the separate tool card.
|
|
205
260
|
this.#revealed = total;
|
|
206
|
-
component.updateContent(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.#revealed,
|
|
210
|
-
this.#hideThinkingBlock,
|
|
211
|
-
this.#proseOnlyThinking,
|
|
212
|
-
this.#countOf,
|
|
213
|
-
),
|
|
214
|
-
{
|
|
215
|
-
transient: true,
|
|
216
|
-
},
|
|
217
|
-
);
|
|
261
|
+
component.updateContent(this.#build(message, this.#revealed), {
|
|
262
|
+
transient: true,
|
|
263
|
+
});
|
|
218
264
|
return;
|
|
219
265
|
}
|
|
220
266
|
this.#renderCurrent();
|
|
@@ -229,10 +275,7 @@ export class StreamingRevealController {
|
|
|
229
275
|
if (!this.#component) return;
|
|
230
276
|
if (!this.#smoothStreaming) {
|
|
231
277
|
const total = this.#visibleUnits(message);
|
|
232
|
-
this.#component.updateContent(
|
|
233
|
-
buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
|
|
234
|
-
{ transient: true },
|
|
235
|
-
);
|
|
278
|
+
this.#component.updateContent(this.#build(message, total), { transient: true });
|
|
236
279
|
return;
|
|
237
280
|
}
|
|
238
281
|
const total = this.#visibleUnits(message);
|
|
@@ -241,18 +284,9 @@ export class StreamingRevealController {
|
|
|
241
284
|
// assistant text before EventController renders the separate tool card.
|
|
242
285
|
this.#revealed = total;
|
|
243
286
|
this.#stopTimer();
|
|
244
|
-
this.#component.updateContent(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.#revealed,
|
|
248
|
-
this.#hideThinkingBlock,
|
|
249
|
-
this.#proseOnlyThinking,
|
|
250
|
-
this.#countOf,
|
|
251
|
-
),
|
|
252
|
-
{
|
|
253
|
-
transient: true,
|
|
254
|
-
},
|
|
255
|
-
);
|
|
287
|
+
this.#component.updateContent(this.#build(message, this.#revealed), {
|
|
288
|
+
transient: true,
|
|
289
|
+
});
|
|
256
290
|
return;
|
|
257
291
|
}
|
|
258
292
|
if (this.#revealed > total) {
|
|
@@ -309,16 +343,7 @@ export class StreamingRevealController {
|
|
|
309
343
|
// Every controller render is an in-flight streaming snapshot, even when
|
|
310
344
|
// smooth reveal has temporarily caught up to the current target. The
|
|
311
345
|
// message_end handler performs the only stable non-transient render.
|
|
312
|
-
this.#component.updateContent(
|
|
313
|
-
buildDisplayMessage(
|
|
314
|
-
this.#target,
|
|
315
|
-
this.#revealed,
|
|
316
|
-
this.#hideThinkingBlock,
|
|
317
|
-
this.#proseOnlyThinking,
|
|
318
|
-
this.#countOf,
|
|
319
|
-
),
|
|
320
|
-
{ transient: true },
|
|
321
|
-
);
|
|
346
|
+
this.#component.updateContent(this.#build(this.#target, this.#revealed), { transient: true });
|
|
322
347
|
}
|
|
323
348
|
|
|
324
349
|
#syncTimer(total = this.#target ? this.#visibleUnits(this.#target) : 0): void {
|
|
@@ -356,12 +381,9 @@ export class StreamingRevealController {
|
|
|
356
381
|
return;
|
|
357
382
|
}
|
|
358
383
|
this.#revealed = Math.min(total, this.#revealed + nextStep(total - this.#revealed));
|
|
359
|
-
component.updateContent(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
transient: true,
|
|
363
|
-
},
|
|
364
|
-
);
|
|
384
|
+
component.updateContent(this.#build(target, this.#revealed), {
|
|
385
|
+
transient: true,
|
|
386
|
+
});
|
|
365
387
|
this.#requestRender();
|
|
366
388
|
if (this.#revealed >= total) {
|
|
367
389
|
this.#stopTimer();
|
|
@@ -6,11 +6,289 @@ type ToolArgsRevealComponent = {
|
|
|
6
6
|
updateArgs(args: unknown, toolCallId?: string): void;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// Top-level string args a renderer reads mid-stream. The streamed-args decode
|
|
10
|
+
// reads these fields incrementally between throttled full-JSON parses so a
|
|
11
|
+
// long payload updates preview args at reveal cadence instead of stalling for
|
|
12
|
+
// STREAMING_JSON_PARSE_MIN_GROWTH bytes at a time. Nested-array modes (edit
|
|
13
|
+
// patch/replace `edits[].diff`) still fall through to the throttled parse.
|
|
14
|
+
const STREAMING_STRING_KEYS_BY_TOOL: Record<string, readonly string[]> = {
|
|
15
|
+
write: ["content"],
|
|
16
|
+
edit: ["input"],
|
|
17
|
+
eval: ["code"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** String fields the streamed-args decode reads incrementally for `toolName`. */
|
|
21
|
+
export function streamingStringKeysForTool(toolName: string, rawInput: boolean): readonly string[] | undefined {
|
|
22
|
+
if (rawInput) return undefined;
|
|
23
|
+
return STREAMING_STRING_KEYS_BY_TOOL[toolName];
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
type ToolArgsRevealControllerOptions = {
|
|
10
27
|
getSmoothStreaming(): boolean;
|
|
11
28
|
requestRender(): void;
|
|
12
29
|
};
|
|
13
30
|
|
|
31
|
+
type StreamingJsonStringExtractorResult = {
|
|
32
|
+
values: Record<string, string>;
|
|
33
|
+
changed: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function decodeJsonStringEscape(ch: string): string {
|
|
37
|
+
switch (ch) {
|
|
38
|
+
case '"':
|
|
39
|
+
case "\\":
|
|
40
|
+
case "/":
|
|
41
|
+
return ch;
|
|
42
|
+
case "b":
|
|
43
|
+
return "\b";
|
|
44
|
+
case "f":
|
|
45
|
+
return "\f";
|
|
46
|
+
case "n":
|
|
47
|
+
return "\n";
|
|
48
|
+
case "r":
|
|
49
|
+
return "\r";
|
|
50
|
+
case "t":
|
|
51
|
+
return "\t";
|
|
52
|
+
default:
|
|
53
|
+
return ch;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isHexDigit(ch: string): boolean {
|
|
58
|
+
return (ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "f") || (ch >= "A" && ch <= "F");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type StreamingJsonStringExtractorState = "scan" | "candidate" | "afterCandidate" | "beforeValue" | "target";
|
|
62
|
+
|
|
63
|
+
class StreamingJsonStringExtractor {
|
|
64
|
+
readonly #keys: Set<string>;
|
|
65
|
+
#source = "";
|
|
66
|
+
#offset = 0;
|
|
67
|
+
/** `{`/`[` nesting outside strings. Candidate keys match only at depth 1 —
|
|
68
|
+
* the top level of the args object — so a nested object's key (e.g.
|
|
69
|
+
* `{"meta":{"content":…}}`) is never captured as a streamed top-level arg. */
|
|
70
|
+
#depth = 0;
|
|
71
|
+
#state: StreamingJsonStringExtractorState = "scan";
|
|
72
|
+
#candidate = "";
|
|
73
|
+
#candidateEscaped = false;
|
|
74
|
+
#candidateUnicode = "";
|
|
75
|
+
#matchedKey: string | undefined;
|
|
76
|
+
#targetKey: string | undefined;
|
|
77
|
+
#targetEscaped = false;
|
|
78
|
+
#targetUnicode = "";
|
|
79
|
+
#values: Record<string, string> = {};
|
|
80
|
+
#changed = false;
|
|
81
|
+
|
|
82
|
+
constructor(keys: readonly string[]) {
|
|
83
|
+
this.#keys = new Set(keys);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
reset(): void {
|
|
87
|
+
this.#source = "";
|
|
88
|
+
this.#offset = 0;
|
|
89
|
+
this.#depth = 0;
|
|
90
|
+
this.#state = "scan";
|
|
91
|
+
this.#candidate = "";
|
|
92
|
+
this.#candidateEscaped = false;
|
|
93
|
+
this.#candidateUnicode = "";
|
|
94
|
+
this.#matchedKey = undefined;
|
|
95
|
+
this.#targetKey = undefined;
|
|
96
|
+
this.#targetEscaped = false;
|
|
97
|
+
this.#targetUnicode = "";
|
|
98
|
+
this.#values = {};
|
|
99
|
+
this.#changed = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
update(prefix: string): StreamingJsonStringExtractorResult {
|
|
103
|
+
if (!prefix.startsWith(this.#source)) {
|
|
104
|
+
this.reset();
|
|
105
|
+
}
|
|
106
|
+
this.#source = prefix;
|
|
107
|
+
this.#changed = false;
|
|
108
|
+
while (this.#offset < prefix.length) {
|
|
109
|
+
const ch = prefix[this.#offset]!;
|
|
110
|
+
switch (this.#state) {
|
|
111
|
+
case "scan":
|
|
112
|
+
this.#scan(ch);
|
|
113
|
+
break;
|
|
114
|
+
case "candidate":
|
|
115
|
+
this.#readCandidate(ch);
|
|
116
|
+
break;
|
|
117
|
+
case "afterCandidate":
|
|
118
|
+
this.#afterCandidate(ch);
|
|
119
|
+
break;
|
|
120
|
+
case "beforeValue":
|
|
121
|
+
this.#beforeValue(ch);
|
|
122
|
+
break;
|
|
123
|
+
case "target":
|
|
124
|
+
this.#readTarget(ch);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { values: { ...this.#values }, changed: this.#changed };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#scan(ch: string): void {
|
|
132
|
+
if (ch === '"') {
|
|
133
|
+
this.#candidate = "";
|
|
134
|
+
this.#candidateEscaped = false;
|
|
135
|
+
this.#candidateUnicode = "";
|
|
136
|
+
this.#state = "candidate";
|
|
137
|
+
} else if (ch === "{" || ch === "[") {
|
|
138
|
+
this.#depth++;
|
|
139
|
+
} else if (ch === "}" || ch === "]") {
|
|
140
|
+
this.#depth--;
|
|
141
|
+
}
|
|
142
|
+
this.#offset++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#readCandidate(ch: string): void {
|
|
146
|
+
if (this.#candidateUnicode) {
|
|
147
|
+
this.#readCandidateUnicode(ch);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (this.#candidateEscaped) {
|
|
151
|
+
if (ch === "u") {
|
|
152
|
+
this.#candidateUnicode = "u";
|
|
153
|
+
} else {
|
|
154
|
+
this.#candidate += decodeJsonStringEscape(ch);
|
|
155
|
+
this.#candidateEscaped = false;
|
|
156
|
+
}
|
|
157
|
+
this.#offset++;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (ch === "\\") {
|
|
161
|
+
this.#candidateEscaped = true;
|
|
162
|
+
this.#offset++;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (ch === '"') {
|
|
166
|
+
this.#matchedKey = this.#depth === 1 && this.#keys.has(this.#candidate) ? this.#candidate : undefined;
|
|
167
|
+
this.#state = "afterCandidate";
|
|
168
|
+
this.#offset++;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#candidate += ch;
|
|
172
|
+
this.#offset++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#readCandidateUnicode(ch: string): void {
|
|
176
|
+
if (isHexDigit(ch)) {
|
|
177
|
+
this.#candidateUnicode += ch;
|
|
178
|
+
if (this.#candidateUnicode.length === 5) {
|
|
179
|
+
this.#candidate += String.fromCharCode(Number.parseInt(this.#candidateUnicode.slice(1), 16));
|
|
180
|
+
this.#candidateUnicode = "";
|
|
181
|
+
this.#candidateEscaped = false;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
this.#candidate += this.#candidateUnicode + ch;
|
|
185
|
+
this.#candidateUnicode = "";
|
|
186
|
+
this.#candidateEscaped = false;
|
|
187
|
+
}
|
|
188
|
+
this.#offset++;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#afterCandidate(ch: string): void {
|
|
192
|
+
if (/\s/.test(ch)) {
|
|
193
|
+
this.#offset++;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const matchedKey = this.#matchedKey;
|
|
197
|
+
this.#matchedKey = undefined;
|
|
198
|
+
if (ch === ":" && matchedKey) {
|
|
199
|
+
this.#targetKey = matchedKey;
|
|
200
|
+
this.#state = "beforeValue";
|
|
201
|
+
this.#offset++;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.#state = "scan";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#beforeValue(ch: string): void {
|
|
208
|
+
if (/\s/.test(ch)) {
|
|
209
|
+
this.#offset++;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (ch === '"' && this.#targetKey) {
|
|
213
|
+
if (this.#values[this.#targetKey]) {
|
|
214
|
+
this.#values[this.#targetKey] = "";
|
|
215
|
+
this.#changed = true;
|
|
216
|
+
}
|
|
217
|
+
this.#targetEscaped = false;
|
|
218
|
+
this.#targetUnicode = "";
|
|
219
|
+
this.#state = "target";
|
|
220
|
+
this.#offset++;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.#targetKey = undefined;
|
|
224
|
+
this.#state = "scan";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#readTarget(ch: string): void {
|
|
228
|
+
if (this.#targetUnicode) {
|
|
229
|
+
this.#readTargetUnicode(ch);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (this.#targetEscaped) {
|
|
233
|
+
if (ch === "u") {
|
|
234
|
+
this.#targetUnicode = "u";
|
|
235
|
+
} else {
|
|
236
|
+
this.#appendTarget(decodeJsonStringEscape(ch));
|
|
237
|
+
this.#targetEscaped = false;
|
|
238
|
+
}
|
|
239
|
+
this.#offset++;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (ch === "\\") {
|
|
243
|
+
this.#targetEscaped = true;
|
|
244
|
+
this.#offset++;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (ch === '"') {
|
|
248
|
+
this.#targetKey = undefined;
|
|
249
|
+
this.#state = "scan";
|
|
250
|
+
this.#offset++;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.#appendTarget(ch);
|
|
254
|
+
this.#offset++;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#readTargetUnicode(ch: string): void {
|
|
258
|
+
if (isHexDigit(ch)) {
|
|
259
|
+
this.#targetUnicode += ch;
|
|
260
|
+
if (this.#targetUnicode.length === 5) {
|
|
261
|
+
this.#appendTarget(String.fromCharCode(Number.parseInt(this.#targetUnicode.slice(1), 16)));
|
|
262
|
+
this.#targetUnicode = "";
|
|
263
|
+
this.#targetEscaped = false;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
this.#appendTarget(this.#targetUnicode + ch);
|
|
267
|
+
this.#targetUnicode = "";
|
|
268
|
+
this.#targetEscaped = false;
|
|
269
|
+
}
|
|
270
|
+
this.#offset++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#appendTarget(text: string): void {
|
|
274
|
+
if (!this.#targetKey || text.length === 0) return;
|
|
275
|
+
this.#values[this.#targetKey] = `${this.#values[this.#targetKey] ?? ""}${text}`;
|
|
276
|
+
this.#changed = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createStringExtractor(keys: readonly string[] | undefined): StreamingJsonStringExtractor | undefined {
|
|
281
|
+
return keys && keys.length > 0 ? new StreamingJsonStringExtractor(keys) : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sameStringKeys(a: readonly string[], b: readonly string[] | undefined): boolean {
|
|
285
|
+
if (a.length !== (b?.length ?? 0)) return false;
|
|
286
|
+
for (let i = 0; i < a.length; i++) {
|
|
287
|
+
if (a[i] !== b?.[i]) return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
14
292
|
type RevealEntry = {
|
|
15
293
|
component: ToolArgsRevealComponent | undefined;
|
|
16
294
|
/** Latest raw streamed argument text (JSON for function tools, raw text for custom tools). */
|
|
@@ -29,6 +307,9 @@ type RevealEntry = {
|
|
|
29
307
|
displayArgs: Record<string, unknown>;
|
|
30
308
|
/** Raw prefix carried by `displayArgs.__partialJson`. */
|
|
31
309
|
displayPrefix: string;
|
|
310
|
+
/** JSON string fields decoded incrementally between full JSON parses. */
|
|
311
|
+
streamingStringKeys: readonly string[];
|
|
312
|
+
stringExtractor: StreamingJsonStringExtractor | undefined;
|
|
32
313
|
};
|
|
33
314
|
|
|
34
315
|
/** Clamp a slice end into `text`, never splitting a surrogate pair: a prefix
|
|
@@ -45,7 +326,7 @@ function clampSliceEnd(text: string, end: number): number {
|
|
|
45
326
|
type ToolArgsRevealTarget = {
|
|
46
327
|
rawInput: boolean;
|
|
47
328
|
exposeRawPartialJson: boolean;
|
|
48
|
-
|
|
329
|
+
streamingStringKeys?: readonly string[];
|
|
49
330
|
};
|
|
50
331
|
|
|
51
332
|
type DisplayArgsStep = {
|
|
@@ -62,6 +343,7 @@ function resetDisplayState(entry: RevealEntry): void {
|
|
|
62
343
|
entry.parsedLen = 0;
|
|
63
344
|
entry.displayArgs = initialDisplayArgs();
|
|
64
345
|
entry.displayPrefix = "";
|
|
346
|
+
entry.stringExtractor?.reset();
|
|
65
347
|
}
|
|
66
348
|
|
|
67
349
|
/** Display args for a revealed prefix. Function-tool JSON is parsed at the same
|
|
@@ -91,6 +373,11 @@ function displayArgsForPrefix(entry: RevealEntry, prefix: string, forceParse = f
|
|
|
91
373
|
parsedChanged = true;
|
|
92
374
|
}
|
|
93
375
|
}
|
|
376
|
+
const extracted = entry.stringExtractor?.update(prefix);
|
|
377
|
+
if (extracted?.changed) {
|
|
378
|
+
entry.parsedArgs = { ...entry.parsedArgs, ...extracted.values };
|
|
379
|
+
parsedChanged = true;
|
|
380
|
+
}
|
|
94
381
|
|
|
95
382
|
const rawPrefixChanged = entry.exposeRawPartialJson && prefix !== entry.displayPrefix;
|
|
96
383
|
if (!parsedChanged && !rawPrefixChanged) return { args: entry.displayArgs, changed: false };
|
|
@@ -102,6 +389,39 @@ function displayArgsForPrefix(entry: RevealEntry, prefix: string, forceParse = f
|
|
|
102
389
|
return { args, changed: true };
|
|
103
390
|
}
|
|
104
391
|
|
|
392
|
+
type StreamedToolArgsSource = {
|
|
393
|
+
/** Custom-tool raw text stream (`customWireName` tools): never JSON-parsed. */
|
|
394
|
+
rawInput: boolean;
|
|
395
|
+
/** Provider-parsed arguments, spread UNDER the fresh decode: a dialect
|
|
396
|
+
* projector may carry keys a raw re-parse cannot recover, but any key the
|
|
397
|
+
* fresh parse does recover wins — provider parses lag the stream by up to
|
|
398
|
+
* STREAMING_JSON_PARSE_MIN_GROWTH bytes mid-stream. */
|
|
399
|
+
fullArgs?: Record<string, unknown>;
|
|
400
|
+
/** See {@link streamingStringKeysForTool}. */
|
|
401
|
+
streamingStringKeys?: readonly string[];
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* One-shot decode of a streamed tool-call argument buffer into display args —
|
|
406
|
+
* the same decode the live reveal applies frame-by-frame, for paths that see
|
|
407
|
+
* the buffer once (transcript rebuilds on theme change, settings, focus
|
|
408
|
+
* replay). Keeps a rebuilt preview identical to the live preview: parsed
|
|
409
|
+
* fields come from a fresh parse of the full buffer, `streamingStringKeys`
|
|
410
|
+
* fields from the incremental string decoder (which also wins ties in the
|
|
411
|
+
* live path), never from the provider's throttled `arguments`.
|
|
412
|
+
*/
|
|
413
|
+
export function decodeStreamedToolArgs(partialJson: string, source: StreamedToolArgsSource): Record<string, unknown> {
|
|
414
|
+
if (source.rawInput) {
|
|
415
|
+
return { input: partialJson, __partialJson: partialJson };
|
|
416
|
+
}
|
|
417
|
+
const parsed = parseStreamingJson<Record<string, unknown>>(partialJson);
|
|
418
|
+
const args: Record<string, unknown> = source.fullArgs ? { ...source.fullArgs, ...parsed } : { ...parsed };
|
|
419
|
+
const extracted = createStringExtractor(source.streamingStringKeys)?.update(partialJson);
|
|
420
|
+
if (extracted) Object.assign(args, extracted.values);
|
|
421
|
+
args.__partialJson = partialJson;
|
|
422
|
+
return args;
|
|
423
|
+
}
|
|
424
|
+
|
|
105
425
|
/**
|
|
106
426
|
* Paces streamed tool-call arguments the same way StreamingRevealController
|
|
107
427
|
* paces assistant text: providers that deliver `partialJson` in large batches
|
|
@@ -129,16 +449,15 @@ export class ToolArgsRevealController {
|
|
|
129
449
|
|
|
130
450
|
/**
|
|
131
451
|
* Record the latest streamed argument text for a tool call and return the
|
|
132
|
-
* args to render right now. With smoothing disabled
|
|
133
|
-
*
|
|
452
|
+
* args to render right now. With smoothing disabled nothing is paced — the
|
|
453
|
+
* full received buffer decodes in one step — but the entry still runs the
|
|
454
|
+
* incremental string decoder + parse throttle, so streamed text fields
|
|
455
|
+
* (write `content`, edit bodies, eval `code`) stay fresh between the
|
|
456
|
+
* provider's own throttled full-JSON parses instead of lagging up to
|
|
457
|
+
* STREAMING_JSON_PARSE_MIN_GROWTH bytes behind.
|
|
134
458
|
*/
|
|
135
459
|
setTarget(id: string, partialJson: string, target: ToolArgsRevealTarget): Record<string, unknown> {
|
|
136
|
-
const { rawInput, exposeRawPartialJson,
|
|
137
|
-
if (!this.#getSmoothStreaming()) {
|
|
138
|
-
// Toggle may flip mid-call: drop any live entry so ticks stop.
|
|
139
|
-
this.#entries.delete(id);
|
|
140
|
-
return { ...fullArgs, __partialJson: partialJson };
|
|
141
|
-
}
|
|
460
|
+
const { rawInput, exposeRawPartialJson, streamingStringKeys } = target;
|
|
142
461
|
let entry = this.#entries.get(id);
|
|
143
462
|
if (!entry) {
|
|
144
463
|
entry = {
|
|
@@ -151,13 +470,21 @@ export class ToolArgsRevealController {
|
|
|
151
470
|
parsedLen: 0,
|
|
152
471
|
displayArgs: initialDisplayArgs(),
|
|
153
472
|
displayPrefix: "",
|
|
473
|
+
streamingStringKeys: streamingStringKeys ?? [],
|
|
474
|
+
stringExtractor: createStringExtractor(streamingStringKeys),
|
|
154
475
|
};
|
|
155
476
|
this.#entries.set(id, entry);
|
|
156
477
|
} else {
|
|
157
|
-
if (
|
|
478
|
+
if (
|
|
479
|
+
entry.rawInput !== rawInput ||
|
|
480
|
+
entry.exposeRawPartialJson !== exposeRawPartialJson ||
|
|
481
|
+
!sameStringKeys(entry.streamingStringKeys, streamingStringKeys)
|
|
482
|
+
) {
|
|
158
483
|
entry.rawInput = rawInput;
|
|
159
484
|
entry.exposeRawPartialJson = exposeRawPartialJson;
|
|
160
485
|
resetDisplayState(entry);
|
|
486
|
+
entry.streamingStringKeys = streamingStringKeys ?? [];
|
|
487
|
+
entry.stringExtractor = createStringExtractor(streamingStringKeys);
|
|
161
488
|
}
|
|
162
489
|
// Streams only append; a non-prefix target means a rewind — snap into range.
|
|
163
490
|
if (!partialJson.startsWith(entry.target)) {
|
|
@@ -166,6 +493,9 @@ export class ToolArgsRevealController {
|
|
|
166
493
|
}
|
|
167
494
|
entry.target = partialJson;
|
|
168
495
|
}
|
|
496
|
+
// Toggle may flip mid-call: snap the reveal to everything received so
|
|
497
|
+
// pacing stops (and never restarts while the toggle stays off).
|
|
498
|
+
if (!this.#getSmoothStreaming()) entry.revealed = entry.target.length;
|
|
169
499
|
entry.revealed = clampSliceEnd(entry.target, entry.revealed);
|
|
170
500
|
this.#syncTimer();
|
|
171
501
|
return displayArgsForPrefix(entry, entry.target.slice(0, entry.revealed)).args;
|