@nklisch/pi-enhanced 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/README.md +4 -2
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +25 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +75 -47
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +112 -105
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -223
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -683
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -119
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +327 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -92
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +330 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
* session-navigator.ts — The `/subagents:sessions` command: pick a subagent and
|
|
3
3
|
* read its transcript through Pi's own per-entry session components.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* renderer — it mounts Pi's interactive components (`AssistantMessageComponent`,
|
|
9
|
-
* `ToolExecutionComponent`, …) into a `Container`, mirroring Pi's own
|
|
10
|
-
* `renderSessionContext` mapping. Rendering lives here, not in the pure module,
|
|
11
|
-
* because the components require a `TUI`, `cwd`, and markdown theme.
|
|
12
|
-
*
|
|
13
|
-
* The overlay is strictly read-only — steering stays in the `steer_subagent` tool
|
|
14
|
-
* and the widget. It consumes a `TranscriptSource`, so the evicted-agent-source
|
|
15
|
-
* follow-up swaps the source without touching the renderer or the overlay.
|
|
5
|
+
* The overlay remains a read-only native Pi transcript. Query state is local to
|
|
6
|
+
* this view and uses the same projection as the parent query tool; it never
|
|
7
|
+
* becomes a persisted index or a second message renderer.
|
|
16
8
|
*/
|
|
17
9
|
|
|
18
10
|
import {
|
|
@@ -30,6 +22,9 @@ import {
|
|
|
30
22
|
import {
|
|
31
23
|
type Component,
|
|
32
24
|
Container,
|
|
25
|
+
type Focusable,
|
|
26
|
+
Input,
|
|
27
|
+
Key,
|
|
33
28
|
type MarkdownTheme,
|
|
34
29
|
matchesKey,
|
|
35
30
|
Spacer,
|
|
@@ -41,7 +36,16 @@ import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
|
41
36
|
import { debugLog } from "#src/debug";
|
|
42
37
|
import type { SessionMessage } from "#src/types";
|
|
43
38
|
import { describeActivity, formatDuration, formatModelThinking, type Theme } from "#src/ui/display";
|
|
44
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
fileSnapshotSource,
|
|
41
|
+
liveFileSource,
|
|
42
|
+
listNavigableAgents,
|
|
43
|
+
type NavigableSubagent,
|
|
44
|
+
type RunDisplayMetadata,
|
|
45
|
+
type TranscriptSource,
|
|
46
|
+
type TranscriptSourceAvailability,
|
|
47
|
+
} from "#src/ui/session-navigation";
|
|
48
|
+
import { projectSessionMessages, querySession, type SessionQueryEntry } from "#src/session/query";
|
|
45
49
|
|
|
46
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
51
|
|
|
@@ -49,6 +53,9 @@ import { fileSnapshotSource, listNavigableAgents, liveSource, type NavigableSuba
|
|
|
49
53
|
const CHROME_LINES = 6;
|
|
50
54
|
const MIN_VIEWPORT = 3;
|
|
51
55
|
const VIEWPORT_HEIGHT_PCT = 70;
|
|
56
|
+
const OVERLAY_QUERY_LIMIT = 50;
|
|
57
|
+
|
|
58
|
+
type MatchMark = { readonly index: number; readonly total: number; readonly selected: boolean };
|
|
52
59
|
|
|
53
60
|
/** Component factory shape Pi's `ui.custom` invokes to mount an overlay. */
|
|
54
61
|
export type OverlayComponentFactory<R> = (
|
|
@@ -111,7 +118,9 @@ export class SessionNavigatorHandler {
|
|
|
111
118
|
|
|
112
119
|
let source: TranscriptSource;
|
|
113
120
|
try {
|
|
114
|
-
source = entry.kind === "live"
|
|
121
|
+
source = entry.kind === "live"
|
|
122
|
+
? liveFileSource(entry.record, readFile)
|
|
123
|
+
: fileSnapshotSource(entry.outputFile, readFile);
|
|
115
124
|
} catch {
|
|
116
125
|
ui.notify("Could not read the session transcript file.", "error");
|
|
117
126
|
return;
|
|
@@ -129,20 +138,20 @@ export class SessionNavigatorHandler {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
/**
|
|
132
|
-
* Read-only scrollable transcript overlay.
|
|
141
|
+
* Read-only scrollable transcript overlay with literal query navigation.
|
|
133
142
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* `buildTranscriptComponents`.
|
|
143
|
+
* Pi's message/tool components remain the body renderer. `MatchChrome` only
|
|
144
|
+
* adds block-level rails and a bounded match label around those components,
|
|
145
|
+
* which is the intentional fallback because the native components do not expose
|
|
146
|
+
* substring-highlighting hooks.
|
|
139
147
|
*/
|
|
140
|
-
export class TranscriptOverlay implements Component {
|
|
148
|
+
export class TranscriptOverlay implements Component, Focusable {
|
|
141
149
|
private scrollOffset = 0;
|
|
142
150
|
private autoScroll = true;
|
|
143
151
|
private unsubscribe: (() => void) | undefined;
|
|
144
152
|
private runtimeInterval: ReturnType<typeof setInterval> | undefined;
|
|
145
153
|
private closed = false;
|
|
154
|
+
private _focused = false;
|
|
146
155
|
|
|
147
156
|
private readonly tui: TUI;
|
|
148
157
|
private readonly theme: Theme;
|
|
@@ -151,7 +160,24 @@ export class TranscriptOverlay implements Component {
|
|
|
151
160
|
private readonly done: (result: undefined) => void;
|
|
152
161
|
private readonly cwd: string;
|
|
153
162
|
private readonly markdownTheme: MarkdownTheme;
|
|
163
|
+
private readonly searchInput = new Input();
|
|
154
164
|
private content: Container;
|
|
165
|
+
private sourceMessages: readonly SessionMessage[] = [];
|
|
166
|
+
private matches: readonly SessionQueryEntry[] = [];
|
|
167
|
+
/** Query total, not the capped visible match list; keeps overflow truthful. */
|
|
168
|
+
private totalMatches = 0;
|
|
169
|
+
private matchMarks = new Map<string, MatchMark>();
|
|
170
|
+
private selectedMatchId: string | undefined;
|
|
171
|
+
private searchActive = false;
|
|
172
|
+
private searchCommitted = false;
|
|
173
|
+
private toolsOnly = false;
|
|
174
|
+
private newMatchCount = 0;
|
|
175
|
+
|
|
176
|
+
get focused(): boolean { return this._focused; }
|
|
177
|
+
set focused(value: boolean) {
|
|
178
|
+
this._focused = value;
|
|
179
|
+
this.searchInput.focused = value;
|
|
180
|
+
}
|
|
155
181
|
|
|
156
182
|
constructor({ tui, theme, source, run, done, cwd, markdownTheme }: TranscriptOverlayOptions) {
|
|
157
183
|
this.tui = tui;
|
|
@@ -162,7 +188,7 @@ export class TranscriptOverlay implements Component {
|
|
|
162
188
|
this.cwd = cwd;
|
|
163
189
|
this.markdownTheme = markdownTheme;
|
|
164
190
|
try {
|
|
165
|
-
this.content = this.
|
|
191
|
+
this.content = this.rebuildFromSource(false);
|
|
166
192
|
} catch (error) {
|
|
167
193
|
debugLog("session navigator initial render", error);
|
|
168
194
|
this.content = new Container();
|
|
@@ -172,43 +198,27 @@ export class TranscriptOverlay implements Component {
|
|
|
172
198
|
} catch (error) {
|
|
173
199
|
debugLog("session navigator subscribe", error);
|
|
174
200
|
}
|
|
175
|
-
if (this.isRunning())
|
|
176
|
-
this.runtimeInterval = setInterval(() => this.refreshRuntime(), 100);
|
|
177
|
-
}
|
|
201
|
+
if (this.isRunning()) this.runtimeInterval = setInterval(() => this.refreshRuntime(), 100);
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
handleInput(data: string): void {
|
|
181
205
|
try {
|
|
182
|
-
if (
|
|
183
|
-
this.
|
|
184
|
-
this.clearRuntimeInterval();
|
|
185
|
-
this.done(undefined);
|
|
206
|
+
if (this.searchActive) {
|
|
207
|
+
this.handleSearchInput(data);
|
|
186
208
|
return;
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
198
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
199
|
-
} else if (matchesKey(data, "pageUp") || matchesKey(data, "shift+up")) {
|
|
200
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
201
|
-
this.autoScroll = false;
|
|
202
|
-
} else if (matchesKey(data, "pageDown") || matchesKey(data, "shift+down")) {
|
|
203
|
-
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
204
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
205
|
-
} else if (matchesKey(data, "home")) {
|
|
206
|
-
this.scrollOffset = 0;
|
|
207
|
-
this.autoScroll = false;
|
|
208
|
-
} else if (matchesKey(data, "end")) {
|
|
209
|
-
this.scrollOffset = maxScroll;
|
|
210
|
-
this.autoScroll = true;
|
|
211
|
+
if (data === "/") {
|
|
212
|
+
this.beginSearch();
|
|
213
|
+
this.requestRender();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
217
|
+
this.close();
|
|
218
|
+
return;
|
|
211
219
|
}
|
|
220
|
+
this.handleScrollInput(data);
|
|
221
|
+
this.requestRender();
|
|
212
222
|
} catch (error) {
|
|
213
223
|
debugLog("session navigator input", error);
|
|
214
224
|
}
|
|
@@ -225,6 +235,7 @@ export class TranscriptOverlay implements Component {
|
|
|
225
235
|
|
|
226
236
|
private renderSafe(width: number): string[] {
|
|
227
237
|
if (width < 6) return [];
|
|
238
|
+
this.refreshProjectionIfSourceChanged();
|
|
228
239
|
const th = this.theme;
|
|
229
240
|
const innerW = width - 4;
|
|
230
241
|
const lines: string[] = [];
|
|
@@ -250,30 +261,22 @@ export class TranscriptOverlay implements Component {
|
|
|
250
261
|
for (let i = 0; i < viewportHeight; i++) lines.push(row(visible[i] ?? ""));
|
|
251
262
|
|
|
252
263
|
lines.push(hrMid);
|
|
253
|
-
|
|
254
|
-
contentLines.length <= viewportHeight
|
|
255
|
-
? "100%"
|
|
256
|
-
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
257
|
-
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
258
|
-
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
|
|
259
|
-
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
260
|
-
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
264
|
+
lines.push(row(this.footer(innerW, contentLines.length, visibleStart, viewportHeight)));
|
|
261
265
|
lines.push(hrBot);
|
|
262
|
-
|
|
263
266
|
return lines;
|
|
264
267
|
}
|
|
265
268
|
|
|
266
|
-
// fallow-ignore-next-line unused-class-member
|
|
267
269
|
invalidate(): void {
|
|
268
270
|
try {
|
|
269
271
|
this.content.invalidate();
|
|
272
|
+
this.searchInput.invalidate();
|
|
270
273
|
} catch (error) {
|
|
271
274
|
debugLog("session navigator invalidate", error);
|
|
272
275
|
}
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
dispose(): void {
|
|
276
|
-
this.
|
|
279
|
+
this.close(false);
|
|
277
280
|
const unsubscribe = this.unsubscribe;
|
|
278
281
|
this.unsubscribe = undefined;
|
|
279
282
|
if (unsubscribe) {
|
|
@@ -283,34 +286,278 @@ export class TranscriptOverlay implements Component {
|
|
|
283
286
|
debugLog("session navigator unsubscribe", error);
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
|
-
this.clearRuntimeInterval();
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
// ----
|
|
291
|
+
// ---- Search and source updates -------------------------------------------
|
|
292
|
+
|
|
293
|
+
private beginSearch(): void {
|
|
294
|
+
this.searchActive = true;
|
|
295
|
+
this.searchInput.focused = true;
|
|
296
|
+
this.searchCommitted = false;
|
|
297
|
+
this.toolsOnly = false;
|
|
298
|
+
this.newMatchCount = 0;
|
|
299
|
+
this.selectedMatchId = undefined;
|
|
300
|
+
this.autoScroll = false;
|
|
301
|
+
this.searchInput.setValue("");
|
|
302
|
+
this.matches = [];
|
|
303
|
+
this.totalMatches = 0;
|
|
304
|
+
this.matchMarks.clear();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private handleSearchInput(data: string): void {
|
|
308
|
+
if (matchesKey(data, Key.escape)) {
|
|
309
|
+
this.clearSearch();
|
|
310
|
+
this.requestRender();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (matchesKey(data, Key.tab)) {
|
|
314
|
+
this.toolsOnly = !this.toolsOnly;
|
|
315
|
+
this.reproject(false);
|
|
316
|
+
this.requestRender();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (matchesKey(data, Key.ctrl("u"))) {
|
|
320
|
+
this.searchInput.setValue("");
|
|
321
|
+
this.searchCommitted = false;
|
|
322
|
+
this.reproject(false);
|
|
323
|
+
this.requestRender();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const previous = this.searchInput.getValue();
|
|
328
|
+
const shiftEnter = matchesKey(data, "shift+enter");
|
|
329
|
+
const enter = matchesKey(data, Key.enter);
|
|
330
|
+
if (shiftEnter) {
|
|
331
|
+
this.searchCommitted = true;
|
|
332
|
+
this.selectNextMatch(-1);
|
|
333
|
+
this.requestRender();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (enter) {
|
|
337
|
+
this.searchCommitted = true;
|
|
338
|
+
this.selectNextMatch(1);
|
|
339
|
+
this.requestRender();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Printable n/N are literal while editing. Once Enter has committed the
|
|
343
|
+
// query, the mock's n/N navigation shortcuts become active.
|
|
344
|
+
if (this.searchCommitted && (data === "n" || data === "N")) {
|
|
345
|
+
this.selectNextMatch(data === "n" ? 1 : -1);
|
|
346
|
+
this.requestRender();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.searchInput.handleInput(data);
|
|
351
|
+
if (this.searchInput.getValue() !== previous) {
|
|
352
|
+
this.searchCommitted = false;
|
|
353
|
+
this.selectedMatchId = undefined;
|
|
354
|
+
this.newMatchCount = 0;
|
|
355
|
+
this.reproject(false);
|
|
356
|
+
this.requestRender();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private clearSearch(): void {
|
|
361
|
+
this.searchActive = false;
|
|
362
|
+
this.searchInput.focused = false;
|
|
363
|
+
this.searchCommitted = false;
|
|
364
|
+
this.toolsOnly = false;
|
|
365
|
+
this.newMatchCount = 0;
|
|
366
|
+
this.selectedMatchId = undefined;
|
|
367
|
+
this.searchInput.setValue("");
|
|
368
|
+
this.matches = [];
|
|
369
|
+
this.totalMatches = 0;
|
|
370
|
+
this.matchMarks.clear();
|
|
371
|
+
// Keep the current offset. Tail-following resumes only if the operator is
|
|
372
|
+
// already at the tail, so clearing cannot move the inspected block.
|
|
373
|
+
const maxScroll = Math.max(0, this.buildContentLines(this.innerWidth()).length - this.viewportHeight());
|
|
374
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
375
|
+
this.content = this.rebuildFromSource(false);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private selectNextMatch(direction: 1 | -1): void {
|
|
379
|
+
if (this.matches.length === 0) return;
|
|
380
|
+
const current = this.selectedMatchId === undefined
|
|
381
|
+
? -1
|
|
382
|
+
: this.matches.findIndex((entry) => entry.id === this.selectedMatchId);
|
|
383
|
+
const next = current < 0
|
|
384
|
+
? direction > 0 ? 0 : this.matches.length - 1
|
|
385
|
+
: (current + direction + this.matches.length) % this.matches.length;
|
|
386
|
+
this.selectedMatchId = this.matches[next]!.id;
|
|
387
|
+
this.newMatchCount = 0;
|
|
388
|
+
this.rebuildFromCurrentMessages();
|
|
389
|
+
this.scrollToSelected();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private reproject(preserveSelection: boolean): void {
|
|
393
|
+
const oldMatches = this.matches;
|
|
394
|
+
const oldIds = new Set(oldMatches.map((entry) => entry.id));
|
|
395
|
+
const priorSelection = this.selectedMatchId;
|
|
396
|
+
const queryResult = this.searchActive && this.searchInput.getValue()
|
|
397
|
+
? querySession(this.sourceMessages, {
|
|
398
|
+
query: this.searchInput.getValue(),
|
|
399
|
+
kind: "all",
|
|
400
|
+
order: "oldest",
|
|
401
|
+
limit: OVERLAY_QUERY_LIMIT,
|
|
402
|
+
entryFamily: this.toolsOnly ? "tools" : "all",
|
|
403
|
+
})
|
|
404
|
+
: { entries: [], totalMatches: 0 };
|
|
405
|
+
const matches = queryResult.entries;
|
|
406
|
+
this.matches = matches;
|
|
407
|
+
this.totalMatches = queryResult.totalMatches;
|
|
408
|
+
if (!preserveSelection) {
|
|
409
|
+
this.selectedMatchId = undefined;
|
|
410
|
+
this.newMatchCount = 0;
|
|
411
|
+
} else if (priorSelection && matches.some((entry) => entry.id === priorSelection)) {
|
|
412
|
+
this.selectedMatchId = priorSelection;
|
|
413
|
+
} else if (priorSelection && matches.length > 0) {
|
|
414
|
+
const priorIndex = oldMatches.findIndex((entry) => entry.id === priorSelection);
|
|
415
|
+
this.selectedMatchId = matches[Math.min(Math.max(priorIndex, 0), matches.length - 1)]!.id;
|
|
416
|
+
} else if (matches.length === 0) {
|
|
417
|
+
this.selectedMatchId = undefined;
|
|
418
|
+
}
|
|
419
|
+
if (preserveSelection && this.searchActive) {
|
|
420
|
+
this.newMatchCount = matches.filter((entry) => !oldIds.has(entry.id)).length;
|
|
421
|
+
}
|
|
422
|
+
this.matchMarks = new Map(matches.map((entry, index) => [entry.id, {
|
|
423
|
+
index: index + 1,
|
|
424
|
+
total: this.totalMatches,
|
|
425
|
+
selected: entry.id === this.selectedMatchId,
|
|
426
|
+
}]));
|
|
427
|
+
}
|
|
290
428
|
|
|
291
429
|
private refreshFromSource(): void {
|
|
292
430
|
if (this.closed) return;
|
|
293
431
|
try {
|
|
294
|
-
|
|
432
|
+
const oldMessages = this.sourceMessages;
|
|
433
|
+
this.sourceMessages = this.source.getMessages();
|
|
434
|
+
if (this.sourceMessages !== oldMessages) this.reproject(this.searchActive);
|
|
435
|
+
this.content = this.buildComponents(this.sourceMessages);
|
|
295
436
|
this.tui.requestRender();
|
|
296
437
|
} catch (error) {
|
|
297
438
|
debugLog("session navigator live refresh", error);
|
|
298
439
|
}
|
|
299
440
|
}
|
|
300
441
|
|
|
301
|
-
private
|
|
442
|
+
private refreshProjectionIfSourceChanged(): void {
|
|
302
443
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
444
|
+
const messages = this.source.getMessages();
|
|
445
|
+
if (messages !== this.sourceMessages) {
|
|
446
|
+
this.sourceMessages = messages;
|
|
447
|
+
this.reproject(true);
|
|
448
|
+
this.content = this.buildComponents(messages);
|
|
306
449
|
}
|
|
307
|
-
this.tui.requestRender();
|
|
308
450
|
} catch (error) {
|
|
309
|
-
debugLog("session navigator
|
|
310
|
-
this.clearRuntimeInterval();
|
|
451
|
+
debugLog("session navigator source projection", error);
|
|
311
452
|
}
|
|
312
453
|
}
|
|
313
454
|
|
|
455
|
+
private rebuildFromSource(preserveSelection: boolean): Container {
|
|
456
|
+
this.sourceMessages = this.source.getMessages();
|
|
457
|
+
this.reproject(preserveSelection);
|
|
458
|
+
return this.buildComponents(this.sourceMessages);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private rebuildFromCurrentMessages(): void {
|
|
462
|
+
this.reproject(true);
|
|
463
|
+
this.content = this.buildComponents(this.sourceMessages);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---- Ordinary transcript navigation -------------------------------------
|
|
467
|
+
|
|
468
|
+
private handleScrollInput(data: string): void {
|
|
469
|
+
const totalLines = this.buildContentLines(this.innerWidth()).length;
|
|
470
|
+
const viewportHeight = this.viewportHeight();
|
|
471
|
+
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
472
|
+
|
|
473
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
474
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
475
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
476
|
+
} else if (matchesKey(data, Key.down) || data === "j") {
|
|
477
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
478
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
479
|
+
} else if (matchesKey(data, Key.pageUp) || matchesKey(data, "shift+up")) {
|
|
480
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
481
|
+
this.autoScroll = false;
|
|
482
|
+
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, "shift+down")) {
|
|
483
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
484
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
485
|
+
} else if (matchesKey(data, Key.home)) {
|
|
486
|
+
this.scrollOffset = 0;
|
|
487
|
+
this.autoScroll = false;
|
|
488
|
+
} else if (matchesKey(data, Key.end)) {
|
|
489
|
+
this.scrollOffset = maxScroll;
|
|
490
|
+
this.autoScroll = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private scrollToSelected(): void {
|
|
495
|
+
if (!this.selectedMatchId) return;
|
|
496
|
+
const width = this.innerWidth();
|
|
497
|
+
let line = 0;
|
|
498
|
+
let selectedLine: number | undefined;
|
|
499
|
+
for (const child of this.content.children) {
|
|
500
|
+
if (isMatchChrome(child) && child.matchId === this.selectedMatchId) selectedLine = line;
|
|
501
|
+
line += child.render(width).length;
|
|
502
|
+
}
|
|
503
|
+
if (selectedLine === undefined) return;
|
|
504
|
+
const viewport = this.viewportHeight();
|
|
505
|
+
const maxScroll = Math.max(0, this.buildContentLines(width).length - viewport);
|
|
506
|
+
this.scrollOffset = Math.min(maxScroll, Math.max(0, selectedLine - Math.floor(viewport / 3)));
|
|
507
|
+
this.autoScroll = false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---- Rendering and lifecycle --------------------------------------------
|
|
511
|
+
|
|
512
|
+
private footer(innerW: number, contentLineCount: number, visibleStart: number, viewportHeight: number): string {
|
|
513
|
+
if (this.searchActive) {
|
|
514
|
+
const input = this.searchInput.render(Math.max(8, Math.min(32, innerW - 48)))[0] ?? "> ";
|
|
515
|
+
const inputLine = "/" + input.slice(1);
|
|
516
|
+
const filter = `[${this.toolsOnly ? "tools" : "all"}]`;
|
|
517
|
+
const query = this.searchInput.getValue();
|
|
518
|
+
const count = !query
|
|
519
|
+
? "type to search"
|
|
520
|
+
: this.totalMatches === 0
|
|
521
|
+
? "0 matches"
|
|
522
|
+
: `${this.totalMatches > OVERLAY_QUERY_LIMIT ? `${OVERLAY_QUERY_LIMIT}+` : this.totalMatches} matches${this.selectedMatchId ? ` [${this.selectedIndex()}/${this.totalMatches}]` : ""}${this.newMatchCount > 0 ? ` (+${this.newMatchCount} new)` : ""}`;
|
|
523
|
+
const source = this.source.availability?.();
|
|
524
|
+
const sourceNote = sourceFooterNote(source);
|
|
525
|
+
const hints = "Enter/n next · Shift+Enter/N prev · Tab filter · Esc clear";
|
|
526
|
+
return truncateToWidth(`${inputLine} ${filter} ${count}${sourceNote} · ${hints}`, innerW, "");
|
|
527
|
+
}
|
|
528
|
+
const scrollPct = contentLineCount <= viewportHeight
|
|
529
|
+
? "100%"
|
|
530
|
+
: `${Math.round(((visibleStart + viewportHeight) / contentLineCount) * 100)}%`;
|
|
531
|
+
const source = this.source.availability?.();
|
|
532
|
+
const sourceNote = sourceFooterNote(source);
|
|
533
|
+
const footerLeft = ` ${contentLineCount} lines · ${scrollPct}${sourceNote}`;
|
|
534
|
+
const footerRight = "↑↓/j/k scroll · PgUp/PgDn · / search · Esc/q close";
|
|
535
|
+
const gap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
536
|
+
return truncateToWidth(footerLeft + " ".repeat(gap) + footerRight, innerW, "");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private selectedIndex(): number {
|
|
540
|
+
const index = this.matches.findIndex((entry) => entry.id === this.selectedMatchId);
|
|
541
|
+
return index < 0 ? 0 : index + 1;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private buildContentLines(innerW: number): string[] {
|
|
545
|
+
if (innerW <= 0) return [];
|
|
546
|
+
const lines = this.content.render(innerW);
|
|
547
|
+
const streaming = this.source.streaming();
|
|
548
|
+
if (streaming) lines.push("", `◍ ${describeActivity(streaming.activeTools, streaming.responseText)}`);
|
|
549
|
+
return lines.map((line) => truncateToWidth(line, innerW));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private buildComponents(messages: readonly SessionMessage[]): Container {
|
|
553
|
+
return buildTranscriptComponents(messages, {
|
|
554
|
+
tui: this.tui,
|
|
555
|
+
cwd: this.cwd,
|
|
556
|
+
markdownTheme: this.markdownTheme,
|
|
557
|
+
getToolDefinition: (name) => this.source.getToolDefinition(name),
|
|
558
|
+
}, this.matchMarks, this.theme);
|
|
559
|
+
}
|
|
560
|
+
|
|
314
561
|
private isRunning(): boolean {
|
|
315
562
|
try {
|
|
316
563
|
return this.run.completedAt() === undefined;
|
|
@@ -320,6 +567,21 @@ export class TranscriptOverlay implements Component {
|
|
|
320
567
|
}
|
|
321
568
|
}
|
|
322
569
|
|
|
570
|
+
private refreshRuntime(): void {
|
|
571
|
+
try {
|
|
572
|
+
if (this.closed) {
|
|
573
|
+
this.clearRuntimeInterval();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
this.refreshProjectionIfSourceChanged();
|
|
577
|
+
if (!this.isRunning()) this.clearRuntimeInterval();
|
|
578
|
+
this.tui.requestRender();
|
|
579
|
+
} catch (error) {
|
|
580
|
+
debugLog("session navigator runtime refresh", error);
|
|
581
|
+
this.clearRuntimeInterval();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
323
585
|
private clearRuntimeInterval(): void {
|
|
324
586
|
if (this.runtimeInterval !== undefined) {
|
|
325
587
|
clearInterval(this.runtimeInterval);
|
|
@@ -327,33 +589,30 @@ export class TranscriptOverlay implements Component {
|
|
|
327
589
|
}
|
|
328
590
|
}
|
|
329
591
|
|
|
330
|
-
private innerWidth(): number {
|
|
331
|
-
return Math.max(0, this.tui.terminal.columns - 4);
|
|
332
|
-
}
|
|
333
|
-
|
|
592
|
+
private innerWidth(): number { return Math.max(0, this.tui.terminal.columns - 4); }
|
|
334
593
|
private viewportHeight(): number {
|
|
335
594
|
const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
336
595
|
return Math.max(MIN_VIEWPORT, maxRows - CHROME_LINES);
|
|
337
596
|
}
|
|
597
|
+
private requestRender(): void {
|
|
598
|
+
try { this.tui.requestRender(); } catch (error) { debugLog("session navigator request render", error); }
|
|
599
|
+
}
|
|
338
600
|
|
|
339
|
-
private
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
601
|
+
private close(notify = true): void {
|
|
602
|
+
if (this.closed) return;
|
|
603
|
+
this.closed = true;
|
|
604
|
+
this.searchInput.focused = false;
|
|
605
|
+
this.clearRuntimeInterval();
|
|
606
|
+
if (notify) {
|
|
607
|
+
try { this.done(undefined); } catch (error) { debugLog("session navigator close", error); }
|
|
345
608
|
}
|
|
346
|
-
return lines.map((l) => truncateToWidth(l, innerW));
|
|
347
609
|
}
|
|
610
|
+
}
|
|
348
611
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
markdownTheme: this.markdownTheme,
|
|
354
|
-
getToolDefinition: (name) => this.source.getToolDefinition(name),
|
|
355
|
-
});
|
|
356
|
-
}
|
|
612
|
+
function sourceFooterNote(source: TranscriptSourceAvailability | undefined): string {
|
|
613
|
+
if (source?.kind === "file") return " · released (snapshot)";
|
|
614
|
+
if (source?.kind === "unavailable") return " · released / transcript unavailable";
|
|
615
|
+
return "";
|
|
357
616
|
}
|
|
358
617
|
|
|
359
618
|
/** Dependencies the per-entry component tree needs from the SDK/TUI environment. */
|
|
@@ -368,17 +627,31 @@ interface TranscriptRenderOptions {
|
|
|
368
627
|
* Build a `Container` of Pi's per-entry components from a message snapshot,
|
|
369
628
|
* mirroring Pi's own interactive-mode `renderSessionContext` mapping. Tool
|
|
370
629
|
* results are matched to their tool-call components by id, exactly as Pi does.
|
|
371
|
-
* `custom`-role messages are skipped — rendering them needs the child session's
|
|
372
|
-
* message-renderer registry, which the navigator does not hold.
|
|
373
630
|
*/
|
|
374
631
|
function buildTranscriptComponents(
|
|
375
632
|
messages: readonly SessionMessage[],
|
|
376
633
|
opts: TranscriptRenderOptions,
|
|
634
|
+
marks: ReadonlyMap<string, MatchMark>,
|
|
635
|
+
theme: Theme,
|
|
377
636
|
): Container {
|
|
378
637
|
const container = new Container();
|
|
379
638
|
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
380
|
-
|
|
381
|
-
|
|
639
|
+
const entries = projectSessionMessages(messages);
|
|
640
|
+
const messageEntries = new Map(entries.filter((entry) => entry.kind === "message").map((entry) => [entry.sourceIndex, entry]));
|
|
641
|
+
const toolEntries = new Map(entries.filter((entry) => entry.kind === "tool_call").map((entry) => [entry.id, entry]));
|
|
642
|
+
for (let sourceIndex = 0; sourceIndex < messages.length; sourceIndex++) {
|
|
643
|
+
const message = messages[sourceIndex]!;
|
|
644
|
+
addMessageComponents(
|
|
645
|
+
container,
|
|
646
|
+
message,
|
|
647
|
+
pendingTools,
|
|
648
|
+
opts,
|
|
649
|
+
marks,
|
|
650
|
+
theme,
|
|
651
|
+
messageEntries.get(sourceIndex),
|
|
652
|
+
toolEntries,
|
|
653
|
+
sourceIndex,
|
|
654
|
+
);
|
|
382
655
|
}
|
|
383
656
|
return container;
|
|
384
657
|
}
|
|
@@ -388,10 +661,19 @@ function addMessageComponents(
|
|
|
388
661
|
message: SessionMessage,
|
|
389
662
|
pendingTools: Map<string, ToolExecutionComponent>,
|
|
390
663
|
opts: TranscriptRenderOptions,
|
|
664
|
+
marks: ReadonlyMap<string, MatchMark>,
|
|
665
|
+
theme: Theme,
|
|
666
|
+
messageEntry: SessionQueryEntry | undefined,
|
|
667
|
+
toolEntries: ReadonlyMap<string, SessionQueryEntry>,
|
|
668
|
+
sourceIndex: number,
|
|
391
669
|
): void {
|
|
670
|
+
const add = (component: Component, entry: SessionQueryEntry | undefined): void => {
|
|
671
|
+
const mark = entry ? marks.get(entry.id) : undefined;
|
|
672
|
+
container.addChild(mark ? new MatchChrome(component, entry!.id, mark, theme) : component);
|
|
673
|
+
};
|
|
392
674
|
switch (message.role) {
|
|
393
675
|
case "assistant": {
|
|
394
|
-
|
|
676
|
+
add(new AssistantMessageComponent(message, false, opts.markdownTheme), messageEntry);
|
|
395
677
|
for (const content of message.content) {
|
|
396
678
|
if (content.type !== "toolCall") continue;
|
|
397
679
|
const tool = new ToolExecutionComponent(
|
|
@@ -404,7 +686,7 @@ function addMessageComponents(
|
|
|
404
686
|
opts.cwd,
|
|
405
687
|
);
|
|
406
688
|
tool.setExpanded(true);
|
|
407
|
-
|
|
689
|
+
add(tool, toolEntries.get(content.id));
|
|
408
690
|
pendingTools.set(content.id, tool);
|
|
409
691
|
}
|
|
410
692
|
break;
|
|
@@ -414,31 +696,28 @@ function addMessageComponents(
|
|
|
414
696
|
pendingTools.delete(message.toolCallId);
|
|
415
697
|
break;
|
|
416
698
|
}
|
|
417
|
-
case "user":
|
|
418
|
-
addUserComponents(container, message.content, opts.markdownTheme);
|
|
699
|
+
case "user":
|
|
700
|
+
addUserComponents(container, message.content, opts.markdownTheme, messageEntry, marks, theme);
|
|
419
701
|
break;
|
|
420
|
-
}
|
|
421
702
|
case "bashExecution": {
|
|
422
703
|
const bash = new BashExecutionComponent(message.command, opts.tui, message.excludeFromContext);
|
|
423
704
|
if (message.output) bash.appendOutput(message.output);
|
|
424
705
|
bash.setComplete(message.exitCode, message.cancelled, undefined, message.fullOutputPath);
|
|
425
|
-
|
|
706
|
+
add(bash, toolEntries.get(`bash:${sourceIndex}`));
|
|
426
707
|
break;
|
|
427
708
|
}
|
|
428
|
-
case "compactionSummary":
|
|
709
|
+
case "compactionSummary":
|
|
429
710
|
container.addChild(new Spacer(1));
|
|
430
711
|
const summary = new CompactionSummaryMessageComponent(message, opts.markdownTheme);
|
|
431
712
|
summary.setExpanded(true);
|
|
432
713
|
container.addChild(summary);
|
|
433
714
|
break;
|
|
434
|
-
|
|
435
|
-
case "branchSummary": {
|
|
715
|
+
case "branchSummary":
|
|
436
716
|
container.addChild(new Spacer(1));
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
container.addChild(
|
|
717
|
+
const branch = new BranchSummaryMessageComponent(message, opts.markdownTheme);
|
|
718
|
+
branch.setExpanded(true);
|
|
719
|
+
container.addChild(branch);
|
|
440
720
|
break;
|
|
441
|
-
}
|
|
442
721
|
}
|
|
443
722
|
}
|
|
444
723
|
|
|
@@ -447,22 +726,25 @@ function addUserComponents(
|
|
|
447
726
|
container: Container,
|
|
448
727
|
content: string | readonly { type: string; text?: string }[],
|
|
449
728
|
markdownTheme: MarkdownTheme,
|
|
729
|
+
entry: SessionQueryEntry | undefined,
|
|
730
|
+
marks: ReadonlyMap<string, MatchMark>,
|
|
731
|
+
theme: Theme,
|
|
450
732
|
): void {
|
|
451
733
|
const text = userMessageText(content);
|
|
452
734
|
if (!text) return;
|
|
735
|
+
const mark = entry ? marks.get(entry.id) : undefined;
|
|
736
|
+
const wrap = (component: Component): void => container.addChild(mark ? new MatchChrome(component, entry!.id, mark, theme) : component);
|
|
453
737
|
if (container.children.length > 0) container.addChild(new Spacer(1));
|
|
454
738
|
|
|
455
739
|
const skillBlock = parseSkillBlock(text);
|
|
456
740
|
if (!skillBlock) {
|
|
457
|
-
|
|
741
|
+
wrap(new UserMessageComponent(text, markdownTheme));
|
|
458
742
|
return;
|
|
459
743
|
}
|
|
460
|
-
|
|
461
|
-
skill.setExpanded(true);
|
|
462
|
-
container.addChild(skill);
|
|
744
|
+
wrap(new SkillInvocationMessageComponent(skillBlock, markdownTheme));
|
|
463
745
|
if (skillBlock.userMessage) {
|
|
464
746
|
container.addChild(new Spacer(1));
|
|
465
|
-
|
|
747
|
+
wrap(new UserMessageComponent(skillBlock.userMessage, markdownTheme));
|
|
466
748
|
}
|
|
467
749
|
}
|
|
468
750
|
|
|
@@ -474,3 +756,39 @@ function userMessageText(content: string | readonly { type: string; text?: strin
|
|
|
474
756
|
.map((block) => block.text ?? "")
|
|
475
757
|
.join("");
|
|
476
758
|
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Block-level chrome intentionally wraps native rendering; it does not inspect
|
|
762
|
+
* or reformat the child body, so Pi's tool and markdown renderers stay in use.
|
|
763
|
+
*/
|
|
764
|
+
class MatchChrome implements Component {
|
|
765
|
+
readonly matchId: string;
|
|
766
|
+
private readonly component: Component;
|
|
767
|
+
private readonly mark: MatchMark;
|
|
768
|
+
private readonly theme: Theme;
|
|
769
|
+
|
|
770
|
+
constructor(component: Component, matchId: string, mark: MatchMark, theme: Theme) {
|
|
771
|
+
this.component = component;
|
|
772
|
+
this.mark = mark;
|
|
773
|
+
this.matchId = matchId;
|
|
774
|
+
this.theme = theme;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
render(width: number): string[] {
|
|
778
|
+
if (width <= 2) return this.component.render(Math.max(1, width));
|
|
779
|
+
const bodyWidth = width - 2;
|
|
780
|
+
const lines = this.component.render(bodyWidth);
|
|
781
|
+
const color = this.mark.selected ? "accent" : "warning";
|
|
782
|
+
const label = `${this.mark.selected ? "▶ " : ""}MATCH ${this.mark.index}/${this.mark.total}`;
|
|
783
|
+
const header = truncateToWidth(this.theme.fg(color, `╭─ ${label} ─`), width, "");
|
|
784
|
+
return [header, ...lines.map((line) =>
|
|
785
|
+
truncateToWidth(this.theme.fg(color, "│") + " " + line, width, ""),
|
|
786
|
+
)];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
invalidate(): void { this.component.invalidate(); }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function isMatchChrome(component: Component): component is MatchChrome {
|
|
793
|
+
return component instanceof MatchChrome;
|
|
794
|
+
}
|