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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SubagentRoster } from "../subagents/roster.ts";
|
|
3
|
+
import type { HandoffBoardSnapshot, UserSessionEntry } from "./board-view-model.ts";
|
|
4
|
+
import { HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE } from "./metadata.ts";
|
|
5
|
+
import { parseHandoffLaunchReceiptEntry } from "./receipt.ts";
|
|
6
|
+
|
|
7
|
+
export interface HandoffBoardServices {
|
|
8
|
+
roster?: SubagentRoster | undefined;
|
|
9
|
+
cancelSubagent?: ((sessionId: string) => Promise<unknown>) | undefined;
|
|
10
|
+
listLiveSessions?: (() => Promise<string[]>) | undefined;
|
|
11
|
+
readSessionEntries?: ((sessionFile: string) => readonly SessionEntry[]) | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadHandoffBoardSnapshot(
|
|
15
|
+
entries: readonly SessionEntry[],
|
|
16
|
+
services: HandoffBoardServices,
|
|
17
|
+
): Promise<HandoffBoardSnapshot> {
|
|
18
|
+
const userSessions = collectUserSessions(entries);
|
|
19
|
+
const [branchRoster, liveSessionIds, hydratedUserSessions] = await Promise.all([
|
|
20
|
+
services.roster?.resolve("branch") ?? Promise.resolve({ entries: [], total: 0 }),
|
|
21
|
+
services.listLiveSessions?.() ?? Promise.resolve([]),
|
|
22
|
+
Promise.all(
|
|
23
|
+
userSessions.map(
|
|
24
|
+
async (entry): Promise<UserSessionEntry> => ({
|
|
25
|
+
...entry,
|
|
26
|
+
runEvidence: loadUserSessionRunEvidence(entry, services),
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
]);
|
|
31
|
+
return {
|
|
32
|
+
subagents: branchRoster.entries,
|
|
33
|
+
userSessions: hydratedUserSessions,
|
|
34
|
+
liveSessionIds: new Set(liveSessionIds),
|
|
35
|
+
hasLiveSessionEvidence: services.listLiveSessions !== undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collectUserSessions(entries: readonly SessionEntry[]): UserSessionEntry[] {
|
|
40
|
+
const bySessionId = new Map<string, UserSessionEntry>();
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const receipt = parseHandoffLaunchReceiptEntry(entry);
|
|
43
|
+
if (!receipt || receipt.launch === "subagent") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const candidate = {
|
|
47
|
+
sessionId: receipt.sessionId,
|
|
48
|
+
timestamp: entry.timestamp,
|
|
49
|
+
receipt,
|
|
50
|
+
};
|
|
51
|
+
const existing = bySessionId.get(candidate.sessionId);
|
|
52
|
+
if (!existing || candidate.timestamp > existing.timestamp) {
|
|
53
|
+
bySessionId.set(candidate.sessionId, candidate);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...bySessionId.values()].sort((left, right) =>
|
|
57
|
+
right.timestamp.localeCompare(left.timestamp),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadUserSessionRunEvidence(
|
|
62
|
+
entry: UserSessionEntry,
|
|
63
|
+
services: HandoffBoardServices,
|
|
64
|
+
): NonNullable<UserSessionEntry["runEvidence"]> {
|
|
65
|
+
if (!services.readSessionEntries) {
|
|
66
|
+
return unavailableRunEvidence();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return {
|
|
71
|
+
transcriptAvailable: true,
|
|
72
|
+
hasStarted: services
|
|
73
|
+
.readSessionEntries(entry.receipt.childSessionFile)
|
|
74
|
+
.some(isSessionStartupEvidence),
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return unavailableRunEvidence();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function unavailableRunEvidence(): NonNullable<UserSessionEntry["runEvidence"]> {
|
|
82
|
+
return { transcriptAvailable: false, hasStarted: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isSessionStartupEvidence(entry: SessionEntry): boolean {
|
|
86
|
+
if (entry.type === "session_info") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (entry.type !== "custom") {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return entry.customType !== HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE;
|
|
93
|
+
}
|
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
2
|
copyToClipboard,
|
|
3
3
|
type ExtensionCommandContext,
|
|
4
|
-
type SessionEntry,
|
|
5
4
|
type Theme,
|
|
6
5
|
type ThemeColor,
|
|
7
6
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type Focusable,
|
|
9
|
+
matchesKey,
|
|
10
|
+
type TuiMouseEvent,
|
|
11
|
+
type TuiMouseEventResult,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
visibleWidth,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import {
|
|
16
|
+
type LegendHit,
|
|
17
|
+
type LegendItem,
|
|
18
|
+
type LegendLine,
|
|
19
|
+
LegendPointer,
|
|
20
|
+
layoutLegend,
|
|
21
|
+
legendHitAt,
|
|
22
|
+
} from "../shared/legend.ts";
|
|
9
23
|
import { formatCompactRelativeTime } from "../shared/time.ts";
|
|
10
24
|
import type { SubagentState } from "../subagents/classify.ts";
|
|
11
|
-
import type
|
|
25
|
+
import { type HandoffBoardServices, loadHandoffBoardSnapshot } from "./board-loading.ts";
|
|
12
26
|
import {
|
|
13
27
|
buildHandoffBoardView,
|
|
14
28
|
type HandoffBoardSnapshot,
|
|
15
29
|
type HandoffBoardTab,
|
|
16
30
|
type HandoffBoardView,
|
|
17
31
|
type HandoffSubagentsView,
|
|
18
|
-
type UserSessionEntry,
|
|
19
32
|
type UserSessionStatus,
|
|
20
33
|
type UserSessionsView,
|
|
21
34
|
} from "./board-view-model.ts";
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
collectUserSessions,
|
|
38
|
+
type HandoffBoardServices,
|
|
39
|
+
loadHandoffBoardSnapshot,
|
|
40
|
+
} from "./board-loading.ts";
|
|
24
41
|
|
|
25
42
|
const BOARD_WIDTH = 86;
|
|
26
43
|
const ROW_VIEWPORT_SIZE = 8;
|
|
@@ -28,11 +45,15 @@ const STATUS_DURATION_MS = 3_000;
|
|
|
28
45
|
|
|
29
46
|
export type { HandoffBoardSnapshot, UserSessionEntry } from "./board-view-model.ts";
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
interface BoardMouseTarget {
|
|
49
|
+
run(): void;
|
|
50
|
+
press?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BoardRowHit {
|
|
54
|
+
tab: HandoffBoardTab;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
end: number;
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
interface HandoffBoardActions {
|
|
@@ -74,87 +95,6 @@ export async function openHandoffBoard(
|
|
|
74
95
|
);
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
export async function loadHandoffBoardSnapshot(
|
|
78
|
-
entries: readonly SessionEntry[],
|
|
79
|
-
services: HandoffBoardServices,
|
|
80
|
-
): Promise<HandoffBoardSnapshot> {
|
|
81
|
-
const userSessions = collectUserSessions(entries);
|
|
82
|
-
const [branchRoster, liveSessionIds, hydratedUserSessions] = await Promise.all([
|
|
83
|
-
services.roster?.resolve("branch") ?? Promise.resolve({ entries: [], total: 0 }),
|
|
84
|
-
services.listLiveSessions?.() ?? Promise.resolve([]),
|
|
85
|
-
Promise.all(
|
|
86
|
-
userSessions.map(
|
|
87
|
-
async (entry): Promise<UserSessionEntry> => ({
|
|
88
|
-
...entry,
|
|
89
|
-
runEvidence: loadUserSessionRunEvidence(entry, services),
|
|
90
|
-
}),
|
|
91
|
-
),
|
|
92
|
-
),
|
|
93
|
-
]);
|
|
94
|
-
return {
|
|
95
|
-
subagents: branchRoster.entries,
|
|
96
|
-
userSessions: hydratedUserSessions,
|
|
97
|
-
liveSessionIds: new Set(liveSessionIds),
|
|
98
|
-
hasLiveSessionEvidence: services.listLiveSessions !== undefined,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function collectUserSessions(entries: readonly SessionEntry[]): UserSessionEntry[] {
|
|
103
|
-
const bySessionId = new Map<string, UserSessionEntry>();
|
|
104
|
-
for (const entry of entries) {
|
|
105
|
-
const receipt = parseHandoffLaunchReceiptEntry(entry);
|
|
106
|
-
if (!receipt || receipt.launch === "subagent") {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const candidate = {
|
|
110
|
-
sessionId: receipt.sessionId,
|
|
111
|
-
timestamp: entry.timestamp,
|
|
112
|
-
receipt,
|
|
113
|
-
};
|
|
114
|
-
const existing = bySessionId.get(candidate.sessionId);
|
|
115
|
-
if (!existing || candidate.timestamp > existing.timestamp) {
|
|
116
|
-
bySessionId.set(candidate.sessionId, candidate);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return [...bySessionId.values()].sort((left, right) =>
|
|
120
|
-
right.timestamp.localeCompare(left.timestamp),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function loadUserSessionRunEvidence(
|
|
125
|
-
entry: UserSessionEntry,
|
|
126
|
-
services: HandoffBoardServices,
|
|
127
|
-
): NonNullable<UserSessionEntry["runEvidence"]> {
|
|
128
|
-
if (!services.readSessionEntries) {
|
|
129
|
-
return unavailableRunEvidence();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
return {
|
|
134
|
-
transcriptAvailable: true,
|
|
135
|
-
hasStarted: services
|
|
136
|
-
.readSessionEntries(entry.receipt.childSessionFile)
|
|
137
|
-
.some(isSessionStartupEvidence),
|
|
138
|
-
};
|
|
139
|
-
} catch {
|
|
140
|
-
return unavailableRunEvidence();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function unavailableRunEvidence(): NonNullable<UserSessionEntry["runEvidence"]> {
|
|
145
|
-
return { transcriptAvailable: false, hasStarted: false };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function isSessionStartupEvidence(entry: SessionEntry): boolean {
|
|
149
|
-
if (entry.type === "session_info") {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
if (entry.type !== "custom") {
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
return entry.customType !== HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
98
|
export class HandoffBoard implements Focusable {
|
|
159
99
|
focused = false;
|
|
160
100
|
private tab: HandoffBoardTab = "subagents";
|
|
@@ -163,6 +103,10 @@ export class HandoffBoard implements Focusable {
|
|
|
163
103
|
private status: string | undefined;
|
|
164
104
|
private statusGeneration = 0;
|
|
165
105
|
private busy = false;
|
|
106
|
+
private rowHits = new Map<number, BoardRowHit>();
|
|
107
|
+
private controlHits = new Map<number, LegendHit[]>();
|
|
108
|
+
private pressedTarget: BoardMouseTarget | undefined;
|
|
109
|
+
private readonly legendPointer = new LegendPointer(() => this.requestRender());
|
|
166
110
|
|
|
167
111
|
constructor(
|
|
168
112
|
private readonly theme: Theme,
|
|
@@ -225,31 +169,100 @@ export class HandoffBoard implements Focusable {
|
|
|
225
169
|
}
|
|
226
170
|
}
|
|
227
171
|
|
|
172
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
173
|
+
if (event.type === "press") this.pressedTarget = undefined;
|
|
174
|
+
const hit =
|
|
175
|
+
event.type === "press" && !this.busy
|
|
176
|
+
? legendHitAt(this.controlHits.get(event.y) ?? [], event.x - 2)
|
|
177
|
+
: undefined;
|
|
178
|
+
const legend = this.legendPointer.handleMouse(
|
|
179
|
+
event,
|
|
180
|
+
hit
|
|
181
|
+
? {
|
|
182
|
+
...hit,
|
|
183
|
+
run: () => {
|
|
184
|
+
if (!this.busy) hit.run();
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
: undefined,
|
|
188
|
+
);
|
|
189
|
+
if (legend) return event.type === "press" ? { ...legend, focus: true } : legend;
|
|
190
|
+
if (event.type === "press") {
|
|
191
|
+
this.pressedTarget =
|
|
192
|
+
event.button === "left" && !this.busy ? this.mouseTargetAt(event) : undefined;
|
|
193
|
+
if (!this.pressedTarget) return undefined;
|
|
194
|
+
this.pressedTarget.press?.();
|
|
195
|
+
return { handled: true, focus: true };
|
|
196
|
+
}
|
|
197
|
+
if (event.type === "click") {
|
|
198
|
+
// Pi keeps press-time coordinates even when selection reflows the board.
|
|
199
|
+
const target = this.pressedTarget;
|
|
200
|
+
this.pressedTarget = undefined;
|
|
201
|
+
if (event.button !== "left" || !target || this.busy) return undefined;
|
|
202
|
+
target.run();
|
|
203
|
+
return { handled: true };
|
|
204
|
+
}
|
|
205
|
+
if (event.type === "wheel" && event.wheelDelta && this.rowHitAt(event)) {
|
|
206
|
+
if (!this.busy) this.moveSelection(Math.sign(event.wheelDelta));
|
|
207
|
+
return { handled: true };
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private rowHitAt(event: TuiMouseEvent): BoardRowHit | undefined {
|
|
213
|
+
const hit = this.rowHits.get(event.y);
|
|
214
|
+
return hit && event.x >= 2 && event.x < hit.end ? hit : undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private mouseTargetAt(event: TuiMouseEvent): BoardMouseTarget | undefined {
|
|
218
|
+
const row = this.rowHitAt(event);
|
|
219
|
+
if (row) {
|
|
220
|
+
const select = () => this.selectSession(row.tab, row.sessionId);
|
|
221
|
+
return { press: select, run: select };
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private sessionIdAt(tab: HandoffBoardTab, index: number): string | undefined {
|
|
227
|
+
return tab === "subagents"
|
|
228
|
+
? this.snapshot.subagents[index]?.sessionId
|
|
229
|
+
: this.snapshot.userSessions[index]?.sessionId;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private selectSession(tab: HandoffBoardTab, sessionId: string): void {
|
|
233
|
+
if (this.tab !== tab) return;
|
|
234
|
+
const entries = tab === "subagents" ? this.snapshot.subagents : this.snapshot.userSessions;
|
|
235
|
+
const index = entries.findIndex((entry) => entry.sessionId === sessionId);
|
|
236
|
+
if (index < 0) return;
|
|
237
|
+
this.selectedByTab[tab] = index;
|
|
238
|
+
this.clearTransientState();
|
|
239
|
+
this.requestRender();
|
|
240
|
+
}
|
|
241
|
+
|
|
228
242
|
render(width: number): string[] {
|
|
243
|
+
this.rowHits.clear();
|
|
244
|
+
this.controlHits.clear();
|
|
229
245
|
const modalWidth = Math.max(20, Math.min(width, BOARD_WIDTH));
|
|
230
246
|
const innerWidth = modalWidth - 2;
|
|
231
247
|
const bodyWidth = innerWidth - 2;
|
|
232
248
|
const border = (text: string) => this.theme.fg("border", text);
|
|
233
249
|
const row = (content = "") => `${border("│")} ${fitCell(content, bodyWidth)} ${border("│")}`;
|
|
234
250
|
const view = this.currentView();
|
|
235
|
-
const
|
|
236
|
-
view.tab === "subagents"
|
|
237
|
-
? this.renderSubagentRows(view, bodyWidth)
|
|
238
|
-
: this.renderUserSessionRows(view, bodyWidth);
|
|
239
|
-
|
|
240
|
-
return [
|
|
251
|
+
const lines = [
|
|
241
252
|
border(`╭${"─".repeat(innerWidth)}╮`),
|
|
242
253
|
row(this.renderHeader(view, bodyWidth)),
|
|
243
254
|
row(),
|
|
244
|
-
row(this.renderTabs()),
|
|
245
|
-
row(),
|
|
246
|
-
...rows.map(row),
|
|
247
|
-
row(),
|
|
248
|
-
...this.renderDetails(view, bodyWidth).map(row),
|
|
249
|
-
row(),
|
|
250
|
-
row(this.renderFooter(view, bodyWidth)),
|
|
251
|
-
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
252
255
|
];
|
|
256
|
+
lines.push(row(this.renderTabs(lines.length, bodyWidth)), row());
|
|
257
|
+
const rows =
|
|
258
|
+
view.tab === "subagents"
|
|
259
|
+
? this.renderSubagentRows(view, bodyWidth, lines.length + 1)
|
|
260
|
+
: this.renderUserSessionRows(view, bodyWidth, lines.length + 1);
|
|
261
|
+
lines.push(...rows.map(row), row(), ...this.renderDetails(view, bodyWidth).map(row), row());
|
|
262
|
+
const footer = this.renderFooter(view, bodyWidth);
|
|
263
|
+
this.controlHits.set(lines.length, footer.hits);
|
|
264
|
+
lines.push(row(footer.text), border(`╰${"─".repeat(innerWidth)}╯`));
|
|
265
|
+
return lines;
|
|
253
266
|
}
|
|
254
267
|
|
|
255
268
|
private renderHeader(view: HandoffBoardView, width: number): string {
|
|
@@ -264,7 +277,14 @@ export class HandoffBoard implements Focusable {
|
|
|
264
277
|
return fitLine(`${title}${" ".repeat(gap)}${count}`, width);
|
|
265
278
|
}
|
|
266
279
|
|
|
267
|
-
private renderTabs(): string {
|
|
280
|
+
private renderTabs(row: number, width: number): string {
|
|
281
|
+
this.controlHits.set(
|
|
282
|
+
row,
|
|
283
|
+
[
|
|
284
|
+
{ id: "tab:subagents", start: 0, end: 9, run: () => this.setTab("subagents") },
|
|
285
|
+
{ id: "tab:user-sessions", start: 11, end: 24, run: () => this.setTab("user-sessions") },
|
|
286
|
+
].filter((hit) => hit.end <= width),
|
|
287
|
+
);
|
|
268
288
|
const subagents =
|
|
269
289
|
this.tab === "subagents"
|
|
270
290
|
? this.theme.fg("accent", this.theme.bold("Subagents"))
|
|
@@ -273,10 +293,22 @@ export class HandoffBoard implements Focusable {
|
|
|
273
293
|
this.tab === "user-sessions"
|
|
274
294
|
? this.theme.fg("accent", this.theme.bold("User sessions"))
|
|
275
295
|
: this.theme.fg("dim", "User sessions");
|
|
276
|
-
|
|
296
|
+
const shownSubagents =
|
|
297
|
+
width >= 9 && this.legendPointer.pressedId === "tab:subagents"
|
|
298
|
+
? this.theme.bg("selectedBg", subagents)
|
|
299
|
+
: subagents;
|
|
300
|
+
const shownUserSessions =
|
|
301
|
+
width >= 24 && this.legendPointer.pressedId === "tab:user-sessions"
|
|
302
|
+
? this.theme.bg("selectedBg", userSessions)
|
|
303
|
+
: userSessions;
|
|
304
|
+
return `${shownSubagents} ${shownUserSessions}`;
|
|
277
305
|
}
|
|
278
306
|
|
|
279
|
-
private renderSubagentRows(
|
|
307
|
+
private renderSubagentRows(
|
|
308
|
+
view: HandoffSubagentsView,
|
|
309
|
+
width: number,
|
|
310
|
+
firstRow: number,
|
|
311
|
+
): string[] {
|
|
280
312
|
const rows = view.rows;
|
|
281
313
|
const statusWidth = 11;
|
|
282
314
|
const ageWidth = 4;
|
|
@@ -298,6 +330,7 @@ export class HandoffBoard implements Focusable {
|
|
|
298
330
|
...rows.slice(start, start + ROW_VIEWPORT_SIZE).map((entry, visibleIndex) => {
|
|
299
331
|
const index = start + visibleIndex;
|
|
300
332
|
const selected = index === this.selectedByTab.subagents;
|
|
333
|
+
this.recordRowHit(firstRow + visibleIndex, view.tab, index, width);
|
|
301
334
|
const indent = " ".repeat(Math.max(0, entry.depth - 1));
|
|
302
335
|
const dot = this.theme.fg(stateColor(entry.status), stateDot(entry.status));
|
|
303
336
|
const first = `${indent}${dot} ${entry.title}`;
|
|
@@ -320,7 +353,7 @@ export class HandoffBoard implements Focusable {
|
|
|
320
353
|
];
|
|
321
354
|
}
|
|
322
355
|
|
|
323
|
-
private renderUserSessionRows(view: UserSessionsView, width: number): string[] {
|
|
356
|
+
private renderUserSessionRows(view: UserSessionsView, width: number, firstRow: number): string[] {
|
|
324
357
|
const rows = view.rows;
|
|
325
358
|
const statusWidth = 11;
|
|
326
359
|
const ageWidth = 4;
|
|
@@ -342,6 +375,7 @@ export class HandoffBoard implements Focusable {
|
|
|
342
375
|
...rows.slice(start, start + ROW_VIEWPORT_SIZE).map((entry, visibleIndex) => {
|
|
343
376
|
const index = start + visibleIndex;
|
|
344
377
|
const selected = index === this.selectedByTab["user-sessions"];
|
|
378
|
+
this.recordRowHit(firstRow + visibleIndex, view.tab, index, width);
|
|
345
379
|
const line = gridRow(
|
|
346
380
|
[
|
|
347
381
|
{ text: entry.title, width: firstWidth },
|
|
@@ -365,6 +399,11 @@ export class HandoffBoard implements Focusable {
|
|
|
365
399
|
];
|
|
366
400
|
}
|
|
367
401
|
|
|
402
|
+
private recordRowHit(row: number, tab: HandoffBoardTab, index: number, width: number): void {
|
|
403
|
+
const sessionId = this.sessionIdAt(tab, index);
|
|
404
|
+
if (sessionId) this.rowHits.set(row, { tab, sessionId, end: width + 2 });
|
|
405
|
+
}
|
|
406
|
+
|
|
368
407
|
private renderDetails(view: HandoffBoardView, width: number): string[] {
|
|
369
408
|
const lines = this.detailLines(view);
|
|
370
409
|
const innerWidth = Math.max(1, width - 2);
|
|
@@ -404,44 +443,117 @@ export class HandoffBoard implements Focusable {
|
|
|
404
443
|
return view.details.map((detail) => detailLine(this.theme, detail.label, detail.value));
|
|
405
444
|
}
|
|
406
445
|
|
|
407
|
-
private renderFooter(view: HandoffBoardView, width: number):
|
|
446
|
+
private renderFooter(view: HandoffBoardView, width: number): LegendLine {
|
|
408
447
|
if (this.busy) {
|
|
409
|
-
return this.theme.fg("dim", "Working…");
|
|
448
|
+
return { text: this.theme.fg("dim", "Working…"), hits: [] };
|
|
410
449
|
}
|
|
411
450
|
if (this.confirmingStopSessionId) {
|
|
451
|
+
const sessionId = this.confirmingStopSessionId;
|
|
412
452
|
const prefix = "Stop “";
|
|
413
453
|
const suffix = "”? x confirm · esc cancel";
|
|
414
454
|
const titleWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(suffix));
|
|
415
455
|
const title = truncateToWidth(view.action?.subagent?.title ?? "subagent", titleWidth, "…");
|
|
416
|
-
|
|
456
|
+
const question = `${prefix}${title}”? `;
|
|
457
|
+
const legend = layoutLegend(
|
|
458
|
+
this.theme,
|
|
459
|
+
[
|
|
460
|
+
{
|
|
461
|
+
key: "x",
|
|
462
|
+
description: "confirm",
|
|
463
|
+
run: this.guardAction(view, () => this.handleStop()),
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
key: "esc",
|
|
467
|
+
description: "cancel",
|
|
468
|
+
run: () => {
|
|
469
|
+
if (this.confirmingStopSessionId !== sessionId) return;
|
|
470
|
+
this.clearTransientState();
|
|
471
|
+
this.requestRender();
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
{
|
|
476
|
+
width: Math.max(0, width - visibleWidth(question)),
|
|
477
|
+
separator: " · ",
|
|
478
|
+
pressedId: this.legendPointer.pressedId,
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
text: this.theme.fg("warning", `${question}${legend.text}`),
|
|
483
|
+
hits: legend.hits.map((hit) => ({
|
|
484
|
+
...hit,
|
|
485
|
+
start: hit.start + visibleWidth(question),
|
|
486
|
+
end: hit.end + visibleWidth(question),
|
|
487
|
+
})),
|
|
488
|
+
};
|
|
417
489
|
}
|
|
418
490
|
if (this.status) {
|
|
419
|
-
return this.theme.fg("dim", this.status);
|
|
491
|
+
return { text: this.theme.fg("dim", this.status), hits: [] };
|
|
420
492
|
}
|
|
421
493
|
|
|
422
494
|
const action = view.action;
|
|
423
|
-
const
|
|
495
|
+
const items: LegendItem[] = [
|
|
496
|
+
{ key: "<>", description: "tab" },
|
|
497
|
+
{ key: "↑↓", description: "select" },
|
|
498
|
+
];
|
|
424
499
|
if (action?.canStop) {
|
|
425
|
-
|
|
500
|
+
items.push({
|
|
501
|
+
key: "x",
|
|
502
|
+
description: "stop",
|
|
503
|
+
run: this.guardAction(view, () => this.handleStop()),
|
|
504
|
+
});
|
|
426
505
|
}
|
|
427
506
|
if (action?.observeCommand) {
|
|
428
|
-
|
|
507
|
+
items.push({
|
|
508
|
+
key: "a",
|
|
509
|
+
description: "copy observe",
|
|
510
|
+
run: this.guardAction(view, () => this.handleCopyObserve()),
|
|
511
|
+
});
|
|
429
512
|
}
|
|
430
513
|
if (action?.resumeCommand) {
|
|
431
|
-
|
|
514
|
+
items.push({
|
|
515
|
+
key: "c",
|
|
516
|
+
description: "copy resume",
|
|
517
|
+
run: this.guardAction(view, () => this.handleCopyResume()),
|
|
518
|
+
});
|
|
432
519
|
}
|
|
433
|
-
|
|
520
|
+
items.push({
|
|
521
|
+
key: "esc",
|
|
522
|
+
description: "close",
|
|
523
|
+
run: () => {
|
|
524
|
+
this.clearTransientState();
|
|
525
|
+
this.done(undefined);
|
|
526
|
+
},
|
|
527
|
+
});
|
|
434
528
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
:
|
|
439
|
-
|
|
440
|
-
|
|
529
|
+
return layoutLegend(this.theme, items, {
|
|
530
|
+
width,
|
|
531
|
+
pressedId: this.legendPointer.pressedId,
|
|
532
|
+
trailing: view.rows.length
|
|
533
|
+
? `${this.selectedByTab[this.tab] + 1} of ${view.rows.length}`
|
|
534
|
+
: "",
|
|
535
|
+
});
|
|
441
536
|
}
|
|
442
537
|
|
|
443
|
-
private
|
|
444
|
-
|
|
538
|
+
private guardAction(view: HandoffBoardView, run: () => void): () => void {
|
|
539
|
+
const sessionId = this.sessionIdAt(view.tab, this.selectedByTab[view.tab]);
|
|
540
|
+
const confirming = this.confirmingStopSessionId;
|
|
541
|
+
return () => {
|
|
542
|
+
if (
|
|
543
|
+
this.tab !== view.tab ||
|
|
544
|
+
this.sessionIdAt(this.tab, this.selectedByTab[this.tab]) !== sessionId
|
|
545
|
+
)
|
|
546
|
+
return;
|
|
547
|
+
if (this.confirmingStopSessionId !== confirming) return;
|
|
548
|
+
const action = this.currentView().action;
|
|
549
|
+
if (
|
|
550
|
+
action?.canStop !== view.action?.canStop ||
|
|
551
|
+
action?.observeCommand !== view.action?.observeCommand ||
|
|
552
|
+
action?.resumeCommand !== view.action?.resumeCommand
|
|
553
|
+
)
|
|
554
|
+
return;
|
|
555
|
+
run();
|
|
556
|
+
};
|
|
445
557
|
}
|
|
446
558
|
|
|
447
559
|
private setTab(tab: HandoffBoardTab): void {
|
|
@@ -31,6 +31,10 @@ import { buildHandoffLaunchSchema, buildHandoffPromptGuidelines } from "./tool-s
|
|
|
31
31
|
import { buildHandoffToolView } from "./tool-view-model.ts";
|
|
32
32
|
import { formatHandoffError } from "./ui.ts";
|
|
33
33
|
|
|
34
|
+
const SESSION_TOKEN_PROMPT_SECTION = "pi_sessions_session_token";
|
|
35
|
+
const SESSION_TOKEN_PROMPT =
|
|
36
|
+
"When the user references @session:<uuid>, treat it as a session token. If you call session_ask, pass only the UUID value, not the @session: prefix.";
|
|
37
|
+
|
|
34
38
|
interface HandoffToolRendererState {
|
|
35
39
|
callComponent?: HandoffToolComponent | undefined;
|
|
36
40
|
}
|
|
@@ -196,11 +200,7 @@ export function installHandoff(
|
|
|
196
200
|
(await splitBackend.identifyTerminalId(ctx.cwd)) ?? identifiedGhosttyTerminalId;
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
|
|
200
|
-
systemPrompt:
|
|
201
|
-
event.systemPrompt +
|
|
202
|
-
"\n\nWhen the user references @session:<uuid>, treat it as a session token. If you call session_ask, pass only the UUID value, not the @session: prefix.",
|
|
203
|
-
};
|
|
203
|
+
event.systemPromptOptions.sections[SESSION_TOKEN_PROMPT_SECTION] = SESSION_TOKEN_PROMPT;
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
return {
|