@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.17
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 +78 -0
- package/dist/cli.js +3531 -3812
- package/dist/types/auto-thinking/classifier.d.ts +4 -2
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +22 -7
- package/dist/types/extensibility/plugins/loader.d.ts +16 -9
- package/dist/types/extensibility/tool-event-input.d.ts +2 -0
- package/dist/types/hindsight/content.d.ts +2 -10
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +6 -6
- package/dist/types/session/provider-image-budget.d.ts +3 -0
- package/dist/types/session/session-context.d.ts +6 -5
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/parallel.d.ts +4 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +23 -1
- package/dist/types/tiny/title-client.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/tools-manager.d.ts +2 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/auto-thinking/classifier.ts +7 -2
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/profile-alias.ts +38 -7
- package/src/cli/session-picker.ts +17 -3
- package/src/cli/usage-cli.ts +5 -1
- package/src/commands/launch.ts +3 -3
- package/src/config/model-discovery.ts +59 -8
- package/src/config/model-registry.ts +74 -3
- package/src/config/settings-schema.ts +13 -1
- package/src/discovery/omp-extension-roots.ts +1 -3
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/extensibility/extensions/wrapper.ts +3 -2
- package/src/extensibility/hooks/tool-wrapper.ts +4 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +40 -8
- package/src/extensibility/plugins/loader.ts +71 -23
- package/src/extensibility/plugins/marketplace/manager.ts +134 -0
- package/src/extensibility/tool-event-input.ts +57 -0
- package/src/hindsight/content.ts +21 -12
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/tool-bridge.ts +27 -2
- package/src/mnemopi/state.ts +5 -2
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/agent-transcript-viewer.ts +193 -41
- package/src/modes/components/chat-transcript-builder.ts +6 -0
- package/src/modes/components/custom-editor.test.ts +18 -1
- package/src/modes/components/custom-editor.ts +77 -45
- package/src/modes/components/hook-editor.ts +15 -2
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +52 -8
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/welcome.ts +12 -14
- package/src/modes/controllers/command-controller.ts +21 -5
- package/src/modes/controllers/input-controller.ts +115 -3
- package/src/modes/controllers/selector-controller.ts +19 -1
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +16 -0
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +8 -10
- package/src/session/agent-session.ts +422 -97
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/session-context.ts +14 -7
- package/src/session/session-storage.ts +24 -2
- package/src/session/snapcompact-inline.ts +19 -3
- package/src/slash-commands/builtin-registry.ts +0 -22
- package/src/slash-commands/helpers/usage-report.ts +9 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/parallel.ts +6 -1
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +29 -2
- package/src/tiny/title-client.ts +75 -21
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/tools-manager.ts +67 -10
- package/src/web/search/providers/exa.ts +85 -1
package/src/tiny/title-client.ts
CHANGED
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
createWorkerHandle,
|
|
6
6
|
createWorkerSubprocess,
|
|
7
7
|
logWorkerMessage,
|
|
8
|
+
type RefCountedWorkerHandle,
|
|
8
9
|
resolveWorkerSpawnCmd,
|
|
9
10
|
SMOKE_TEST_TIMEOUT_MS,
|
|
10
11
|
type SpawnedSubprocess,
|
|
11
12
|
smokeTestWorker,
|
|
12
13
|
spawnWorkerOrUnavailable,
|
|
13
|
-
type WorkerHandle,
|
|
14
14
|
workerEnvFromParent,
|
|
15
15
|
} from "../subprocess/worker-client";
|
|
16
16
|
import { safeSend } from "../utils/ipc";
|
|
@@ -128,33 +128,60 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess<TinyTitleWorkerOu
|
|
|
128
128
|
|
|
129
129
|
function wrapSubprocess(
|
|
130
130
|
spawned: SpawnedSubprocess<TinyTitleWorkerOutbound>,
|
|
131
|
-
):
|
|
131
|
+
): RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
|
|
132
132
|
const { proc } = spawned;
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
return {
|
|
134
|
+
...createWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>(spawned, message =>
|
|
135
|
+
safeSend(proc, message, "tiny-title"),
|
|
136
|
+
),
|
|
137
|
+
ref() {
|
|
138
|
+
try {
|
|
139
|
+
proc.ref();
|
|
140
|
+
} catch {
|
|
141
|
+
// Already gone.
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
unref() {
|
|
145
|
+
try {
|
|
146
|
+
proc.unref();
|
|
147
|
+
} catch {
|
|
148
|
+
// Already gone.
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function spawnInlineUnavailableWorker(
|
|
155
|
+
error: unknown,
|
|
156
|
+
): RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
|
|
157
|
+
return {
|
|
158
|
+
...createUnavailableWorker<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>(error),
|
|
159
|
+
ref() {},
|
|
160
|
+
unref() {},
|
|
161
|
+
};
|
|
136
162
|
}
|
|
137
163
|
|
|
138
|
-
function spawnTinyTitleWorker():
|
|
164
|
+
function spawnTinyTitleWorker(): RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
|
|
139
165
|
return spawnWorkerOrUnavailable(
|
|
140
166
|
() => wrapSubprocess(createTinyTitleSubprocess()),
|
|
141
|
-
|
|
167
|
+
spawnInlineUnavailableWorker,
|
|
142
168
|
"Tiny title worker spawn failed; local titles disabled",
|
|
143
169
|
);
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
export class TinyTitleClient {
|
|
147
|
-
#worker:
|
|
173
|
+
#worker: RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> | null = null;
|
|
148
174
|
#unsubscribeMessage: (() => void) | null = null;
|
|
149
175
|
#unsubscribeError: (() => void) | null = null;
|
|
150
176
|
#pending = new Map<string, PendingRequest>();
|
|
151
177
|
#failedModels = new Set<TinyLocalModelKey>();
|
|
152
178
|
#progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
|
|
153
179
|
#nextRequestId = 0;
|
|
154
|
-
#
|
|
180
|
+
#refed = false;
|
|
181
|
+
#spawnWorker: () => RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>;
|
|
155
182
|
|
|
156
183
|
constructor(
|
|
157
|
-
spawnWorker: () =>
|
|
184
|
+
spawnWorker: () => RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> = spawnTinyTitleWorker,
|
|
158
185
|
) {
|
|
159
186
|
this.#spawnWorker = spawnWorker;
|
|
160
187
|
}
|
|
@@ -179,11 +206,11 @@ export class TinyTitleClient {
|
|
|
179
206
|
const worker = this.#ensureWorker();
|
|
180
207
|
const id = String(++this.#nextRequestId);
|
|
181
208
|
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
182
|
-
this.#
|
|
209
|
+
this.#addPending(id, { kind: "generate", modelKey, resolve });
|
|
183
210
|
const abort = (): void => {
|
|
184
211
|
const pending = this.#pending.get(id);
|
|
185
212
|
if (pending?.kind !== "generate") return;
|
|
186
|
-
this.#
|
|
213
|
+
this.#deletePending(id);
|
|
187
214
|
pending.resolve(null);
|
|
188
215
|
};
|
|
189
216
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -195,7 +222,7 @@ export class TinyTitleClient {
|
|
|
195
222
|
return await promise;
|
|
196
223
|
} finally {
|
|
197
224
|
options.signal?.removeEventListener("abort", abort);
|
|
198
|
-
this.#
|
|
225
|
+
this.#deletePending(id);
|
|
199
226
|
}
|
|
200
227
|
} catch (error) {
|
|
201
228
|
logger.debug("tiny-title: local generation failed", {
|
|
@@ -218,11 +245,11 @@ export class TinyTitleClient {
|
|
|
218
245
|
const worker = this.#ensureWorker();
|
|
219
246
|
const id = String(++this.#nextRequestId);
|
|
220
247
|
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
221
|
-
this.#
|
|
248
|
+
this.#addPending(id, { kind: "complete", modelKey, resolve });
|
|
222
249
|
const abort = (): void => {
|
|
223
250
|
const pending = this.#pending.get(id);
|
|
224
251
|
if (pending?.kind !== "complete") return;
|
|
225
|
-
this.#
|
|
252
|
+
this.#deletePending(id);
|
|
226
253
|
pending.resolve(null);
|
|
227
254
|
};
|
|
228
255
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -231,7 +258,7 @@ export class TinyTitleClient {
|
|
|
231
258
|
return await promise;
|
|
232
259
|
} finally {
|
|
233
260
|
options.signal?.removeEventListener("abort", abort);
|
|
234
|
-
this.#
|
|
261
|
+
this.#deletePending(id);
|
|
235
262
|
}
|
|
236
263
|
} catch (error) {
|
|
237
264
|
logger.debug("tiny-model: local completion failed", {
|
|
@@ -251,11 +278,11 @@ export class TinyTitleClient {
|
|
|
251
278
|
const worker = this.#ensureWorker();
|
|
252
279
|
const id = String(++this.#nextRequestId);
|
|
253
280
|
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
254
|
-
this.#
|
|
281
|
+
this.#addPending(id, { kind: "download", modelKey, resolve });
|
|
255
282
|
const abort = (): void => {
|
|
256
283
|
const pending = this.#pending.get(id);
|
|
257
284
|
if (pending?.kind !== "download") return;
|
|
258
|
-
this.#
|
|
285
|
+
this.#deletePending(id);
|
|
259
286
|
pending.resolve(false);
|
|
260
287
|
};
|
|
261
288
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -264,7 +291,7 @@ export class TinyTitleClient {
|
|
|
264
291
|
return await promise;
|
|
265
292
|
} finally {
|
|
266
293
|
options.signal?.removeEventListener("abort", abort);
|
|
267
|
-
this.#
|
|
294
|
+
this.#deletePending(id);
|
|
268
295
|
}
|
|
269
296
|
} catch (error) {
|
|
270
297
|
logger.debug("tiny-title: local model download failed", {
|
|
@@ -290,6 +317,7 @@ export class TinyTitleClient {
|
|
|
290
317
|
else pending.resolve(false);
|
|
291
318
|
}
|
|
292
319
|
this.#pending.clear();
|
|
320
|
+
this.#refed = false;
|
|
293
321
|
try {
|
|
294
322
|
await worker?.terminate();
|
|
295
323
|
} catch {
|
|
@@ -297,7 +325,7 @@ export class TinyTitleClient {
|
|
|
297
325
|
}
|
|
298
326
|
}
|
|
299
327
|
|
|
300
|
-
#ensureWorker():
|
|
328
|
+
#ensureWorker(): RefCountedWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
|
|
301
329
|
if (this.#worker) return this.#worker;
|
|
302
330
|
const worker = this.#spawnWorker();
|
|
303
331
|
this.#worker = worker;
|
|
@@ -306,6 +334,32 @@ export class TinyTitleClient {
|
|
|
306
334
|
return worker;
|
|
307
335
|
}
|
|
308
336
|
|
|
337
|
+
/** Register a pending request and keep the worker referenced while work is in flight. */
|
|
338
|
+
#addPending(id: string, request: PendingRequest): void {
|
|
339
|
+
this.#pending.set(id, request);
|
|
340
|
+
this.#syncWorkerRef();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Drop a pending request and unref the worker once nothing is in flight. */
|
|
344
|
+
#deletePending(id: string): void {
|
|
345
|
+
if (this.#pending.delete(id)) this.#syncWorkerRef();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Tiny-model workers are spawned `unref`'d so idle TUI sessions can exit.
|
|
350
|
+
* Short-lived CLI downloads need the opposite while awaiting worker IPC, or
|
|
351
|
+
* Bun can drain the event loop before the subprocess answers.
|
|
352
|
+
*/
|
|
353
|
+
#syncWorkerRef(): void {
|
|
354
|
+
const worker = this.#worker;
|
|
355
|
+
if (!worker) return;
|
|
356
|
+
const shouldRef = this.#pending.size > 0;
|
|
357
|
+
if (shouldRef === this.#refed) return;
|
|
358
|
+
this.#refed = shouldRef;
|
|
359
|
+
if (shouldRef) worker.ref();
|
|
360
|
+
else worker.unref();
|
|
361
|
+
}
|
|
362
|
+
|
|
309
363
|
#handleMessage(message: TinyTitleWorkerOutbound): void {
|
|
310
364
|
if (message.type === "log") {
|
|
311
365
|
logWorkerMessage(message);
|
|
@@ -319,7 +373,7 @@ export class TinyTitleClient {
|
|
|
319
373
|
|
|
320
374
|
const pending = this.#pending.get(message.id);
|
|
321
375
|
if (!pending) return;
|
|
322
|
-
this.#
|
|
376
|
+
this.#deletePending(message.id);
|
|
323
377
|
if (message.type === "title") {
|
|
324
378
|
if (pending.kind === "generate") pending.resolve(message.title);
|
|
325
379
|
return;
|
package/src/tools/ask.ts
CHANGED
|
@@ -308,7 +308,7 @@ async function askSingleQuestion(
|
|
|
308
308
|
}
|
|
309
309
|
const customResult = await promptForCustomInput();
|
|
310
310
|
if (customResult.input === undefined) {
|
|
311
|
-
|
|
311
|
+
continue;
|
|
312
312
|
}
|
|
313
313
|
customInput = customResult.input;
|
|
314
314
|
break;
|
|
@@ -332,51 +332,57 @@ async function askSingleQuestion(
|
|
|
332
332
|
}
|
|
333
333
|
selectedOptions = Array.from(selected);
|
|
334
334
|
} else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
335
|
+
while (true) {
|
|
336
|
+
const displayOptions = addRecommendedSuffix(questionOptions, recommended);
|
|
337
|
+
const optionsWithNavigation: ExtensionUISelectItem[] = [...displayOptions, OTHER_OPTION];
|
|
338
|
+
|
|
339
|
+
let initialIndex = recommended;
|
|
340
|
+
const previouslySelected = selectedOptions[0];
|
|
341
|
+
if (previouslySelected) {
|
|
342
|
+
const selectedIndex = questionOptions.findIndex(option => option.label === previouslySelected);
|
|
343
|
+
if (selectedIndex >= 0) initialIndex = selectedIndex;
|
|
344
|
+
} else if (customInput !== undefined) {
|
|
345
|
+
initialIndex = displayOptions.length;
|
|
346
|
+
}
|
|
347
|
+
if (initialIndex !== undefined) {
|
|
348
|
+
const maxIndex = Math.max(optionsWithNavigation.length - 1, 0);
|
|
349
|
+
initialIndex = Math.max(0, Math.min(initialIndex, maxIndex));
|
|
350
|
+
}
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
352
|
+
const {
|
|
353
|
+
choice,
|
|
354
|
+
timedOut: selectTimedOut,
|
|
355
|
+
navigation: arrowNavigation,
|
|
356
|
+
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex, {
|
|
357
|
+
selectionMarker: "radio",
|
|
358
|
+
markableCount: displayOptions.length,
|
|
359
|
+
});
|
|
360
|
+
timedOut = selectTimedOut;
|
|
360
361
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
362
|
+
if (arrowNavigation) {
|
|
363
|
+
return { selectedOptions, customInput, timedOut, navigation: arrowNavigation };
|
|
364
|
+
}
|
|
365
|
+
if (choice === undefined) {
|
|
366
|
+
if (!timedOut) {
|
|
367
|
+
return { selectedOptions, customInput, timedOut, cancelled: true };
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
367
370
|
}
|
|
368
|
-
|
|
369
|
-
|
|
371
|
+
if (choice === OTHER_OPTION) {
|
|
372
|
+
if (selectTimedOut) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
370
375
|
const customResult = await promptForCustomInput();
|
|
371
|
-
if (customResult.input
|
|
372
|
-
|
|
373
|
-
selectedOptions = [];
|
|
376
|
+
if (customResult.input === undefined) {
|
|
377
|
+
continue;
|
|
374
378
|
}
|
|
375
|
-
|
|
379
|
+
customInput = customResult.input;
|
|
380
|
+
selectedOptions = [];
|
|
381
|
+
break;
|
|
376
382
|
}
|
|
377
|
-
} else {
|
|
378
383
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
|
379
384
|
customInput = undefined;
|
|
385
|
+
break;
|
|
380
386
|
}
|
|
381
387
|
if (navigation?.allowForward) {
|
|
382
388
|
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
package/src/tools/bash.ts
CHANGED
|
@@ -19,7 +19,7 @@ import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
|
19
19
|
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
20
20
|
import { DEFAULT_MAX_BYTES, enforceInlineByteCap, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
21
21
|
import { renderStatusLine } from "../tui";
|
|
22
|
-
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
22
|
+
import { CachedOutputBlock, markFramedBlockComponent, outputBlockContentWidth } from "../tui/output-block";
|
|
23
23
|
import { getSixelLineMask } from "../utils/sixel";
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { truncateForPrompt } from "./approval";
|
|
@@ -1334,7 +1334,14 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1334
1334
|
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
|
1335
1335
|
.join("\n");
|
|
1336
1336
|
const textContent = styledOutput;
|
|
1337
|
-
|
|
1337
|
+
// Cap the collapsed/streaming output to a viewport-sized tail and
|
|
1338
|
+
// measure it at the box's INNER width. Otherwise a growing tail
|
|
1339
|
+
// window scrolls its (mutating) rows above the live-region window
|
|
1340
|
+
// and the engine re-commits a fresh snapshot every frame —
|
|
1341
|
+
// spraying duplicate "… ctrl+o to expand" banners into native
|
|
1342
|
+
// scrollback (the box never overflows the viewport now).
|
|
1343
|
+
const previewBudget = Math.min(previewLines, previewWindow);
|
|
1344
|
+
const result = truncateToVisualLines(textContent, previewBudget, outputBlockContentWidth(width));
|
|
1338
1345
|
if (result.skippedCount > 0) {
|
|
1339
1346
|
outputLines.push(
|
|
1340
1347
|
uiTheme.fg(
|
|
@@ -800,7 +800,7 @@ export class WorkerCore {
|
|
|
800
800
|
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
801
801
|
return;
|
|
802
802
|
}
|
|
803
|
-
// status — surface as compact JSON so helper side effects (read/write/
|
|
803
|
+
// status — surface as compact JSON so helper side effects (read/write/env) appear in
|
|
804
804
|
// the cell result alongside explicit display() output.
|
|
805
805
|
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
806
806
|
}
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
18
18
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
19
19
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
20
20
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
21
|
-
import { markFramedBlockComponent, renderCodeCell } from "../tui";
|
|
21
|
+
import { markFramedBlockComponent, outputBlockContentWidth, renderCodeCell } from "../tui";
|
|
22
22
|
import {
|
|
23
23
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
24
24
|
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
@@ -56,6 +56,9 @@ interface EvalRenderCellArg {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
interface EvalRenderArgs {
|
|
59
|
+
language?: string;
|
|
60
|
+
code?: string;
|
|
61
|
+
title?: string;
|
|
59
62
|
cells?: EvalRenderCellArg[];
|
|
60
63
|
__partialJson?: string;
|
|
61
64
|
}
|
|
@@ -81,8 +84,8 @@ function normalizeRenderLanguage(value: string | undefined): EvalLanguage {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
if (!args) return [];
|
|
88
|
+
const raw = Array.isArray(args.cells) ? args.cells : typeof args.code === "string" ? [args] : [];
|
|
86
89
|
const out: EvalRenderCell[] = [];
|
|
87
90
|
for (const cell of raw) {
|
|
88
91
|
if (!cell || typeof cell !== "object") continue;
|
|
@@ -238,14 +241,12 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
238
241
|
const opIcons: Record<string, AvailableIcon> = {
|
|
239
242
|
read: "icon.file",
|
|
240
243
|
write: "icon.file",
|
|
241
|
-
append: "icon.file",
|
|
242
244
|
cat: "icon.file",
|
|
243
245
|
touch: "icon.file",
|
|
244
246
|
ls: "icon.folder",
|
|
245
247
|
cd: "icon.folder",
|
|
246
248
|
pwd: "icon.folder",
|
|
247
249
|
mkdir: "icon.folder",
|
|
248
|
-
tree: "icon.folder",
|
|
249
250
|
git_status: "icon.git",
|
|
250
251
|
git_diff: "icon.git",
|
|
251
252
|
git_log: "icon.git",
|
|
@@ -277,7 +278,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
277
278
|
if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
|
|
278
279
|
break;
|
|
279
280
|
case "write":
|
|
280
|
-
case "append":
|
|
281
281
|
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
|
282
282
|
if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
|
|
283
283
|
break;
|
|
@@ -316,13 +316,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
316
316
|
parts.push(`${data.lines} line${(data.lines as number) !== 1 ? "s" : ""}`);
|
|
317
317
|
if (data.staged) parts.push("(staged)");
|
|
318
318
|
break;
|
|
319
|
-
case "diff":
|
|
320
|
-
if (data.identical) {
|
|
321
|
-
parts.push("files identical");
|
|
322
|
-
} else {
|
|
323
|
-
parts.push("files differ");
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
319
|
case "batch":
|
|
327
320
|
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
|
|
328
321
|
break;
|
|
@@ -412,8 +405,6 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
|
|
|
412
405
|
case "cat":
|
|
413
406
|
case "head":
|
|
414
407
|
case "tail":
|
|
415
|
-
case "tree":
|
|
416
|
-
case "diff":
|
|
417
408
|
case "git_diff":
|
|
418
409
|
case "sh":
|
|
419
410
|
if (data.preview) addPreview(String(data.preview));
|
|
@@ -468,23 +459,33 @@ function formatCellOutputLines(
|
|
|
468
459
|
return { lines: [], hiddenCount: 0 };
|
|
469
460
|
}
|
|
470
461
|
|
|
462
|
+
// Cell output lands in renderCodeCell → renderOutputBlock, which re-wraps it
|
|
463
|
+
// at the box's inner content width. Bound the collapsed tail by VISUAL rows
|
|
464
|
+
// at that width so a long-line tail can't wrap into more rows than budgeted
|
|
465
|
+
// and scroll its mutating preview above the live-region window — the
|
|
466
|
+
// duplicate "ctrl+o to expand" scrollback spray.
|
|
467
|
+
const innerWidth = outputBlockContentWidth(width);
|
|
468
|
+
|
|
471
469
|
if (cell.hasMarkdown && cell.status !== "error") {
|
|
472
470
|
const md = new Markdown(cell.output, 0, 0, getMarkdownTheme());
|
|
473
|
-
const allLines = md.render(
|
|
471
|
+
const allLines = md.render(innerWidth);
|
|
474
472
|
const displayLines = expanded ? allLines : allLines.slice(-previewLines);
|
|
475
473
|
const hiddenCount = allLines.length - displayLines.length;
|
|
476
474
|
return { lines: displayLines, hiddenCount };
|
|
477
475
|
}
|
|
478
476
|
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
477
|
+
const styledOutput = cell.output
|
|
478
|
+
.split("\n")
|
|
479
|
+
.map(line => {
|
|
480
|
+
const cleaned = replaceTabs(line);
|
|
481
|
+
return cell.status === "error" ? theme.fg("error", cleaned) : theme.fg("toolOutput", cleaned);
|
|
482
|
+
})
|
|
483
|
+
.join("\n");
|
|
484
|
+
if (expanded) {
|
|
485
|
+
return { lines: styledOutput.split("\n"), hiddenCount: 0 };
|
|
486
|
+
}
|
|
487
|
+
const { visualLines, skippedCount } = truncateToVisualLines(styledOutput, previewLines, innerWidth);
|
|
488
|
+
return { lines: visualLines, hiddenCount: skippedCount };
|
|
488
489
|
}
|
|
489
490
|
|
|
490
491
|
export const evalToolRenderer = {
|
|
@@ -586,7 +587,10 @@ export const evalToolRenderer = {
|
|
|
586
587
|
return markFramedBlockComponent({
|
|
587
588
|
render: (width: number): readonly string[] => {
|
|
588
589
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
589
|
-
const previewLines =
|
|
590
|
+
const previewLines = Math.min(
|
|
591
|
+
options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES,
|
|
592
|
+
previewWindowRows(),
|
|
593
|
+
);
|
|
590
594
|
const key = `${expanded}|${previewLines}|${options.spinnerFrame}|${previewWindowRows()}`;
|
|
591
595
|
if (cached && cached.key === key && cached.width === width) {
|
|
592
596
|
return cached.result;
|
|
@@ -717,7 +721,10 @@ export const evalToolRenderer = {
|
|
|
717
721
|
|
|
718
722
|
return {
|
|
719
723
|
render: (width: number): readonly string[] => {
|
|
720
|
-
const previewLines =
|
|
724
|
+
const previewLines = Math.min(
|
|
725
|
+
options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES,
|
|
726
|
+
previewWindowRows(),
|
|
727
|
+
);
|
|
721
728
|
if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
|
|
722
729
|
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
723
730
|
cachedLines = result.visualLines;
|