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