@oh-my-pi/pi-coding-agent 11.8.2 → 11.8.3
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/docs/tui.md +9 -9
- package/package.json +7 -7
- package/src/cli/file-processor.ts +8 -13
- package/src/cli/oclif-help.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/commit/git/index.ts +16 -16
- package/src/config/keybindings.ts +11 -11
- package/src/config/model-registry.ts +31 -66
- package/src/config/settings.ts +88 -95
- package/src/config.ts +2 -2
- package/src/cursor.ts +4 -4
- package/src/debug/index.ts +28 -28
- package/src/discovery/codex.ts +5 -13
- package/src/discovery/cursor.ts +2 -7
- package/src/exa/mcp-client.ts +2 -2
- package/src/exa/websets.ts +2 -2
- package/src/export/html/index.ts +3 -3
- package/src/export/ttsr.ts +27 -27
- package/src/extensibility/custom-tools/loader.ts +9 -9
- package/src/extensibility/extensions/runner.ts +64 -64
- package/src/extensibility/hooks/runner.ts +46 -46
- package/src/extensibility/plugins/manager.ts +49 -49
- package/src/index.ts +0 -1
- package/src/internal-urls/router.ts +5 -5
- package/src/ipy/kernel.ts +61 -57
- package/src/lsp/client.ts +1 -1
- package/src/lsp/clients/biome-client.ts +2 -2
- package/src/lsp/clients/lsp-linter-client.ts +7 -7
- package/src/lsp/index.ts +9 -9
- package/src/mcp/manager.ts +47 -47
- package/src/mcp/tool-bridge.ts +12 -12
- package/src/mcp/transports/http.ts +34 -34
- package/src/mcp/transports/stdio.ts +47 -47
- package/src/modes/components/assistant-message.ts +25 -25
- package/src/modes/components/bash-execution.ts +51 -51
- package/src/modes/components/bordered-loader.ts +7 -7
- package/src/modes/components/branch-summary-message.ts +7 -7
- package/src/modes/components/compaction-summary-message.ts +7 -7
- package/src/modes/components/countdown-timer.ts +15 -15
- package/src/modes/components/custom-editor.ts +22 -22
- package/src/modes/components/custom-message.ts +21 -21
- package/src/modes/components/dynamic-border.ts +3 -3
- package/src/modes/components/extensions/extension-dashboard.ts +72 -72
- package/src/modes/components/extensions/extension-list.ts +99 -97
- package/src/modes/components/extensions/inspector-panel.ts +26 -26
- package/src/modes/components/footer.ts +36 -36
- package/src/modes/components/history-search.ts +52 -52
- package/src/modes/components/hook-editor.ts +20 -20
- package/src/modes/components/hook-input.ts +20 -20
- package/src/modes/components/hook-message.ts +22 -22
- package/src/modes/components/hook-selector.ts +52 -52
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/login-dialog.ts +57 -57
- package/src/modes/components/model-selector.ts +173 -173
- package/src/modes/components/oauth-selector.ts +45 -45
- package/src/modes/components/plugin-settings.ts +52 -52
- package/src/modes/components/python-execution.ts +53 -53
- package/src/modes/components/queue-mode-selector.ts +7 -7
- package/src/modes/components/read-tool-group.ts +23 -23
- package/src/modes/components/session-selector.ts +40 -37
- package/src/modes/components/settings-selector.ts +80 -80
- package/src/modes/components/show-images-selector.ts +7 -7
- package/src/modes/components/skill-message.ts +27 -27
- package/src/modes/components/status-line-segment-editor.ts +81 -81
- package/src/modes/components/status-line.ts +73 -73
- package/src/modes/components/theme-selector.ts +11 -11
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/todo-display.ts +19 -19
- package/src/modes/components/todo-reminder.ts +9 -9
- package/src/modes/components/tool-execution.ts +204 -196
- package/src/modes/components/tree-selector.ts +144 -144
- package/src/modes/components/ttsr-notification.ts +17 -17
- package/src/modes/components/user-message-selector.ts +18 -18
- package/src/modes/components/welcome.ts +10 -10
- package/src/modes/controllers/command-controller.ts +0 -7
- package/src/modes/controllers/event-controller.ts +23 -23
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +4 -9
- package/src/modes/interactive-mode.ts +234 -241
- package/src/modes/rpc/rpc-client.ts +77 -77
- package/src/modes/rpc/rpc-mode.ts +5 -5
- package/src/modes/theme/theme.ts +113 -113
- package/src/modes/types.ts +0 -1
- package/src/patch/index.ts +45 -45
- package/src/prompts/tools/task.md +22 -2
- package/src/session/agent-session.ts +463 -476
- package/src/session/agent-storage.ts +72 -75
- package/src/session/auth-storage.ts +186 -252
- package/src/session/history-storage.ts +36 -38
- package/src/session/session-manager.ts +300 -299
- package/src/session/session-storage.ts +65 -90
- package/src/ssh/connection-manager.ts +9 -9
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +2 -2
- package/src/task/index.ts +13 -12
- package/src/task/subprocess-tool-registry.ts +5 -5
- package/src/tools/ask.ts +7 -7
- package/src/tools/bash.ts +8 -7
- package/src/tools/browser.ts +123 -123
- package/src/tools/calculator.ts +46 -46
- package/src/tools/context.ts +9 -9
- package/src/tools/exit-plan-mode.ts +5 -5
- package/src/tools/fetch.ts +5 -5
- package/src/tools/find.ts +16 -16
- package/src/tools/grep.ts +10 -10
- package/src/tools/notebook.ts +6 -6
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/python.ts +12 -11
- package/src/tools/read.ts +17 -17
- package/src/tools/ssh.ts +9 -9
- package/src/tools/submit-result.ts +13 -13
- package/src/tools/todo-write.ts +6 -6
- package/src/tools/write.ts +10 -10
- package/src/tui/output-block.ts +6 -6
- package/src/tui/utils.ts +9 -9
- package/src/utils/event-bus.ts +10 -10
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/ignore-files.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/providers/anthropic.ts +7 -2
- package/examples/hooks/snake.ts +0 -342
- package/src/modes/components/armin.ts +0 -379
|
@@ -47,19 +47,19 @@ interface ToolCallInfo {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
class TreeList implements Component {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
#flatNodes: FlatNode[] = [];
|
|
51
|
+
#filteredNodes: FlatNode[] = [];
|
|
52
|
+
#selectedIndex = 0;
|
|
53
|
+
#filterMode: FilterMode = "default";
|
|
54
|
+
#searchQuery = "";
|
|
55
|
+
#toolCallMap: Map<string, ToolCallInfo> = new Map();
|
|
56
|
+
#multipleRoots = false;
|
|
57
|
+
#activePathIds: Set<string> = new Set();
|
|
58
|
+
#lastSelectedId: string | null = null;
|
|
59
|
+
|
|
60
|
+
onSelect?: (entryId: string) => void;
|
|
61
|
+
onCancel?: () => void;
|
|
62
|
+
onLabelEdit?: (entryId: string, currentLabel: string | undefined) => void;
|
|
63
63
|
|
|
64
64
|
constructor(
|
|
65
65
|
tree: SessionTreeNode[],
|
|
@@ -67,32 +67,32 @@ class TreeList implements Component {
|
|
|
67
67
|
private readonly maxVisibleLines: number,
|
|
68
68
|
initialSelectedId?: string,
|
|
69
69
|
) {
|
|
70
|
-
this
|
|
71
|
-
this
|
|
72
|
-
this
|
|
73
|
-
this
|
|
70
|
+
this.#multipleRoots = tree.length > 1;
|
|
71
|
+
this.#flatNodes = this.#flattenTree(tree);
|
|
72
|
+
this.#buildActivePath();
|
|
73
|
+
this.#applyFilter();
|
|
74
74
|
|
|
75
75
|
// Start with initialSelectedId if provided, otherwise current leaf
|
|
76
76
|
const targetId = initialSelectedId ?? currentLeafId;
|
|
77
|
-
this
|
|
78
|
-
this
|
|
77
|
+
this.#selectedIndex = this.#findNearestVisibleIndex(targetId);
|
|
78
|
+
this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** Build the set of entry IDs on the path from root to current leaf */
|
|
82
|
-
|
|
83
|
-
this
|
|
82
|
+
#buildActivePath(): void {
|
|
83
|
+
this.#activePathIds.clear();
|
|
84
84
|
if (!this.currentLeafId) return;
|
|
85
85
|
|
|
86
86
|
// Build a map of id -> entry for parent lookup
|
|
87
87
|
const entryMap = new Map<string, FlatNode>();
|
|
88
|
-
for (const flatNode of this
|
|
88
|
+
for (const flatNode of this.#flatNodes) {
|
|
89
89
|
entryMap.set(flatNode.node.entry.id, flatNode);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Walk from leaf to root
|
|
93
93
|
let currentId: string | null = this.currentLeafId;
|
|
94
94
|
while (currentId) {
|
|
95
|
-
this
|
|
95
|
+
this.#activePathIds.add(currentId);
|
|
96
96
|
const node = entryMap.get(currentId);
|
|
97
97
|
if (!node) break;
|
|
98
98
|
currentId = node.node.entry.parentId ?? null;
|
|
@@ -103,17 +103,17 @@ class TreeList implements Component {
|
|
|
103
103
|
* Find the index of the nearest visible entry, walking up the parent chain if needed.
|
|
104
104
|
* Returns the index in filteredNodes, or the last index as fallback.
|
|
105
105
|
*/
|
|
106
|
-
|
|
107
|
-
if (this
|
|
106
|
+
#findNearestVisibleIndex(entryId: string | null): number {
|
|
107
|
+
if (this.#filteredNodes.length === 0) return 0;
|
|
108
108
|
|
|
109
109
|
// Build a map for parent lookup
|
|
110
110
|
const entryMap = new Map<string, FlatNode>();
|
|
111
|
-
for (const flatNode of this
|
|
111
|
+
for (const flatNode of this.#flatNodes) {
|
|
112
112
|
entryMap.set(flatNode.node.entry.id, flatNode);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// Build a map of visible entry IDs to their indices in filteredNodes
|
|
116
|
-
const visibleIdToIndex = new Map<string, number>(this
|
|
116
|
+
const visibleIdToIndex = new Map<string, number>(this.#filteredNodes.map((node, i) => [node.node.entry.id, i]));
|
|
117
117
|
|
|
118
118
|
// Walk from entryId up to root, looking for a visible entry
|
|
119
119
|
let currentId = entryId;
|
|
@@ -126,12 +126,12 @@ class TreeList implements Component {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Fallback: last visible entry
|
|
129
|
-
return this
|
|
129
|
+
return this.#filteredNodes.length - 1;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
#flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
|
133
133
|
const result: FlatNode[] = [];
|
|
134
|
-
this
|
|
134
|
+
this.#toolCallMap.clear();
|
|
135
135
|
|
|
136
136
|
// Indentation rules:
|
|
137
137
|
// - At indent 0: stay at 0 unless parent has >1 children (then +1)
|
|
@@ -191,7 +191,7 @@ class TreeList implements Component {
|
|
|
191
191
|
for (const block of content) {
|
|
192
192
|
if (typeof block === "object" && block !== null && "type" in block && block.type === "toolCall") {
|
|
193
193
|
const tc = block as { id: string; name: string; arguments: Record<string, unknown> };
|
|
194
|
-
this
|
|
194
|
+
this.#toolCallMap.set(tc.id, { name: tc.name, arguments: tc.arguments });
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -235,7 +235,7 @@ class TreeList implements Component {
|
|
|
235
235
|
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
|
236
236
|
// When connector is displayed, add a gutter entry at the connector's position
|
|
237
237
|
// Connector is at position (displayIndent - 1), so gutter should be there too
|
|
238
|
-
const currentDisplayIndent = this
|
|
238
|
+
const currentDisplayIndent = this.#multipleRoots ? Math.max(0, indent - 1) : indent;
|
|
239
239
|
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
|
240
240
|
const childGutters: GutterInfo[] = connectorDisplayed
|
|
241
241
|
? [...gutters, { position: connectorPosition, show: !isLast }]
|
|
@@ -259,16 +259,16 @@ class TreeList implements Component {
|
|
|
259
259
|
return result;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
#applyFilter(): void {
|
|
263
263
|
// Update lastSelectedId only when we have a valid selection (non-empty list)
|
|
264
264
|
// This preserves the selection when switching through empty filter results
|
|
265
|
-
if (this
|
|
266
|
-
this
|
|
265
|
+
if (this.#filteredNodes.length > 0) {
|
|
266
|
+
this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? this.#lastSelectedId;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
const searchTokens = this
|
|
269
|
+
const searchTokens = this.#searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
|
270
270
|
|
|
271
|
-
this
|
|
271
|
+
this.#filteredNodes = this.#flatNodes.filter(flatNode => {
|
|
272
272
|
const entry = flatNode.node.entry;
|
|
273
273
|
const isCurrentLeaf = entry.id === this.currentLeafId;
|
|
274
274
|
|
|
@@ -276,7 +276,7 @@ class TreeList implements Component {
|
|
|
276
276
|
// Always show current leaf so active position is visible
|
|
277
277
|
if (entry.type === "message" && entry.message.role === "assistant" && !isCurrentLeaf) {
|
|
278
278
|
const msg = entry.message as { stopReason?: string; content?: unknown };
|
|
279
|
-
const hasText = this
|
|
279
|
+
const hasText = this.#hasTextContent(msg.content);
|
|
280
280
|
const isErrorOrAborted = msg.stopReason && msg.stopReason !== "stop" && msg.stopReason !== "toolUse";
|
|
281
281
|
// Only hide if no text AND not an error/aborted message
|
|
282
282
|
if (!hasText && !isErrorOrAborted) {
|
|
@@ -293,7 +293,7 @@ class TreeList implements Component {
|
|
|
293
293
|
entry.type === "model_change" ||
|
|
294
294
|
entry.type === "thinking_level_change";
|
|
295
295
|
|
|
296
|
-
switch (this
|
|
296
|
+
switch (this.#filterMode) {
|
|
297
297
|
case "user-only":
|
|
298
298
|
// Just user messages
|
|
299
299
|
passesFilter = entry.type === "message" && entry.message.role === "user";
|
|
@@ -320,7 +320,7 @@ class TreeList implements Component {
|
|
|
320
320
|
|
|
321
321
|
// Apply search filter
|
|
322
322
|
if (searchTokens.length > 0) {
|
|
323
|
-
const nodeText = this
|
|
323
|
+
const nodeText = this.#getSearchableText(flatNode.node).toLowerCase();
|
|
324
324
|
return searchTokens.every(token => nodeText.includes(token));
|
|
325
325
|
}
|
|
326
326
|
|
|
@@ -328,21 +328,21 @@ class TreeList implements Component {
|
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
// Try to preserve cursor on the same node, or find nearest visible ancestor
|
|
331
|
-
if (this
|
|
332
|
-
this
|
|
333
|
-
} else if (this
|
|
331
|
+
if (this.#lastSelectedId) {
|
|
332
|
+
this.#selectedIndex = this.#findNearestVisibleIndex(this.#lastSelectedId);
|
|
333
|
+
} else if (this.#selectedIndex >= this.#filteredNodes.length) {
|
|
334
334
|
// Clamp index if out of bounds
|
|
335
|
-
this
|
|
335
|
+
this.#selectedIndex = Math.max(0, this.#filteredNodes.length - 1);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
// Update lastSelectedId to the actual selection (may have changed due to parent walk)
|
|
339
|
-
if (this
|
|
340
|
-
this
|
|
339
|
+
if (this.#filteredNodes.length > 0) {
|
|
340
|
+
this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? this.#lastSelectedId;
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
/** Get searchable text content from a node */
|
|
345
|
-
|
|
345
|
+
#getSearchableText(node: SessionTreeNode): string {
|
|
346
346
|
const entry = node.entry;
|
|
347
347
|
const parts: string[] = [];
|
|
348
348
|
|
|
@@ -355,7 +355,7 @@ class TreeList implements Component {
|
|
|
355
355
|
const msg = entry.message;
|
|
356
356
|
parts.push(msg.role);
|
|
357
357
|
if ("content" in msg && msg.content) {
|
|
358
|
-
parts.push(this
|
|
358
|
+
parts.push(this.#extractContent(msg.content));
|
|
359
359
|
}
|
|
360
360
|
if (msg.role === "bashExecution") {
|
|
361
361
|
const bashMsg = msg as { command?: string };
|
|
@@ -368,7 +368,7 @@ class TreeList implements Component {
|
|
|
368
368
|
if (typeof entry.content === "string") {
|
|
369
369
|
parts.push(entry.content);
|
|
370
370
|
} else {
|
|
371
|
-
parts.push(this
|
|
371
|
+
parts.push(this.#extractContent(entry.content));
|
|
372
372
|
}
|
|
373
373
|
break;
|
|
374
374
|
}
|
|
@@ -398,15 +398,15 @@ class TreeList implements Component {
|
|
|
398
398
|
invalidate(): void {}
|
|
399
399
|
|
|
400
400
|
getSearchQuery(): string {
|
|
401
|
-
return this
|
|
401
|
+
return this.#searchQuery;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
getSelectedNode(): SessionTreeNode | undefined {
|
|
405
|
-
return this
|
|
405
|
+
return this.#filteredNodes[this.#selectedIndex]?.node;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
updateNodeLabel(entryId: string, label: string | undefined): void {
|
|
409
|
-
for (const flatNode of this
|
|
409
|
+
for (const flatNode of this.#flatNodes) {
|
|
410
410
|
if (flatNode.node.entry.id === entryId) {
|
|
411
411
|
flatNode.node.label = label;
|
|
412
412
|
break;
|
|
@@ -414,8 +414,8 @@ class TreeList implements Component {
|
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
|
|
418
|
-
switch (this
|
|
417
|
+
#getFilterLabel(): string {
|
|
418
|
+
switch (this.#filterMode) {
|
|
419
419
|
case "no-tools":
|
|
420
420
|
return " [no-tools]";
|
|
421
421
|
case "user-only":
|
|
@@ -432,31 +432,31 @@ class TreeList implements Component {
|
|
|
432
432
|
render(width: number): string[] {
|
|
433
433
|
const lines: string[] = [];
|
|
434
434
|
|
|
435
|
-
if (this
|
|
435
|
+
if (this.#filteredNodes.length === 0) {
|
|
436
436
|
lines.push(truncateToWidth(theme.fg("muted", " No entries found"), width));
|
|
437
|
-
lines.push(truncateToWidth(theme.fg("muted", ` (0/0)${this
|
|
437
|
+
lines.push(truncateToWidth(theme.fg("muted", ` (0/0)${this.#getFilterLabel()}`), width));
|
|
438
438
|
return lines;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
const startIndex = Math.max(
|
|
442
442
|
0,
|
|
443
443
|
Math.min(
|
|
444
|
-
this
|
|
445
|
-
this
|
|
444
|
+
this.#selectedIndex - Math.floor(this.maxVisibleLines / 2),
|
|
445
|
+
this.#filteredNodes.length - this.maxVisibleLines,
|
|
446
446
|
),
|
|
447
447
|
);
|
|
448
|
-
const endIndex = Math.min(startIndex + this.maxVisibleLines, this
|
|
448
|
+
const endIndex = Math.min(startIndex + this.maxVisibleLines, this.#filteredNodes.length);
|
|
449
449
|
|
|
450
450
|
for (let i = startIndex; i < endIndex; i++) {
|
|
451
|
-
const flatNode = this
|
|
451
|
+
const flatNode = this.#filteredNodes[i];
|
|
452
452
|
const entry = flatNode.node.entry;
|
|
453
|
-
const isSelected = i === this
|
|
453
|
+
const isSelected = i === this.#selectedIndex;
|
|
454
454
|
|
|
455
455
|
// Build line: cursor + prefix + path marker + label + content
|
|
456
456
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
457
457
|
|
|
458
458
|
// If multiple roots, shift display (roots at 0, not 1)
|
|
459
|
-
const displayIndent = this
|
|
459
|
+
const displayIndent = this.#multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
|
|
460
460
|
|
|
461
461
|
// Build prefix with gutters at their correct positions
|
|
462
462
|
// Each gutter has a position (displayIndent where its connector was shown)
|
|
@@ -496,11 +496,11 @@ class TreeList implements Component {
|
|
|
496
496
|
const prefix = prefixChars.join("");
|
|
497
497
|
|
|
498
498
|
// Active path marker - shown right before the entry text
|
|
499
|
-
const isOnActivePath = this
|
|
499
|
+
const isOnActivePath = this.#activePathIds.has(entry.id);
|
|
500
500
|
const pathMarker = isOnActivePath ? theme.fg("accent", `${theme.md.bullet} `) : "";
|
|
501
501
|
|
|
502
502
|
const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
|
|
503
|
-
const content = this
|
|
503
|
+
const content = this.#getEntryDisplayText(flatNode.node, isSelected);
|
|
504
504
|
|
|
505
505
|
let line = cursor + theme.fg("dim", prefix) + pathMarker + label + content;
|
|
506
506
|
if (isSelected) {
|
|
@@ -511,7 +511,7 @@ class TreeList implements Component {
|
|
|
511
511
|
|
|
512
512
|
lines.push(
|
|
513
513
|
truncateToWidth(
|
|
514
|
-
theme.fg("muted", ` (${this
|
|
514
|
+
theme.fg("muted", ` (${this.#selectedIndex + 1}/${this.#filteredNodes.length})${this.#getFilterLabel()}`),
|
|
515
515
|
width,
|
|
516
516
|
),
|
|
517
517
|
);
|
|
@@ -519,7 +519,7 @@ class TreeList implements Component {
|
|
|
519
519
|
return lines;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
#getEntryDisplayText(node: SessionTreeNode, isSelected: boolean): string {
|
|
523
523
|
const entry = node.entry;
|
|
524
524
|
let result: string;
|
|
525
525
|
|
|
@@ -531,11 +531,11 @@ class TreeList implements Component {
|
|
|
531
531
|
const role = msg.role;
|
|
532
532
|
if (role === "user") {
|
|
533
533
|
const msgWithContent = msg as { content?: unknown };
|
|
534
|
-
const content = normalize(this
|
|
534
|
+
const content = normalize(this.#extractContent(msgWithContent.content));
|
|
535
535
|
result = theme.fg("accent", "user: ") + content;
|
|
536
536
|
} else if (role === "assistant") {
|
|
537
537
|
const msgWithContent = msg as { content?: unknown; stopReason?: string; errorMessage?: string };
|
|
538
|
-
const textContent = normalize(this
|
|
538
|
+
const textContent = normalize(this.#extractContent(msgWithContent.content));
|
|
539
539
|
if (textContent) {
|
|
540
540
|
result = theme.fg("success", "assistant: ") + textContent;
|
|
541
541
|
} else if (msgWithContent.stopReason === "aborted") {
|
|
@@ -548,9 +548,9 @@ class TreeList implements Component {
|
|
|
548
548
|
}
|
|
549
549
|
} else if (role === "toolResult") {
|
|
550
550
|
const toolMsg = msg as { toolCallId?: string; toolName?: string };
|
|
551
|
-
const toolCall = toolMsg.toolCallId ? this
|
|
551
|
+
const toolCall = toolMsg.toolCallId ? this.#toolCallMap.get(toolMsg.toolCallId) : undefined;
|
|
552
552
|
if (toolCall) {
|
|
553
|
-
result = theme.fg("muted", this
|
|
553
|
+
result = theme.fg("muted", this.#formatToolCall(toolCall.name, toolCall.arguments));
|
|
554
554
|
} else {
|
|
555
555
|
result = theme.fg("muted", `[${toolMsg.toolName ?? "tool"}]`);
|
|
556
556
|
}
|
|
@@ -600,7 +600,7 @@ class TreeList implements Component {
|
|
|
600
600
|
return isSelected ? theme.bold(result) : result;
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
-
|
|
603
|
+
#extractContent(content: unknown): string {
|
|
604
604
|
const maxLen = 200;
|
|
605
605
|
if (typeof content === "string") return content.slice(0, maxLen);
|
|
606
606
|
if (Array.isArray(content)) {
|
|
@@ -616,7 +616,7 @@ class TreeList implements Component {
|
|
|
616
616
|
return "";
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
#hasTextContent(content: unknown): boolean {
|
|
620
620
|
if (typeof content === "string") return content.trim().length > 0;
|
|
621
621
|
if (Array.isArray(content)) {
|
|
622
622
|
for (const c of content) {
|
|
@@ -629,7 +629,7 @@ class TreeList implements Component {
|
|
|
629
629
|
return false;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
|
|
632
|
+
#formatToolCall(name: string, args: Record<string, unknown>): string {
|
|
633
633
|
switch (name) {
|
|
634
634
|
case "read": {
|
|
635
635
|
const path = shortenPath(String(args.path || args.file_path || ""));
|
|
@@ -683,24 +683,24 @@ class TreeList implements Component {
|
|
|
683
683
|
|
|
684
684
|
handleInput(keyData: string): void {
|
|
685
685
|
if (matchesKey(keyData, "up")) {
|
|
686
|
-
this
|
|
686
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredNodes.length - 1 : this.#selectedIndex - 1;
|
|
687
687
|
} else if (matchesKey(keyData, "down")) {
|
|
688
|
-
this
|
|
688
|
+
this.#selectedIndex = this.#selectedIndex === this.#filteredNodes.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
689
689
|
} else if (matchesKey(keyData, "left")) {
|
|
690
690
|
// Page up
|
|
691
|
-
this
|
|
691
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.maxVisibleLines);
|
|
692
692
|
} else if (matchesKey(keyData, "right")) {
|
|
693
693
|
// Page down
|
|
694
|
-
this
|
|
694
|
+
this.#selectedIndex = Math.min(this.#filteredNodes.length - 1, this.#selectedIndex + this.maxVisibleLines);
|
|
695
695
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
696
|
-
const selected = this
|
|
696
|
+
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
697
697
|
if (selected && this.onSelect) {
|
|
698
698
|
this.onSelect(selected.node.entry.id);
|
|
699
699
|
}
|
|
700
700
|
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
701
|
-
if (this
|
|
702
|
-
this
|
|
703
|
-
this
|
|
701
|
+
if (this.#searchQuery) {
|
|
702
|
+
this.#searchQuery = "";
|
|
703
|
+
this.#applyFilter();
|
|
704
704
|
} else {
|
|
705
705
|
this.onCancel?.();
|
|
706
706
|
}
|
|
@@ -709,37 +709,37 @@ class TreeList implements Component {
|
|
|
709
709
|
} else if (matchesKey(keyData, "shift+ctrl+o") || matchesKey(keyData, "ctrl+shift+o")) {
|
|
710
710
|
// Cycle filter backwards
|
|
711
711
|
const modes: FilterMode[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
|
|
712
|
-
const currentIndex = modes.indexOf(this
|
|
713
|
-
this
|
|
714
|
-
this
|
|
712
|
+
const currentIndex = modes.indexOf(this.#filterMode);
|
|
713
|
+
this.#filterMode = modes[(currentIndex - 1 + modes.length) % modes.length];
|
|
714
|
+
this.#applyFilter();
|
|
715
715
|
} else if (matchesKey(keyData, "ctrl+o")) {
|
|
716
716
|
// Cycle filter forwards: default → no-tools → user-only → labeled-only → all → default
|
|
717
717
|
const modes: FilterMode[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
|
|
718
|
-
const currentIndex = modes.indexOf(this
|
|
719
|
-
this
|
|
720
|
-
this
|
|
718
|
+
const currentIndex = modes.indexOf(this.#filterMode);
|
|
719
|
+
this.#filterMode = modes[(currentIndex + 1) % modes.length];
|
|
720
|
+
this.#applyFilter();
|
|
721
721
|
} else if (matchesKey(keyData, "alt+d")) {
|
|
722
|
-
this
|
|
723
|
-
this
|
|
722
|
+
this.#filterMode = "default";
|
|
723
|
+
this.#applyFilter();
|
|
724
724
|
} else if (matchesKey(keyData, "alt+t")) {
|
|
725
|
-
this
|
|
726
|
-
this
|
|
725
|
+
this.#filterMode = "no-tools";
|
|
726
|
+
this.#applyFilter();
|
|
727
727
|
} else if (matchesKey(keyData, "alt+u")) {
|
|
728
|
-
this
|
|
729
|
-
this
|
|
728
|
+
this.#filterMode = "user-only";
|
|
729
|
+
this.#applyFilter();
|
|
730
730
|
} else if (matchesKey(keyData, "alt+l")) {
|
|
731
|
-
this
|
|
732
|
-
this
|
|
731
|
+
this.#filterMode = "labeled-only";
|
|
732
|
+
this.#applyFilter();
|
|
733
733
|
} else if (matchesKey(keyData, "alt+a")) {
|
|
734
|
-
this
|
|
735
|
-
this
|
|
734
|
+
this.#filterMode = "all";
|
|
735
|
+
this.#applyFilter();
|
|
736
736
|
} else if (matchesKey(keyData, "backspace")) {
|
|
737
|
-
if (this
|
|
738
|
-
this
|
|
739
|
-
this
|
|
737
|
+
if (this.#searchQuery.length > 0) {
|
|
738
|
+
this.#searchQuery = this.#searchQuery.slice(0, -1);
|
|
739
|
+
this.#applyFilter();
|
|
740
740
|
}
|
|
741
|
-
} else if (matchesKey(keyData, "shift+l") && !this
|
|
742
|
-
const selected = this
|
|
741
|
+
} else if (matchesKey(keyData, "shift+l") && !this.#searchQuery) {
|
|
742
|
+
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
743
743
|
if (selected && this.onLabelEdit) {
|
|
744
744
|
this.onLabelEdit(selected.node.entry.id, selected.node.label);
|
|
745
745
|
}
|
|
@@ -749,8 +749,8 @@ class TreeList implements Component {
|
|
|
749
749
|
return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
|
|
750
750
|
});
|
|
751
751
|
if (!hasControlChars && keyData.length > 0) {
|
|
752
|
-
this
|
|
753
|
-
this
|
|
752
|
+
this.#searchQuery += keyData;
|
|
753
|
+
this.#applyFilter();
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
756
|
}
|
|
@@ -775,17 +775,17 @@ class SearchLine implements Component {
|
|
|
775
775
|
|
|
776
776
|
/** Label input component shown when editing a label */
|
|
777
777
|
class LabelInput implements Component {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
778
|
+
#input: Input;
|
|
779
|
+
onSubmit?: (entryId: string, label: string | undefined) => void;
|
|
780
|
+
onCancel?: () => void;
|
|
781
781
|
|
|
782
782
|
constructor(
|
|
783
783
|
private readonly entryId: string,
|
|
784
784
|
currentLabel: string | undefined,
|
|
785
785
|
) {
|
|
786
|
-
this
|
|
786
|
+
this.#input = new Input();
|
|
787
787
|
if (currentLabel) {
|
|
788
|
-
this
|
|
788
|
+
this.#input.setValue(currentLabel);
|
|
789
789
|
}
|
|
790
790
|
}
|
|
791
791
|
|
|
@@ -796,19 +796,19 @@ class LabelInput implements Component {
|
|
|
796
796
|
const indent = " ";
|
|
797
797
|
const availableWidth = width - indent.length;
|
|
798
798
|
lines.push(truncateToWidth(`${indent}${theme.fg("muted", "Label (empty to remove):")}`, width));
|
|
799
|
-
lines.push(...this
|
|
799
|
+
lines.push(...this.#input.render(availableWidth).map(line => truncateToWidth(`${indent}${line}`, width)));
|
|
800
800
|
lines.push(truncateToWidth(`${indent}${theme.fg("dim", "enter: save esc: cancel")}`, width));
|
|
801
801
|
return lines;
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
handleInput(keyData: string): void {
|
|
805
805
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
806
|
-
const value = this
|
|
806
|
+
const value = this.#input.getValue().trim();
|
|
807
807
|
this.onSubmit?.(this.entryId, value || undefined);
|
|
808
808
|
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
809
809
|
this.onCancel?.();
|
|
810
810
|
} else {
|
|
811
|
-
this
|
|
811
|
+
this.#input.handleInput(keyData);
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
814
|
}
|
|
@@ -817,10 +817,10 @@ class LabelInput implements Component {
|
|
|
817
817
|
* Component that renders a session tree selector for navigation
|
|
818
818
|
*/
|
|
819
819
|
export class TreeSelectorComponent extends Container {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
820
|
+
#treeList: TreeList;
|
|
821
|
+
#labelInput: LabelInput | null = null;
|
|
822
|
+
#labelInputContainer: Container;
|
|
823
|
+
#treeContainer: Container;
|
|
824
824
|
|
|
825
825
|
constructor(
|
|
826
826
|
tree: SessionTreeNode[],
|
|
@@ -833,15 +833,15 @@ export class TreeSelectorComponent extends Container {
|
|
|
833
833
|
super();
|
|
834
834
|
const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2));
|
|
835
835
|
|
|
836
|
-
this
|
|
837
|
-
this
|
|
838
|
-
this
|
|
839
|
-
this
|
|
836
|
+
this.#treeList = new TreeList(tree, currentLeafId, maxVisibleLines);
|
|
837
|
+
this.#treeList.onSelect = onSelect;
|
|
838
|
+
this.#treeList.onCancel = onCancel;
|
|
839
|
+
this.#treeList.onLabelEdit = (entryId, currentLabel) => this.#showLabelInput(entryId, currentLabel);
|
|
840
840
|
|
|
841
|
-
this
|
|
842
|
-
this
|
|
841
|
+
this.#treeContainer = new Container();
|
|
842
|
+
this.#treeContainer.addChild(this.#treeList);
|
|
843
843
|
|
|
844
|
-
this
|
|
844
|
+
this.#labelInputContainer = new Container();
|
|
845
845
|
|
|
846
846
|
this.addChild(new Spacer(1));
|
|
847
847
|
this.addChild(new DynamicBorder());
|
|
@@ -856,11 +856,11 @@ export class TreeSelectorComponent extends Container {
|
|
|
856
856
|
0,
|
|
857
857
|
),
|
|
858
858
|
);
|
|
859
|
-
this.addChild(new SearchLine(this
|
|
859
|
+
this.addChild(new SearchLine(this.#treeList));
|
|
860
860
|
this.addChild(new DynamicBorder());
|
|
861
861
|
this.addChild(new Spacer(1));
|
|
862
|
-
this.addChild(this
|
|
863
|
-
this.addChild(this
|
|
862
|
+
this.addChild(this.#treeContainer);
|
|
863
|
+
this.addChild(this.#labelInputContainer);
|
|
864
864
|
this.addChild(new Spacer(1));
|
|
865
865
|
this.addChild(new DynamicBorder());
|
|
866
866
|
|
|
@@ -869,36 +869,36 @@ export class TreeSelectorComponent extends Container {
|
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
-
|
|
873
|
-
this
|
|
874
|
-
this
|
|
875
|
-
this
|
|
872
|
+
#showLabelInput(entryId: string, currentLabel: string | undefined): void {
|
|
873
|
+
this.#labelInput = new LabelInput(entryId, currentLabel);
|
|
874
|
+
this.#labelInput.onSubmit = (id, label) => {
|
|
875
|
+
this.#treeList.updateNodeLabel(id, label);
|
|
876
876
|
this.onLabelChangeCallback?.(id, label);
|
|
877
|
-
this
|
|
877
|
+
this.#hideLabelInput();
|
|
878
878
|
};
|
|
879
|
-
this
|
|
879
|
+
this.#labelInput.onCancel = () => this.#hideLabelInput();
|
|
880
880
|
|
|
881
|
-
this
|
|
882
|
-
this
|
|
883
|
-
this
|
|
881
|
+
this.#treeContainer.clear();
|
|
882
|
+
this.#labelInputContainer.clear();
|
|
883
|
+
this.#labelInputContainer.addChild(this.#labelInput);
|
|
884
884
|
}
|
|
885
885
|
|
|
886
|
-
|
|
887
|
-
this
|
|
888
|
-
this
|
|
889
|
-
this
|
|
890
|
-
this
|
|
886
|
+
#hideLabelInput(): void {
|
|
887
|
+
this.#labelInput = null;
|
|
888
|
+
this.#labelInputContainer.clear();
|
|
889
|
+
this.#treeContainer.clear();
|
|
890
|
+
this.#treeContainer.addChild(this.#treeList);
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
handleInput(keyData: string): void {
|
|
894
|
-
if (this
|
|
895
|
-
this
|
|
894
|
+
if (this.#labelInput) {
|
|
895
|
+
this.#labelInput.handleInput(keyData);
|
|
896
896
|
} else {
|
|
897
|
-
this
|
|
897
|
+
this.#treeList.handleInput(keyData);
|
|
898
898
|
}
|
|
899
899
|
}
|
|
900
900
|
|
|
901
901
|
getTreeList(): TreeList {
|
|
902
|
-
return this
|
|
902
|
+
return this.#treeList;
|
|
903
903
|
}
|
|
904
904
|
}
|