@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -1
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/config/keybindings.d.ts +10 -2
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +43 -7
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +33 -0
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/args.ts +2 -2
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/gallery-cli.ts +223 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +221 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/config/keybindings.ts +68 -2
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +16 -16
- package/src/config/settings-schema.ts +29 -6
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +43 -55
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +102 -58
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/agent-bridge.ts +38 -12
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/lsp/client.ts +179 -52
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +47 -52
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +22 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +83 -24
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/controllers/command-controller.ts +13 -118
- package/src/modes/controllers/event-controller.ts +26 -10
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +40 -3
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +2 -2
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +13 -9
- package/src/slash-commands/builtin-registry.ts +4 -12
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +37 -11
- package/src/telemetry-export.ts +25 -7
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +28 -10
- package/src/tools/eval.ts +19 -23
- package/src/tools/fetch.ts +99 -89
- package/src/tools/read.ts +7 -7
- package/src/tools/render-utils.ts +63 -3
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +173 -81
- package/src/tools/ssh.ts +21 -8
- package/src/tools/todo.ts +20 -7
- package/src/tools/write.ts +39 -9
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/output-block.ts +14 -0
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/render.ts +42 -57
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
- /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
matchesKey,
|
|
28
28
|
padding,
|
|
29
29
|
replaceTabs,
|
|
30
|
+
ScrollView,
|
|
30
31
|
Spacer,
|
|
31
32
|
Text,
|
|
32
33
|
truncateToWidth,
|
|
@@ -205,9 +206,12 @@ class AgentListPane implements Component {
|
|
|
205
206
|
return lines;
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
const overflow = this.agents.length > this.maxVisible;
|
|
210
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
208
211
|
const start = this.scrollOffset;
|
|
209
212
|
const end = Math.min(start + this.maxVisible, this.agents.length);
|
|
210
213
|
|
|
214
|
+
const rows: string[] = [];
|
|
211
215
|
for (let i = start; i < end; i++) {
|
|
212
216
|
const agent = this.agents[i];
|
|
213
217
|
const selected = i === this.selectedIndex;
|
|
@@ -224,12 +228,17 @@ class AgentListPane implements Component {
|
|
|
224
228
|
line = theme.fg("dim", line);
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
|
|
231
|
+
rows.push(truncateToWidth(line, rowWidth));
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
const sv = new ScrollView(rows, {
|
|
235
|
+
height: rows.length,
|
|
236
|
+
scrollbar: "auto",
|
|
237
|
+
totalRows: this.agents.length,
|
|
238
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
239
|
+
});
|
|
240
|
+
sv.setScrollOffset(this.scrollOffset);
|
|
241
|
+
lines.push(...sv.render(width));
|
|
233
242
|
|
|
234
243
|
return lines;
|
|
235
244
|
}
|
|
@@ -18,6 +18,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
18
18
|
#convertedKittyImages = new Map<string, ImageContent>();
|
|
19
19
|
#kittyConversionsInFlight = new Set<string>();
|
|
20
20
|
#transcriptBlockFinalized: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* When true, the turn-ending `Error: …` line for `stopReason === "error"` is
|
|
23
|
+
* suppressed because the same error is currently shown in the pinned banner
|
|
24
|
+
* above the editor (see `EventController` + `ErrorBannerComponent`). Avoids
|
|
25
|
+
* rendering the identical error twice (inline + banner) at the error moment.
|
|
26
|
+
* Restored to `false` when the banner is cleared at the next turn so the
|
|
27
|
+
* transcript keeps the error in history.
|
|
28
|
+
*/
|
|
29
|
+
#errorPinned = false;
|
|
21
30
|
|
|
22
31
|
constructor(
|
|
23
32
|
message?: AssistantMessage,
|
|
@@ -49,6 +58,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
49
58
|
this.hideThinkingBlock = hide;
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Toggle suppression of the inline `Error: …` line while the same error is
|
|
63
|
+
* pinned in the banner above the editor. Re-renders so the change is visible.
|
|
64
|
+
*/
|
|
65
|
+
setErrorPinned(pinned: boolean): void {
|
|
66
|
+
if (this.#errorPinned === pinned) return;
|
|
67
|
+
this.#errorPinned = pinned;
|
|
68
|
+
if (this.#lastMessage) {
|
|
69
|
+
this.updateContent(this.#lastMessage);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
isTranscriptBlockFinalized(): boolean {
|
|
53
74
|
return this.#transcriptBlockFinalized;
|
|
54
75
|
}
|
|
@@ -246,7 +267,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
246
267
|
this.#contentContainer.addChild(new Spacer(1));
|
|
247
268
|
}
|
|
248
269
|
this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
249
|
-
} else if (message.stopReason === "error") {
|
|
270
|
+
} else if (message.stopReason === "error" && !this.#errorPinned) {
|
|
250
271
|
const errorMsg = message.errorMessage || "Unknown error";
|
|
251
272
|
this.#contentContainer.addChild(new Spacer(1));
|
|
252
273
|
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { type Component, matchesKey, padding, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { replaceTabs } from "../../tools/render-utils";
|
|
3
|
+
import { highlightCode, theme } from "../theme/theme";
|
|
4
|
+
import type { CopyTarget } from "../utils/copy-targets";
|
|
5
|
+
import {
|
|
6
|
+
matchesSelectCancel,
|
|
7
|
+
matchesSelectDown,
|
|
8
|
+
matchesSelectPageDown,
|
|
9
|
+
matchesSelectPageUp,
|
|
10
|
+
matchesSelectUp,
|
|
11
|
+
} from "../utils/keybinding-matchers";
|
|
12
|
+
import { keyHint, rawKeyHint } from "./keybinding-hints";
|
|
13
|
+
|
|
14
|
+
/** Minimum rows reserved for the tree even on short terminals. */
|
|
15
|
+
const MIN_TREE_ROWS = 3;
|
|
16
|
+
/** Fixed chrome rows: top border, two dividers, footer, bottom border. */
|
|
17
|
+
const CHROME_ROWS = 5;
|
|
18
|
+
|
|
19
|
+
export interface CopySelectorCallbacks {
|
|
20
|
+
/** A copy target was chosen — copy its `content`. */
|
|
21
|
+
onPick: (target: CopyTarget) => void;
|
|
22
|
+
/** The picker was dismissed. */
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FlatNode {
|
|
27
|
+
target: CopyTarget;
|
|
28
|
+
depth: number;
|
|
29
|
+
/** Last among its siblings (drives └─ vs ├─). */
|
|
30
|
+
isLast: boolean;
|
|
31
|
+
/** Per-ancestor flag: does ancestor at that level have a following sibling? */
|
|
32
|
+
ancestorHasNext: boolean[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
36
|
+
function fit(text: string, width: number): string {
|
|
37
|
+
if (width <= 0) return "";
|
|
38
|
+
const w = visibleWidth(text);
|
|
39
|
+
if (w === width) return text;
|
|
40
|
+
if (w < width) return text + padding(width - w);
|
|
41
|
+
const cut = truncateToWidth(text, width);
|
|
42
|
+
const cw = visibleWidth(cut);
|
|
43
|
+
return cw < width ? cut + padding(width - cw) : cut;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function paint(s: string): string {
|
|
47
|
+
return theme.fg("border", s);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function topBorder(width: number, title: string): string {
|
|
51
|
+
const box = theme.boxSharp;
|
|
52
|
+
const inner = Math.max(0, width - 2);
|
|
53
|
+
if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
54
|
+
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
55
|
+
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
56
|
+
return (
|
|
57
|
+
paint(box.topLeft + box.horizontal) +
|
|
58
|
+
theme.bold(theme.fg("accent", shown)) +
|
|
59
|
+
paint(box.horizontal.repeat(fillWidth) + box.topRight)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function divider(width: number): string {
|
|
64
|
+
const box = theme.boxSharp;
|
|
65
|
+
return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function bottomBorder(width: number): string {
|
|
69
|
+
const box = theme.boxSharp;
|
|
70
|
+
return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
74
|
+
function row(content: string, width: number): string {
|
|
75
|
+
const box = theme.boxSharp;
|
|
76
|
+
return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Render one tree connector as exactly three cells (e.g. "├─ ", "└─ ", "|--"). */
|
|
80
|
+
function connectorCells(symbol: string): string {
|
|
81
|
+
const chars = Array.from(symbol);
|
|
82
|
+
return (chars[0] ?? " ") + (chars[1] ?? theme.tree.horizontal) + (chars[2] ?? " ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The 3-cell ancestor gutter: a vertical guide when the ancestor continues. */
|
|
86
|
+
function gutterCells(hasNext: boolean): string {
|
|
87
|
+
return `${hasNext ? theme.tree.vertical : " "} `;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fullscreen `/copy` picker rendered as a `/tree`-style tree inside one
|
|
92
|
+
* outlined box: a title, the tree of copy targets (recent assistant messages
|
|
93
|
+
* with their code blocks nested beneath), a live preview of the highlighted
|
|
94
|
+
* node, and a keybinding footer. Every node copies its `content` on Enter.
|
|
95
|
+
*/
|
|
96
|
+
export class CopySelectorComponent implements Component {
|
|
97
|
+
#roots: CopyTarget[];
|
|
98
|
+
#cursorId: string;
|
|
99
|
+
#treeRows = MIN_TREE_ROWS;
|
|
100
|
+
// Reused across renders to wrap preview content to the pane width.
|
|
101
|
+
#previewText = new Text("", 0, 0);
|
|
102
|
+
|
|
103
|
+
constructor(
|
|
104
|
+
roots: CopyTarget[],
|
|
105
|
+
private readonly callbacks: CopySelectorCallbacks,
|
|
106
|
+
) {
|
|
107
|
+
this.#roots = roots;
|
|
108
|
+
this.#cursorId = roots[0]?.id ?? "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
invalidate(): void {}
|
|
112
|
+
|
|
113
|
+
#flatten(): FlatNode[] {
|
|
114
|
+
const out: FlatNode[] = [];
|
|
115
|
+
const walk = (nodes: CopyTarget[], depth: number, ancestorHasNext: boolean[]) => {
|
|
116
|
+
nodes.forEach((target, i) => {
|
|
117
|
+
const isLast = i === nodes.length - 1;
|
|
118
|
+
out.push({ target, depth, isLast, ancestorHasNext });
|
|
119
|
+
if (target.children?.length) walk(target.children, depth + 1, [...ancestorHasNext, !isLast]);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
walk(this.#roots, 0, []);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleInput(keyData: string): void {
|
|
127
|
+
if (matchesSelectCancel(keyData)) {
|
|
128
|
+
this.callbacks.onCancel();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const flat = this.#flatten();
|
|
133
|
+
if (flat.length === 0) return;
|
|
134
|
+
const idx = Math.max(
|
|
135
|
+
0,
|
|
136
|
+
flat.findIndex(n => n.target.id === this.#cursorId),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (matchesSelectUp(keyData)) {
|
|
140
|
+
this.#cursorId = flat[idx === 0 ? flat.length - 1 : idx - 1]!.target.id;
|
|
141
|
+
} else if (matchesSelectDown(keyData)) {
|
|
142
|
+
this.#cursorId = flat[idx === flat.length - 1 ? 0 : idx + 1]!.target.id;
|
|
143
|
+
} else if (matchesSelectPageUp(keyData)) {
|
|
144
|
+
this.#cursorId = flat[Math.max(0, idx - this.#treeRows)]!.target.id;
|
|
145
|
+
} else if (matchesSelectPageDown(keyData)) {
|
|
146
|
+
this.#cursorId = flat[Math.min(flat.length - 1, idx + this.#treeRows)]!.target.id;
|
|
147
|
+
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
148
|
+
const target = flat[idx]!.target;
|
|
149
|
+
if (target.content !== undefined) this.callbacks.onPick(target);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#renderTree(width: number, flat: FlatNode[], cursorIdx: number, rows: number): string[] {
|
|
154
|
+
const inner = Math.max(0, width - 4);
|
|
155
|
+
const start = Math.max(0, Math.min(cursorIdx - Math.floor(rows / 2), Math.max(0, flat.length - rows)));
|
|
156
|
+
const out: string[] = [];
|
|
157
|
+
for (let r = 0; r < rows; r++) {
|
|
158
|
+
const i = start + r;
|
|
159
|
+
const node = flat[i];
|
|
160
|
+
if (!node) {
|
|
161
|
+
out.push(row("", width));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const target = node.target;
|
|
165
|
+
const isSelected = i === cursorIdx;
|
|
166
|
+
|
|
167
|
+
let prefix = "";
|
|
168
|
+
for (let l = 0; l < node.depth - 1; l++) prefix += gutterCells(node.ancestorHasNext[l]!);
|
|
169
|
+
if (node.depth > 0) prefix += connectorCells(node.isLast ? theme.tree.last : theme.tree.branch);
|
|
170
|
+
|
|
171
|
+
const cursor = isSelected ? "❯ " : " ";
|
|
172
|
+
const hint = target.hint ?? "";
|
|
173
|
+
const hintWidth = hint ? visibleWidth(hint) + 2 : 0;
|
|
174
|
+
const used = visibleWidth(cursor) + visibleWidth(prefix);
|
|
175
|
+
const labelPlain = truncateToWidth(target.label, Math.max(1, inner - used - hintWidth));
|
|
176
|
+
const left = isSelected
|
|
177
|
+
? theme.fg("accent", cursor) + theme.fg("dim", prefix) + theme.bold(theme.fg("accent", labelPlain))
|
|
178
|
+
: cursor + theme.fg("dim", prefix) + labelPlain;
|
|
179
|
+
const gap = Math.max(1, inner - used - visibleWidth(labelPlain) - visibleWidth(hint));
|
|
180
|
+
out.push(row(left + padding(gap) + (hint ? theme.fg("dim", hint) : ""), width));
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#renderPreview(width: number, target: CopyTarget | undefined, rows: number): string[] {
|
|
186
|
+
const out: string[] = [];
|
|
187
|
+
const hint = target?.hint;
|
|
188
|
+
out.push(row(theme.fg("dim", `Preview${hint ? ` · ${hint}` : ""}`), width));
|
|
189
|
+
|
|
190
|
+
const contentRows = rows - 1;
|
|
191
|
+
if (!target || contentRows <= 0) {
|
|
192
|
+
while (out.length < rows) out.push(row("", width));
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Code/command previews are syntax-highlighted; everything else is shown
|
|
197
|
+
// as plain text. Both are wrapped (not hard-truncated) to the pane width.
|
|
198
|
+
const isCode = target.language !== undefined;
|
|
199
|
+
const source = isCode
|
|
200
|
+
? highlightCode(replaceTabs(target.preview), target.language).join("\n")
|
|
201
|
+
: replaceTabs(target.preview);
|
|
202
|
+
this.#previewText.setText(source);
|
|
203
|
+
const wrapped = this.#previewText.render(Math.max(1, width - 4));
|
|
204
|
+
|
|
205
|
+
const hasMore = wrapped.length > contentRows;
|
|
206
|
+
const visibleCount = hasMore ? contentRows - 1 : Math.min(wrapped.length, contentRows);
|
|
207
|
+
for (let k = 0; k < contentRows; k++) {
|
|
208
|
+
if (k < visibleCount) {
|
|
209
|
+
out.push(row(isCode ? wrapped[k]! : theme.fg("muted", wrapped[k]!), width));
|
|
210
|
+
} else if (k === visibleCount && hasMore) {
|
|
211
|
+
out.push(row(theme.fg("dim", `… ${wrapped.length - visibleCount} more lines`), width));
|
|
212
|
+
} else {
|
|
213
|
+
out.push(row("", width));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
render(width: number): string[] {
|
|
220
|
+
const height = process.stdout.rows || 40;
|
|
221
|
+
const flat = this.#flatten();
|
|
222
|
+
const cursorIdx = Math.max(
|
|
223
|
+
0,
|
|
224
|
+
flat.findIndex(n => n.target.id === this.#cursorId),
|
|
225
|
+
);
|
|
226
|
+
const selected = flat[cursorIdx]?.target;
|
|
227
|
+
|
|
228
|
+
const available = Math.max(MIN_TREE_ROWS + 1, height - CHROME_ROWS);
|
|
229
|
+
const treeRows = Math.max(1, Math.min(flat.length, Math.floor(available / 2)));
|
|
230
|
+
this.#treeRows = treeRows;
|
|
231
|
+
const previewRows = Math.max(1, available - treeRows);
|
|
232
|
+
|
|
233
|
+
const footer = [
|
|
234
|
+
rawKeyHint("↑↓", "move"),
|
|
235
|
+
keyHint("tui.select.confirm", "copy"),
|
|
236
|
+
keyHint("tui.select.cancel", "quit"),
|
|
237
|
+
].join(theme.fg("dim", " · "));
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
topBorder(width, "Copy to clipboard"),
|
|
241
|
+
...this.#renderTree(width, flat, cursorIdx, treeRows),
|
|
242
|
+
divider(width),
|
|
243
|
+
...this.#renderPreview(width, selected, previewRows),
|
|
244
|
+
divider(width),
|
|
245
|
+
row(footer, width),
|
|
246
|
+
bottomBorder(width),
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -10,6 +10,7 @@ type ConfigurableEditorAction = Extract<
|
|
|
10
10
|
| "app.clear"
|
|
11
11
|
| "app.exit"
|
|
12
12
|
| "app.suspend"
|
|
13
|
+
| "app.display.reset"
|
|
13
14
|
| "app.thinking.cycle"
|
|
14
15
|
| "app.model.cycleForward"
|
|
15
16
|
| "app.model.cycleBackward"
|
|
@@ -30,10 +31,11 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
30
31
|
"app.clear": ["ctrl+c"],
|
|
31
32
|
"app.exit": ["ctrl+d"],
|
|
32
33
|
"app.suspend": ["ctrl+z"],
|
|
34
|
+
"app.display.reset": ["ctrl+l"],
|
|
33
35
|
"app.thinking.cycle": ["shift+tab"],
|
|
34
36
|
"app.model.cycleForward": ["ctrl+p"],
|
|
35
37
|
"app.model.cycleBackward": ["shift+ctrl+p"],
|
|
36
|
-
"app.model.select": ["
|
|
38
|
+
"app.model.select": ["alt+m"],
|
|
37
39
|
"app.model.selectTemporary": ["alt+p"],
|
|
38
40
|
"app.tools.expand": ["ctrl+o"],
|
|
39
41
|
"app.thinking.toggle": ["ctrl+t"],
|
|
@@ -65,6 +67,7 @@ export class CustomEditor extends Editor {
|
|
|
65
67
|
onEscape?: () => void;
|
|
66
68
|
onClear?: () => void;
|
|
67
69
|
onExit?: () => void;
|
|
70
|
+
onDisplayReset?: () => void;
|
|
68
71
|
onCycleThinkingLevel?: () => void;
|
|
69
72
|
onCycleModelForward?: () => void;
|
|
70
73
|
onCycleModelBackward?: () => void;
|
|
@@ -158,6 +161,12 @@ export class CustomEditor extends Editor {
|
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
// Intercept configured display reset shortcut
|
|
165
|
+
if (this.#matchesAction(data, "app.display.reset") && this.onDisplayReset) {
|
|
166
|
+
this.onDisplayReset();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
// Intercept configured suspend shortcut
|
|
162
171
|
if (this.#matchesAction(data, "app.suspend") && this.onSuspend) {
|
|
163
172
|
this.onSuspend();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
extractPrintableText,
|
|
11
11
|
matchesKey,
|
|
12
12
|
padding,
|
|
13
|
+
ScrollView,
|
|
13
14
|
truncateToWidth,
|
|
14
15
|
visibleWidth,
|
|
15
16
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -134,25 +135,33 @@ export class ExtensionList implements Component {
|
|
|
134
135
|
const startIdx = this.#scrollOffset;
|
|
135
136
|
const endIdx = Math.min(startIdx + this.#maxVisible, this.#listItems.length);
|
|
136
137
|
|
|
138
|
+
// Reserve the rightmost column for the scrollbar when overflowing
|
|
139
|
+
const overflow = this.#listItems.length > this.#maxVisible;
|
|
140
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
141
|
+
|
|
137
142
|
// Render visible items
|
|
143
|
+
const rows: string[] = [];
|
|
138
144
|
for (let i = startIdx; i < endIdx; i++) {
|
|
139
145
|
const listItem = this.#listItems[i];
|
|
140
146
|
const isSelected = this.#focused && i === this.#selectedIndex;
|
|
141
147
|
|
|
142
148
|
if (listItem.type === "master") {
|
|
143
|
-
|
|
149
|
+
rows.push(this.#renderMasterSwitch(listItem, isSelected, rowWidth));
|
|
144
150
|
} else if (listItem.type === "kind-header") {
|
|
145
|
-
|
|
151
|
+
rows.push(this.#renderKindHeader(listItem, isSelected, rowWidth));
|
|
146
152
|
} else {
|
|
147
|
-
|
|
153
|
+
rows.push(this.#renderExtensionRow(listItem.item, isSelected, rowWidth, masterDisabled));
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
const sv = new ScrollView(rows, {
|
|
158
|
+
height: rows.length,
|
|
159
|
+
scrollbar: "auto",
|
|
160
|
+
totalRows: this.#listItems.length,
|
|
161
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
162
|
+
});
|
|
163
|
+
sv.setScrollOffset(this.#scrollOffset);
|
|
164
|
+
lines.push(...sv.render(width));
|
|
156
165
|
|
|
157
166
|
return lines;
|
|
158
167
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Input,
|
|
6
6
|
matchesKey,
|
|
7
7
|
padding,
|
|
8
|
+
ScrollView,
|
|
8
9
|
Spacer,
|
|
9
10
|
Text,
|
|
10
11
|
truncateToWidth,
|
|
@@ -115,15 +116,19 @@ class HistoryResultsList implements Component {
|
|
|
115
116
|
);
|
|
116
117
|
const endIndex = Math.min(startIndex + this.#maxVisible, this.#results.length);
|
|
117
118
|
|
|
119
|
+
const overflow = this.#results.length > this.#maxVisible;
|
|
120
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
121
|
+
const rows: string[] = [];
|
|
122
|
+
|
|
118
123
|
for (let i = startIndex; i < endIndex; i++) {
|
|
119
124
|
const entry = this.#results[i];
|
|
120
125
|
const isSelected = i === this.#selectedIndex;
|
|
121
126
|
|
|
122
127
|
const timeStr = relativeTime(entry.created_at);
|
|
123
128
|
const timeWidth = visibleWidth(timeStr);
|
|
124
|
-
const showTime =
|
|
129
|
+
const showTime = rowWidth >= gutterWidth + 12 + timeWidth;
|
|
125
130
|
|
|
126
|
-
const promptBudget = Math.max(4,
|
|
131
|
+
const promptBudget = Math.max(4, rowWidth - gutterWidth - (showTime ? timeWidth + 1 : 0));
|
|
127
132
|
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|
|
128
133
|
const plain = truncateToWidth(normalized, promptBudget);
|
|
129
134
|
const highlighted = highlightTokens(plain, this.#tokens);
|
|
@@ -133,21 +138,24 @@ class HistoryResultsList implements Component {
|
|
|
133
138
|
|
|
134
139
|
if (showTime) {
|
|
135
140
|
// Pad the prompt region so the timestamp sits flush right with a one-cell gap.
|
|
136
|
-
line = `${truncateToWidth(line,
|
|
141
|
+
line = `${truncateToWidth(line, rowWidth - timeWidth - 1, Ellipsis.Unicode, true)} ${theme.fg("dim", timeStr)}`;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
|
|
144
|
+
rows.push(
|
|
140
145
|
isSelected
|
|
141
|
-
? theme.bg("selectedBg", truncateToWidth(line,
|
|
142
|
-
: truncateToWidth(line,
|
|
146
|
+
? theme.bg("selectedBg", truncateToWidth(line, rowWidth, Ellipsis.Omit, true))
|
|
147
|
+
: truncateToWidth(line, rowWidth),
|
|
143
148
|
);
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
const sv = new ScrollView(rows, {
|
|
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));
|
|
151
159
|
return lines;
|
|
152
160
|
}
|
|
153
161
|
}
|