@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.15
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 +48 -0
- package/dist/cli.js +5615 -2833
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +64 -14
- 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/export/share.d.ts +8 -1
- 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/mcp/transports/stdio.d.ts +12 -1
- 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/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -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/secrets/obfuscator.d.ts +3 -3
- 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/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/parallel.d.ts +6 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +74 -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/exec/bash-executor.ts +44 -0
- package/src/export/share.ts +51 -28
- 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/mcp/transports/stdio.ts +20 -4
- 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.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +29 -1
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/footer.ts +4 -3
- 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/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/command-controller.ts +1 -0
- 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 +5 -5
- package/src/prompts/tools/bash.md +2 -3
- 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/sdk.ts +13 -7
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +42 -9
- package/src/slash-commands/builtin-registry.ts +2 -1
- 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/utils/shell-snapshot.ts +63 -1
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
- package/src/web/search/providers/perplexity.ts +18 -6
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
type Component,
|
|
22
22
|
Container,
|
|
23
23
|
Editor,
|
|
24
|
-
extractPrintableText,
|
|
25
24
|
fuzzyMatch,
|
|
26
25
|
Input,
|
|
27
26
|
matchesKey,
|
|
@@ -59,6 +58,7 @@ import {
|
|
|
59
58
|
matchesSelectUp,
|
|
60
59
|
} from "../utils/keybinding-matchers";
|
|
61
60
|
import { DynamicBorder } from "./dynamic-border";
|
|
61
|
+
import { clampSelection, handleTabSwitchKey, padLinesToHeight, searchableChar } from "./selector-helpers";
|
|
62
62
|
|
|
63
63
|
type SourceTabId = "all" | AgentSource;
|
|
64
64
|
type AgentScope = "project" | "user";
|
|
@@ -521,31 +521,19 @@ export class AgentDashboard extends Container {
|
|
|
521
521
|
const lines = super.render(width);
|
|
522
522
|
// Pad to the full viewport so every state (list, edit, create) covers the
|
|
523
523
|
// screen as a true full-screen view instead of letting the transcript peek
|
|
524
|
-
// through below it.
|
|
525
|
-
|
|
526
|
-
const rows = this.#terminalRows();
|
|
527
|
-
if (lines.length >= rows) return lines;
|
|
528
|
-
const padded = lines.slice();
|
|
529
|
-
while (padded.length < rows) padded.push("");
|
|
530
|
-
return padded;
|
|
524
|
+
// through below it.
|
|
525
|
+
return padLinesToHeight(lines, this.#terminalRows());
|
|
531
526
|
}
|
|
532
527
|
|
|
533
528
|
#clampSelection(): void {
|
|
534
|
-
|
|
535
|
-
this.#selectedIndex
|
|
536
|
-
this.#scrollOffset
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this.#selectedIndex =
|
|
541
|
-
this.#
|
|
542
|
-
|
|
543
|
-
const maxVisible = this.#getMaxVisibleItems();
|
|
544
|
-
if (this.#selectedIndex < this.#scrollOffset) {
|
|
545
|
-
this.#scrollOffset = this.#selectedIndex;
|
|
546
|
-
} else if (this.#selectedIndex >= this.#scrollOffset + maxVisible) {
|
|
547
|
-
this.#scrollOffset = this.#selectedIndex - maxVisible + 1;
|
|
548
|
-
}
|
|
529
|
+
const next = clampSelection(
|
|
530
|
+
this.#selectedIndex,
|
|
531
|
+
this.#scrollOffset,
|
|
532
|
+
this.#filteredAgents.length,
|
|
533
|
+
this.#getMaxVisibleItems(),
|
|
534
|
+
);
|
|
535
|
+
this.#selectedIndex = next.selectedIndex;
|
|
536
|
+
this.#scrollOffset = next.scrollOffset;
|
|
549
537
|
}
|
|
550
538
|
|
|
551
539
|
#persistDisabledAgents(): void {
|
|
@@ -1150,12 +1138,7 @@ export class AgentDashboard extends Container {
|
|
|
1150
1138
|
return;
|
|
1151
1139
|
}
|
|
1152
1140
|
|
|
1153
|
-
if (
|
|
1154
|
-
this.#switchTab(1);
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
1158
|
-
this.#switchTab(-1);
|
|
1141
|
+
if (handleTabSwitchKey(data, direction => this.#switchTab(direction))) {
|
|
1159
1142
|
return;
|
|
1160
1143
|
}
|
|
1161
1144
|
|
|
@@ -1190,17 +1173,11 @@ export class AgentDashboard extends Container {
|
|
|
1190
1173
|
return;
|
|
1191
1174
|
}
|
|
1192
1175
|
|
|
1193
|
-
const
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
this.#searchQuery += printableText;
|
|
1201
|
-
this.#applyFilters();
|
|
1202
|
-
this.#buildLayout();
|
|
1203
|
-
}
|
|
1176
|
+
const char = searchableChar(data);
|
|
1177
|
+
if (char !== null) {
|
|
1178
|
+
this.#searchQuery += char;
|
|
1179
|
+
this.#applyFilters();
|
|
1180
|
+
this.#buildLayout();
|
|
1204
1181
|
}
|
|
1205
1182
|
}
|
|
1206
1183
|
}
|
|
@@ -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") {
|
|
@@ -3,6 +3,7 @@ import { $ } from "bun";
|
|
|
3
3
|
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
4
4
|
import {
|
|
5
5
|
CustomEditor,
|
|
6
|
+
extractBracketedImagePastePaths,
|
|
6
7
|
SPACE_HOLD_MECHANICAL_RUN,
|
|
7
8
|
SPACE_HOLD_RELEASE_MS,
|
|
8
9
|
SPACE_REPEAT_MAX_GAP_MS,
|
|
@@ -21,6 +22,12 @@ function makeEditor() {
|
|
|
21
22
|
const REPEAT_GAP_MS = 30;
|
|
22
23
|
/** A gap above the threshold — looks like a deliberate keypress. */
|
|
23
24
|
const TAP_GAP_MS = SPACE_REPEAT_MAX_GAP_MS + 80;
|
|
25
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
26
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
27
|
+
|
|
28
|
+
function bracketedPaste(text: string): string {
|
|
29
|
+
return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
|
|
30
|
+
}
|
|
24
31
|
|
|
25
32
|
/** Feed `count` spaces `gapMs` apart on the fake clock. The first space of a run has no prior
|
|
26
33
|
* space, so its gap is effectively infinite and it always reads as a deliberate tap. */
|
|
@@ -66,6 +73,21 @@ describe("CustomEditor placeholder decoration", () => {
|
|
|
66
73
|
});
|
|
67
74
|
});
|
|
68
75
|
|
|
76
|
+
describe("CustomEditor bracketed image-path paste", () => {
|
|
77
|
+
it("leaves a pasted bare .png filename on the normal text path", () => {
|
|
78
|
+
expect(extractBracketedImagePastePaths(bracketedPaste("icon-photo-default.png"))).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("extracts explicit local image paths for attachment", () => {
|
|
82
|
+
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/icon-photo-default.png"))).toEqual([
|
|
83
|
+
"/tmp/icon-photo-default.png",
|
|
84
|
+
]);
|
|
85
|
+
expect(extractBracketedImagePastePaths(bracketedPaste("C:\\Users\\me\\icon-photo-default.png"))).toEqual([
|
|
86
|
+
"C:\\Users\\me\\icon-photo-default.png",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
69
91
|
describe("CustomEditor space-hold push-to-talk", () => {
|
|
70
92
|
beforeAll(async () => {
|
|
71
93
|
await initTheme();
|
|
@@ -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";
|
|
@@ -63,6 +64,9 @@ const BRACKETED_PASTE_END = "\x1b[201~";
|
|
|
63
64
|
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
64
65
|
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
|
|
65
66
|
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
67
|
+
const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
68
|
+
const FILE_URI_REGEX = /^file:\/\//i;
|
|
69
|
+
const WINDOWS_DRIVE_PATH_REGEX = /^[a-z]:[\\/]/i;
|
|
66
70
|
|
|
67
71
|
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
68
72
|
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
@@ -117,6 +121,12 @@ function normalizePastedImagePath(path: string): string {
|
|
|
117
121
|
return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
function isExplicitPastedImagePath(path: string): boolean {
|
|
125
|
+
if (WINDOWS_DRIVE_PATH_REGEX.test(path) || FILE_URI_REGEX.test(path)) return true;
|
|
126
|
+
if (URI_SCHEME_REGEX.test(path)) return false;
|
|
127
|
+
return path.includes("/") || path.includes("\\");
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
export function extractBracketedImagePastePaths(data: string): string[] | undefined {
|
|
121
131
|
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
122
132
|
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
@@ -138,7 +148,7 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi
|
|
|
138
148
|
if (boundaryEnd === undefined) continue;
|
|
139
149
|
|
|
140
150
|
const path = normalizePastedImagePath(pasted.slice(segmentStart, boundaryEnd));
|
|
141
|
-
if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path)) return undefined;
|
|
151
|
+
if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path) || !isExplicitPastedImagePath(path)) return undefined;
|
|
142
152
|
paths.push(path);
|
|
143
153
|
|
|
144
154
|
segmentStart = boundaryEnd;
|
|
@@ -163,6 +173,24 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
|
|
163
173
|
export class CustomEditor extends Editor {
|
|
164
174
|
imageLinks?: readonly (string | undefined)[];
|
|
165
175
|
|
|
176
|
+
/** Draft images pasted into the composer, consumed on submit. Co-located with
|
|
177
|
+
* {@link imageLinks} so every piece of draft-image state lives on the editor. */
|
|
178
|
+
pendingImages: ImageContent[] = [];
|
|
179
|
+
/** Per-image source links (file:// targets) parallel to {@link pendingImages};
|
|
180
|
+
* `undefined` entries are images without a backing reference yet. */
|
|
181
|
+
pendingImageLinks: (string | undefined)[] = [];
|
|
182
|
+
|
|
183
|
+
/** Clear the composer draft: optionally commit `historyText` to history, then
|
|
184
|
+
* reset the editor text and all pending draft-image state. The shared tail of
|
|
185
|
+
* every "message submitted" path; pass no argument for a plain discard. */
|
|
186
|
+
clearDraft(historyText?: string): void {
|
|
187
|
+
if (historyText !== undefined) this.addToHistory(historyText);
|
|
188
|
+
this.setText("");
|
|
189
|
+
this.imageLinks = undefined;
|
|
190
|
+
this.pendingImages = [];
|
|
191
|
+
this.pendingImageLinks = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
/** Treat image/paste markers as indivisible: a stray backspace deletes the whole token
|
|
167
195
|
* instead of corrupting `[Paste #1, +30 lines]` into plain text. */
|
|
168
196
|
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
|
|
|
@@ -142,7 +142,8 @@ export class FooterComponent implements Component {
|
|
|
142
142
|
// After compaction, tokens are unknown until the next LLM response.
|
|
143
143
|
const contextUsage = this.session.getContextUsage();
|
|
144
144
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
145
|
-
const
|
|
145
|
+
const contextTokens = contextUsage?.tokens ?? 0;
|
|
146
|
+
const contextPercentValue = contextWindow > 0 ? (contextUsage?.percent ?? 0) : null;
|
|
146
147
|
|
|
147
148
|
// Replace home directory with ~
|
|
148
149
|
let pwd = shortenPath(getProjectDir());
|
|
@@ -186,8 +187,8 @@ export class FooterComponent implements Component {
|
|
|
186
187
|
// Colorize context percentage based on usage
|
|
187
188
|
let contextPercentStr: string;
|
|
188
189
|
const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
|
|
189
|
-
const contextPercentDisplay = `${formatContextUsage(contextPercentValue, contextWindow)}${autoIndicator}`;
|
|
190
|
-
if (contextUsage) {
|
|
190
|
+
const contextPercentDisplay = `${formatContextUsage(contextPercentValue, contextWindow, contextTokens)}${autoIndicator}`;
|
|
191
|
+
if (contextUsage && contextPercentValue !== null) {
|
|
191
192
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
192
193
|
contextPercentStr =
|
|
193
194
|
color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
|
|
@@ -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
|
}
|