@esso0428/pi-subagents 0.15.5 → 0.15.7
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 +10 -19
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +19 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +69 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +2 -1
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +32 -34
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +24 -8
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +171 -93
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +1 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +75 -86
- package/dist/agent-runner.js.map +1 -0
- package/dist/agent-types.d.ts +1 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +28 -45
- package/dist/agent-types.js.map +1 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -6
- package/dist/context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +1 -0
- package/dist/cross-extension-rpc.d.ts.map +1 -0
- package/dist/cross-extension-rpc.js +6 -9
- package/dist/cross-extension-rpc.js.map +1 -0
- package/dist/custom-agents.d.ts +1 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +16 -18
- package/dist/custom-agents.js.map +1 -0
- package/dist/default-agents.d.ts +1 -0
- package/dist/default-agents.d.ts.map +1 -0
- package/dist/default-agents.js +2 -4
- package/dist/default-agents.js.map +1 -0
- package/dist/enabled-models.d.ts +1 -0
- package/dist/enabled-models.d.ts.map +1 -0
- package/dist/enabled-models.js +12 -16
- package/dist/enabled-models.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -4
- package/dist/env.js.map +1 -0
- package/dist/group-join.d.ts +1 -0
- package/dist/group-join.d.ts.map +1 -0
- package/dist/group-join.js +2 -5
- package/dist/group-join.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -292
- package/dist/index.js.map +1 -0
- package/dist/invocation-config.d.ts +1 -0
- package/dist/invocation-config.d.ts.map +1 -0
- package/dist/invocation-config.js +3 -6
- package/dist/invocation-config.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -34
- package/dist/memory.js.map +1 -0
- package/dist/model-resolver.d.ts +1 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +2 -4
- package/dist/model-resolver.js.map +1 -0
- package/dist/nico-overrides.d.ts +1 -0
- package/dist/nico-overrides.d.ts.map +1 -0
- package/dist/nico-overrides.js +15 -20
- package/dist/nico-overrides.js.map +1 -0
- package/dist/output-file.d.ts +1 -0
- package/dist/output-file.d.ts.map +1 -0
- package/dist/output-file.js +17 -22
- package/dist/output-file.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +2 -4
- package/dist/prompts.js.map +1 -0
- package/dist/schedule-store.d.ts +1 -0
- package/dist/schedule-store.d.ts.map +1 -0
- package/dist/schedule-store.js +17 -21
- package/dist/schedule-store.js.map +1 -0
- package/dist/schedule.d.ts +1 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +9 -12
- package/dist/schedule.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +16 -23
- package/dist/settings.js.map +1 -0
- package/dist/skill-loader.d.ts +1 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +21 -23
- package/dist/skill-loader.js.map +1 -0
- package/dist/status-note.d.ts +1 -0
- package/dist/status-note.d.ts.map +1 -0
- package/dist/status-note.js +2 -4
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +2 -5
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +29 -51
- package/dist/ui/agent-widget.js.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.js +25 -0
- package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/types.d.ts +22 -0
- package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/types.js +10 -0
- package/dist/ui/ccstyle/diff/types.js.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.js +58 -0
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
- package/dist/ui/ccstyle/tool-result.d.ts +24 -0
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-result.js +193 -0
- package/dist/ui/ccstyle/tool-result.js.map +1 -0
- package/dist/ui/conversation-blocks.d.ts +46 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +306 -0
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +8 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +117 -0
- package/dist/ui/conversation-nvim.js.map +1 -0
- package/dist/ui/conversation-role.d.ts +13 -0
- package/dist/ui/conversation-role.d.ts.map +1 -0
- package/dist/ui/conversation-role.js +39 -0
- package/dist/ui/conversation-role.js.map +1 -0
- package/dist/ui/conversation-search.d.ts +39 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +124 -0
- package/dist/ui/conversation-search.js.map +1 -0
- package/dist/ui/conversation-timeline.d.ts +62 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -0
- package/dist/ui/conversation-timeline.js +263 -0
- package/dist/ui/conversation-timeline.js.map +1 -0
- package/dist/ui/conversation-viewer.d.ts +62 -20
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +449 -224
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +24 -11
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +86 -78
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -0
- package/dist/ui/schedule-menu.js +2 -4
- package/dist/ui/schedule-menu.js.map +1 -0
- package/dist/ui/viewer-keys.d.ts +1 -0
- package/dist/ui/viewer-keys.d.ts.map +1 -0
- package/dist/ui/viewer-keys.js +8 -10
- package/dist/ui/viewer-keys.js.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +5 -10
- package/dist/usage.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +27 -31
- package/dist/worktree.js.map +1 -0
- package/dist/write-execution.d.ts +42 -0
- package/dist/write-execution.d.ts.map +1 -0
- package/dist/write-execution.js +138 -0
- package/dist/write-execution.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +169 -81
- package/src/agent-runner.ts +12 -13
- package/src/index.ts +117 -98
- package/src/output-file.ts +1 -1
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +3 -9
- package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
- package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
- package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
- package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
- package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
- package/src/ui/ccstyle/diff/render-utils.ts +32 -0
- package/src/ui/ccstyle/diff/types.ts +33 -0
- package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
- package/src/ui/ccstyle/tool-renderer.ts +71 -0
- package/src/ui/ccstyle/tool-result.ts +238 -0
- package/src/ui/conversation-blocks.ts +404 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +418 -210
- package/src/ui/fleet-list.ts +73 -52
- package/src/write-execution.ts +180 -0
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +103 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +182 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
|
-
* conversation-viewer.ts — Live conversation overlay
|
|
2
|
+
* conversation-viewer.ts — Live and historical conversation overlay.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The viewer deliberately keeps rendering state separate from the transcript:
|
|
5
|
+
* blocks are formatted data, while Markdown/ANSI lines are a bounded cache.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
10
|
+
import { buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
|
|
11
|
+
import { formatConversationMessages } from "./conversation-blocks.js";
|
|
12
|
+
import { ConversationTimeline } from "./conversation-timeline.js";
|
|
13
|
+
import { renderConversationRoleHeader } from "./conversation-role.js";
|
|
14
|
+
import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
|
|
15
|
+
import { findConversationMatches, highlightConversationLine, nearestConversationMatchAfter, nearestConversationMatchBefore, stripAnsi } from "./conversation-search.js";
|
|
16
|
+
import { createViewerKeys } from "./viewer-keys.js";
|
|
17
|
+
/** Base lines consumed by the overlay: borders, header, separator and footer. */
|
|
17
18
|
const CHROME_LINES_BASE = 6;
|
|
18
19
|
const MIN_VIEWPORT = 3;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
21
|
+
const MAX_COPY_CHARS = 500_000;
|
|
22
|
+
const MATCH_BG = "\x1b[48;5;238m";
|
|
23
|
+
const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
|
|
24
|
+
const BG_RESET = "\x1b[49m\x1b[39m";
|
|
25
|
+
export function createStaticConversationSource(messages) {
|
|
22
26
|
return { messages, subscribe: () => () => { } };
|
|
23
27
|
}
|
|
24
|
-
class ConversationViewer {
|
|
28
|
+
export class ConversationViewer {
|
|
25
29
|
tui;
|
|
26
30
|
session;
|
|
27
31
|
record;
|
|
@@ -30,23 +34,38 @@ class ConversationViewer {
|
|
|
30
34
|
done;
|
|
31
35
|
onStop;
|
|
32
36
|
onSteer;
|
|
37
|
+
operations;
|
|
38
|
+
blocks = [];
|
|
39
|
+
showTools = true;
|
|
40
|
+
selectedMessageIdx = 0;
|
|
41
|
+
cachedWidth = 0;
|
|
42
|
+
cachedLines = [];
|
|
43
|
+
cachedMessageStarts = [];
|
|
44
|
+
cachedMessageBlocks = [];
|
|
45
|
+
cacheVersion = 0;
|
|
46
|
+
cachedVersion = -1;
|
|
33
47
|
scrollOffset = 0;
|
|
34
48
|
autoScroll = true;
|
|
35
49
|
unsubscribe;
|
|
36
50
|
lastInnerW = 0;
|
|
37
51
|
closed = false;
|
|
38
|
-
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
39
52
|
stopArmed = false;
|
|
40
53
|
keys;
|
|
41
|
-
/** Steering composer — present while the user is typing a message to the agent. */
|
|
42
54
|
composer;
|
|
55
|
+
searchMode = false;
|
|
56
|
+
searchQuery = "";
|
|
57
|
+
searchMatches = [];
|
|
58
|
+
currentMatchIdx = -1;
|
|
59
|
+
timeline;
|
|
43
60
|
constructor(tui, session, record, activity, theme, done,
|
|
44
|
-
/** Abort the agent shown here. Omitted
|
|
61
|
+
/** Abort the agent shown here. Omitted means read-only history. */
|
|
45
62
|
onStop,
|
|
46
|
-
/** User keybindings from
|
|
63
|
+
/** User keybindings from ctx.ui.custom(). */
|
|
47
64
|
keybindings,
|
|
48
|
-
/** Send a steering message to the agent.
|
|
49
|
-
onSteer
|
|
65
|
+
/** Send a steering message to the agent. */
|
|
66
|
+
onSteer,
|
|
67
|
+
/** Optional clipboard/nvim operation context, appended for compatibility. */
|
|
68
|
+
operations) {
|
|
50
69
|
this.tui = tui;
|
|
51
70
|
this.session = session;
|
|
52
71
|
this.record = record;
|
|
@@ -55,37 +74,57 @@ class ConversationViewer {
|
|
|
55
74
|
this.done = done;
|
|
56
75
|
this.onStop = onStop;
|
|
57
76
|
this.onSteer = onSteer;
|
|
58
|
-
this.
|
|
77
|
+
this.operations = operations;
|
|
78
|
+
this.keys = createViewerKeys(keybindings);
|
|
79
|
+
this.blocks = formatConversationMessages(session.messages);
|
|
80
|
+
this.timeline = new ConversationTimeline(tui, theme, {
|
|
81
|
+
cwd: operations?.ctx.cwd,
|
|
82
|
+
record,
|
|
83
|
+
onChange: () => {
|
|
84
|
+
this.cacheVersion++;
|
|
85
|
+
this.invalidate();
|
|
86
|
+
this.tui.requestRender();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
this.timeline.setBlocks(this.blocks);
|
|
59
90
|
this.unsubscribe = session.subscribe(() => {
|
|
60
91
|
if (this.closed)
|
|
61
92
|
return;
|
|
93
|
+
this.blocks = formatConversationMessages(this.session.messages);
|
|
94
|
+
this.timeline.setBlocks(this.blocks);
|
|
95
|
+
this.cacheVersion++;
|
|
96
|
+
this.invalidate();
|
|
62
97
|
this.tui.requestRender();
|
|
63
98
|
});
|
|
64
99
|
}
|
|
65
100
|
handleInput(data) {
|
|
66
|
-
// While composing a steer message, the input owns all keys (Enter sends,
|
|
67
|
-
// Esc cancels — both wired in openComposer()). Editing keys flow through.
|
|
68
101
|
if (this.composer) {
|
|
69
102
|
this.composer.handleInput(data);
|
|
70
103
|
this.tui.requestRender();
|
|
71
104
|
return;
|
|
72
105
|
}
|
|
73
|
-
if (
|
|
106
|
+
if (this.searchMode) {
|
|
107
|
+
this.handleSearchInput(data);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
74
111
|
this.closed = true;
|
|
75
112
|
this.done(undefined);
|
|
76
113
|
return;
|
|
77
114
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
115
|
+
if (matchesKey(data, "enter")) {
|
|
116
|
+
this.buildContentLines(this.lastInnerW || 80);
|
|
117
|
+
if (this.timeline.isToolFocused()) {
|
|
118
|
+
this.timeline.toggleFocusedTool();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (this.canSteer()) {
|
|
122
|
+
this.stopArmed = false;
|
|
123
|
+
this.openComposer();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
85
126
|
}
|
|
86
|
-
|
|
87
|
-
// first "x" arms, second confirms — any other key disarms.
|
|
88
|
-
if ((0, pi_tui_1.matchesKey)(data, "x")) {
|
|
127
|
+
if (matchesKey(data, "x")) {
|
|
89
128
|
if (this.isStoppable()) {
|
|
90
129
|
if (this.stopArmed) {
|
|
91
130
|
this.stopArmed = false;
|
|
@@ -100,137 +139,435 @@ class ConversationViewer {
|
|
|
100
139
|
}
|
|
101
140
|
if (this.stopArmed)
|
|
102
141
|
this.stopArmed = false;
|
|
103
|
-
|
|
142
|
+
// Input can arrive before the first render in tests and in a freshly-opened overlay.
|
|
143
|
+
this.buildContentLines(this.lastInnerW || 80);
|
|
104
144
|
const viewportHeight = this.viewportHeight();
|
|
105
|
-
const maxScroll = Math.max(0,
|
|
106
|
-
if (
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
145
|
+
const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
|
|
146
|
+
if (data === "t") {
|
|
147
|
+
this.showTools = !this.showTools;
|
|
148
|
+
this.timeline.setShowTools(this.showTools);
|
|
149
|
+
this.invalidate();
|
|
150
|
+
this.tui.requestRender();
|
|
151
|
+
}
|
|
152
|
+
else if (data === "\t") {
|
|
153
|
+
this.timeline.moveFocus(1);
|
|
154
|
+
this.invalidate();
|
|
155
|
+
this.tui.requestRender();
|
|
156
|
+
}
|
|
157
|
+
else if (matchesKey(data, Key.shift("tab"))) {
|
|
158
|
+
this.timeline.moveFocus(-1);
|
|
159
|
+
this.invalidate();
|
|
160
|
+
this.tui.requestRender();
|
|
161
|
+
}
|
|
162
|
+
else if (data === "J" || matchesKey(data, Key.shift("j"))) {
|
|
163
|
+
this.jumpToMessage(1);
|
|
164
|
+
}
|
|
165
|
+
else if (data === "K" || matchesKey(data, Key.shift("k"))) {
|
|
166
|
+
this.jumpToMessage(-1);
|
|
167
|
+
}
|
|
168
|
+
else if (data === "g") {
|
|
169
|
+
this.scrollToTop();
|
|
170
|
+
}
|
|
171
|
+
else if (data === "G" || matchesKey(data, Key.shift("g"))) {
|
|
172
|
+
this.scrollToBottom();
|
|
173
|
+
}
|
|
174
|
+
else if (data === "/") {
|
|
175
|
+
this.enterSearch();
|
|
176
|
+
}
|
|
177
|
+
else if (data === "n") {
|
|
178
|
+
this.gotoMatch(1);
|
|
179
|
+
}
|
|
180
|
+
else if (data === "N") {
|
|
181
|
+
this.gotoMatch(-1);
|
|
182
|
+
}
|
|
183
|
+
else if (data === "M") {
|
|
184
|
+
void this.copyCurrentMessage();
|
|
185
|
+
}
|
|
186
|
+
else if (data === "o") {
|
|
187
|
+
void this.viewCurrentInNvim();
|
|
188
|
+
}
|
|
189
|
+
else if (data === "O" && this.isLive()) {
|
|
190
|
+
void this.editCurrentInNvim();
|
|
191
|
+
}
|
|
192
|
+
else if (this.keys.scrollUp(data)) {
|
|
193
|
+
this.scrollBy(-1);
|
|
109
194
|
}
|
|
110
195
|
else if (this.keys.scrollDown(data)) {
|
|
111
|
-
this.
|
|
112
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
196
|
+
this.scrollBy(1);
|
|
113
197
|
}
|
|
114
198
|
else if (this.keys.pageUp(data)) {
|
|
115
|
-
this.
|
|
116
|
-
this.autoScroll = false;
|
|
199
|
+
this.scrollBy(-viewportHeight);
|
|
117
200
|
}
|
|
118
201
|
else if (this.keys.pageDown(data)) {
|
|
119
|
-
this.
|
|
120
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
202
|
+
this.scrollBy(viewportHeight);
|
|
121
203
|
}
|
|
122
|
-
else if (
|
|
204
|
+
else if (matchesKey(data, "home")) {
|
|
123
205
|
this.scrollOffset = 0;
|
|
206
|
+
this.selectedMessageIdx = 0;
|
|
124
207
|
this.autoScroll = false;
|
|
208
|
+
this.tui.requestRender();
|
|
125
209
|
}
|
|
126
|
-
else if (
|
|
127
|
-
this.
|
|
128
|
-
|
|
210
|
+
else if (matchesKey(data, "end")) {
|
|
211
|
+
this.scrollToBottom();
|
|
212
|
+
}
|
|
213
|
+
// Keep the computed max alive for the key path even when the cache is empty.
|
|
214
|
+
void maxScroll;
|
|
215
|
+
}
|
|
216
|
+
handleMouse(event) {
|
|
217
|
+
if (this.composer || this.searchMode)
|
|
218
|
+
return undefined;
|
|
219
|
+
const innerW = Math.max(1, this.lastInnerW || event.width - 4);
|
|
220
|
+
const contentLines = this.buildContentLines(innerW);
|
|
221
|
+
const viewportHeight = this.viewportHeight();
|
|
222
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
223
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
224
|
+
const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
|
|
225
|
+
const contentY = event.y - contentOrigin + visibleStart;
|
|
226
|
+
if (contentY < 0 || contentY >= contentLines.length)
|
|
227
|
+
return undefined;
|
|
228
|
+
const result = this.timeline.handleMouse({ ...event, y: contentY });
|
|
229
|
+
if (result?.handled) {
|
|
230
|
+
this.cacheVersion++;
|
|
231
|
+
this.invalidate();
|
|
232
|
+
this.tui.requestRender();
|
|
129
233
|
}
|
|
234
|
+
return result;
|
|
130
235
|
}
|
|
131
236
|
render(width) {
|
|
132
237
|
if (width < 6)
|
|
133
|
-
return [];
|
|
238
|
+
return [];
|
|
134
239
|
const th = this.theme;
|
|
135
|
-
const innerW = width - 4;
|
|
240
|
+
const innerW = Math.max(1, width - 4);
|
|
136
241
|
this.lastInnerW = innerW;
|
|
137
242
|
const lines = [];
|
|
138
|
-
const pad = (s, len) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
const row = (content) => th.fg("border", "│") + " " + (0, pi_tui_1.truncateToWidth)(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
|
|
143
|
-
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
144
|
-
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
243
|
+
const pad = (s, len) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
|
|
244
|
+
const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
|
|
245
|
+
const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
|
|
246
|
+
const hrBot = th.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
|
|
145
247
|
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
146
|
-
// Header
|
|
147
248
|
lines.push(hrTop);
|
|
148
|
-
const name =
|
|
149
|
-
const modeLabel =
|
|
249
|
+
const name = getDisplayName(this.record.type);
|
|
250
|
+
const modeLabel = getPromptModeLabel(this.record.type);
|
|
150
251
|
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
151
|
-
const statusIcon = this.record.status === "running"
|
|
152
|
-
|
|
153
|
-
: this.record.status === "completed"
|
|
154
|
-
? th.fg("success", "✓")
|
|
155
|
-
: this.record.status === "error"
|
|
156
|
-
? th.fg("error", "✗")
|
|
157
|
-
: th.fg("dim", "○");
|
|
158
|
-
const duration = (0, agent_widget_js_1.formatDuration)(this.record.startedAt, this.record.completedAt);
|
|
252
|
+
const statusIcon = this.record.status === "running" ? th.fg("accent", "●") : this.record.status === "completed" ? th.fg("success", "✓") : this.record.status === "error" ? th.fg("error", "✗") : th.fg("dim", "○");
|
|
253
|
+
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
159
254
|
const headerParts = [duration];
|
|
160
255
|
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
161
256
|
if (toolUses > 0)
|
|
162
257
|
headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
163
|
-
const tokens =
|
|
258
|
+
const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
|
|
164
259
|
if (tokens > 0) {
|
|
165
|
-
const percent =
|
|
166
|
-
headerParts.push(
|
|
260
|
+
const percent = getSessionContextPercent(this.activity?.session);
|
|
261
|
+
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
167
262
|
}
|
|
168
|
-
lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${
|
|
263
|
+
lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`));
|
|
169
264
|
const invocationLine = this.invocationLine();
|
|
170
265
|
if (invocationLine)
|
|
171
266
|
lines.push(row(invocationLine));
|
|
172
267
|
lines.push(hrMid);
|
|
173
|
-
// Content area — rebuild every render (live data, no cache needed)
|
|
174
268
|
const contentLines = this.buildContentLines(innerW);
|
|
175
269
|
const viewportHeight = this.viewportHeight();
|
|
176
270
|
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
177
|
-
if (this.autoScroll)
|
|
271
|
+
if (this.autoScroll)
|
|
178
272
|
this.scrollOffset = maxScroll;
|
|
179
|
-
}
|
|
180
273
|
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
181
274
|
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
const currentBlockIdx = this.currentBlockGlobalIndex();
|
|
276
|
+
const headerLine = this.activeHeaderLine();
|
|
277
|
+
const showSticky = headerLine >= 0 && headerLine < visibleStart && visible.length > 0;
|
|
278
|
+
const displayed = visible.slice();
|
|
279
|
+
if (showSticky) {
|
|
280
|
+
const block = this.cachedMessageBlocks[this.currentMessageIndex()];
|
|
281
|
+
if (block) {
|
|
282
|
+
const header = renderConversationRoleHeader(block, this.theme);
|
|
283
|
+
displayed[0] = { text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (let i = 0; i < displayed.length; i++) {
|
|
287
|
+
const line = displayed[i];
|
|
288
|
+
let text = line.text;
|
|
289
|
+
if (this.searchMatches.length > 0 && !(showSticky && i === 0)) {
|
|
290
|
+
const hits = this.searchMatches.filter((match) => match.lineIdx === visibleStart + i).map((match) => ({ lineIdx: match.lineIdx, col: match.col, len: match.len, current: this.searchMatches.indexOf(match) === this.currentMatchIdx }));
|
|
291
|
+
if (hits.length > 0)
|
|
292
|
+
text = highlightConversationLine(text, hits, MATCH_BG, MATCH_BG_CURRENT, BG_RESET);
|
|
293
|
+
}
|
|
294
|
+
if (line.railable && line.blockIndex === currentBlockIdx) {
|
|
295
|
+
text = text.startsWith(" ") ? th.fg("accent", "▎") + text.slice(1) : th.fg("accent", "▎") + text;
|
|
296
|
+
}
|
|
297
|
+
lines.push(row(text));
|
|
184
298
|
}
|
|
185
|
-
|
|
299
|
+
for (let i = displayed.length; i < viewportHeight; i++)
|
|
300
|
+
lines.push(row(""));
|
|
186
301
|
lines.push(hrMid);
|
|
187
302
|
if (this.composer) {
|
|
188
|
-
// Composer row: the Input renders its own `> ` prompt and cursor.
|
|
189
303
|
lines.push(row(this.composer.render(innerW)[0] ?? ""));
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
304
|
+
const hint = th.fg("dim", "Enter send · Esc cancel");
|
|
305
|
+
const label = th.fg("accent", "✎ steer");
|
|
306
|
+
lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
|
|
307
|
+
}
|
|
308
|
+
else if (this.searchMode) {
|
|
309
|
+
const query = `/${this.searchQuery}`;
|
|
310
|
+
const matchCount = this.searchMatches.length > 0
|
|
311
|
+
? ` · ${this.searchMatches.length} match${this.searchMatches.length === 1 ? "" : "es"}`
|
|
312
|
+
: "";
|
|
313
|
+
const hints = th.fg("dim", " · Enter apply · Esc cancel");
|
|
314
|
+
lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
|
|
194
315
|
}
|
|
195
316
|
else {
|
|
196
|
-
// Actions on the left, navigation on the right. The scroll hint keeps its
|
|
197
|
-
// full key list so the less-obvious bindings stay discoverable; it leads
|
|
198
|
-
// the right group so "Esc close" is the only part that truncates first.
|
|
199
|
-
const sep = th.fg("dim", " · ");
|
|
200
317
|
const actions = [];
|
|
201
318
|
if (this.canSteer())
|
|
202
319
|
actions.push(th.fg("dim", "Enter steer"));
|
|
203
|
-
if (this.isStoppable())
|
|
320
|
+
if (this.isStoppable())
|
|
204
321
|
actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
322
|
+
const editHint = this.isLive() ? " O" : "";
|
|
323
|
+
const shortcutHints = [
|
|
324
|
+
`j/k · J/K · g/G t M o${editHint} /n N q`,
|
|
325
|
+
`M o${editHint} /n N q`,
|
|
326
|
+
`M o${editHint} /n N q`,
|
|
327
|
+
];
|
|
328
|
+
const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
212
329
|
const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
330
|
+
const countWidth = visibleWidth(count);
|
|
331
|
+
let status = "";
|
|
332
|
+
for (const hint of shortcutHints) {
|
|
333
|
+
const styledHint = th.fg("dim", hint);
|
|
334
|
+
const withActions = [styledHint, ...actions].join(" ");
|
|
335
|
+
if (visibleWidth(withActions) <= innerW) {
|
|
336
|
+
status = withActions;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
if (visibleWidth(styledHint) <= innerW)
|
|
340
|
+
status ||= styledHint;
|
|
341
|
+
}
|
|
342
|
+
if (!status)
|
|
343
|
+
status = th.fg("dim", shortcutHints[shortcutHints.length - 1]);
|
|
344
|
+
const statusWidth = visibleWidth(status);
|
|
345
|
+
const footer = statusWidth + countWidth + 1 <= innerW
|
|
346
|
+
? status + " ".repeat(Math.max(1, innerW - statusWidth - countWidth)) + count
|
|
347
|
+
: status;
|
|
348
|
+
lines.push(row(truncateToWidth(footer, innerW, "...", true)));
|
|
219
349
|
}
|
|
220
350
|
lines.push(hrBot);
|
|
221
351
|
return lines;
|
|
222
352
|
}
|
|
223
|
-
|
|
353
|
+
invalidate() {
|
|
354
|
+
this.cachedWidth = 0;
|
|
355
|
+
this.cachedLines = [];
|
|
356
|
+
this.cachedMessageStarts = [];
|
|
357
|
+
this.cachedMessageBlocks = [];
|
|
358
|
+
this.cachedVersion = -1;
|
|
359
|
+
}
|
|
360
|
+
dispose() {
|
|
361
|
+
this.closed = true;
|
|
362
|
+
this.unsubscribe?.();
|
|
363
|
+
this.unsubscribe = undefined;
|
|
364
|
+
}
|
|
365
|
+
viewportHeight() {
|
|
366
|
+
const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
367
|
+
return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
|
|
368
|
+
}
|
|
369
|
+
chromeLines() {
|
|
370
|
+
return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
|
|
371
|
+
}
|
|
372
|
+
invocationLine() {
|
|
373
|
+
const liveModel = this.isLive() ? this.record.session?.model : undefined;
|
|
374
|
+
const invocation = this.record.invocation
|
|
375
|
+
? {
|
|
376
|
+
...this.record.invocation,
|
|
377
|
+
...(liveModel && { effectiveModelName: liveModel.name ?? liveModel.id }),
|
|
378
|
+
...(this.isLive() && { effectiveThinking: this.record.session?.thinkingLevel }),
|
|
379
|
+
}
|
|
380
|
+
: liveModel || (this.isLive() && this.record.session?.thinkingLevel)
|
|
381
|
+
? {
|
|
382
|
+
effectiveModelName: liveModel?.name ?? liveModel?.id,
|
|
383
|
+
effectiveThinking: this.record.session?.thinkingLevel,
|
|
384
|
+
}
|
|
385
|
+
: undefined;
|
|
386
|
+
if (!invocation)
|
|
387
|
+
return this.theme.fg("dim", " ↳ model: unknown · thinking: unknown");
|
|
388
|
+
const { modelName, tags } = buildInvocationTags(invocation);
|
|
389
|
+
const effectiveModelName = invocation.effectiveModelName ?? modelName;
|
|
390
|
+
const parts = effectiveModelName ? [`model: ${effectiveModelName}`, ...tags] : ["model: inherit", ...tags];
|
|
391
|
+
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
|
|
392
|
+
}
|
|
393
|
+
buildContentLines(width) {
|
|
394
|
+
if (width <= 0)
|
|
395
|
+
return [];
|
|
396
|
+
if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width)
|
|
397
|
+
return this.cachedLines;
|
|
398
|
+
this.timeline.setShowTools(this.showTools);
|
|
399
|
+
this.timeline.render(width);
|
|
400
|
+
const timelineSnapshot = this.timeline.getSnapshot();
|
|
401
|
+
const lines = timelineSnapshot.lines.map((line) => ({ ...line }));
|
|
402
|
+
this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
|
|
403
|
+
this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
|
|
404
|
+
if (this.record.status === "running" && this.activity) {
|
|
405
|
+
const act = describeActivity(this.activity.activeTools, this.activity.responseText);
|
|
406
|
+
const text = `▍ ${act}`;
|
|
407
|
+
lines.push({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 });
|
|
408
|
+
}
|
|
409
|
+
this.cachedWidth = width;
|
|
410
|
+
this.cachedVersion = this.cacheVersion;
|
|
411
|
+
this.cachedLines = lines;
|
|
412
|
+
if (this.searchMode || this.searchQuery)
|
|
413
|
+
this.recomputeMatches();
|
|
414
|
+
return lines;
|
|
415
|
+
}
|
|
416
|
+
currentMessageIndex() {
|
|
417
|
+
return this.cachedMessageStarts.length === 0 ? 0 : Math.max(0, Math.min(this.selectedMessageIdx, this.cachedMessageStarts.length - 1));
|
|
418
|
+
}
|
|
419
|
+
currentBlockGlobalIndex() {
|
|
420
|
+
const block = this.cachedMessageBlocks[this.currentMessageIndex()];
|
|
421
|
+
return block ? this.blocks.indexOf(block) : -1;
|
|
422
|
+
}
|
|
423
|
+
activeHeaderLine() {
|
|
424
|
+
return this.cachedMessageStarts[this.currentMessageIndex()] ?? -1;
|
|
425
|
+
}
|
|
426
|
+
currentBlock() {
|
|
427
|
+
return this.cachedMessageBlocks[this.currentMessageIndex()];
|
|
428
|
+
}
|
|
429
|
+
messageIndexForOffset(offset) {
|
|
430
|
+
let result = 0;
|
|
431
|
+
for (let i = 0; i < this.cachedMessageStarts.length; i++) {
|
|
432
|
+
if (this.cachedMessageStarts[i] <= offset)
|
|
433
|
+
result = i;
|
|
434
|
+
else
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
jumpToMessage(dir) {
|
|
440
|
+
if (this.cachedMessageStarts.length === 0)
|
|
441
|
+
return;
|
|
442
|
+
const target = this.currentMessageIndex() + dir;
|
|
443
|
+
if (target < 0 || target >= this.cachedMessageStarts.length)
|
|
444
|
+
return;
|
|
445
|
+
this.selectedMessageIdx = target;
|
|
446
|
+
this.scrollOffset = Math.min(this.cachedMessageStarts[target], Math.max(0, this.cachedLines.length - this.viewportHeight()));
|
|
447
|
+
this.autoScroll = false;
|
|
448
|
+
this.tui.requestRender();
|
|
449
|
+
}
|
|
450
|
+
scrollBy(delta) {
|
|
451
|
+
const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
|
|
452
|
+
this.scrollOffset = Math.max(0, Math.min(max, this.scrollOffset + delta));
|
|
453
|
+
this.selectedMessageIdx = this.messageIndexForOffset(this.scrollOffset);
|
|
454
|
+
this.autoScroll = this.scrollOffset >= max;
|
|
455
|
+
this.tui.requestRender();
|
|
456
|
+
}
|
|
457
|
+
scrollToTop() {
|
|
458
|
+
this.scrollOffset = 0;
|
|
459
|
+
this.selectedMessageIdx = 0;
|
|
460
|
+
this.autoScroll = false;
|
|
461
|
+
this.tui.requestRender();
|
|
462
|
+
}
|
|
463
|
+
scrollToBottom() {
|
|
464
|
+
this.scrollOffset = Math.max(0, this.cachedLines.length - this.viewportHeight());
|
|
465
|
+
this.selectedMessageIdx = Math.max(0, this.cachedMessageStarts.length - 1);
|
|
466
|
+
this.autoScroll = true;
|
|
467
|
+
this.tui.requestRender();
|
|
468
|
+
}
|
|
469
|
+
enterSearch() {
|
|
470
|
+
this.searchMode = true;
|
|
471
|
+
this.searchQuery = "";
|
|
472
|
+
this.searchMatches = [];
|
|
473
|
+
this.currentMatchIdx = -1;
|
|
474
|
+
this.tui.requestRender();
|
|
475
|
+
}
|
|
476
|
+
handleSearchInput(data) {
|
|
477
|
+
if (matchesKey(data, "escape") || matchesKey(data, Key.ctrl("c"))) {
|
|
478
|
+
this.searchMode = false;
|
|
479
|
+
this.searchQuery = "";
|
|
480
|
+
this.searchMatches = [];
|
|
481
|
+
this.currentMatchIdx = -1;
|
|
482
|
+
}
|
|
483
|
+
else if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
|
|
484
|
+
this.searchMode = false;
|
|
485
|
+
if (this.searchMatches.length > 0)
|
|
486
|
+
this.scrollToMatch(this.currentMatchIdx < 0 ? 0 : this.currentMatchIdx);
|
|
487
|
+
}
|
|
488
|
+
else if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
|
|
489
|
+
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
490
|
+
this.recomputeMatches();
|
|
491
|
+
}
|
|
492
|
+
else if (data.length === 1 && data >= " " && data !== "\x7f") {
|
|
493
|
+
this.searchQuery += data;
|
|
494
|
+
this.recomputeMatches();
|
|
495
|
+
}
|
|
496
|
+
this.tui.requestRender();
|
|
497
|
+
}
|
|
498
|
+
recomputeMatches() {
|
|
499
|
+
this.searchMatches = findConversationMatches(this.cachedLines, this.searchQuery);
|
|
500
|
+
this.currentMatchIdx = this.searchMatches.length > 0 ? Math.min(Math.max(this.currentMatchIdx, 0), this.searchMatches.length - 1) : -1;
|
|
501
|
+
}
|
|
502
|
+
gotoMatch(dir) {
|
|
503
|
+
if (this.searchMatches.length === 0)
|
|
504
|
+
return;
|
|
505
|
+
this.currentMatchIdx = this.currentMatchIdx < 0
|
|
506
|
+
? (dir === 1 ? nearestConversationMatchAfter(this.searchMatches, this.scrollOffset) : nearestConversationMatchBefore(this.searchMatches, this.scrollOffset))
|
|
507
|
+
: (this.currentMatchIdx + dir + this.searchMatches.length) % this.searchMatches.length;
|
|
508
|
+
this.scrollToMatch(this.currentMatchIdx);
|
|
509
|
+
this.tui.requestRender();
|
|
510
|
+
}
|
|
511
|
+
scrollToMatch(index) {
|
|
512
|
+
const match = this.searchMatches[index];
|
|
513
|
+
if (!match)
|
|
514
|
+
return;
|
|
515
|
+
const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
|
|
516
|
+
this.scrollOffset = Math.max(0, Math.min(max, match.lineIdx - Math.floor(this.viewportHeight() / 2)));
|
|
517
|
+
this.selectedMessageIdx = this.messageIndexForOffset(this.scrollOffset);
|
|
518
|
+
this.autoScroll = false;
|
|
519
|
+
}
|
|
520
|
+
async copyCurrentMessage() {
|
|
521
|
+
const block = this.currentBlock();
|
|
522
|
+
if (!block || !this.operations) {
|
|
523
|
+
if (!this.operations)
|
|
524
|
+
return;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const source = `${stripAnsi(block.header)}\n${block.copyText}`.trimEnd();
|
|
528
|
+
const clipped = source.length > MAX_COPY_CHARS ? `${source.slice(0, MAX_COPY_CHARS)}\n… [truncated by subagent copy]` : source;
|
|
529
|
+
try {
|
|
530
|
+
await copyToClipboard(clipped);
|
|
531
|
+
this.operations.ctx.ui.notify(`Copied current message (${clipped.length} chars)`, "info");
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
this.operations.ctx.ui.notify(`Copy failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async viewCurrentInNvim() {
|
|
538
|
+
const block = this.currentBlock();
|
|
539
|
+
if (!block || !this.operations)
|
|
540
|
+
return;
|
|
541
|
+
await viewConversationBlockInNvim(this.tui, this.operations.ctx, block);
|
|
542
|
+
this.invalidate();
|
|
543
|
+
this.tui.requestRender(true);
|
|
544
|
+
}
|
|
545
|
+
async editCurrentInNvim() {
|
|
546
|
+
const block = this.currentBlock();
|
|
547
|
+
if (!block || !this.operations || !this.isLive() || block.role !== "assistant")
|
|
548
|
+
return;
|
|
549
|
+
const sent = await editLiveAssistantBlockInNvim(this.operations.pi, this.tui, this.operations.ctx, block);
|
|
550
|
+
if (sent)
|
|
551
|
+
this.done(undefined);
|
|
552
|
+
else {
|
|
553
|
+
this.invalidate();
|
|
554
|
+
this.tui.requestRender(true);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
isReadOnly() {
|
|
558
|
+
return this.operations?.readOnly === true;
|
|
559
|
+
}
|
|
560
|
+
isLive() {
|
|
561
|
+
return !this.isReadOnly() && this.record.session !== undefined;
|
|
562
|
+
}
|
|
224
563
|
isStoppable() {
|
|
225
|
-
return !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
|
|
564
|
+
return !this.isReadOnly() && !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
|
|
226
565
|
}
|
|
227
|
-
/** Steerable only when a steer handler exists and the agent is still active. */
|
|
228
566
|
canSteer() {
|
|
229
|
-
return !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
|
|
567
|
+
return !this.isReadOnly() && !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
|
|
230
568
|
}
|
|
231
|
-
/** Open the inline steering composer and route subsequent input to it. */
|
|
232
569
|
openComposer() {
|
|
233
|
-
const input = new
|
|
570
|
+
const input = new Input();
|
|
234
571
|
input.focused = true;
|
|
235
572
|
input.onSubmit = (value) => {
|
|
236
573
|
const message = value.trim();
|
|
@@ -246,117 +583,5 @@ class ConversationViewer {
|
|
|
246
583
|
this.composer = input;
|
|
247
584
|
this.tui.requestRender();
|
|
248
585
|
}
|
|
249
|
-
invalidate() { }
|
|
250
|
-
dispose() {
|
|
251
|
-
this.closed = true;
|
|
252
|
-
if (this.unsubscribe) {
|
|
253
|
-
this.unsubscribe();
|
|
254
|
-
this.unsubscribe = undefined;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// ---- Private ----
|
|
258
|
-
viewportHeight() {
|
|
259
|
-
// Cap mirrors the overlay's maxHeight — otherwise the viewer would render
|
|
260
|
-
// more lines than the overlay shows and clip the footer.
|
|
261
|
-
const maxRows = Math.floor((this.tui.terminal.rows * exports.VIEWPORT_HEIGHT_PCT) / 100);
|
|
262
|
-
return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
|
|
263
|
-
}
|
|
264
|
-
chromeLines() {
|
|
265
|
-
// The composer adds one row above the footer hint while it's open.
|
|
266
|
-
return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
|
|
267
|
-
}
|
|
268
|
-
invocationLine() {
|
|
269
|
-
const { modelName, tags } = (0, agent_widget_js_1.buildInvocationTags)(this.record.invocation);
|
|
270
|
-
const parts = modelName ? [modelName, ...tags] : tags;
|
|
271
|
-
if (parts.length === 0)
|
|
272
|
-
return undefined;
|
|
273
|
-
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
|
|
274
|
-
}
|
|
275
|
-
buildContentLines(width) {
|
|
276
|
-
if (width <= 0)
|
|
277
|
-
return [];
|
|
278
|
-
const th = this.theme;
|
|
279
|
-
const messages = this.session.messages;
|
|
280
|
-
const lines = [];
|
|
281
|
-
if (messages.length === 0) {
|
|
282
|
-
lines.push(th.fg("dim", "(waiting for first message...)"));
|
|
283
|
-
return lines;
|
|
284
|
-
}
|
|
285
|
-
let needsSeparator = false;
|
|
286
|
-
for (const msg of messages) {
|
|
287
|
-
if (msg.role === "user") {
|
|
288
|
-
const text = typeof msg.content === "string"
|
|
289
|
-
? msg.content
|
|
290
|
-
: (0, context_js_1.extractText)(msg.content);
|
|
291
|
-
if (!text.trim())
|
|
292
|
-
continue;
|
|
293
|
-
if (needsSeparator)
|
|
294
|
-
lines.push(th.fg("dim", "───"));
|
|
295
|
-
lines.push(th.fg("accent", "[User]"));
|
|
296
|
-
for (const line of (0, pi_tui_1.wrapTextWithAnsi)(text.trim(), width)) {
|
|
297
|
-
lines.push(line);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
else if (msg.role === "assistant") {
|
|
301
|
-
const textParts = [];
|
|
302
|
-
const toolCalls = [];
|
|
303
|
-
for (const c of msg.content) {
|
|
304
|
-
if (c.type === "text" && c.text)
|
|
305
|
-
textParts.push(c.text);
|
|
306
|
-
else if (c.type === "toolCall") {
|
|
307
|
-
toolCalls.push(c.name ?? c.toolName ?? "unknown");
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (needsSeparator)
|
|
311
|
-
lines.push(th.fg("dim", "───"));
|
|
312
|
-
lines.push(th.bold("[Assistant]"));
|
|
313
|
-
if (textParts.length > 0) {
|
|
314
|
-
for (const line of (0, pi_tui_1.wrapTextWithAnsi)(textParts.join("\n").trim(), width)) {
|
|
315
|
-
lines.push(line);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
for (const name of toolCalls) {
|
|
319
|
-
lines.push((0, pi_tui_1.truncateToWidth)(th.fg("muted", ` [Tool: ${name}]`), width));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
else if (msg.role === "toolResult") {
|
|
323
|
-
const text = (0, context_js_1.extractText)(msg.content);
|
|
324
|
-
const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
|
|
325
|
-
if (!truncated.trim())
|
|
326
|
-
continue;
|
|
327
|
-
if (needsSeparator)
|
|
328
|
-
lines.push(th.fg("dim", "───"));
|
|
329
|
-
lines.push(th.fg("dim", "[Result]"));
|
|
330
|
-
for (const line of (0, pi_tui_1.wrapTextWithAnsi)(truncated.trim(), width)) {
|
|
331
|
-
lines.push(th.fg("dim", line));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else if (msg.role === "bashExecution") {
|
|
335
|
-
const bash = msg;
|
|
336
|
-
if (needsSeparator)
|
|
337
|
-
lines.push(th.fg("dim", "───"));
|
|
338
|
-
lines.push((0, pi_tui_1.truncateToWidth)(th.fg("muted", ` $ ${bash.command}`), width));
|
|
339
|
-
if (bash.output?.trim()) {
|
|
340
|
-
const out = bash.output.length > 500
|
|
341
|
-
? bash.output.slice(0, 500) + "... (truncated)"
|
|
342
|
-
: bash.output;
|
|
343
|
-
for (const line of (0, pi_tui_1.wrapTextWithAnsi)(out.trim(), width)) {
|
|
344
|
-
lines.push(th.fg("dim", line));
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
needsSeparator = true;
|
|
352
|
-
}
|
|
353
|
-
// Streaming indicator for running agents
|
|
354
|
-
if (this.record.status === "running" && this.activity) {
|
|
355
|
-
const act = (0, agent_widget_js_1.describeActivity)(this.activity.activeTools, this.activity.responseText);
|
|
356
|
-
lines.push("");
|
|
357
|
-
lines.push((0, pi_tui_1.truncateToWidth)(th.fg("accent", "▍ ") + th.fg("dim", act), width));
|
|
358
|
-
}
|
|
359
|
-
return lines.map(l => (0, pi_tui_1.truncateToWidth)(l, width));
|
|
360
|
-
}
|
|
361
586
|
}
|
|
362
|
-
|
|
587
|
+
//# sourceMappingURL=conversation-viewer.js.map
|