pi-sessions 0.5.1 → 0.7.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/README.md +22 -1
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +82 -134
- package/extensions/session-auto-title/command.ts +1 -1
- package/extensions/session-auto-title/controller.ts +3 -3
- package/extensions/session-auto-title/generate.ts +2 -2
- package/extensions/session-auto-title/model.ts +4 -12
- package/extensions/session-auto-title/retitle.ts +5 -5
- package/extensions/session-auto-title/state.ts +1 -1
- package/extensions/session-auto-title/wizard.ts +4 -4
- package/extensions/session-auto-title.ts +8 -8
- package/extensions/session-handoff/extract.ts +18 -5
- package/extensions/session-handoff/metadata.ts +4 -1
- package/extensions/session-handoff/picker.ts +52 -6
- package/extensions/session-handoff/query.ts +78 -62
- package/extensions/session-handoff/refs.ts +14 -20
- package/extensions/session-handoff/spawn.ts +1 -1
- package/extensions/session-handoff.ts +54 -35
- package/extensions/session-hooks.ts +3 -3
- package/extensions/session-index.ts +4 -4
- package/extensions/session-messaging/broker/process.ts +302 -0
- package/extensions/session-messaging/broker/spawn.ts +164 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +114 -0
- package/extensions/session-messaging/pi/message-contracts.ts +36 -0
- package/extensions/session-messaging/pi/message-view.ts +131 -0
- package/extensions/session-messaging/pi/renderer.ts +16 -0
- package/extensions/session-messaging/pi/service.ts +370 -0
- package/extensions/session-messaging/pi/tools.ts +92 -0
- package/extensions/session-messaging.ts +30 -0
- package/extensions/session-search/extract.ts +90 -440
- package/extensions/session-search/hooks.ts +20 -28
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +3 -2
- package/extensions/session-search.ts +161 -132
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/shared/session-broker/client.ts +253 -0
- package/extensions/shared/session-broker/framing.ts +72 -0
- package/extensions/shared/session-broker/protocol.ts +95 -0
- package/extensions/shared/session-broker/socket-path.ts +30 -0
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +46 -51
- package/extensions/shared/session-index/index.ts +6 -5
- package/extensions/shared/session-index/lineage.ts +19 -2
- package/extensions/shared/session-index/query/ast.ts +40 -0
- package/extensions/shared/session-index/query/compiler.ts +146 -0
- package/extensions/shared/session-index/query/lexer.ts +140 -0
- package/extensions/shared/session-index/query/parser.ts +178 -0
- package/extensions/shared/session-index/schema.ts +25 -9
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +555 -281
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +14 -23
- package/extensions/shared/settings.ts +62 -5
- package/extensions/shared/text.ts +50 -0
- package/package.json +4 -3
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
stripSearchSnippetMarkers,
|
|
13
13
|
transformSearchSnippetMatches,
|
|
14
|
-
} from "../shared/search-snippet.
|
|
15
|
-
import { listSessionPickerItems, normalizeDisplayText, type SessionPickerItem } from "./query.
|
|
14
|
+
} from "../shared/search-snippet.ts";
|
|
15
|
+
import { listSessionPickerItems, normalizeDisplayText, type SessionPickerItem } from "./query.ts";
|
|
16
16
|
|
|
17
17
|
const MAX_VISIBLE_BROWSE_ROWS = 10;
|
|
18
18
|
const MAX_VISIBLE_SEARCH_ROWS = 4;
|
|
19
|
+
const SEARCH_RELOAD_DEBOUNCE_MS = 200;
|
|
19
20
|
|
|
20
21
|
interface PickerRightColumnWidths {
|
|
21
22
|
marker: number;
|
|
@@ -65,6 +66,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
65
66
|
private includeAll = false;
|
|
66
67
|
private items: SessionPickerItem[] = [];
|
|
67
68
|
private selectedIndex = 0;
|
|
69
|
+
private searchReloadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
70
|
|
|
69
71
|
get focused(): boolean {
|
|
70
72
|
return this._focused;
|
|
@@ -89,17 +91,18 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
89
91
|
|
|
90
92
|
handleInput(data: string): void {
|
|
91
93
|
if (matchesKey(data, this.options.shortcut)) {
|
|
92
|
-
this.
|
|
94
|
+
this.finish({ kind: "cancel" });
|
|
93
95
|
return;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
97
|
-
this.
|
|
99
|
+
this.finish({ kind: "cancel" });
|
|
98
100
|
return;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
if (this.keybindings.matches(data, "tui.input.tab")) {
|
|
102
104
|
this.includeAll = !this.includeAll;
|
|
105
|
+
this.cancelSearchReload();
|
|
103
106
|
this.reload();
|
|
104
107
|
this.tui.requestRender();
|
|
105
108
|
return;
|
|
@@ -130,9 +133,10 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
136
|
+
this.flushSearchReload();
|
|
133
137
|
const selected = this.items[this.selectedIndex];
|
|
134
138
|
if (selected?.kind === "session") {
|
|
135
|
-
this.
|
|
139
|
+
this.finish({ kind: "insert-session-token", sessionId: selected.sessionId });
|
|
136
140
|
}
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
@@ -140,7 +144,7 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
140
144
|
const before = this.input.getValue();
|
|
141
145
|
this.input.handleInput(data);
|
|
142
146
|
if (this.input.getValue() !== before) {
|
|
143
|
-
this.
|
|
147
|
+
this.reloadAfterInputChange();
|
|
144
148
|
this.tui.requestRender();
|
|
145
149
|
}
|
|
146
150
|
}
|
|
@@ -284,6 +288,48 @@ export class SessionReferencePickerComponent implements Focusable {
|
|
|
284
288
|
.trim();
|
|
285
289
|
}
|
|
286
290
|
|
|
291
|
+
private reloadAfterInputChange(): void {
|
|
292
|
+
if (!this.input.getValue().trim()) {
|
|
293
|
+
this.cancelSearchReload();
|
|
294
|
+
this.reload();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.scheduleSearchReload();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private scheduleSearchReload(): void {
|
|
302
|
+
this.cancelSearchReload();
|
|
303
|
+
this.searchReloadTimer = setTimeout(() => {
|
|
304
|
+
this.searchReloadTimer = undefined;
|
|
305
|
+
this.reload();
|
|
306
|
+
this.tui.requestRender();
|
|
307
|
+
}, SEARCH_RELOAD_DEBOUNCE_MS);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private flushSearchReload(): void {
|
|
311
|
+
if (!this.searchReloadTimer) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.cancelSearchReload();
|
|
316
|
+
this.reload();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private cancelSearchReload(): void {
|
|
320
|
+
if (!this.searchReloadTimer) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
clearTimeout(this.searchReloadTimer);
|
|
325
|
+
this.searchReloadTimer = undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private finish(result: SessionPickerResult): void {
|
|
329
|
+
this.cancelSearchReload();
|
|
330
|
+
this.done(result);
|
|
331
|
+
}
|
|
332
|
+
|
|
287
333
|
private reload(): void {
|
|
288
334
|
this.items = listSessionPickerItems({
|
|
289
335
|
currentSessionPath: this.options.getCurrentSessionPath(),
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { stripSearchSnippetMarkers } from "../shared/search-snippet.
|
|
1
|
+
import { stripSearchSnippetMarkers } from "../shared/search-snippet.ts";
|
|
2
2
|
import {
|
|
3
|
-
getIndexStatus,
|
|
4
|
-
getLineageSessions,
|
|
5
3
|
getSessionByPath,
|
|
6
|
-
INDEX_SCHEMA_VERSION,
|
|
7
|
-
openIndexDatabase,
|
|
8
4
|
type SearchSessionResult,
|
|
9
5
|
type SessionIndexDatabase,
|
|
10
6
|
type SessionLineageRelation,
|
|
11
7
|
searchSessions,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
8
|
+
withSessionIndex,
|
|
9
|
+
} from "../shared/session-index/index.ts";
|
|
10
|
+
import { SearchQuerySyntaxError } from "../shared/session-index/query/ast.ts";
|
|
11
|
+
import { shortenSessionId } from "../shared/session-ui.ts";
|
|
12
|
+
import { formatCompactRelativeTime } from "../shared/time.ts";
|
|
15
13
|
|
|
16
14
|
export const SESSION_TOKEN_PREFIX = "@session:";
|
|
17
15
|
|
|
@@ -19,7 +17,6 @@ const MAX_TREE_DEPTH = 3;
|
|
|
19
17
|
|
|
20
18
|
interface SessionPickerPresentationContext {
|
|
21
19
|
currentSessionId?: string | undefined;
|
|
22
|
-
relationBySessionId: Map<string, SessionLineageRelation>;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
export interface SessionPickerSessionItem {
|
|
@@ -32,7 +29,7 @@ export interface SessionPickerSessionItem {
|
|
|
32
29
|
modifiedAtText?: string | undefined;
|
|
33
30
|
prefix: string;
|
|
34
31
|
snippet?: string | undefined;
|
|
35
|
-
relation?: SessionLineageRelation |
|
|
32
|
+
relation?: SessionLineageRelation | undefined;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export interface SessionPickerNoticeItem {
|
|
@@ -69,69 +66,75 @@ export function listSessionPickerItems(
|
|
|
69
66
|
): SessionPickerQueryResult {
|
|
70
67
|
const scopeMode = options.includeAll ? "all" : "default";
|
|
71
68
|
const defaultScopeLabel = options.currentCwd ? "current folder" : undefined;
|
|
72
|
-
const status = getIndexStatus(options.indexPath);
|
|
73
|
-
if (!status.exists || status.schemaVersion !== INDEX_SCHEMA_VERSION) {
|
|
74
|
-
return {
|
|
75
|
-
items: [buildIndexErrorItem()],
|
|
76
|
-
scopeMode,
|
|
77
|
-
defaultScopeLabel,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const db = openIndexDatabase(status.dbPath, { create: false });
|
|
82
69
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
db,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
return (
|
|
71
|
+
withSessionIndex(options.indexPath, { mode: "read", required: false }, ({ db }) => {
|
|
72
|
+
const currentSession = options.currentSessionPath
|
|
73
|
+
? getSessionByPath(db, options.currentSessionPath)
|
|
74
|
+
: undefined;
|
|
75
|
+
const context = buildPresentationContext(currentSession?.sessionId);
|
|
76
|
+
const searchResults = searchSessionsSafely(db, options, currentSession?.sessionId);
|
|
77
|
+
const rankedResults =
|
|
78
|
+
options.mode === "browse"
|
|
79
|
+
? prioritizeSessionResults(searchResults, context)
|
|
80
|
+
: searchResults;
|
|
81
|
+
|
|
82
|
+
if (rankedResults.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
items: [buildEmptyResultItem(options.mode)],
|
|
85
|
+
scopeMode,
|
|
86
|
+
defaultScopeLabel,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sessionItems =
|
|
91
|
+
options.mode === "browse"
|
|
92
|
+
? buildBrowseSessionItems(rankedResults, context)
|
|
93
|
+
: rankedResults.map((result) =>
|
|
94
|
+
buildSessionItem(result, context, "", getBestTextSnippet(result)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
items: sessionItems,
|
|
99
|
+
scopeMode,
|
|
100
|
+
defaultScopeLabel,
|
|
101
|
+
};
|
|
102
|
+
}) ?? {
|
|
103
|
+
items: [buildIndexErrorItem()],
|
|
104
|
+
scopeMode,
|
|
105
|
+
defaultScopeLabel,
|
|
106
|
+
}
|
|
98
107
|
);
|
|
99
|
-
|
|
100
|
-
if (
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof SearchQuerySyntaxError) {
|
|
101
110
|
return {
|
|
102
|
-
items: [
|
|
111
|
+
items: [buildSearchSyntaxErrorItem(error)],
|
|
103
112
|
scopeMode,
|
|
104
113
|
defaultScopeLabel,
|
|
105
114
|
};
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
options.mode === "browse"
|
|
110
|
-
? buildBrowseSessionItems(rankedResults, context)
|
|
111
|
-
: rankedResults.map((result) => buildSessionItem(result, context, "", result.snippet));
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
items: sessionItems,
|
|
115
|
-
scopeMode,
|
|
116
|
-
defaultScopeLabel,
|
|
117
|
-
};
|
|
118
|
-
} finally {
|
|
119
|
-
db.close();
|
|
117
|
+
throw error;
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
function
|
|
121
|
+
function searchSessionsSafely(
|
|
124
122
|
db: SessionIndexDatabase,
|
|
123
|
+
options: ListSessionPickerItemsOptions,
|
|
124
|
+
currentSessionId?: string | undefined,
|
|
125
|
+
): SearchSessionResult[] {
|
|
126
|
+
return searchSessions(db, {
|
|
127
|
+
cwd: options.includeAll ? undefined : options.currentCwd,
|
|
128
|
+
query: options.mode === "search" ? options.query : undefined,
|
|
129
|
+
limit: options.limit,
|
|
130
|
+
relativeToSessionId: currentSessionId,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildPresentationContext(
|
|
125
135
|
currentSessionId?: string | undefined,
|
|
126
136
|
): SessionPickerPresentationContext {
|
|
127
|
-
return {
|
|
128
|
-
currentSessionId,
|
|
129
|
-
relationBySessionId: currentSessionId
|
|
130
|
-
? new Map(
|
|
131
|
-
getLineageSessions(db, currentSessionId).map((row) => [row.sessionId, row.relation]),
|
|
132
|
-
)
|
|
133
|
-
: new Map<string, SessionLineageRelation>(),
|
|
134
|
-
};
|
|
137
|
+
return { currentSessionId };
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
function prioritizeSessionResults(
|
|
@@ -253,13 +256,18 @@ function buildSessionItem(
|
|
|
253
256
|
};
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
function getBestTextSnippet(result: SearchSessionResult): string | undefined {
|
|
260
|
+
const textEvidence = result.evidence.find((evidence) => evidence.kind === "text");
|
|
261
|
+
return textEvidence?.kind === "text" ? textEvidence.snippet : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
256
264
|
function getSessionTitle(result: SearchSessionResult): string {
|
|
257
265
|
return (
|
|
258
266
|
normalizeDisplayText(result.sessionName) ??
|
|
259
267
|
normalizeDisplayText(result.handoffNextTask) ??
|
|
260
268
|
normalizeDisplayText(result.handoffGoal) ??
|
|
261
269
|
normalizeDisplayText(result.firstUserPrompt) ??
|
|
262
|
-
normalizeDisplayText(stripSearchSnippetMarkers(result
|
|
270
|
+
normalizeDisplayText(stripSearchSnippetMarkers(getBestTextSnippet(result))) ??
|
|
263
271
|
shortenSessionId(result.sessionId)
|
|
264
272
|
);
|
|
265
273
|
}
|
|
@@ -276,12 +284,12 @@ export function normalizeDisplayText(value?: string): string | undefined {
|
|
|
276
284
|
function getSessionRelation(
|
|
277
285
|
result: SearchSessionResult,
|
|
278
286
|
context: SessionPickerPresentationContext,
|
|
279
|
-
): SessionLineageRelation |
|
|
287
|
+
): SessionLineageRelation | undefined {
|
|
280
288
|
if (context.currentSessionId && result.sessionId === context.currentSessionId) {
|
|
281
289
|
return "self";
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
return
|
|
292
|
+
return result.relation;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
function getSessionMarker(
|
|
@@ -310,6 +318,14 @@ function buildIndexErrorItem(): SessionPickerNoticeItem {
|
|
|
310
318
|
};
|
|
311
319
|
}
|
|
312
320
|
|
|
321
|
+
function buildSearchSyntaxErrorItem(error: SearchQuerySyntaxError): SessionPickerNoticeItem {
|
|
322
|
+
return {
|
|
323
|
+
kind: "error",
|
|
324
|
+
title: "Invalid search query",
|
|
325
|
+
description: error.message,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
313
329
|
function buildEmptyResultItem(mode: "browse" | "search"): SessionPickerNoticeItem {
|
|
314
330
|
return {
|
|
315
331
|
kind: "empty",
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { parseSessionFile } from "../session-search/extract.
|
|
2
|
+
import { parseSessionFile } from "../session-search/extract.ts";
|
|
3
3
|
import {
|
|
4
|
-
getIndexStatus,
|
|
5
4
|
getSessionById,
|
|
6
|
-
INDEX_SCHEMA_VERSION,
|
|
7
|
-
openIndexDatabase,
|
|
8
5
|
type SessionLineageRow,
|
|
9
6
|
type SessionOrigin,
|
|
10
|
-
|
|
7
|
+
withSessionIndex,
|
|
8
|
+
} from "../shared/session-index/index.ts";
|
|
11
9
|
|
|
12
10
|
const HANDOFF_REF_PREFIX = "@handoff/";
|
|
13
11
|
const SESSION_REFERENCE_HELP =
|
|
@@ -103,24 +101,20 @@ function resolveIndexedReference(
|
|
|
103
101
|
return { error: `Invalid session reference: ${input}. ${SESSION_REFERENCE_HELP}` };
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
try {
|
|
105
|
+
return withSessionIndex(options.indexPath, { mode: "read", required: true }, ({ db }) => {
|
|
106
|
+
const exactMatch = getSessionById(db, value);
|
|
107
|
+
if (exactMatch) {
|
|
108
|
+
return { resolved: buildResolvedReference(input, kind, exactMatch) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { error: `No session found for reference: ${input}. ${SESSION_REFERENCE_HELP}` };
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
108
114
|
return {
|
|
109
|
-
error: `Session reference resolution requires a current index at ${
|
|
115
|
+
error: `Session reference resolution requires a current index at ${options.indexPath}. Run /session-index and press r to rebuild it.`,
|
|
110
116
|
};
|
|
111
117
|
}
|
|
112
|
-
|
|
113
|
-
const db = openIndexDatabase(status.dbPath, { create: false });
|
|
114
|
-
try {
|
|
115
|
-
const exactMatch = getSessionById(db, value);
|
|
116
|
-
if (exactMatch) {
|
|
117
|
-
return { resolved: buildResolvedReference(input, kind, exactMatch) };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { error: `No session found for reference: ${input}. ${SESSION_REFERENCE_HELP}` };
|
|
121
|
-
} finally {
|
|
122
|
-
db.close();
|
|
123
|
-
}
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
function buildResolvedReference(
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type SessionHeader,
|
|
9
9
|
type SessionInfoEntry,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.
|
|
11
|
+
import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.ts";
|
|
12
12
|
|
|
13
13
|
const GHOSTTY_MACOS_ONLY_MESSAGE = "Split handoff currently supports Ghostty on macOS only.";
|
|
14
14
|
const GHOSTTY_REQUIRED_MESSAGE = "Split handoff requires running inside Ghostty.";
|
|
@@ -9,13 +9,13 @@ import type {
|
|
|
9
9
|
ExtensionUIContext,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { buildSessionContext, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
12
|
+
import { Key, matchesKey, Text } from "@earendil-works/pi-tui";
|
|
13
13
|
import { Type } from "typebox";
|
|
14
14
|
import {
|
|
15
15
|
generateHandoffDraft,
|
|
16
16
|
generateHandoffDraftFromSessionManager,
|
|
17
17
|
type HandoffDraftResult,
|
|
18
|
-
} from "./session-handoff/extract.
|
|
18
|
+
} from "./session-handoff/extract.ts";
|
|
19
19
|
import {
|
|
20
20
|
type ChildGeneratedHandoffBootstrap,
|
|
21
21
|
createChildGeneratedHandoffBootstrap,
|
|
@@ -29,14 +29,14 @@ import {
|
|
|
29
29
|
hasUserMessages,
|
|
30
30
|
isChildGeneratedHandoffBootstrap,
|
|
31
31
|
parseHandoffBootstrap,
|
|
32
|
-
} from "./session-handoff/metadata.
|
|
33
|
-
import { openSessionReferencePicker } from "./session-handoff/picker.
|
|
34
|
-
import { SESSION_TOKEN_PREFIX } from "./session-handoff/query.
|
|
32
|
+
} from "./session-handoff/metadata.ts";
|
|
33
|
+
import { openSessionReferencePicker } from "./session-handoff/picker.ts";
|
|
34
|
+
import { SESSION_TOKEN_PREFIX } from "./session-handoff/query.ts";
|
|
35
35
|
import {
|
|
36
36
|
renderStrongModal,
|
|
37
37
|
reviewHandoffDraft,
|
|
38
38
|
reviewHandoffDraftForSend,
|
|
39
|
-
} from "./session-handoff/review.
|
|
39
|
+
} from "./session-handoff/review.ts";
|
|
40
40
|
import {
|
|
41
41
|
buildPiResumeCommand,
|
|
42
42
|
createHandoffSession,
|
|
@@ -45,9 +45,9 @@ import {
|
|
|
45
45
|
isGhosttyHandoffAvailable,
|
|
46
46
|
launchSplitHandoffSession,
|
|
47
47
|
validateSplitHandoffPrerequisites,
|
|
48
|
-
} from "./session-handoff/spawn.
|
|
49
|
-
import { isTuiMode } from "./shared/pi-mode.
|
|
50
|
-
import { loadSettings } from "./shared/settings.
|
|
48
|
+
} from "./session-handoff/spawn.ts";
|
|
49
|
+
import { isTuiMode } from "./shared/pi-mode.ts";
|
|
50
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
51
51
|
|
|
52
52
|
const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
|
|
53
53
|
const TOOL_HANDOFF_PROVISIONAL_TITLE = "Session handoff";
|
|
@@ -58,10 +58,10 @@ interface HandoffToolParams {
|
|
|
58
58
|
goal: string;
|
|
59
59
|
splitDirection: HandoffSplitDirection;
|
|
60
60
|
cwd?: string | undefined;
|
|
61
|
+
requestResponse?: boolean | undefined;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface HandoffToolDetails {
|
|
64
|
-
error: boolean;
|
|
65
65
|
sessionId?: string | undefined;
|
|
66
66
|
title?: string | undefined;
|
|
67
67
|
splitDirection?: HandoffSplitDirection | undefined;
|
|
@@ -89,6 +89,7 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
89
89
|
"Use session_handoff only when it is clear the work should be forked to a new context.",
|
|
90
90
|
"Prefer using session_handoff by direction of the user, not as an unsolicited default.",
|
|
91
91
|
"The goal should capture enough detail to encompass the ask and include any directions the next sessions should consider.",
|
|
92
|
+
"Only request a response when there is a specific ask-and-response expectation: the user asked for a report back, or the current session needs the child result before it can continue. Do not set requestResponse for independent or unrelated background work; most split-off tasks should run without reporting back.",
|
|
92
93
|
"Only capable of a background handoff; to replace the current session, tell the user to run /handoff instead.",
|
|
93
94
|
],
|
|
94
95
|
executionMode: "sequential",
|
|
@@ -110,7 +111,30 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
110
111
|
"Optional target working directory. Relative paths resolve from the current session cwd.",
|
|
111
112
|
}),
|
|
112
113
|
),
|
|
114
|
+
requestResponse: Type.Optional(
|
|
115
|
+
Type.Boolean({
|
|
116
|
+
description:
|
|
117
|
+
"Whether the child session should report completion/results of its task back to this session.",
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
113
120
|
}),
|
|
121
|
+
renderResult(result, _options, theme, context) {
|
|
122
|
+
const text = getFirstText(result);
|
|
123
|
+
if (context.isError) {
|
|
124
|
+
return new Text(theme.fg("error", text), 0, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const details = result.details as HandoffToolDetails | undefined;
|
|
128
|
+
if (!details?.sessionId || !details.splitDirection || !details.cwd) {
|
|
129
|
+
return new Text(text, 0, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return new Text(
|
|
133
|
+
`Started handoff session ${theme.bold(details.sessionId)} (${details.splitDirection}) in ${theme.fg("dim", details.cwd)}.`,
|
|
134
|
+
0,
|
|
135
|
+
0,
|
|
136
|
+
);
|
|
137
|
+
},
|
|
114
138
|
async execute(_toolCallId, params: HandoffToolParams, _signal, _onUpdate, ctx) {
|
|
115
139
|
return executeSessionHandoffTool(pi, params, ctx, identifiedGhosttyTerminalId);
|
|
116
140
|
},
|
|
@@ -354,30 +378,30 @@ async function executeSessionHandoffTool(
|
|
|
354
378
|
) {
|
|
355
379
|
const goal = params.goal.trim();
|
|
356
380
|
if (!goal) {
|
|
357
|
-
|
|
381
|
+
throw new Error("session_handoff requires a goal.");
|
|
358
382
|
}
|
|
359
383
|
|
|
360
384
|
if (!ctx.model) {
|
|
361
|
-
|
|
385
|
+
throw new Error("No model selected.");
|
|
362
386
|
}
|
|
363
387
|
|
|
364
388
|
const preflightError = await validateSplitHandoffPrerequisites(pi, ctx);
|
|
365
389
|
if (preflightError) {
|
|
366
|
-
|
|
390
|
+
throw new Error(preflightError);
|
|
367
391
|
}
|
|
368
392
|
|
|
369
393
|
if (!terminalId) {
|
|
370
|
-
|
|
394
|
+
throw new Error(NO_IDENTIFIED_TERMINAL_MESSAGE);
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
const targetCwd = resolveHandoffCwd(ctx.cwd, params.cwd);
|
|
374
398
|
if (targetCwd.error) {
|
|
375
|
-
|
|
399
|
+
throw new Error(targetCwd.message);
|
|
376
400
|
}
|
|
377
401
|
|
|
378
402
|
const parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
379
403
|
if (!parentSessionFile) {
|
|
380
|
-
|
|
404
|
+
throw new Error("Handoff requires a persisted current session.");
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
const sessionContext = buildSessionContext(
|
|
@@ -385,9 +409,10 @@ async function executeSessionHandoffTool(
|
|
|
385
409
|
ctx.sessionManager.getLeafId(),
|
|
386
410
|
);
|
|
387
411
|
if (sessionContext.messages.length === 0) {
|
|
388
|
-
|
|
412
|
+
throw new Error("No conversation to hand off.");
|
|
389
413
|
}
|
|
390
414
|
|
|
415
|
+
const requestResponse = params.requestResponse ?? false;
|
|
391
416
|
const createdSession = createHandoffSession({
|
|
392
417
|
cwd: targetCwd.path,
|
|
393
418
|
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
@@ -400,6 +425,7 @@ async function executeSessionHandoffTool(
|
|
|
400
425
|
goal,
|
|
401
426
|
title: TOOL_HANDOFF_PROVISIONAL_TITLE,
|
|
402
427
|
parentSessionFile,
|
|
428
|
+
requestResponse,
|
|
403
429
|
}),
|
|
404
430
|
);
|
|
405
431
|
const model = formatModelArgument(ctx.model, pi.getThinkingLevel());
|
|
@@ -416,7 +442,7 @@ async function executeSessionHandoffTool(
|
|
|
416
442
|
});
|
|
417
443
|
|
|
418
444
|
if (!launchResult.success) {
|
|
419
|
-
|
|
445
|
+
throw new Error(
|
|
420
446
|
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(
|
|
421
447
|
ctx.sessionManager.getSessionDir(),
|
|
422
448
|
createdSession.sessionId,
|
|
@@ -424,17 +450,10 @@ async function executeSessionHandoffTool(
|
|
|
424
450
|
TOOL_HANDOFF_PROVISIONAL_TITLE,
|
|
425
451
|
model,
|
|
426
452
|
)}`,
|
|
427
|
-
{
|
|
428
|
-
sessionId: createdSession.sessionId,
|
|
429
|
-
title: TOOL_HANDOFF_PROVISIONAL_TITLE,
|
|
430
|
-
splitDirection: params.splitDirection,
|
|
431
|
-
cwd: targetCwd.path,
|
|
432
|
-
},
|
|
433
453
|
);
|
|
434
454
|
}
|
|
435
455
|
|
|
436
456
|
const details: HandoffToolDetails = {
|
|
437
|
-
error: false,
|
|
438
457
|
sessionId: createdSession.sessionId,
|
|
439
458
|
title: TOOL_HANDOFF_PROVISIONAL_TITLE,
|
|
440
459
|
splitDirection: params.splitDirection,
|
|
@@ -445,7 +464,7 @@ async function executeSessionHandoffTool(
|
|
|
445
464
|
content: [
|
|
446
465
|
{
|
|
447
466
|
type: "text" as const,
|
|
448
|
-
text:
|
|
467
|
+
text: formatHandoffToolResultForModel(details, requestResponse),
|
|
449
468
|
},
|
|
450
469
|
],
|
|
451
470
|
details,
|
|
@@ -482,6 +501,7 @@ async function startChildGeneratedHandoff(
|
|
|
482
501
|
bootstrap.goal,
|
|
483
502
|
thinkingLevel,
|
|
484
503
|
signal,
|
|
504
|
+
bootstrap.requestResponse ?? false,
|
|
485
505
|
),
|
|
486
506
|
);
|
|
487
507
|
if (!generatedDraft) {
|
|
@@ -517,16 +537,15 @@ async function startChildGeneratedHandoff(
|
|
|
517
537
|
}
|
|
518
538
|
}
|
|
519
539
|
|
|
520
|
-
function
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
};
|
|
540
|
+
function formatHandoffToolResultForModel(
|
|
541
|
+
details: HandoffToolDetails,
|
|
542
|
+
requestResponse: boolean,
|
|
543
|
+
): string {
|
|
544
|
+
return JSON.stringify({ ...details, requestResponse }, null, 2);
|
|
545
|
+
}
|
|
525
546
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
details: resultDetails,
|
|
529
|
-
};
|
|
547
|
+
function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
548
|
+
return result.content.find((item) => item.type === "text")?.text ?? "";
|
|
530
549
|
}
|
|
531
550
|
|
|
532
551
|
function resolveHandoffCwd(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { HANDOFF_BOOTSTRAP_ENV, parseHandoffBootstrap } from "./session-handoff/metadata.
|
|
3
|
-
import { createSessionHookController } from "./session-search/hooks.
|
|
4
|
-
import { loadSettings } from "./shared/settings.
|
|
2
|
+
import { HANDOFF_BOOTSTRAP_ENV, parseHandoffBootstrap } from "./session-handoff/metadata.ts";
|
|
3
|
+
import { createSessionHookController } from "./session-search/hooks.ts";
|
|
4
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
5
5
|
|
|
6
6
|
interface SessionStartLifecycleEvent {
|
|
7
7
|
reason?: "startup" | "reload" | "new" | "resume" | "fork";
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
type Theme,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
-
import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.
|
|
9
|
-
import { isTuiMode } from "./shared/pi-mode.
|
|
10
|
-
import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.
|
|
11
|
-
import { loadSettings } from "./shared/settings.
|
|
8
|
+
import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.ts";
|
|
9
|
+
import { isTuiMode } from "./shared/pi-mode.ts";
|
|
10
|
+
import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.ts";
|
|
11
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
12
12
|
|
|
13
13
|
type SessionIndexAction = "reindex" | undefined;
|
|
14
14
|
|