@oh-my-pi/pi-coding-agent 16.1.12 → 16.1.14
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 +27 -0
- package/dist/cli.js +5650 -2860
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +0 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
15
15
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
16
|
-
import {
|
|
17
|
-
import { formatBytes, formatDuration } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import type { TUI } from "@oh-my-pi/pi-tui";
|
|
18
17
|
import type { AdvisorMessageDetails } from "../../advisor";
|
|
19
18
|
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
20
19
|
import { settings } from "../../config/settings";
|
|
@@ -22,16 +21,20 @@ import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
|
22
21
|
import {
|
|
23
22
|
BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE,
|
|
24
23
|
type CustomMessage,
|
|
25
|
-
isSilentAbort,
|
|
26
24
|
LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
|
|
27
|
-
resolveAbortLabel,
|
|
28
25
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
29
26
|
type SkillPromptDetails,
|
|
30
27
|
} from "../../session/messages";
|
|
31
28
|
import type { SessionMessageEntry } from "../../session/session-entries";
|
|
32
|
-
import { createIrcMessageCard } from "../../tools/irc";
|
|
33
|
-
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
34
29
|
import { theme } from "../theme/theme";
|
|
30
|
+
import {
|
|
31
|
+
assistantHasVisibleContent,
|
|
32
|
+
buildAsyncResultBlock,
|
|
33
|
+
buildFileMentionBlock,
|
|
34
|
+
buildIrcMessageCard,
|
|
35
|
+
normalizeToolArgs,
|
|
36
|
+
resolveAssistantErrorMessage,
|
|
37
|
+
} from "../utils/transcript-render-helpers";
|
|
35
38
|
import { createAdvisorMessageCard } from "./advisor-message";
|
|
36
39
|
import { AssistantMessageComponent } from "./assistant-message";
|
|
37
40
|
import { createBackgroundTanDispatchBlock } from "./background-tan-message";
|
|
@@ -49,7 +52,7 @@ import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./lat
|
|
|
49
52
|
import { ReadToolGroupComponent, readArgsHaveTarget, readArgsTargetInternalUrl } from "./read-tool-group";
|
|
50
53
|
import { SkillMessageComponent } from "./skill-message";
|
|
51
54
|
import { ToolExecutionComponent } from "./tool-execution";
|
|
52
|
-
import {
|
|
55
|
+
import { TranscriptContainer } from "./transcript-container";
|
|
53
56
|
import { createUsageRowBlock } from "./usage-row";
|
|
54
57
|
import { UserMessageComponent } from "./user-message";
|
|
55
58
|
|
|
@@ -218,27 +221,9 @@ export class ChatTranscriptBuilder {
|
|
|
218
221
|
break;
|
|
219
222
|
}
|
|
220
223
|
case "fileMention": {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (file.skippedReason === "tooLarge") {
|
|
225
|
-
const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
|
|
226
|
-
suffix = `(skipped: ${size})`;
|
|
227
|
-
} else {
|
|
228
|
-
suffix = file.image
|
|
229
|
-
? "(image)"
|
|
230
|
-
: file.lineCount === undefined
|
|
231
|
-
? "(unknown lines)"
|
|
232
|
-
: `(${file.lineCount} lines)`;
|
|
233
|
-
}
|
|
234
|
-
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
235
|
-
"accent",
|
|
236
|
-
file.path,
|
|
237
|
-
)} ${theme.fg("dim", suffix)}`;
|
|
238
|
-
// Indent one column to match the transcript's other rows (the viewer renders
|
|
239
|
-
// body rows without an outer gutter; rows own their left pad).
|
|
240
|
-
block.addChild(new Text(text, 1, 0));
|
|
241
|
-
}
|
|
224
|
+
// Indent one column to match the transcript's other rows (the viewer renders
|
|
225
|
+
// body rows without an outer gutter; rows own their left pad).
|
|
226
|
+
const block = buildFileMentionBlock(message.files, 1);
|
|
242
227
|
if (block.children.length > 0) this.container.addChild(block);
|
|
243
228
|
break;
|
|
244
229
|
}
|
|
@@ -266,24 +251,14 @@ export class ChatTranscriptBuilder {
|
|
|
266
251
|
this.#lastAssistantUsage = message.usage;
|
|
267
252
|
}
|
|
268
253
|
|
|
269
|
-
const hasVisibleAssistantContent = message
|
|
270
|
-
content =>
|
|
271
|
-
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
272
|
-
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
273
|
-
);
|
|
254
|
+
const hasVisibleAssistantContent = assistantHasVisibleContent(message);
|
|
274
255
|
if (hasVisibleAssistantContent) {
|
|
275
256
|
// New visible turn content closes the current read run (mirrors rebuild).
|
|
276
257
|
this.#readGroup?.seal();
|
|
277
258
|
this.#readGroup = null;
|
|
278
259
|
}
|
|
279
260
|
|
|
280
|
-
const
|
|
281
|
-
const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
282
|
-
const errorMessage = hasErrorStop
|
|
283
|
-
? message.stopReason === "aborted"
|
|
284
|
-
? resolveAbortLabel(message.errorMessage)
|
|
285
|
-
: message.errorMessage || "Error"
|
|
286
|
-
: null;
|
|
261
|
+
const { hasErrorStop, errorMessage } = resolveAssistantErrorMessage(message);
|
|
287
262
|
|
|
288
263
|
for (const content of message.content) {
|
|
289
264
|
if (content.type !== "toolCall") continue;
|
|
@@ -303,10 +278,7 @@ export class ChatTranscriptBuilder {
|
|
|
303
278
|
content.id,
|
|
304
279
|
);
|
|
305
280
|
} else {
|
|
306
|
-
const normalizedArgs =
|
|
307
|
-
content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
|
|
308
|
-
? (content.arguments as Record<string, unknown>)
|
|
309
|
-
: {};
|
|
281
|
+
const normalizedArgs = normalizeToolArgs(content.arguments);
|
|
310
282
|
this.#readArgs.set(content.id, normalizedArgs);
|
|
311
283
|
}
|
|
312
284
|
continue;
|
|
@@ -373,42 +345,7 @@ export class ChatTranscriptBuilder {
|
|
|
373
345
|
#appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
|
|
374
346
|
if (!message.display) return;
|
|
375
347
|
if (message.customType === "async-result") {
|
|
376
|
-
|
|
377
|
-
message as CustomMessage<{
|
|
378
|
-
jobId?: string;
|
|
379
|
-
type?: "bash" | "task";
|
|
380
|
-
label?: string;
|
|
381
|
-
durationMs?: number;
|
|
382
|
-
jobs?: Array<{ jobId?: string; type?: "bash" | "task"; label?: string; durationMs?: number }>;
|
|
383
|
-
}>
|
|
384
|
-
).details;
|
|
385
|
-
const jobs =
|
|
386
|
-
details?.jobs && details.jobs.length > 0
|
|
387
|
-
? details.jobs
|
|
388
|
-
: [
|
|
389
|
-
{
|
|
390
|
-
jobId: details?.jobId,
|
|
391
|
-
type: details?.type,
|
|
392
|
-
label: details?.label,
|
|
393
|
-
durationMs: details?.durationMs,
|
|
394
|
-
},
|
|
395
|
-
];
|
|
396
|
-
const block = new TranscriptBlock();
|
|
397
|
-
for (const job of jobs) {
|
|
398
|
-
const jobId = job.jobId ?? "unknown";
|
|
399
|
-
const typeLabel = job.type ? `[${job.type}]` : "[job]";
|
|
400
|
-
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
401
|
-
const line = [
|
|
402
|
-
theme.fg("success", `${theme.status.done} Background job completed`),
|
|
403
|
-
theme.fg("dim", typeLabel),
|
|
404
|
-
theme.fg("accent", jobId),
|
|
405
|
-
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
406
|
-
]
|
|
407
|
-
.filter(Boolean)
|
|
408
|
-
.join(" ");
|
|
409
|
-
block.addChild(new Text(line, 1, 0));
|
|
410
|
-
}
|
|
411
|
-
this.container.addChild(block);
|
|
348
|
+
this.container.addChild(buildAsyncResultBlock(message));
|
|
412
349
|
return;
|
|
413
350
|
}
|
|
414
351
|
if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
|
|
@@ -433,28 +370,7 @@ export class ChatTranscriptBuilder {
|
|
|
433
370
|
message.customType === "irc:autoreply" ||
|
|
434
371
|
message.customType === "irc:relay"
|
|
435
372
|
) {
|
|
436
|
-
|
|
437
|
-
message as CustomMessage<{ from?: string; to?: string; message?: string; body?: string; replyTo?: string }>
|
|
438
|
-
).details;
|
|
439
|
-
const kind =
|
|
440
|
-
message.customType === "irc:incoming"
|
|
441
|
-
? ("incoming" as const)
|
|
442
|
-
: message.customType === "irc:autoreply"
|
|
443
|
-
? ("autoreply" as const)
|
|
444
|
-
: ("relay" as const);
|
|
445
|
-
const card = createIrcMessageCard(
|
|
446
|
-
{
|
|
447
|
-
kind,
|
|
448
|
-
from: details?.from,
|
|
449
|
-
to: details?.to,
|
|
450
|
-
body: kind === "incoming" ? details?.message : details?.body,
|
|
451
|
-
replyTo: details?.replyTo,
|
|
452
|
-
timestamp: message.timestamp,
|
|
453
|
-
},
|
|
454
|
-
() => this.#expanded,
|
|
455
|
-
theme,
|
|
456
|
-
);
|
|
457
|
-
this.container.addChild(card);
|
|
373
|
+
this.container.addChild(buildIrcMessageCard(message, () => this.#expanded));
|
|
458
374
|
return;
|
|
459
375
|
}
|
|
460
376
|
if (message.customType === "advisor") {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
1
2
|
import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
2
3
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
3
4
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
@@ -163,6 +164,24 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
|
|
163
164
|
export class CustomEditor extends Editor {
|
|
164
165
|
imageLinks?: readonly (string | undefined)[];
|
|
165
166
|
|
|
167
|
+
/** Draft images pasted into the composer, consumed on submit. Co-located with
|
|
168
|
+
* {@link imageLinks} so every piece of draft-image state lives on the editor. */
|
|
169
|
+
pendingImages: ImageContent[] = [];
|
|
170
|
+
/** Per-image source links (file:// targets) parallel to {@link pendingImages};
|
|
171
|
+
* `undefined` entries are images without a backing reference yet. */
|
|
172
|
+
pendingImageLinks: (string | undefined)[] = [];
|
|
173
|
+
|
|
174
|
+
/** Clear the composer draft: optionally commit `historyText` to history, then
|
|
175
|
+
* reset the editor text and all pending draft-image state. The shared tail of
|
|
176
|
+
* every "message submitted" path; pass no argument for a plain discard. */
|
|
177
|
+
clearDraft(historyText?: string): void {
|
|
178
|
+
if (historyText !== undefined) this.addToHistory(historyText);
|
|
179
|
+
this.setText("");
|
|
180
|
+
this.imageLinks = undefined;
|
|
181
|
+
this.pendingImages = [];
|
|
182
|
+
this.pendingImageLinks = [];
|
|
183
|
+
}
|
|
184
|
+
|
|
166
185
|
/** Treat image/paste markers as indivisible: a stray backspace deletes the whole token
|
|
167
186
|
* instead of corrupting `[Paste #1, +30 lines]` into plain text. */
|
|
168
187
|
override atomicTokenPattern = PLACEHOLDER_REGEX;
|
|
@@ -26,6 +26,7 @@ import { Settings } from "../../../config/settings";
|
|
|
26
26
|
import { DynamicBorder } from "../../../modes/components/dynamic-border";
|
|
27
27
|
import { theme } from "../../../modes/theme/theme";
|
|
28
28
|
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
|
29
|
+
import { handleTabSwitchKey, padLinesToHeight } from "../selector-helpers";
|
|
29
30
|
import { ExtensionList } from "./extension-list";
|
|
30
31
|
import { InspectorPanel } from "./inspector-panel";
|
|
31
32
|
import {
|
|
@@ -145,13 +146,8 @@ export class ExtensionDashboard extends Container {
|
|
|
145
146
|
}
|
|
146
147
|
const lines = super.render(width);
|
|
147
148
|
// Pad to the full viewport so the dashboard covers the screen instead of
|
|
148
|
-
// letting the transcript peek through below it.
|
|
149
|
-
|
|
150
|
-
const rows = this.#terminalRows();
|
|
151
|
-
if (lines.length >= rows) return lines;
|
|
152
|
-
const padded = lines.slice();
|
|
153
|
-
while (padded.length < rows) padded.push("");
|
|
154
|
-
return padded;
|
|
149
|
+
// letting the transcript peek through below it.
|
|
150
|
+
return padLinesToHeight(lines, this.#terminalRows());
|
|
155
151
|
}
|
|
156
152
|
|
|
157
153
|
#buildLayout(): void {
|
|
@@ -339,12 +335,7 @@ export class ExtensionDashboard extends Container {
|
|
|
339
335
|
}
|
|
340
336
|
|
|
341
337
|
// Tab/Shift+Tab or Left/Right: Cycle through tabs
|
|
342
|
-
if (
|
|
343
|
-
this.#switchTab(1);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
347
|
-
this.#switchTab(-1);
|
|
338
|
+
if (handleTabSwitchKey(data, direction => this.#switchTab(direction))) {
|
|
348
339
|
return;
|
|
349
340
|
}
|
|
350
341
|
|
|
@@ -5,18 +5,11 @@
|
|
|
5
5
|
* that toggles the entire provider. All items below are dimmed when the
|
|
6
6
|
* master switch is off.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
type Component,
|
|
10
|
-
extractPrintableText,
|
|
11
|
-
matchesKey,
|
|
12
|
-
padding,
|
|
13
|
-
ScrollView,
|
|
14
|
-
truncateToWidth,
|
|
15
|
-
visibleWidth,
|
|
16
|
-
} from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
17
9
|
import { isProviderEnabled } from "../../../discovery";
|
|
18
10
|
import { theme } from "../../../modes/theme/theme";
|
|
19
11
|
import { matchesSelectDown, matchesSelectUp } from "../../utils/keybinding-matchers";
|
|
12
|
+
import { clampSelection, contentRowWidth, renderScrollableList, searchableChar } from "../selector-helpers";
|
|
20
13
|
import { applyFilter } from "./state-manager";
|
|
21
14
|
import type { Extension, ExtensionKind, ExtensionState } from "./types";
|
|
22
15
|
|
|
@@ -136,8 +129,7 @@ export class ExtensionList implements Component {
|
|
|
136
129
|
const endIdx = Math.min(startIdx + this.#maxVisible, this.#listItems.length);
|
|
137
130
|
|
|
138
131
|
// Reserve the rightmost column for the scrollbar when overflowing
|
|
139
|
-
const
|
|
140
|
-
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
132
|
+
const rowWidth = contentRowWidth(width, this.#listItems.length, this.#maxVisible);
|
|
141
133
|
|
|
142
134
|
// Render visible items
|
|
143
135
|
const rows: string[] = [];
|
|
@@ -154,14 +146,13 @@ export class ExtensionList implements Component {
|
|
|
154
146
|
}
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
lines.push(...sv.render(width));
|
|
149
|
+
lines.push(
|
|
150
|
+
...renderScrollableList(rows, {
|
|
151
|
+
width,
|
|
152
|
+
totalRows: this.#listItems.length,
|
|
153
|
+
scrollOffset: this.#scrollOffset,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
165
156
|
|
|
166
157
|
return lines;
|
|
167
158
|
}
|
|
@@ -391,21 +382,9 @@ export class ExtensionList implements Component {
|
|
|
391
382
|
}
|
|
392
383
|
|
|
393
384
|
#clampSelection(): void {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
this.#selectedIndex = Math.min(this.#selectedIndex, this.#listItems.length - 1);
|
|
401
|
-
this.#selectedIndex = Math.max(0, this.#selectedIndex);
|
|
402
|
-
|
|
403
|
-
// Adjust scroll offset
|
|
404
|
-
if (this.#selectedIndex < this.#scrollOffset) {
|
|
405
|
-
this.#scrollOffset = this.#selectedIndex;
|
|
406
|
-
} else if (this.#selectedIndex >= this.#scrollOffset + this.#maxVisible) {
|
|
407
|
-
this.#scrollOffset = this.#selectedIndex - this.#maxVisible + 1;
|
|
408
|
-
}
|
|
385
|
+
const next = clampSelection(this.#selectedIndex, this.#scrollOffset, this.#listItems.length, this.#maxVisible);
|
|
386
|
+
this.#selectedIndex = next.selectedIndex;
|
|
387
|
+
this.#scrollOffset = next.scrollOffset;
|
|
409
388
|
}
|
|
410
389
|
|
|
411
390
|
handleInput(data: string): void {
|
|
@@ -462,16 +441,9 @@ export class ExtensionList implements Component {
|
|
|
462
441
|
}
|
|
463
442
|
|
|
464
443
|
// Printable characters -> search
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
if (printableCharCode > 32 && printableCharCode < 127) {
|
|
469
|
-
if (printableText === "j" || printableText === "k") {
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
this.setSearchQuery(this.#searchQuery + printableText);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
444
|
+
const char = searchableChar(data);
|
|
445
|
+
if (char !== null) {
|
|
446
|
+
this.setSearchQuery(this.#searchQuery + char);
|
|
475
447
|
}
|
|
476
448
|
}
|
|
477
449
|
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
Input,
|
|
6
6
|
matchesKey,
|
|
7
7
|
padding,
|
|
8
|
-
ScrollView,
|
|
9
8
|
Spacer,
|
|
10
9
|
Text,
|
|
11
10
|
truncateToWidth,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
|
|
23
22
|
import { DynamicBorder } from "./dynamic-border";
|
|
24
23
|
import { rawKeyHint } from "./keybinding-hints";
|
|
24
|
+
import { centeredWindow, contentRowWidth, renderScrollableList } from "./selector-helpers";
|
|
25
25
|
|
|
26
26
|
/** Visible result rows; also the jump distance for PageUp/PageDown. */
|
|
27
27
|
const MAX_VISIBLE = 10;
|
|
@@ -110,14 +110,9 @@ class HistoryResultsList implements Component {
|
|
|
110
110
|
const cursorSymbol = `${theme.nav.cursor} `;
|
|
111
111
|
const gutterWidth = visibleWidth(cursorSymbol);
|
|
112
112
|
|
|
113
|
-
const startIndex =
|
|
114
|
-
0,
|
|
115
|
-
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), this.#results.length - this.#maxVisible),
|
|
116
|
-
);
|
|
117
|
-
const endIndex = Math.min(startIndex + this.#maxVisible, this.#results.length);
|
|
113
|
+
const { startIndex, endIndex } = centeredWindow(this.#selectedIndex, this.#results.length, this.#maxVisible);
|
|
118
114
|
|
|
119
|
-
const
|
|
120
|
-
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
115
|
+
const rowWidth = contentRowWidth(width, this.#results.length, this.#maxVisible);
|
|
121
116
|
const rows: string[] = [];
|
|
122
117
|
|
|
123
118
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -148,14 +143,7 @@ class HistoryResultsList implements Component {
|
|
|
148
143
|
);
|
|
149
144
|
}
|
|
150
145
|
|
|
151
|
-
|
|
152
|
-
height: rows.length,
|
|
153
|
-
scrollbar: "auto",
|
|
154
|
-
totalRows: this.#results.length,
|
|
155
|
-
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
156
|
-
});
|
|
157
|
-
sv.setScrollOffset(startIndex);
|
|
158
|
-
lines.push(...sv.render(width));
|
|
146
|
+
lines.push(...renderScrollableList(rows, { width, totalRows: this.#results.length, scrollOffset: startIndex }));
|
|
159
147
|
return lines;
|
|
160
148
|
}
|
|
161
149
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared scaffolding for the TUI selector/list/dashboard components: viewport
|
|
3
|
+
* windowing, scrollbar-aware row widths, ScrollView rendering, selection
|
|
4
|
+
* clamping, search-character classification, tab-cycling keys, and full-screen
|
|
5
|
+
* padding. Behaviour is identical to the per-component copies these helpers
|
|
6
|
+
* replace.
|
|
7
|
+
*/
|
|
8
|
+
import { extractPrintableText, matchesKey, ScrollView } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { theme } from "../theme/theme";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render `rows` through a {@link ScrollView} with the shared list theme (muted
|
|
13
|
+
* track / accent thumb) and an "auto" scrollbar, positioned at `scrollOffset`.
|
|
14
|
+
* Returns the rendered lines for the caller to append to its output.
|
|
15
|
+
*/
|
|
16
|
+
export function renderScrollableList(
|
|
17
|
+
rows: readonly string[],
|
|
18
|
+
options: { width: number; totalRows: number; scrollOffset: number },
|
|
19
|
+
): readonly string[] {
|
|
20
|
+
const sv = new ScrollView(rows, {
|
|
21
|
+
height: rows.length,
|
|
22
|
+
scrollbar: "auto",
|
|
23
|
+
totalRows: options.totalRows,
|
|
24
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
25
|
+
});
|
|
26
|
+
sv.setScrollOffset(options.scrollOffset);
|
|
27
|
+
return sv.render(options.width);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Center a viewport window of `maxVisible` rows on `selectedIndex` within a
|
|
32
|
+
* list of `total` rows, clamped to valid bounds. Used by the selection-centered
|
|
33
|
+
* list panes (history search, tree selector).
|
|
34
|
+
*/
|
|
35
|
+
export function centeredWindow(
|
|
36
|
+
selectedIndex: number,
|
|
37
|
+
total: number,
|
|
38
|
+
maxVisible: number,
|
|
39
|
+
): { startIndex: number; endIndex: number } {
|
|
40
|
+
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
41
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
42
|
+
return { startIndex, endIndex };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Width available for row content, reserving the rightmost column for the
|
|
47
|
+
* scrollbar when the list overflows its visible window.
|
|
48
|
+
*/
|
|
49
|
+
export function contentRowWidth(width: number, total: number, maxVisible: number): number {
|
|
50
|
+
const overflow = total > maxVisible;
|
|
51
|
+
return Math.max(0, width - (overflow ? 1 : 0));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clamp `selectedIndex` into `[0, total)` and nudge `scrollOffset` so the
|
|
56
|
+
* selection stays within the visible window of `maxVisible` rows. Returns the
|
|
57
|
+
* adjusted pair; on an empty list both reset to 0.
|
|
58
|
+
*/
|
|
59
|
+
export function clampSelection(
|
|
60
|
+
selectedIndex: number,
|
|
61
|
+
scrollOffset: number,
|
|
62
|
+
total: number,
|
|
63
|
+
maxVisible: number,
|
|
64
|
+
): { selectedIndex: number; scrollOffset: number } {
|
|
65
|
+
if (total === 0) {
|
|
66
|
+
return { selectedIndex: 0, scrollOffset: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const selected = Math.max(0, Math.min(selectedIndex, total - 1));
|
|
70
|
+
|
|
71
|
+
let scroll = scrollOffset;
|
|
72
|
+
if (selected < scroll) {
|
|
73
|
+
scroll = selected;
|
|
74
|
+
} else if (selected >= scroll + maxVisible) {
|
|
75
|
+
scroll = selected - maxVisible + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { selectedIndex: selected, scrollOffset: scroll };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Classify a key event for search-query text entry. Returns the single
|
|
83
|
+
* printable character to append to the query, or `null` when the key is not a
|
|
84
|
+
* searchable character: non-printable, multi-byte, or a reserved `j`/`k`
|
|
85
|
+
* navigation key.
|
|
86
|
+
*/
|
|
87
|
+
export function searchableChar(data: string): string | null {
|
|
88
|
+
const printableText = extractPrintableText(data);
|
|
89
|
+
if (printableText && printableText.length === 1) {
|
|
90
|
+
const printableCharCode = printableText.charCodeAt(0);
|
|
91
|
+
if (printableCharCode > 32 && printableCharCode < 127) {
|
|
92
|
+
if (printableText === "j" || printableText === "k") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return printableText;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle the shared tab-cycling keys: Tab/Right advance to the next tab,
|
|
103
|
+
* Shift+Tab/Left to the previous. Invokes `switchTab` with the direction and
|
|
104
|
+
* returns true when the key was consumed.
|
|
105
|
+
*/
|
|
106
|
+
export function handleTabSwitchKey(data: string, switchTab: (direction: 1 | -1) => void): boolean {
|
|
107
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
108
|
+
switchTab(1);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
112
|
+
switchTab(-1);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pad `lines` with blank rows up to `rows` so a full-screen overlay covers the
|
|
120
|
+
* viewport instead of letting the transcript peek through below it. Copies
|
|
121
|
+
* before padding — the source array may be component-owned and must not be
|
|
122
|
+
* mutated.
|
|
123
|
+
*/
|
|
124
|
+
export function padLinesToHeight(lines: readonly string[], rows: number): readonly string[] {
|
|
125
|
+
if (lines.length >= rows) return lines;
|
|
126
|
+
const padded = lines.slice();
|
|
127
|
+
while (padded.length < rows) padded.push("");
|
|
128
|
+
return padded;
|
|
129
|
+
}
|
|
@@ -473,15 +473,17 @@ export class SettingsSelectorComponent implements Component {
|
|
|
473
473
|
return true;
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
if (event.wheel !== null) {
|
|
477
|
-
list?.handleWheel(event.wheel);
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
476
|
const tabLine = event.row - this.#tabRowStart;
|
|
482
477
|
const overTabs = tabLine >= 0 && tabLine < this.#tabRowCount;
|
|
483
478
|
const overContent = contentLine >= 0 && contentLine < this.#contentRowCount;
|
|
484
479
|
|
|
480
|
+
if (event.wheel !== null) {
|
|
481
|
+
if (overContent) {
|
|
482
|
+
list?.handleWheelAt(event.wheel, contentLine, innerCol);
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
485
487
|
if (event.motion) {
|
|
486
488
|
const hovered = overTabs ? this.#tabBar.tabAt(tabLine, innerCol) : undefined;
|
|
487
489
|
this.#tabBar.setHoverTab(hovered && !hovered.muted ? hovered.id : null);
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
fuzzyMatch,
|
|
7
7
|
Input,
|
|
8
8
|
matchesKey,
|
|
9
|
-
ScrollView,
|
|
10
9
|
Spacer,
|
|
11
10
|
Text,
|
|
12
11
|
TruncatedText,
|
|
@@ -20,6 +19,7 @@ import { shortenPath } from "../../tools/render-utils";
|
|
|
20
19
|
import { toPathList } from "../../tools/search";
|
|
21
20
|
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
22
21
|
import { DynamicBorder } from "./dynamic-border";
|
|
22
|
+
import { centeredWindow, contentRowWidth, renderScrollableList } from "./selector-helpers";
|
|
23
23
|
|
|
24
24
|
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
|
|
25
25
|
interface GutterInfo {
|
|
@@ -475,14 +475,11 @@ class TreeList implements Component {
|
|
|
475
475
|
return lines;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
const startIndex =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.#filteredNodes.length - this.maxVisibleLines,
|
|
483
|
-
),
|
|
478
|
+
const { startIndex, endIndex } = centeredWindow(
|
|
479
|
+
this.#selectedIndex,
|
|
480
|
+
this.#filteredNodes.length,
|
|
481
|
+
this.maxVisibleLines,
|
|
484
482
|
);
|
|
485
|
-
const endIndex = Math.min(startIndex + this.maxVisibleLines, this.#filteredNodes.length);
|
|
486
483
|
|
|
487
484
|
// Cap the per-row gutter prefix so a content budget is always preserved.
|
|
488
485
|
// Each indent level renders as 3 cells; deep branching would otherwise eat the
|
|
@@ -494,8 +491,7 @@ class TreeList implements Component {
|
|
|
494
491
|
const contentReserve = Math.max(MIN_CONTENT_COLS, Math.floor(width / 2));
|
|
495
492
|
const maxIndentLevels = Math.max(1, Math.floor((width - contentReserve - OVERHEAD_COLS) / 3));
|
|
496
493
|
|
|
497
|
-
const
|
|
498
|
-
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
494
|
+
const rowWidth = contentRowWidth(width, this.#filteredNodes.length, this.maxVisibleLines);
|
|
499
495
|
const rows: string[] = [];
|
|
500
496
|
|
|
501
497
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -584,14 +580,13 @@ class TreeList implements Component {
|
|
|
584
580
|
rows.push(truncateToWidth(line, rowWidth));
|
|
585
581
|
}
|
|
586
582
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
lines.push(...sv.render(width));
|
|
583
|
+
lines.push(
|
|
584
|
+
...renderScrollableList(rows, {
|
|
585
|
+
width,
|
|
586
|
+
totalRows: this.#filteredNodes.length,
|
|
587
|
+
scrollOffset: startIndex,
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
595
590
|
|
|
596
591
|
const filterLabel = this.#getFilterLabel();
|
|
597
592
|
if (filterLabel) {
|
|
@@ -5,7 +5,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
|
5
5
|
import { extractTextContent } from "../../commit/utils";
|
|
6
6
|
import { settings } from "../../config/settings";
|
|
7
7
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
8
|
-
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
8
|
+
import type { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
9
9
|
import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
|
|
10
10
|
import {
|
|
11
11
|
ReadToolGroupComponent,
|
|
@@ -25,6 +25,7 @@ import type { ResolveToolDetails } from "../../tools/resolve";
|
|
|
25
25
|
import { vocalizer } from "../../tts/vocalizer";
|
|
26
26
|
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
27
27
|
import { interruptHint } from "../shared";
|
|
28
|
+
import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
|
|
28
29
|
import { StreamingRevealController } from "./streaming-reveal";
|
|
29
30
|
import { ToolArgsRevealController } from "./tool-args-reveal";
|
|
30
31
|
|
|
@@ -332,14 +333,7 @@ export class EventController {
|
|
|
332
333
|
this.ctx.ui.requestRender();
|
|
333
334
|
} else if (event.message.role === "assistant") {
|
|
334
335
|
this.#lastVisibleBlockCount = 0;
|
|
335
|
-
this.ctx.streamingComponent =
|
|
336
|
-
undefined,
|
|
337
|
-
this.ctx.hideThinkingBlock,
|
|
338
|
-
() => this.ctx.ui.requestRender(),
|
|
339
|
-
this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
|
|
340
|
-
this.ctx.ui.imageBudget,
|
|
341
|
-
this.ctx.proseOnlyThinking,
|
|
342
|
-
);
|
|
336
|
+
this.ctx.streamingComponent = createAssistantMessageComponent(this.ctx);
|
|
343
337
|
this.ctx.streamingMessage = event.message;
|
|
344
338
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
345
339
|
this.#streamingReveal.begin(this.ctx.streamingComponent, this.ctx.streamingMessage);
|