pi-sessions 0.6.0 → 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 -11
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +79 -131
- package/extensions/session-auto-title/model.ts +3 -11
- package/extensions/session-handoff/picker.ts +50 -4
- package/extensions/session-handoff/query.ts +74 -56
- package/extensions/session-handoff/refs.ts +12 -18
- package/extensions/session-handoff.ts +36 -27
- package/extensions/session-messaging/broker/process.ts +23 -26
- package/extensions/session-messaging/broker/spawn.ts +7 -2
- package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
- package/extensions/session-messaging/pi/message-contracts.ts +9 -10
- package/extensions/session-messaging/pi/message-view.ts +12 -1
- package/extensions/session-messaging/pi/service.ts +117 -62
- package/extensions/session-messaging/pi/tools.ts +4 -56
- package/extensions/session-search/extract.ts +86 -436
- package/extensions/session-search/hooks.ts +17 -25
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +1 -0
- package/extensions/session-search.ts +157 -128
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
- package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
- package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +43 -48
- package/extensions/shared/session-index/index.ts +1 -0
- package/extensions/shared/session-index/lineage.ts +10 -0
- 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 +22 -6
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +552 -278
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +12 -21
- package/extensions/shared/settings.ts +61 -4
- package/extensions/shared/text.ts +50 -0
- package/package.json +1 -1
- /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
|
@@ -8,13 +8,10 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
clearSessionChunksBySourceKind,
|
|
10
10
|
clearSessionIndexedData,
|
|
11
|
-
getMetadata,
|
|
12
11
|
getSessionById,
|
|
13
12
|
getSessionRowByPath,
|
|
14
|
-
INDEX_SCHEMA_VERSION,
|
|
15
13
|
insertSessionFileTouch,
|
|
16
14
|
insertTextChunk,
|
|
17
|
-
openIndexDatabase,
|
|
18
15
|
refreshSessionLineageRelationsFor,
|
|
19
16
|
type SessionIndexDatabase,
|
|
20
17
|
type SessionLineageRow,
|
|
@@ -22,6 +19,7 @@ import {
|
|
|
22
19
|
type SessionRow,
|
|
23
20
|
setMetadata,
|
|
24
21
|
upsertSession,
|
|
22
|
+
withSessionIndex,
|
|
25
23
|
} from "../shared/session-index/index.ts";
|
|
26
24
|
import {
|
|
27
25
|
createSessionNameChunk,
|
|
@@ -198,24 +196,19 @@ function syncSessionFile(
|
|
|
198
196
|
state?: SessionHookState,
|
|
199
197
|
sessionOrigin?: SessionOrigin,
|
|
200
198
|
): boolean {
|
|
201
|
-
if (!sessionFile || !existsSync(sessionFile)
|
|
199
|
+
if (!sessionFile || !existsSync(sessionFile)) {
|
|
202
200
|
return false;
|
|
203
201
|
}
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
return synced;
|
|
216
|
-
} finally {
|
|
217
|
-
db.close();
|
|
218
|
-
}
|
|
203
|
+
return (
|
|
204
|
+
withSessionIndex(indexPath, { mode: "write", required: false }, ({ db }) => {
|
|
205
|
+
const synced = syncSessionFileWithDb(db, sessionFile, eventType, sessionOrigin);
|
|
206
|
+
if (synced && state) {
|
|
207
|
+
state.lastFlushedSessionFile = sessionFile;
|
|
208
|
+
}
|
|
209
|
+
return synced;
|
|
210
|
+
}) ?? false
|
|
211
|
+
);
|
|
219
212
|
}
|
|
220
213
|
|
|
221
214
|
interface TailSyncBaseline extends SessionRow {
|
|
@@ -346,10 +339,14 @@ function applyTailSync(
|
|
|
346
339
|
|
|
347
340
|
if (scan.sessionName !== undefined && scan.sessionName !== baseline.sessionName) {
|
|
348
341
|
clearSessionChunksBySourceKind(db, baseline.sessionId, "session_name");
|
|
349
|
-
if (scan.sessionName) {
|
|
342
|
+
if (scan.sessionName && scan.sessionNameEntryId) {
|
|
350
343
|
insertTextChunk(db, {
|
|
351
344
|
sessionId: baseline.sessionId,
|
|
352
|
-
...createSessionNameChunk(
|
|
345
|
+
...createSessionNameChunk(
|
|
346
|
+
scan.sessionName,
|
|
347
|
+
scan.sessionNameTs ?? baseline.startedAt,
|
|
348
|
+
scan.sessionNameEntryId,
|
|
349
|
+
),
|
|
353
350
|
});
|
|
354
351
|
}
|
|
355
352
|
}
|
|
@@ -439,11 +436,6 @@ function writeHookSyncMetadata(db: SessionIndexDatabase, eventType: string): voi
|
|
|
439
436
|
setMetadata(db, "hook_last_event", eventType);
|
|
440
437
|
}
|
|
441
438
|
|
|
442
|
-
function readIndexSchemaVersion(db: SessionIndexDatabase): number | undefined {
|
|
443
|
-
const raw = getMetadata(db, "schema_version");
|
|
444
|
-
return raw === undefined ? undefined : Number(raw);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
439
|
function mergeSessionLineage(
|
|
448
440
|
extracted: ExtractedSessionRecord,
|
|
449
441
|
existing: SessionLineageRow | undefined,
|
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export type FileTouchOp = "changed" | "read"
|
|
5
|
+
export type FileTouchOp = "changed" | "read";
|
|
6
6
|
export type FileTouchSource = "tool_call" | "branch_summary_details" | "compaction_details";
|
|
7
7
|
export type PathScope = "absolute" | "relative" | "basename";
|
|
8
8
|
|
|
@@ -66,6 +66,7 @@ function dropIndexTables(db: SessionIndexDatabase): void {
|
|
|
66
66
|
// implicit DELETE that parent-side constraints would reject.
|
|
67
67
|
db.exec(`
|
|
68
68
|
DROP TABLE IF EXISTS session_lineage_relations;
|
|
69
|
+
DROP TABLE IF EXISTS session_repo_roots;
|
|
69
70
|
DROP TABLE IF EXISTS session_text_chunks;
|
|
70
71
|
DROP TABLE IF EXISTS session_file_touches;
|
|
71
72
|
DROP TABLE IF EXISTS session_text_chunks_fts;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import {
|
|
6
6
|
stripSearchSnippetMarkers,
|
|
7
7
|
transformSearchSnippetMatches,
|
|
8
8
|
} from "./shared/search-snippet.ts";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
type ActiveSessionBrokerConnection,
|
|
11
|
+
getActiveSessionBrokerConnection,
|
|
12
|
+
} from "./shared/session-broker/active.ts";
|
|
13
|
+
import {
|
|
13
14
|
type SearchSessionResult,
|
|
14
15
|
type SearchSessionsParams,
|
|
16
|
+
type SearchSort,
|
|
15
17
|
type SessionIndexStatus,
|
|
18
|
+
type SessionSearchEvidence,
|
|
16
19
|
searchSessions,
|
|
20
|
+
withSessionIndex,
|
|
17
21
|
} from "./shared/session-index/index.ts";
|
|
18
22
|
import { formatSessionTitleOrShortId } from "./shared/session-ui.ts";
|
|
19
23
|
import { loadSettings } from "./shared/settings.ts";
|
|
@@ -22,6 +26,7 @@ interface SessionSearchToolParams {
|
|
|
22
26
|
query?: string;
|
|
23
27
|
files?: {
|
|
24
28
|
touched?: string[];
|
|
29
|
+
changed?: string[];
|
|
25
30
|
};
|
|
26
31
|
repo?: string;
|
|
27
32
|
cwd?: string;
|
|
@@ -29,11 +34,12 @@ interface SessionSearchToolParams {
|
|
|
29
34
|
after?: string;
|
|
30
35
|
before?: string;
|
|
31
36
|
};
|
|
37
|
+
sort?: SearchSort;
|
|
32
38
|
limit?: number;
|
|
39
|
+
live?: boolean;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
interface SessionSearchToolDetails {
|
|
36
|
-
error: boolean;
|
|
37
43
|
params?: SessionSearchToolParams | undefined;
|
|
38
44
|
results: SearchSessionResult[];
|
|
39
45
|
status?: SessionIndexStatus | undefined;
|
|
@@ -42,23 +48,32 @@ interface SessionSearchToolDetails {
|
|
|
42
48
|
const DEFAULT_SESSION_SEARCH_LIMIT = 6;
|
|
43
49
|
const COLLAPSED_RESULT_PREVIEW_ROWS = 6;
|
|
44
50
|
|
|
45
|
-
export
|
|
51
|
+
export interface SessionSearchDeps {
|
|
52
|
+
getBrokerConnection: () => ActiveSessionBrokerConnection | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function sessionSearchExtension(
|
|
56
|
+
pi: ExtensionAPI,
|
|
57
|
+
deps: SessionSearchDeps = {
|
|
58
|
+
getBrokerConnection: getActiveSessionBrokerConnection,
|
|
59
|
+
},
|
|
60
|
+
): void {
|
|
46
61
|
const settings = loadSettings();
|
|
47
62
|
|
|
48
63
|
pi.registerTool({
|
|
49
64
|
name: "session_search",
|
|
50
65
|
label: "Session Search",
|
|
51
|
-
description: "Search
|
|
52
|
-
promptSnippet: "Use when you need to locate
|
|
66
|
+
description: "Search Pi sessions",
|
|
67
|
+
promptSnippet: "Use when you need to locate a session to do a detailed follow-up",
|
|
53
68
|
promptGuidelines: [
|
|
54
|
-
"query
|
|
55
|
-
"If you want alternatives, run multiple session_search calls with different queries",
|
|
69
|
+
"Omit query to list matching sessions chronologically",
|
|
56
70
|
"Once you have the right session id, switch to session_ask for questions about that session",
|
|
57
71
|
],
|
|
58
72
|
parameters: Type.Object({
|
|
59
73
|
query: Type.Optional(
|
|
60
74
|
Type.String({
|
|
61
|
-
description:
|
|
75
|
+
description:
|
|
76
|
+
"Use plain adjacent terms for normal search. Supports quoted phrases, AND/OR/NOT, parentheses, and -term negation when matching needs to be stricter.",
|
|
62
77
|
}),
|
|
63
78
|
),
|
|
64
79
|
files: Type.Optional(
|
|
@@ -66,7 +81,14 @@ export default function sessionSearchExtension(pi: ExtensionAPI): void {
|
|
|
66
81
|
touched: Type.Optional(
|
|
67
82
|
Type.Array(
|
|
68
83
|
Type.String({
|
|
69
|
-
description: "File path
|
|
84
|
+
description: "File path read or changed in the session",
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
changed: Type.Optional(
|
|
89
|
+
Type.Array(
|
|
90
|
+
Type.String({
|
|
91
|
+
description: "File path changed in the session",
|
|
70
92
|
}),
|
|
71
93
|
),
|
|
72
94
|
),
|
|
@@ -79,23 +101,36 @@ export default function sessionSearchExtension(pi: ExtensionAPI): void {
|
|
|
79
101
|
),
|
|
80
102
|
cwd: Type.Optional(
|
|
81
103
|
Type.String({
|
|
82
|
-
description: "Directory the session
|
|
104
|
+
description: "Directory of the session",
|
|
83
105
|
}),
|
|
84
106
|
),
|
|
85
107
|
time: Type.Optional(
|
|
86
108
|
Type.Object({
|
|
87
109
|
after: Type.Optional(
|
|
88
110
|
Type.String({
|
|
89
|
-
description: "Inclusive lower bound for session
|
|
111
|
+
description: "Inclusive lower bound for the session activity interval, in ISO format",
|
|
90
112
|
}),
|
|
91
113
|
),
|
|
92
114
|
before: Type.Optional(
|
|
93
115
|
Type.String({
|
|
94
|
-
description: "Inclusive upper bound for session
|
|
116
|
+
description: "Inclusive upper bound for the session activity interval, in ISO format",
|
|
95
117
|
}),
|
|
96
118
|
),
|
|
97
119
|
}),
|
|
98
120
|
),
|
|
121
|
+
sort: Type.Optional(
|
|
122
|
+
Type.Union(
|
|
123
|
+
[Type.Literal("relevance"), Type.Literal("modified_desc"), Type.Literal("modified_asc")],
|
|
124
|
+
{
|
|
125
|
+
description: "Display order for returned matches",
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
live: Type.Optional(
|
|
130
|
+
Type.Boolean({
|
|
131
|
+
description: "When true, only return currently active sessions",
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
99
134
|
limit: Type.Optional(
|
|
100
135
|
Type.Number({
|
|
101
136
|
description: "Number of matches to return",
|
|
@@ -105,19 +140,10 @@ export default function sessionSearchExtension(pi: ExtensionAPI): void {
|
|
|
105
140
|
async execute(_toolCallId, params: SessionSearchToolParams, _signal, onUpdate, ctx) {
|
|
106
141
|
const validationError = validateSearchParams(params);
|
|
107
142
|
if (validationError) {
|
|
108
|
-
|
|
109
|
-
error: true,
|
|
110
|
-
params,
|
|
111
|
-
results: [],
|
|
112
|
-
};
|
|
113
|
-
return {
|
|
114
|
-
content: [{ type: "text", text: validationError }],
|
|
115
|
-
details,
|
|
116
|
-
};
|
|
143
|
+
throw new Error(validationError);
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
const progressDetails: SessionSearchToolDetails = {
|
|
120
|
-
error: false,
|
|
121
147
|
params,
|
|
122
148
|
results: [],
|
|
123
149
|
};
|
|
@@ -127,77 +153,53 @@ export default function sessionSearchExtension(pi: ExtensionAPI): void {
|
|
|
127
153
|
});
|
|
128
154
|
|
|
129
155
|
const indexPath = settings.index.path;
|
|
130
|
-
const
|
|
131
|
-
|
|
156
|
+
const liveSessionIds = params.live
|
|
157
|
+
? await getLiveSessionIds(deps.getBrokerConnection)
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
return withSessionIndex(indexPath, { mode: "read", required: true }, ({ db, status }) => {
|
|
161
|
+
const results = searchSessions(db, buildSearchParams(params, ctx, liveSessionIds));
|
|
132
162
|
const details: SessionSearchToolDetails = {
|
|
133
|
-
error: true,
|
|
134
163
|
params,
|
|
135
164
|
status,
|
|
136
|
-
results
|
|
165
|
+
results,
|
|
137
166
|
};
|
|
138
167
|
return {
|
|
139
168
|
content: [
|
|
140
169
|
{
|
|
141
|
-
type: "text",
|
|
142
|
-
text:
|
|
170
|
+
type: "text" as const,
|
|
171
|
+
text: formatSearchResultsForModel(details),
|
|
143
172
|
},
|
|
144
173
|
],
|
|
145
174
|
details,
|
|
146
175
|
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const db = openIndexDatabase(status.dbPath, { create: false });
|
|
150
|
-
try {
|
|
151
|
-
const results = searchSessions(db, buildSearchParams(params, ctx));
|
|
152
|
-
|
|
153
|
-
if (results.length === 0) {
|
|
154
|
-
const details: SessionSearchToolDetails = {
|
|
155
|
-
error: false,
|
|
156
|
-
params,
|
|
157
|
-
status,
|
|
158
|
-
results: [],
|
|
159
|
-
};
|
|
160
|
-
return {
|
|
161
|
-
content: [{ type: "text", text: "No matching sessions found." }],
|
|
162
|
-
details,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const details: SessionSearchToolDetails = {
|
|
167
|
-
error: false,
|
|
168
|
-
params,
|
|
169
|
-
status,
|
|
170
|
-
results,
|
|
171
|
-
};
|
|
172
|
-
return {
|
|
173
|
-
content: [{ type: "text", text: formatSearchResults(results) }],
|
|
174
|
-
details,
|
|
175
|
-
};
|
|
176
|
-
} finally {
|
|
177
|
-
db.close();
|
|
178
|
-
}
|
|
176
|
+
});
|
|
179
177
|
},
|
|
180
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
178
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
181
179
|
const details = result.details as SessionSearchToolDetails | undefined;
|
|
182
180
|
const content = result.content[0];
|
|
183
181
|
if (content?.type !== "text") {
|
|
184
182
|
return new Text(theme.fg("error", "No search output"), 0, 0);
|
|
185
183
|
}
|
|
186
184
|
|
|
185
|
+
if (context.isError) {
|
|
186
|
+
return new Text(theme.fg("error", content.text), 0, 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
187
189
|
if (isPartial) {
|
|
188
190
|
const lines = [theme.bold(theme.fg("warning", "Searching sessions..."))];
|
|
189
191
|
lines.push(...formatSessionSearchContextLines(details?.params, theme));
|
|
190
192
|
return new Text(lines.join("\n"), 0, 0);
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
if (!details
|
|
195
|
+
if (!details) {
|
|
194
196
|
return new Text(theme.fg("error", content.text), 0, 0);
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
const lines = formatSessionSearchContextLines(details.params, theme);
|
|
198
200
|
if (details.results.length === 0) {
|
|
199
201
|
if (lines.length > 0) lines.push("");
|
|
200
|
-
lines.push(theme.fg("warning",
|
|
202
|
+
lines.push(theme.fg("warning", "No matching sessions found."));
|
|
201
203
|
return new Text(lines.join("\n"), 0, 0);
|
|
202
204
|
}
|
|
203
205
|
|
|
@@ -213,21 +215,42 @@ export default function sessionSearchExtension(pi: ExtensionAPI): void {
|
|
|
213
215
|
function buildSearchParams(
|
|
214
216
|
params: SessionSearchToolParams,
|
|
215
217
|
ctx: ExtensionContext,
|
|
218
|
+
liveSessionIds?: string[] | undefined,
|
|
216
219
|
): SearchSessionsParams {
|
|
217
220
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
218
221
|
|
|
219
222
|
return {
|
|
220
223
|
query: params.query,
|
|
221
224
|
touched: params.files?.touched,
|
|
225
|
+
changed: params.files?.changed,
|
|
222
226
|
repo: params.repo,
|
|
223
227
|
cwd: params.cwd,
|
|
224
228
|
after: params.time?.after,
|
|
225
229
|
before: params.time?.before,
|
|
230
|
+
sort: params.sort,
|
|
226
231
|
limit: params.limit ?? DEFAULT_SESSION_SEARCH_LIMIT,
|
|
227
|
-
|
|
232
|
+
includeSessionIds: liveSessionIds,
|
|
233
|
+
relativeToSessionId: currentSessionId,
|
|
228
234
|
};
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
async function getLiveSessionIds(
|
|
238
|
+
getBrokerConnection: SessionSearchDeps["getBrokerConnection"],
|
|
239
|
+
): Promise<string[]> {
|
|
240
|
+
const connection = getBrokerConnection();
|
|
241
|
+
if (!connection) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"Session messaging is not active; live session search requires session messaging.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
return await connection.listSessionIds();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new Error(`Session messaging is unavailable: ${formatError(error)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
231
254
|
function formatSessionSearchContextLines(
|
|
232
255
|
params: SessionSearchToolParams | undefined,
|
|
233
256
|
theme: Theme,
|
|
@@ -242,7 +265,12 @@ function formatSessionSearchContextLines(
|
|
|
242
265
|
const filters: string[] = [];
|
|
243
266
|
if (params.repo?.trim()) filters.push(`repo: ${params.repo.trim()}`);
|
|
244
267
|
if (params.cwd?.trim()) filters.push(`cwd: ${params.cwd.trim()}`);
|
|
245
|
-
if (params.files?.touched?.length)
|
|
268
|
+
if (params.files?.touched?.length)
|
|
269
|
+
filters.push(`files.touched: ${params.files.touched.join(", ")}`);
|
|
270
|
+
if (params.files?.changed?.length)
|
|
271
|
+
filters.push(`files.changed: ${params.files.changed.join(", ")}`);
|
|
272
|
+
if (params.live) filters.push("live: true");
|
|
273
|
+
if (params.sort) filters.push(`sort: ${params.sort}`);
|
|
246
274
|
if (params.time?.after?.trim()) filters.push(`after: ${params.time.after.trim()}`);
|
|
247
275
|
if (params.time?.before?.trim()) filters.push(`before: ${params.time.before.trim()}`);
|
|
248
276
|
if (params.limit !== undefined) filters.push(`limit: ${params.limit}`);
|
|
@@ -267,13 +295,16 @@ function formatSessionSearchPanelResults(
|
|
|
267
295
|
const visibleResults = expanded ? results : results.slice(0, COLLAPSED_RESULT_PREVIEW_ROWS);
|
|
268
296
|
const lines = visibleResults.flatMap((result, index) => {
|
|
269
297
|
const location = formatSearchResultLocation(result.cwd);
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
298
|
+
const relation = result.relation ? ` ${theme.fg("dim", `[${result.relation}]`)}` : "";
|
|
299
|
+
const heading = `${index + 1}. ${theme.bold(formatSearchResultLabel(result))}${relation}${location ? ` ${theme.fg("dim", `(${location})`)}` : ""}`;
|
|
300
|
+
const snippets = params?.query ? formatSearchSnippets(result).slice(0, 3) : [];
|
|
301
|
+
return snippets.length > 0
|
|
302
|
+
? [heading, ...snippets.map((snippet) => theme.fg("dim", ` - ${snippet}`))]
|
|
303
|
+
: [heading];
|
|
273
304
|
});
|
|
274
305
|
|
|
275
306
|
if (!expanded && results.length > visibleResults.length) {
|
|
276
|
-
lines.push(
|
|
307
|
+
lines.push(formatOverflowHint(results.length - visibleResults.length, results.length, theme));
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
return lines;
|
|
@@ -289,76 +320,70 @@ function formatSearchResultLocation(cwd: string | undefined): string | undefined
|
|
|
289
320
|
return base || cwd;
|
|
290
321
|
}
|
|
291
322
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
?.replace(/\s+/g, " ")
|
|
298
|
-
.trim();
|
|
323
|
+
function formatSearchSnippets(result: SearchSessionResult): string[] {
|
|
324
|
+
return result.evidence
|
|
325
|
+
.filter((evidence): evidence is SearchTextEvidence => evidence.kind === "text")
|
|
326
|
+
.map((evidence) => formatSearchSnippetText(evidence.snippet))
|
|
327
|
+
.filter((snippet): snippet is string => Boolean(snippet));
|
|
299
328
|
}
|
|
300
329
|
|
|
301
|
-
interface
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
interface SearchTextEvidence {
|
|
331
|
+
kind: "text";
|
|
332
|
+
sourceKind: string;
|
|
333
|
+
snippet: string;
|
|
334
|
+
score: number;
|
|
335
|
+
entryId: string;
|
|
305
336
|
}
|
|
306
337
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (const result of results) {
|
|
314
|
-
const bucket = groups.get(result.cwd);
|
|
315
|
-
if (bucket) {
|
|
316
|
-
bucket.push(result);
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
groups.set(result.cwd, [result]);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return [...groups.entries()]
|
|
324
|
-
.flatMap(([cwd, groupResults], groupIndex) => {
|
|
325
|
-
const lines = [styles.cwd(`cwd: ${cwd}`)];
|
|
326
|
-
for (const result of groupResults) {
|
|
327
|
-
lines.push(...formatSearchResult(result, styles));
|
|
328
|
-
}
|
|
329
|
-
if (groupIndex < groups.size - 1) {
|
|
330
|
-
lines.push("");
|
|
331
|
-
}
|
|
332
|
-
return lines;
|
|
333
|
-
})
|
|
334
|
-
.join("\n")
|
|
338
|
+
function formatSearchSnippetText(snippet: string): string | undefined {
|
|
339
|
+
const plainSnippet = stripSearchSnippetMarkers(snippet)?.replace(/\s+/g, " ").trim();
|
|
340
|
+
if (!plainSnippet) return undefined;
|
|
341
|
+
return transformSearchSnippetMatches(snippet, (match) => `[${match}]`)
|
|
342
|
+
?.replace(/\s+/g, " ")
|
|
335
343
|
.trim();
|
|
336
344
|
}
|
|
337
345
|
|
|
338
|
-
function
|
|
339
|
-
|
|
346
|
+
function formatSearchResultsForModel(details: SessionSearchToolDetails): string {
|
|
347
|
+
return JSON.stringify(
|
|
348
|
+
{
|
|
349
|
+
params: details.params,
|
|
350
|
+
status: details.status,
|
|
351
|
+
results: details.results.map(formatSearchResultForModel),
|
|
352
|
+
},
|
|
353
|
+
null,
|
|
354
|
+
2,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
340
357
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
358
|
+
function formatSearchResultForModel(result: SearchSessionResult): SearchSessionResult {
|
|
359
|
+
return {
|
|
360
|
+
...result,
|
|
361
|
+
snippet: stripSearchSnippetMarkers(result.snippet) ?? result.snippet,
|
|
362
|
+
evidence: result.evidence.map(formatEvidenceForModel),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
344
365
|
|
|
345
|
-
|
|
346
|
-
|
|
366
|
+
function formatEvidenceForModel(evidence: SessionSearchEvidence): SessionSearchEvidence {
|
|
367
|
+
if (evidence.kind !== "text") {
|
|
368
|
+
return evidence;
|
|
347
369
|
}
|
|
348
370
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
371
|
+
return {
|
|
372
|
+
...evidence,
|
|
373
|
+
snippet: stripSearchSnippetMarkers(evidence.snippet) ?? evidence.snippet,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
353
376
|
|
|
354
|
-
|
|
377
|
+
function formatOverflowHint(remaining: number, total: number, theme: Theme): string {
|
|
378
|
+
return `${theme.fg("muted", `... (${remaining} more lines, ${total} total,`)} ${theme.fg(
|
|
379
|
+
"dim",
|
|
380
|
+
formatKeyHint("app.tools.expand"),
|
|
381
|
+
)}${theme.fg("muted", " to expand)")}`;
|
|
355
382
|
}
|
|
356
383
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
secondary: (text) => text,
|
|
361
|
-
};
|
|
384
|
+
function formatKeyHint(keybinding: Keybinding): string {
|
|
385
|
+
return getKeybindings().getKeys(keybinding).join("/");
|
|
386
|
+
}
|
|
362
387
|
|
|
363
388
|
function validateSearchParams(params: SessionSearchToolParams): string | undefined {
|
|
364
389
|
if (params.time?.after && !isValidIsoDateLike(params.time.after)) {
|
|
@@ -388,3 +413,7 @@ function isValidIsoDateLike(value: string): boolean {
|
|
|
388
413
|
const date = new Date(value);
|
|
389
414
|
return !Number.isNaN(date.getTime());
|
|
390
415
|
}
|
|
416
|
+
|
|
417
|
+
function formatError(error: unknown): string {
|
|
418
|
+
return error instanceof Error && error.message.trim() ? error.message : String(error);
|
|
419
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelReference } from "./settings.ts";
|
|
3
|
+
|
|
4
|
+
export function findModelByReference(
|
|
5
|
+
availableModels: Model<Api>[],
|
|
6
|
+
reference: ModelReference,
|
|
7
|
+
): Model<Api> | undefined {
|
|
8
|
+
return availableModels.find(
|
|
9
|
+
(model) => model.provider === reference.provider && model.id === reference.modelId,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveConfiguredModel(
|
|
14
|
+
availableModels: Model<Api>[],
|
|
15
|
+
configuredModel: ModelReference | undefined,
|
|
16
|
+
): Model<Api> | undefined {
|
|
17
|
+
return configuredModel ? findModelByReference(availableModels, configuredModel) : undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ActiveSessionBrokerConnection {
|
|
2
|
+
listSessionIds(): Promise<string[]>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Pi loads extension entrypoints through separate jiti module graphs, so module-local
|
|
6
|
+
// singletons are not shared between session-messaging and session-search. The process
|
|
7
|
+
// global is the only way to share a single connection
|
|
8
|
+
const ACTIVE_CONNECTION_KEY = Symbol.for("pi-sessions.active-session-broker-connection");
|
|
9
|
+
|
|
10
|
+
type ActiveConnectionGlobal = typeof globalThis & {
|
|
11
|
+
[ACTIVE_CONNECTION_KEY]?: ActiveSessionBrokerConnection | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getActiveConnectionGlobal(): ActiveConnectionGlobal {
|
|
15
|
+
return globalThis as ActiveConnectionGlobal;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setActiveSessionBrokerConnection(
|
|
19
|
+
connection: ActiveSessionBrokerConnection | undefined,
|
|
20
|
+
): void {
|
|
21
|
+
getActiveConnectionGlobal()[ACTIVE_CONNECTION_KEY] = connection;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getActiveSessionBrokerConnection(): ActiveSessionBrokerConnection | undefined {
|
|
25
|
+
return getActiveConnectionGlobal()[ACTIVE_CONNECTION_KEY];
|
|
26
|
+
}
|