pi-sessions 0.12.1 → 0.13.1
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/README.md +6 -3
- package/extensions/pi-sessions.ts +20 -6
- package/extensions/session-ask/install.ts +24 -10
- package/extensions/session-auto-title/generate.ts +17 -14
- package/extensions/session-auto-title/install.ts +6 -29
- package/extensions/session-auto-title/retitle.ts +6 -19
- package/extensions/session-auto-title/wizard.ts +97 -31
- package/extensions/session-handoff/board-loading.ts +93 -0
- package/extensions/session-handoff/board.ts +239 -127
- package/extensions/session-handoff/install.ts +5 -5
- package/extensions/session-handoff/picker.ts +167 -19
- package/extensions/session-handoff/review.ts +56 -7
- package/extensions/session-handoff/ui.ts +32 -7
- package/extensions/session-hooks/install.ts +7 -20
- package/extensions/session-index/install.ts +8 -86
- package/extensions/session-index/loader.ts +80 -0
- package/extensions/session-index/panel.ts +103 -0
- package/extensions/session-messaging/broker/spawn.ts +35 -12
- package/extensions/session-messaging/pi/reachable-tool.ts +6 -1
- package/extensions/session-messaging/pi/tools.ts +79 -12
- package/extensions/session-search/extract.ts +120 -91
- package/extensions/session-search/hooks.ts +25 -203
- package/extensions/session-search/reindex.ts +13 -8
- package/extensions/shared/legend.ts +116 -0
- package/extensions/shared/session-index/scoring.ts +0 -1
- package/extensions/shared/session-index/search.ts +76 -49
- package/extensions/shared/settings.ts +22 -0
- package/extensions/subagents/context-limit.ts +58 -0
- package/extensions/subagents/install.ts +110 -30
- package/extensions/subagents/reconcile.ts +58 -19
- package/extensions/subagents/report.ts +0 -2
- package/extensions/subagents/settle.ts +31 -0
- package/extensions/subagents/should-message.ts +59 -0
- package/package.json +13 -13
- package/extensions/session-handoff/refs.ts +0 -145
|
@@ -5,9 +5,12 @@ import {
|
|
|
5
5
|
type KeyId,
|
|
6
6
|
matchesKey,
|
|
7
7
|
type TUI,
|
|
8
|
+
type TuiMouseEvent,
|
|
9
|
+
type TuiMouseEventResult,
|
|
8
10
|
truncateToWidth,
|
|
9
11
|
visibleWidth,
|
|
10
12
|
} from "@earendil-works/pi-tui";
|
|
13
|
+
import { type LegendHit, LegendPointer, layoutLegend, legendHitAt } from "../shared/legend.ts";
|
|
11
14
|
import {
|
|
12
15
|
stripSearchSnippetMarkers,
|
|
13
16
|
transformSearchSnippetMatches,
|
|
@@ -24,6 +27,12 @@ interface PickerRightColumnWidths {
|
|
|
24
27
|
modifiedAt: number;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
interface PickerSessionHit {
|
|
31
|
+
row: number;
|
|
32
|
+
end: number;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
export type SessionPickerResult =
|
|
28
37
|
| { kind: "cancel" }
|
|
29
38
|
| { kind: "insert-session-token"; sessionId: string };
|
|
@@ -66,6 +75,11 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
66
75
|
private includeAll = false;
|
|
67
76
|
private items: SessionPickerItem[] = [];
|
|
68
77
|
private selectedIndex = 0;
|
|
78
|
+
private sessionHits: PickerSessionHit[] = [];
|
|
79
|
+
private actionRows = new Map<number, LegendHit[]>();
|
|
80
|
+
private inputBounds: { row: number; width: number } | undefined;
|
|
81
|
+
private pressedSessionId: string | undefined;
|
|
82
|
+
private readonly legendPointer = new LegendPointer(() => this.tui.requestRender());
|
|
69
83
|
private searchReloadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
70
84
|
|
|
71
85
|
get focused(): boolean {
|
|
@@ -101,10 +115,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
if (this.keybindings.matches(data, "tui.input.tab")) {
|
|
104
|
-
this.
|
|
105
|
-
this.cancelSearchReload();
|
|
106
|
-
this.reload();
|
|
107
|
-
this.tui.requestRender();
|
|
118
|
+
this.setScope(!this.includeAll);
|
|
108
119
|
return;
|
|
109
120
|
}
|
|
110
121
|
|
|
@@ -133,11 +144,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
136
|
-
this.
|
|
137
|
-
const selected = this.items[this.selectedIndex];
|
|
138
|
-
if (selected?.kind === "session") {
|
|
139
|
-
this.finish({ kind: "insert-session-token", sessionId: selected.sessionId });
|
|
140
|
-
}
|
|
147
|
+
this.confirmSelection();
|
|
141
148
|
return;
|
|
142
149
|
}
|
|
143
150
|
|
|
@@ -149,12 +156,88 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
160
|
+
if (event.type === "press") this.pressedSessionId = undefined;
|
|
161
|
+
const action =
|
|
162
|
+
event.type === "press"
|
|
163
|
+
? legendHitAt(this.actionRows.get(event.y) ?? [], event.x - 1)
|
|
164
|
+
: undefined;
|
|
165
|
+
const legend = this.legendPointer.handleMouse(event, action);
|
|
166
|
+
if (legend) return event.type === "press" ? { ...legend, focus: true } : legend;
|
|
167
|
+
if (event.type === "press") {
|
|
168
|
+
if (event.button !== "left") return undefined;
|
|
169
|
+
const session = this.sessionAt(event);
|
|
170
|
+
if (session) {
|
|
171
|
+
this.pressedSessionId = session.sessionId;
|
|
172
|
+
const index = this.items.findIndex(
|
|
173
|
+
(item) => item.kind === "session" && item.sessionId === session.sessionId,
|
|
174
|
+
);
|
|
175
|
+
if (index >= 0) this.selectedIndex = index;
|
|
176
|
+
return { handled: true, focus: true };
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
if (event.type === "wheel" && event.wheelDelta && this.sessionAt(event)) {
|
|
181
|
+
this.moveSelection(event.wheelDelta < 0 ? -1 : 1);
|
|
182
|
+
return { handled: true };
|
|
183
|
+
}
|
|
184
|
+
if (event.type !== "click" || event.button !== "left") return undefined;
|
|
185
|
+
const sessionId = this.pressedSessionId;
|
|
186
|
+
this.pressedSessionId = undefined;
|
|
187
|
+
if (sessionId) {
|
|
188
|
+
this.finish({ kind: "insert-session-token", sessionId });
|
|
189
|
+
return { handled: true };
|
|
190
|
+
}
|
|
191
|
+
const input = this.inputBounds;
|
|
192
|
+
if (input && event.y === input.row && event.x >= 1 && event.x < input.width + 1) {
|
|
193
|
+
// Input places its cursor on press; defer that call until click so text drags stay unhandled.
|
|
194
|
+
return this.input.handleMouse({
|
|
195
|
+
...event,
|
|
196
|
+
type: "press",
|
|
197
|
+
x: event.x - 1,
|
|
198
|
+
y: 0,
|
|
199
|
+
width: input.width,
|
|
200
|
+
height: 1,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private sessionAt(event: TuiMouseEvent): PickerSessionHit | undefined {
|
|
207
|
+
return this.sessionHits.find((hit) => hit.row === event.y && event.x >= 1 && event.x < hit.end);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private setScope(includeAll: boolean): void {
|
|
211
|
+
this.includeAll = includeAll;
|
|
212
|
+
this.cancelSearchReload();
|
|
213
|
+
this.reload();
|
|
214
|
+
this.tui.requestRender();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private confirmSelection(): void {
|
|
218
|
+
this.flushSearchReload();
|
|
219
|
+
const selected = this.items[this.selectedIndex];
|
|
220
|
+
if (selected?.kind === "session") {
|
|
221
|
+
this.finish({ kind: "insert-session-token", sessionId: selected.sessionId });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
152
225
|
render(width: number): string[] {
|
|
226
|
+
this.sessionHits = [];
|
|
227
|
+
this.actionRows.clear();
|
|
228
|
+
this.inputBounds = undefined;
|
|
153
229
|
const panelWidth = Math.max(0, width);
|
|
154
230
|
const innerWidth = Math.max(0, panelWidth - 2);
|
|
155
|
-
const
|
|
156
|
-
?
|
|
157
|
-
|
|
231
|
+
const folder = this.theme.fg(
|
|
232
|
+
this.includeAll ? "muted" : "accent",
|
|
233
|
+
`${this.includeAll ? "○" : "◉"} Current Folder`,
|
|
234
|
+
);
|
|
235
|
+
const all = this.theme.fg(
|
|
236
|
+
this.includeAll ? "accent" : "muted",
|
|
237
|
+
`${this.includeAll ? "◉" : "○"} All`,
|
|
238
|
+
);
|
|
239
|
+
const separator = this.theme.fg("muted", " | ");
|
|
240
|
+
const scopeText = `${folder}${separator}${all}`;
|
|
158
241
|
const title = this.theme.bold("Add Session Reference to Prompt");
|
|
159
242
|
const titleWidth = Math.max(0, innerWidth - visibleWidth(scopeText) - 1);
|
|
160
243
|
const titleText = truncateToWidth(title, titleWidth, "…", true);
|
|
@@ -164,15 +247,57 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
164
247
|
);
|
|
165
248
|
|
|
166
249
|
const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
167
|
-
|
|
250
|
+
const scopeStart = visibleWidth(titleText) + headerSpacing;
|
|
251
|
+
if (scopeStart + visibleWidth(scopeText) <= innerWidth) {
|
|
252
|
+
const folderEnd = scopeStart + visibleWidth(folder);
|
|
253
|
+
const allStart = folderEnd + visibleWidth(separator);
|
|
254
|
+
this.actionRows.set(1, [
|
|
255
|
+
{ id: "scope:folder", start: scopeStart, end: folderEnd, run: () => this.setScope(false) },
|
|
256
|
+
{
|
|
257
|
+
id: "scope:all",
|
|
258
|
+
start: allStart,
|
|
259
|
+
end: allStart + visibleWidth(all),
|
|
260
|
+
run: () => this.setScope(true),
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
const shownFolder =
|
|
265
|
+
this.actionRows.has(1) && this.legendPointer.pressedId === "scope:folder"
|
|
266
|
+
? this.theme.bg("selectedBg", folder)
|
|
267
|
+
: folder;
|
|
268
|
+
const shownAll =
|
|
269
|
+
this.actionRows.has(1) && this.legendPointer.pressedId === "scope:all"
|
|
270
|
+
? this.theme.bg("selectedBg", all)
|
|
271
|
+
: all;
|
|
168
272
|
lines.push(
|
|
169
273
|
this.renderRow(
|
|
170
|
-
|
|
274
|
+
`${titleText}${" ".repeat(headerSpacing)}${shownFolder}${separator}${shownAll}`,
|
|
171
275
|
innerWidth,
|
|
172
276
|
),
|
|
173
277
|
);
|
|
278
|
+
const selected = this.items[this.selectedIndex];
|
|
279
|
+
const legend = layoutLegend(
|
|
280
|
+
this.theme,
|
|
281
|
+
[
|
|
282
|
+
{
|
|
283
|
+
key: "enter",
|
|
284
|
+
description: "add to prompt",
|
|
285
|
+
run: () => {
|
|
286
|
+
if (selected?.kind === "session") {
|
|
287
|
+
this.finish({ kind: "insert-session-token", sessionId: selected.sessionId });
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{ key: "esc", description: "cancel", run: () => this.finish({ kind: "cancel" }) },
|
|
292
|
+
{ key: "tab", description: "scope", run: () => this.setScope(!this.includeAll) },
|
|
293
|
+
],
|
|
294
|
+
{ width: innerWidth, separator: " · ", pressedId: this.legendPointer.pressedId },
|
|
295
|
+
);
|
|
296
|
+
this.actionRows.set(lines.length, legend.hits);
|
|
297
|
+
lines.push(this.renderRow(legend.text, innerWidth));
|
|
174
298
|
lines.push(this.renderRow("", innerWidth));
|
|
175
299
|
|
|
300
|
+
this.inputBounds = { row: lines.length, width: innerWidth };
|
|
176
301
|
for (const inputLine of this.input.render(innerWidth)) {
|
|
177
302
|
lines.push(this.renderRow(inputLine, innerWidth));
|
|
178
303
|
}
|
|
@@ -183,7 +308,13 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
183
308
|
const rightWidths = this.getRightColumnWidths();
|
|
184
309
|
for (const { item, index } of visibleItems) {
|
|
185
310
|
lines.push(
|
|
186
|
-
...this.renderPickerItem(
|
|
311
|
+
...this.renderPickerItem(
|
|
312
|
+
item,
|
|
313
|
+
index === this.selectedIndex,
|
|
314
|
+
innerWidth,
|
|
315
|
+
rightWidths,
|
|
316
|
+
lines.length,
|
|
317
|
+
),
|
|
187
318
|
);
|
|
188
319
|
}
|
|
189
320
|
|
|
@@ -210,6 +341,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
210
341
|
selected: boolean,
|
|
211
342
|
innerWidth: number,
|
|
212
343
|
rightWidths: PickerRightColumnWidths,
|
|
344
|
+
firstRow: number,
|
|
213
345
|
): string[] {
|
|
214
346
|
if (item.kind !== "session") {
|
|
215
347
|
const message = [item.title, item.description].filter(Boolean).join(" — ");
|
|
@@ -232,7 +364,9 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
232
364
|
innerWidth - 2 - visibleWidth(cursor) - visibleWidth(left) - visibleWidth(right),
|
|
233
365
|
);
|
|
234
366
|
const titleLine = `${cursor}${left}${" ".repeat(spacing)}${right}`;
|
|
235
|
-
const lines = [
|
|
367
|
+
const lines = [
|
|
368
|
+
this.renderSelectableRow(titleLine, innerWidth, selected, firstRow, item.sessionId),
|
|
369
|
+
];
|
|
236
370
|
|
|
237
371
|
if (item.snippet && !highlightedTitle) {
|
|
238
372
|
const snippet = this.formatSnippet(
|
|
@@ -241,7 +375,13 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
241
375
|
);
|
|
242
376
|
if (snippet) {
|
|
243
377
|
lines.push(
|
|
244
|
-
this.renderSelectableRow(
|
|
378
|
+
this.renderSelectableRow(
|
|
379
|
+
` ${this.theme.fg("dim", snippet)}`,
|
|
380
|
+
innerWidth,
|
|
381
|
+
selected,
|
|
382
|
+
firstRow + lines.length,
|
|
383
|
+
item.sessionId,
|
|
384
|
+
),
|
|
245
385
|
);
|
|
246
386
|
}
|
|
247
387
|
}
|
|
@@ -250,11 +390,19 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
250
390
|
}
|
|
251
391
|
|
|
252
392
|
private renderRow(content: string, innerWidth: number): string {
|
|
393
|
+
content = truncateToWidth(content, innerWidth, "…");
|
|
253
394
|
const pad = Math.max(0, innerWidth - visibleWidth(content));
|
|
254
395
|
return `${this.theme.fg("border", "│")}${content}${" ".repeat(pad)}${this.theme.fg("border", "│")}`;
|
|
255
396
|
}
|
|
256
397
|
|
|
257
|
-
private renderSelectableRow(
|
|
398
|
+
private renderSelectableRow(
|
|
399
|
+
content: string,
|
|
400
|
+
innerWidth: number,
|
|
401
|
+
selected: boolean,
|
|
402
|
+
row: number,
|
|
403
|
+
sessionId: string,
|
|
404
|
+
): string {
|
|
405
|
+
this.sessionHits.push({ row, end: 1 + Math.min(innerWidth, visibleWidth(content)), sessionId });
|
|
258
406
|
return this.renderRow(selected ? this.theme.bg("selectedBg", content) : content, innerWidth);
|
|
259
407
|
}
|
|
260
408
|
|
|
@@ -277,7 +425,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
277
425
|
return undefined;
|
|
278
426
|
}
|
|
279
427
|
|
|
280
|
-
return truncateToWidth(rendered, maxWidth, "…"
|
|
428
|
+
return truncateToWidth(rendered, maxWidth, "…");
|
|
281
429
|
}
|
|
282
430
|
|
|
283
431
|
private highlightSnippetMatches(snippet: string): string | undefined {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
3
3
|
import {
|
|
4
4
|
Key,
|
|
5
5
|
matchesKey,
|
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
visibleWidth,
|
|
8
8
|
wrapTextWithAnsi,
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
|
+
import {
|
|
11
|
+
type LegendHit,
|
|
12
|
+
type LegendItem,
|
|
13
|
+
LegendPointer,
|
|
14
|
+
layoutLegend,
|
|
15
|
+
legendHitAt,
|
|
16
|
+
} from "../shared/legend.ts";
|
|
10
17
|
import { renderStrongModal } from "./strong-modal.ts";
|
|
11
18
|
|
|
12
19
|
const PREVIEW_TIMEOUT_MS = 8_000;
|
|
@@ -59,6 +66,8 @@ export class HandoffPreviewComponent implements Component {
|
|
|
59
66
|
private isTimerActive = true;
|
|
60
67
|
private scrollOffset = 0;
|
|
61
68
|
private previewWidth = 80;
|
|
69
|
+
private legendRows = new Map<number, LegendHit[]>();
|
|
70
|
+
private readonly legendPointer = new LegendPointer(() => this.requestRender());
|
|
62
71
|
|
|
63
72
|
public constructor(
|
|
64
73
|
draft: string,
|
|
@@ -119,7 +128,26 @@ export class HandoffPreviewComponent implements Component {
|
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
public handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
132
|
+
const hit =
|
|
133
|
+
event.type === "press" && !this.isDone
|
|
134
|
+
? legendHitAt(this.legendRows.get(event.y) ?? [], event.x - 2)
|
|
135
|
+
: undefined;
|
|
136
|
+
const legend = this.legendPointer.handleMouse(event, hit);
|
|
137
|
+
if (legend) {
|
|
138
|
+
if (event.type === "press") this.disableTimer();
|
|
139
|
+
return legend;
|
|
140
|
+
}
|
|
141
|
+
if (event.type === "wheel" && event.wheelDelta && !this.isDone) {
|
|
142
|
+
this.disableTimer();
|
|
143
|
+
this.scrollBy(event.wheelDelta);
|
|
144
|
+
return { handled: true };
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
public render(width: number): string[] {
|
|
150
|
+
this.legendRows.clear();
|
|
123
151
|
const modalWidth = Math.max(24, width - 4);
|
|
124
152
|
const promptBoxWidth = Math.max(20, modalWidth - 4);
|
|
125
153
|
this.previewWidth = Math.max(16, promptBoxWidth - 4);
|
|
@@ -133,8 +161,18 @@ export class HandoffPreviewComponent implements Component {
|
|
|
133
161
|
const contentLines = [
|
|
134
162
|
this.getTitleLine(),
|
|
135
163
|
"",
|
|
136
|
-
this.
|
|
137
|
-
|
|
164
|
+
this.renderKeymapLine(
|
|
165
|
+
3,
|
|
166
|
+
width,
|
|
167
|
+
{ key: "Enter:", description: "start session", run: () => this.finish("accept") },
|
|
168
|
+
{ key: "Esc:", description: "cancel", run: () => this.finish("cancel") },
|
|
169
|
+
),
|
|
170
|
+
this.renderKeymapLine(
|
|
171
|
+
4,
|
|
172
|
+
width,
|
|
173
|
+
{ key: "e:", description: "edit prompt", run: () => this.finish("edit") },
|
|
174
|
+
{ key: "j/k:", description: "scroll" },
|
|
175
|
+
),
|
|
138
176
|
"",
|
|
139
177
|
...promptLines,
|
|
140
178
|
];
|
|
@@ -142,6 +180,21 @@ export class HandoffPreviewComponent implements Component {
|
|
|
142
180
|
return renderStrongModal(contentLines, width, this.theme);
|
|
143
181
|
}
|
|
144
182
|
|
|
183
|
+
private renderKeymapLine(
|
|
184
|
+
row: number,
|
|
185
|
+
width: number,
|
|
186
|
+
left: LegendItem,
|
|
187
|
+
right: LegendItem,
|
|
188
|
+
): string {
|
|
189
|
+
const legend = layoutLegend(this.theme, [left, right], {
|
|
190
|
+
width: Math.max(20, width - 4),
|
|
191
|
+
pressedId: this.legendPointer.pressedId,
|
|
192
|
+
separator: " ".repeat(Math.max(0, 27 - visibleWidth(`${left.key} ${left.description}`))),
|
|
193
|
+
});
|
|
194
|
+
this.legendRows.set(row, legend.hits);
|
|
195
|
+
return legend.text;
|
|
196
|
+
}
|
|
197
|
+
|
|
145
198
|
public invalidate(): void {}
|
|
146
199
|
|
|
147
200
|
public stop(): void {
|
|
@@ -202,10 +255,6 @@ export class HandoffPreviewComponent implements Component {
|
|
|
202
255
|
}
|
|
203
256
|
}
|
|
204
257
|
|
|
205
|
-
function formatKeymapLine(left: string, right: string): string {
|
|
206
|
-
return `${left.padEnd(27, " ")}${right}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
258
|
function renderPromptSection(lines: string[], width: number, _theme: Theme): string[] {
|
|
210
259
|
const innerWidth = Math.max(16, width - 4);
|
|
211
260
|
const line = (left: string, text: string, right: string) => {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Key,
|
|
4
|
+
matchesKey,
|
|
5
|
+
type TuiMouseEvent,
|
|
6
|
+
type TuiMouseEventResult,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
import { type LegendHit, LegendPointer, layoutLegend, legendHitAt } from "../shared/legend.ts";
|
|
3
9
|
import { renderStrongModal } from "./strong-modal.ts";
|
|
4
10
|
|
|
5
11
|
export async function runHandoffTaskWithLoader<T>(
|
|
@@ -12,6 +18,13 @@ export async function runHandoffTaskWithLoader<T>(
|
|
|
12
18
|
const result = await ctx.ui.custom<T | undefined>(
|
|
13
19
|
(tui, theme, _keybindings, done) => {
|
|
14
20
|
const abortController = new AbortController();
|
|
21
|
+
let cancelHits: LegendHit[] = [];
|
|
22
|
+
const pointer = new LegendPointer(() => tui.requestRender());
|
|
23
|
+
const cancel = () => {
|
|
24
|
+
abortController.abort();
|
|
25
|
+
done(undefined);
|
|
26
|
+
tui.requestRender();
|
|
27
|
+
};
|
|
15
28
|
|
|
16
29
|
task(abortController.signal)
|
|
17
30
|
.then(done)
|
|
@@ -24,19 +37,31 @@ export async function runHandoffTaskWithLoader<T>(
|
|
|
24
37
|
|
|
25
38
|
return {
|
|
26
39
|
render(width: number): string[] {
|
|
40
|
+
const legend = layoutLegend(
|
|
41
|
+
theme,
|
|
42
|
+
[{ key: "Esc", description: "to cancel.", run: cancel }],
|
|
43
|
+
{ width: Math.max(20, width - 4) - 6, pressedId: pointer.pressedId },
|
|
44
|
+
);
|
|
45
|
+
cancelHits = legend.hits;
|
|
27
46
|
return renderStrongModal(
|
|
28
|
-
[
|
|
47
|
+
[
|
|
48
|
+
theme.fg("accent", theme.bold(label)),
|
|
49
|
+
"",
|
|
50
|
+
`${theme.fg("muted", "Press ")}${legend.text}`,
|
|
51
|
+
],
|
|
29
52
|
width,
|
|
30
53
|
theme,
|
|
31
54
|
);
|
|
32
55
|
},
|
|
33
56
|
invalidate(): void {},
|
|
57
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
58
|
+
return pointer.handleMouse(
|
|
59
|
+
event,
|
|
60
|
+
event.y === 3 ? legendHitAt(cancelHits, event.x - 8) : undefined,
|
|
61
|
+
);
|
|
62
|
+
},
|
|
34
63
|
handleInput(data: string): void {
|
|
35
|
-
if (matchesKey(data, Key.escape))
|
|
36
|
-
abortController.abort();
|
|
37
|
-
done(undefined);
|
|
38
|
-
tui.requestRender();
|
|
39
|
-
}
|
|
64
|
+
if (matchesKey(data, Key.escape)) cancel();
|
|
40
65
|
},
|
|
41
66
|
};
|
|
42
67
|
},
|
|
@@ -5,32 +5,20 @@ import {
|
|
|
5
5
|
} from "../session-handoff/metadata.ts";
|
|
6
6
|
import { createSessionHookController } from "../session-search/hooks.ts";
|
|
7
7
|
import type { IndexHandle, SessionLifecycle } from "../shared/composition.ts";
|
|
8
|
-
import type { SessionSettings } from "../shared/settings.ts";
|
|
9
8
|
|
|
10
|
-
export function installHooks(
|
|
11
|
-
pi: ExtensionAPI,
|
|
12
|
-
deps: { settings: SessionSettings; index: IndexHandle },
|
|
13
|
-
): SessionLifecycle {
|
|
9
|
+
export function installHooks(pi: ExtensionAPI, deps: { index: IndexHandle }): SessionLifecycle {
|
|
14
10
|
const controller = createSessionHookController({ indexPath: deps.index.path });
|
|
15
11
|
|
|
16
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
17
|
-
controller.handleToolCall(event, ctx.sessionManager.getSessionFile(), ctx.cwd);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
pi.on("tool_result", async (event) => {
|
|
21
|
-
controller.handleToolResult(event);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
12
|
pi.on("turn_end", async (_event, ctx) => {
|
|
25
|
-
await controller.handleTurnEnd(ctx.sessionManager.getSessionFile()
|
|
13
|
+
await controller.handleTurnEnd(ctx.sessionManager.getSessionFile());
|
|
26
14
|
});
|
|
27
15
|
|
|
28
16
|
pi.on("session_tree", async (_event, ctx) => {
|
|
29
|
-
await controller.handleSessionTree(ctx.sessionManager.getSessionFile()
|
|
17
|
+
await controller.handleSessionTree(ctx.sessionManager.getSessionFile());
|
|
30
18
|
});
|
|
31
19
|
|
|
32
20
|
pi.on("session_compact", async (_event, ctx) => {
|
|
33
|
-
await controller.handleSessionCompact(ctx.sessionManager.getSessionFile()
|
|
21
|
+
await controller.handleSessionCompact(ctx.sessionManager.getSessionFile());
|
|
34
22
|
});
|
|
35
23
|
|
|
36
24
|
return {
|
|
@@ -44,20 +32,19 @@ export function installHooks(
|
|
|
44
32
|
await controller.handleSessionSwitch(
|
|
45
33
|
previousSessionFile,
|
|
46
34
|
sessionFile,
|
|
47
|
-
ctx.cwd,
|
|
48
35
|
getSessionStartOrigin(ctx),
|
|
49
36
|
);
|
|
50
37
|
break;
|
|
51
38
|
case "fork":
|
|
52
|
-
await controller.handleSessionFork(previousSessionFile, sessionFile
|
|
39
|
+
await controller.handleSessionFork(previousSessionFile, sessionFile);
|
|
53
40
|
break;
|
|
54
41
|
default:
|
|
55
|
-
await controller.handleSessionStart(sessionFile
|
|
42
|
+
await controller.handleSessionStart(sessionFile);
|
|
56
43
|
break;
|
|
57
44
|
}
|
|
58
45
|
},
|
|
59
46
|
async onSessionShutdown(_event, ctx) {
|
|
60
|
-
await controller.handleSessionShutdown(ctx.sessionManager.getSessionFile()
|
|
47
|
+
await controller.handleSessionShutdown(ctx.sessionManager.getSessionFile());
|
|
61
48
|
},
|
|
62
49
|
};
|
|
63
50
|
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BorderedLoader,
|
|
3
|
-
type ExtensionAPI,
|
|
4
|
-
type ExtensionCommandContext,
|
|
5
|
-
type Theme,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
8
2
|
import { type ReindexResult, rebuildSessionIndex } from "../session-search/reindex.ts";
|
|
9
3
|
import type { IndexHandle } from "../shared/composition.ts";
|
|
10
4
|
import { isTuiMode } from "../shared/pi-mode.ts";
|
|
11
|
-
import { getIndexStatus
|
|
5
|
+
import { getIndexStatus } from "../shared/session-index/index.ts";
|
|
12
6
|
import type { SessionSettings } from "../shared/settings.ts";
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
import { ReindexLoader } from "./loader.ts";
|
|
9
|
+
import { type SessionIndexAction, SessionIndexPanel } from "./panel.ts";
|
|
15
10
|
|
|
16
11
|
export function installIndex(pi: ExtensionAPI, deps: { settings: SessionSettings }): IndexHandle {
|
|
17
12
|
const indexPath = deps.settings.index.path;
|
|
@@ -26,7 +21,8 @@ export function installIndex(pi: ExtensionAPI, deps: { settings: SessionSettings
|
|
|
26
21
|
|
|
27
22
|
const status = getIndexStatus(indexPath);
|
|
28
23
|
const action = await ctx.ui.custom<SessionIndexAction>(
|
|
29
|
-
(
|
|
24
|
+
(tui, theme, _keybindings, done) =>
|
|
25
|
+
new SessionIndexPanel(theme, status, done, () => tui.requestRender()),
|
|
30
26
|
{
|
|
31
27
|
overlay: true,
|
|
32
28
|
overlayOptions: {
|
|
@@ -66,78 +62,12 @@ export function installIndex(pi: ExtensionAPI, deps: { settings: SessionSettings
|
|
|
66
62
|
return { path: indexPath };
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
class SessionIndexPanel implements Focusable {
|
|
70
|
-
readonly width = 72;
|
|
71
|
-
focused = false;
|
|
72
|
-
|
|
73
|
-
invalidate(): void {}
|
|
74
|
-
|
|
75
|
-
constructor(
|
|
76
|
-
private readonly theme: Theme,
|
|
77
|
-
private readonly status: SessionIndexStatus,
|
|
78
|
-
private readonly done: (result: SessionIndexAction) => void,
|
|
79
|
-
) {}
|
|
80
|
-
|
|
81
|
-
handleInput(data: string): void {
|
|
82
|
-
if (isCloseKey(data)) {
|
|
83
|
-
this.done(undefined);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (isReindexKey(data)) {
|
|
88
|
-
this.done("reindex");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
render(_width: number): string[] {
|
|
93
|
-
const innerWidth = this.width - 2;
|
|
94
|
-
const lines: string[] = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
95
|
-
|
|
96
|
-
lines.push(
|
|
97
|
-
this.renderRow(innerWidth, ` ${this.theme.bold(this.theme.fg("accent", "Session Index"))}`),
|
|
98
|
-
);
|
|
99
|
-
lines.push(this.renderRow(innerWidth, ""));
|
|
100
|
-
lines.push(
|
|
101
|
-
this.renderRow(
|
|
102
|
-
innerWidth,
|
|
103
|
-
` Path: ${this.status.exists ? this.status.dbPath : "<no index found>"}`,
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
lines.push(
|
|
107
|
-
this.renderRow(
|
|
108
|
-
innerWidth,
|
|
109
|
-
` Schema version: ${this.status.schemaVersion !== undefined ? String(this.status.schemaVersion) : "n/a"}`,
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
lines.push(
|
|
113
|
-
this.renderRow(
|
|
114
|
-
innerWidth,
|
|
115
|
-
` Session count: ${this.status.sessionCount !== undefined ? String(this.status.sessionCount) : "n/a"}`,
|
|
116
|
-
),
|
|
117
|
-
);
|
|
118
|
-
lines.push(
|
|
119
|
-
this.renderRow(innerWidth, ` Last full reindex: ${this.status.lastFullReindexAt ?? "n/a"}`),
|
|
120
|
-
);
|
|
121
|
-
lines.push(this.renderRow(innerWidth, ""));
|
|
122
|
-
lines.push(this.renderRow(innerWidth, ` ${this.theme.fg("accent", "R")} rebuild from disk`));
|
|
123
|
-
lines.push(this.renderRow(innerWidth, ` ${this.theme.fg("dim", "Enter / Esc")} close`));
|
|
124
|
-
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
125
|
-
return lines;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private renderRow(innerWidth: number, content: string): string {
|
|
129
|
-
const pad = Math.max(0, innerWidth - visibleWidth(content));
|
|
130
|
-
return `${this.theme.fg("border", "│")}${content}${" ".repeat(pad)}${this.theme.fg("border", "│")}`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
65
|
async function runReindexWithLoader(
|
|
135
66
|
ctx: ExtensionCommandContext,
|
|
136
67
|
indexPath: string,
|
|
137
68
|
): Promise<ReindexResult | null> {
|
|
138
|
-
return ctx.ui.custom<ReindexResult | null>((tui, theme,
|
|
139
|
-
const loader = new
|
|
140
|
-
loader.onAbort = () => done(null);
|
|
69
|
+
return ctx.ui.custom<ReindexResult | null>((tui, theme, keybindings, done) => {
|
|
70
|
+
const loader = new ReindexLoader(tui, theme, keybindings, () => done(null));
|
|
141
71
|
|
|
142
72
|
void (async () => {
|
|
143
73
|
try {
|
|
@@ -152,11 +82,3 @@ async function runReindexWithLoader(
|
|
|
152
82
|
return loader;
|
|
153
83
|
});
|
|
154
84
|
}
|
|
155
|
-
|
|
156
|
-
function isCloseKey(data: string): boolean {
|
|
157
|
-
return matchesKey(data, "escape") || matchesKey(data, "enter");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isReindexKey(data: string): boolean {
|
|
161
|
-
return data === "r" || data === "R" || matchesKey(data, "r");
|
|
162
|
-
}
|