pi-sessions 0.7.2 → 0.9.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.
Files changed (104) hide show
  1. package/README.md +71 -37
  2. package/extensions/pi-sessions.ts +125 -0
  3. package/extensions/session-ask/agent.ts +34 -14
  4. package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
  5. package/extensions/session-ask/navigate.ts +3 -1
  6. package/extensions/session-ask/presenter.ts +23 -0
  7. package/extensions/session-ask/renderer.ts +63 -0
  8. package/extensions/session-ask/tool-contract.ts +27 -0
  9. package/extensions/session-ask/view-model.ts +18 -0
  10. package/extensions/session-auto-title/generate.ts +18 -18
  11. package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +67 -46
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +26 -12
  14. package/extensions/session-auto-title/wizard.ts +12 -6
  15. package/extensions/session-handoff/board-view-model.ts +181 -0
  16. package/extensions/session-handoff/board.ts +663 -0
  17. package/extensions/session-handoff/bootstrap.ts +174 -0
  18. package/extensions/session-handoff/extract.ts +101 -122
  19. package/extensions/session-handoff/install.ts +225 -0
  20. package/extensions/session-handoff/kickoff.ts +92 -0
  21. package/extensions/session-handoff/launch/backend.ts +16 -0
  22. package/extensions/session-handoff/launch/deferred.ts +20 -0
  23. package/extensions/session-handoff/launch/ghostty.ts +99 -0
  24. package/extensions/session-handoff/launch/resolution.ts +57 -0
  25. package/extensions/session-handoff/launch/shell.ts +7 -0
  26. package/extensions/session-handoff/launch/tmux.ts +31 -0
  27. package/extensions/session-handoff/launch-options.ts +61 -0
  28. package/extensions/session-handoff/launch-target.ts +91 -0
  29. package/extensions/session-handoff/metadata.ts +152 -63
  30. package/extensions/session-handoff/model.ts +58 -0
  31. package/extensions/session-handoff/query.ts +0 -1
  32. package/extensions/session-handoff/receipt.ts +96 -0
  33. package/extensions/session-handoff/review.ts +2 -28
  34. package/extensions/session-handoff/spawn.ts +76 -181
  35. package/extensions/session-handoff/strong-modal.ts +13 -0
  36. package/extensions/session-handoff/tool-contract.ts +12 -0
  37. package/extensions/session-handoff/tool-presenter.ts +35 -0
  38. package/extensions/session-handoff/tool-renderer.ts +66 -0
  39. package/extensions/session-handoff/tool-schema.ts +54 -0
  40. package/extensions/session-handoff/tool-view-model.ts +49 -0
  41. package/extensions/session-handoff/tool.ts +206 -0
  42. package/extensions/session-handoff/ui.ts +66 -0
  43. package/extensions/session-hooks/install.ts +75 -0
  44. package/extensions/{session-index.ts → session-index/install.ts} +9 -7
  45. package/extensions/session-messaging/broker/process.ts +21 -28
  46. package/extensions/session-messaging/install.ts +78 -0
  47. package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
  48. package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
  49. package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
  50. package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
  51. package/extensions/session-messaging/pi/message-contracts.ts +32 -0
  52. package/extensions/session-messaging/pi/renderer.ts +19 -12
  53. package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
  54. package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
  55. package/extensions/session-messaging/pi/service.ts +262 -81
  56. package/extensions/session-messaging/pi/tools.ts +184 -40
  57. package/extensions/session-search/extract.ts +60 -14
  58. package/extensions/session-search/hooks.ts +10 -13
  59. package/extensions/session-search/install.ts +373 -0
  60. package/extensions/session-search/renderer.ts +195 -0
  61. package/extensions/session-search/tool-contract.ts +51 -0
  62. package/extensions/shared/composition.ts +20 -0
  63. package/extensions/shared/errors.ts +3 -0
  64. package/extensions/shared/model-resolution.ts +73 -0
  65. package/extensions/shared/model-runtime.ts +33 -0
  66. package/extensions/shared/model.ts +7 -10
  67. package/extensions/shared/rendering/collapsible-text.ts +89 -0
  68. package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
  69. package/extensions/shared/rendering/theme.ts +5 -0
  70. package/extensions/shared/session-broker/client.ts +30 -29
  71. package/extensions/shared/session-broker/protocol.ts +128 -10
  72. package/extensions/shared/session-index/common.ts +5 -5
  73. package/extensions/shared/session-index/lineage.ts +0 -3
  74. package/extensions/shared/session-index/schema.ts +0 -1
  75. package/extensions/shared/session-index/scoring.ts +1 -2
  76. package/extensions/shared/session-index/search.ts +11 -4
  77. package/extensions/shared/session-index/store.ts +4 -9
  78. package/extensions/shared/settings.ts +80 -38
  79. package/extensions/shared/text.ts +5 -0
  80. package/extensions/shared/thinking-levels.ts +22 -0
  81. package/extensions/shared/tmux.ts +205 -0
  82. package/extensions/subagents/cancel.ts +111 -0
  83. package/extensions/subagents/classify.ts +52 -0
  84. package/extensions/subagents/install.ts +344 -0
  85. package/extensions/subagents/launch-target.ts +75 -0
  86. package/extensions/subagents/ledger.ts +217 -0
  87. package/extensions/subagents/reconcile.ts +386 -0
  88. package/extensions/subagents/report-message-presenter.ts +36 -0
  89. package/extensions/subagents/report-message-renderer.ts +21 -0
  90. package/extensions/subagents/report-message-view-model.ts +29 -0
  91. package/extensions/subagents/report.ts +159 -0
  92. package/extensions/subagents/roster.ts +262 -0
  93. package/extensions/subagents/wake.ts +173 -0
  94. package/images/handoff-board-subagents.png +0 -0
  95. package/images/handoff-board-user-sessions.png +0 -0
  96. package/images/handoff-subagent-report.png +0 -0
  97. package/images/session-handoff-tool.png +0 -0
  98. package/package.json +10 -16
  99. package/extensions/session-handoff.ts +0 -741
  100. package/extensions/session-hooks.ts +0 -75
  101. package/extensions/session-messaging/pi/message-view.ts +0 -131
  102. package/extensions/session-messaging.ts +0 -30
  103. package/extensions/session-search.ts +0 -419
  104. package/extensions/shared/session-broker/active.ts +0 -26
@@ -1,75 +0,0 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
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
-
6
- interface SessionStartLifecycleEvent {
7
- reason?: "startup" | "reload" | "new" | "resume" | "fork";
8
- previousSessionFile?: string;
9
- }
10
-
11
- export default function sessionHooksExtension(pi: ExtensionAPI): void {
12
- const settings = loadSettings();
13
- const controller = createSessionHookController({ indexPath: settings.index.path });
14
-
15
- pi.on("session_start", async (event, ctx) => {
16
- const { reason, previousSessionFile } = event as SessionStartLifecycleEvent;
17
- const sessionFile = ctx.sessionManager.getSessionFile();
18
-
19
- switch (reason) {
20
- case "new":
21
- case "resume":
22
- await controller.handleSessionSwitch(
23
- previousSessionFile,
24
- sessionFile,
25
- ctx.cwd,
26
- getSessionStartOrigin(ctx),
27
- );
28
- break;
29
- case "fork":
30
- await controller.handleSessionFork(previousSessionFile, sessionFile, ctx.cwd);
31
- break;
32
- default:
33
- await controller.handleSessionStart(sessionFile, ctx.cwd);
34
- break;
35
- }
36
- });
37
-
38
- pi.on("tool_call", async (event, ctx) => {
39
- controller.handleToolCall(event, ctx.sessionManager.getSessionFile(), ctx.cwd);
40
- });
41
-
42
- pi.on("tool_result", async (event) => {
43
- controller.handleToolResult(event);
44
- });
45
-
46
- pi.on("turn_end", async (_event, ctx) => {
47
- await controller.handleTurnEnd(ctx.sessionManager.getSessionFile(), ctx.cwd);
48
- });
49
-
50
- pi.on("session_tree", async (_event, ctx) => {
51
- await controller.handleSessionTree(ctx.sessionManager.getSessionFile(), ctx.cwd);
52
- });
53
-
54
- pi.on("session_compact", async (_event, ctx) => {
55
- await controller.handleSessionCompact(ctx.sessionManager.getSessionFile(), ctx.cwd);
56
- });
57
-
58
- pi.on("session_shutdown", async (_event, ctx) => {
59
- await controller.handleSessionShutdown(ctx.sessionManager.getSessionFile(), ctx.cwd);
60
- });
61
- }
62
-
63
- function getSessionStartOrigin(ctx: ExtensionContext): "handoff" | undefined {
64
- const encodedBootstrap = process.env[HANDOFF_BOOTSTRAP_ENV];
65
- if (!encodedBootstrap) {
66
- return undefined;
67
- }
68
-
69
- const bootstrap = parseHandoffBootstrap(encodedBootstrap);
70
- if (!bootstrap) {
71
- return undefined;
72
- }
73
-
74
- return bootstrap.sessionId === ctx.sessionManager.getSessionId() ? "handoff" : undefined;
75
- }
@@ -1,131 +0,0 @@
1
- import { Box, type Component, getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
2
- import type { ReceivedMessageEntry } from "./message-contracts.ts";
3
-
4
- const COLLAPSED_MESSAGE_LINES = 10;
5
-
6
- interface MessageViewTheme {
7
- bold(text: string): string;
8
- fg(token: string, text: string): string;
9
- bg(token: string, text: string): string;
10
- }
11
-
12
- interface MessageViewOptions {
13
- expanded: boolean;
14
- status: "sending" | "received";
15
- targetSessionId?: string | undefined;
16
- sourceSessionId?: string | undefined;
17
- sourceSessionName?: string | undefined;
18
- message: string;
19
- requestResponse?: boolean | undefined;
20
- relation?: string | undefined;
21
- }
22
-
23
- export function renderSessionMessageView(
24
- options: MessageViewOptions,
25
- theme: MessageViewTheme,
26
- ): string {
27
- const header = formatHeader(options, theme);
28
- const body = formatMessageBody(options.message, options.expanded, theme);
29
- return body ? `${header}\n\n${body}` : header;
30
- }
31
-
32
- export function createReceivedSessionMessageComponent(
33
- received: ReceivedMessageEntry,
34
- expanded: boolean,
35
- theme: MessageViewTheme,
36
- ): Component {
37
- const box = new Box(1, 1, (text: string) => theme.bg("toolSuccessBg", text));
38
- box.addChild(
39
- new Text(
40
- renderSessionMessageView(
41
- {
42
- expanded,
43
- status: "received",
44
- sourceSessionId: received.source.sessionId,
45
- sourceSessionName: received.source.sessionName,
46
- message: received.body,
47
- requestResponse: received.requestResponse,
48
- relation: received.relation,
49
- },
50
- theme,
51
- ),
52
- 0,
53
- 0,
54
- ),
55
- );
56
- return box;
57
- }
58
-
59
- export function formatSendMessageCall(
60
- args: { session?: string; message?: string; requestResponse?: boolean } | undefined,
61
- expanded: boolean,
62
- status: "sending",
63
- theme: MessageViewTheme,
64
- relation?: string | undefined,
65
- ): string {
66
- return renderSessionMessageView(
67
- {
68
- expanded,
69
- status,
70
- targetSessionId: args?.session,
71
- message: args?.message ?? "",
72
- requestResponse: args?.requestResponse,
73
- relation,
74
- },
75
- theme,
76
- );
77
- }
78
-
79
- function formatHeader(options: MessageViewOptions, theme: MessageViewTheme): string {
80
- const toolName = options.status === "received" ? "incoming_message" : "session_send_message";
81
- const name = theme.fg("toolTitle", theme.bold(toolName));
82
- const metadata = [
83
- options.relation,
84
- options.requestResponse ? "response requested" : undefined,
85
- ].filter((item): item is string => Boolean(item));
86
- const metadataHint = metadata.length > 0 ? theme.fg("muted", ` (${metadata.join(", ")})`) : "";
87
- switch (options.status) {
88
- case "received":
89
- return `${name} ${theme.fg("muted", `from ${formatSourceLabel(options)}`)}${metadataHint}`;
90
- case "sending":
91
- return `${name} ${theme.fg("muted", `to ${options.targetSessionId ?? "[target pending]"}`)}${metadataHint}`;
92
- }
93
- }
94
-
95
- function formatSourceLabel(options: MessageViewOptions): string {
96
- if (!options.sourceSessionId) {
97
- return "[source unknown]";
98
- }
99
-
100
- const title = options.sourceSessionName?.trim();
101
- return title ? `${title} (${options.sourceSessionId})` : options.sourceSessionId;
102
- }
103
-
104
- function formatKeyHint(
105
- keybinding: Keybinding,
106
- description: string,
107
- theme: MessageViewTheme,
108
- ): string {
109
- const keyText = getKeybindings().getKeys(keybinding).join("/");
110
- return `${theme.fg("dim", keyText)}${theme.fg("muted", ` ${description}`)}`;
111
- }
112
-
113
- function formatMessageBody(message: string, expanded: boolean, theme: MessageViewTheme): string {
114
- if (!message) {
115
- return "";
116
- }
117
-
118
- const lines = message.replaceAll("\t", " ").split("\n");
119
- const maxLines = expanded ? lines.length : COLLAPSED_MESSAGE_LINES;
120
- const visibleLines = lines.slice(0, maxLines);
121
- const remaining = lines.length - visibleLines.length;
122
- const body = visibleLines.map((line) => theme.fg("toolOutput", line)).join("\n");
123
- if (remaining <= 0) {
124
- return body;
125
- }
126
-
127
- return `${body}${theme.fg(
128
- "muted",
129
- `\n... (${remaining} more lines, ${lines.length} total,`,
130
- )} ${formatKeyHint("app.tools.expand", "to expand", theme)}${theme.fg("muted", ")")}`;
131
- }
@@ -1,30 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { IncomingSessionMessageRuntime } from "./session-messaging/pi/incoming-runtime.ts";
3
- import { registerSessionMessagingRenderer } from "./session-messaging/pi/renderer.ts";
4
- import { SessionMessagingService } from "./session-messaging/pi/service.ts";
5
- import { registerSessionMessagingTools } from "./session-messaging/pi/tools.ts";
6
- import { loadSettings } from "./shared/settings.ts";
7
-
8
- export default function sessionMessagingExtension(pi: ExtensionAPI): void {
9
- const settings = loadSettings();
10
- const incomingRuntime = new IncomingSessionMessageRuntime(pi);
11
- const service = new SessionMessagingService(
12
- settings.index.path,
13
- incomingRuntime,
14
- pi.appendEntry.bind(pi),
15
- );
16
-
17
- pi.on("session_start", async (_event, ctx) => {
18
- incomingRuntime.replayPending(ctx);
19
- try {
20
- await service.start(ctx);
21
- } catch {}
22
- });
23
-
24
- pi.on("session_shutdown", () => {
25
- service.stop();
26
- });
27
-
28
- registerSessionMessagingTools(pi, service);
29
- registerSessionMessagingRenderer(pi);
30
- }
@@ -1,419 +0,0 @@
1
- import path from "node:path";
2
- import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
- import { getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
4
- import { Type } from "typebox";
5
- import {
6
- stripSearchSnippetMarkers,
7
- transformSearchSnippetMatches,
8
- } from "./shared/search-snippet.ts";
9
- import {
10
- type ActiveSessionBrokerConnection,
11
- getActiveSessionBrokerConnection,
12
- } from "./shared/session-broker/active.ts";
13
- import {
14
- type SearchSessionResult,
15
- type SearchSessionsParams,
16
- type SearchSort,
17
- type SessionIndexStatus,
18
- type SessionSearchEvidence,
19
- searchSessions,
20
- withSessionIndex,
21
- } from "./shared/session-index/index.ts";
22
- import { formatSessionTitleOrShortId } from "./shared/session-ui.ts";
23
- import { loadSettings } from "./shared/settings.ts";
24
-
25
- interface SessionSearchToolParams {
26
- query?: string;
27
- files?: {
28
- touched?: string[];
29
- changed?: string[];
30
- };
31
- repo?: string;
32
- cwd?: string;
33
- time?: {
34
- after?: string;
35
- before?: string;
36
- };
37
- sort?: SearchSort;
38
- limit?: number;
39
- live?: boolean;
40
- }
41
-
42
- interface SessionSearchToolDetails {
43
- params?: SessionSearchToolParams | undefined;
44
- results: SearchSessionResult[];
45
- status?: SessionIndexStatus | undefined;
46
- }
47
-
48
- const DEFAULT_SESSION_SEARCH_LIMIT = 6;
49
- const COLLAPSED_RESULT_PREVIEW_ROWS = 6;
50
-
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 {
61
- const settings = loadSettings();
62
-
63
- pi.registerTool({
64
- name: "session_search",
65
- label: "Session Search",
66
- description: "Search Pi sessions",
67
- promptSnippet: "Locate Pi sessions for follow-up",
68
- promptGuidelines: [
69
- "Omit queries in session_search to list matching sessions chronologically.",
70
- "After session_search finds a session id, switch to session_ask for questions about it.",
71
- ],
72
- parameters: Type.Object({
73
- query: Type.Optional(
74
- Type.String({
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.",
77
- }),
78
- ),
79
- files: Type.Optional(
80
- Type.Object({
81
- touched: Type.Optional(
82
- Type.Array(
83
- Type.String({
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",
92
- }),
93
- ),
94
- ),
95
- }),
96
- ),
97
- repo: Type.Optional(
98
- Type.String({
99
- description: "Git repository touched in the session",
100
- }),
101
- ),
102
- cwd: Type.Optional(
103
- Type.String({
104
- description: "Directory of the session",
105
- }),
106
- ),
107
- time: Type.Optional(
108
- Type.Object({
109
- after: Type.Optional(
110
- Type.String({
111
- description: "Inclusive lower bound for the session activity interval, in ISO format",
112
- }),
113
- ),
114
- before: Type.Optional(
115
- Type.String({
116
- description: "Inclusive upper bound for the session activity interval, in ISO format",
117
- }),
118
- ),
119
- }),
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
- ),
134
- limit: Type.Optional(
135
- Type.Number({
136
- description: "Number of matches to return",
137
- }),
138
- ),
139
- }),
140
- async execute(_toolCallId, params: SessionSearchToolParams, _signal, onUpdate, ctx) {
141
- const validationError = validateSearchParams(params);
142
- if (validationError) {
143
- throw new Error(validationError);
144
- }
145
-
146
- const progressDetails: SessionSearchToolDetails = {
147
- params,
148
- results: [],
149
- };
150
- onUpdate?.({
151
- content: [{ type: "text", text: "Searching sessions..." }],
152
- details: progressDetails,
153
- });
154
-
155
- const indexPath = settings.index.path;
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));
162
- const details: SessionSearchToolDetails = {
163
- params,
164
- status,
165
- results,
166
- };
167
- return {
168
- content: [
169
- {
170
- type: "text" as const,
171
- text: formatSearchResultsForModel(details),
172
- },
173
- ],
174
- details,
175
- };
176
- });
177
- },
178
- renderResult(result, { expanded, isPartial }, theme, context) {
179
- const details = result.details as SessionSearchToolDetails | undefined;
180
- const content = result.content[0];
181
- if (content?.type !== "text") {
182
- return new Text(theme.fg("error", "No search output"), 0, 0);
183
- }
184
-
185
- if (context.isError) {
186
- return new Text(theme.fg("error", content.text), 0, 0);
187
- }
188
-
189
- if (isPartial) {
190
- const lines = [theme.bold(theme.fg("warning", "Searching sessions..."))];
191
- lines.push(...formatSessionSearchContextLines(details?.params, theme));
192
- return new Text(lines.join("\n"), 0, 0);
193
- }
194
-
195
- if (!details) {
196
- return new Text(theme.fg("error", content.text), 0, 0);
197
- }
198
-
199
- const lines = formatSessionSearchContextLines(details.params, theme);
200
- if (details.results.length === 0) {
201
- if (lines.length > 0) lines.push("");
202
- lines.push(theme.fg("warning", "No matching sessions found."));
203
- return new Text(lines.join("\n"), 0, 0);
204
- }
205
-
206
- if (lines.length > 0) lines.push("");
207
- lines.push(
208
- ...formatSessionSearchPanelResults(details.results, details.params, expanded, theme),
209
- );
210
- return new Text(lines.join("\n"), 0, 0);
211
- },
212
- });
213
- }
214
-
215
- function buildSearchParams(
216
- params: SessionSearchToolParams,
217
- ctx: ExtensionContext,
218
- liveSessionIds?: string[] | undefined,
219
- ): SearchSessionsParams {
220
- const currentSessionId = ctx.sessionManager.getSessionId();
221
-
222
- return {
223
- query: params.query,
224
- touched: params.files?.touched,
225
- changed: params.files?.changed,
226
- repo: params.repo,
227
- cwd: params.cwd,
228
- after: params.time?.after,
229
- before: params.time?.before,
230
- sort: params.sort,
231
- limit: params.limit ?? DEFAULT_SESSION_SEARCH_LIMIT,
232
- includeSessionIds: liveSessionIds,
233
- relativeToSessionId: currentSessionId,
234
- };
235
- }
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
-
254
- function formatSessionSearchContextLines(
255
- params: SessionSearchToolParams | undefined,
256
- theme: Theme,
257
- ): string[] {
258
- if (!params) return [];
259
-
260
- const lines: string[] = [];
261
- if (params.query?.trim()) {
262
- lines.push(theme.fg("muted", `query: ${params.query.trim()}`));
263
- }
264
-
265
- const filters: string[] = [];
266
- if (params.repo?.trim()) filters.push(`repo: ${params.repo.trim()}`);
267
- if (params.cwd?.trim()) filters.push(`cwd: ${params.cwd.trim()}`);
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}`);
274
- if (params.time?.after?.trim()) filters.push(`after: ${params.time.after.trim()}`);
275
- if (params.time?.before?.trim()) filters.push(`before: ${params.time.before.trim()}`);
276
- if (params.limit !== undefined) filters.push(`limit: ${params.limit}`);
277
-
278
- if (filters.length > 0) {
279
- lines.push(theme.fg("dim", filters.join(" • ")));
280
- }
281
-
282
- if (lines.length === 0) {
283
- lines.push(theme.fg("dim", "all sessions"));
284
- }
285
-
286
- return lines;
287
- }
288
-
289
- function formatSessionSearchPanelResults(
290
- results: SearchSessionResult[],
291
- params: SessionSearchToolParams | undefined,
292
- expanded: boolean,
293
- theme: Theme,
294
- ): string[] {
295
- const visibleResults = expanded ? results : results.slice(0, COLLAPSED_RESULT_PREVIEW_ROWS);
296
- const lines = visibleResults.flatMap((result, index) => {
297
- const location = formatSearchResultLocation(result.cwd);
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];
304
- });
305
-
306
- if (!expanded && results.length > visibleResults.length) {
307
- lines.push(formatOverflowHint(results.length - visibleResults.length, results.length, theme));
308
- }
309
-
310
- return lines;
311
- }
312
-
313
- function formatSearchResultLabel(result: SearchSessionResult): string {
314
- return formatSessionTitleOrShortId(result.sessionName, result.sessionId);
315
- }
316
-
317
- function formatSearchResultLocation(cwd: string | undefined): string | undefined {
318
- if (!cwd) return undefined;
319
- const base = path.basename(cwd);
320
- return base || cwd;
321
- }
322
-
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));
328
- }
329
-
330
- interface SearchTextEvidence {
331
- kind: "text";
332
- sourceKind: string;
333
- snippet: string;
334
- score: number;
335
- entryId: string;
336
- }
337
-
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, " ")
343
- .trim();
344
- }
345
-
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
- }
357
-
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
- }
365
-
366
- function formatEvidenceForModel(evidence: SessionSearchEvidence): SessionSearchEvidence {
367
- if (evidence.kind !== "text") {
368
- return evidence;
369
- }
370
-
371
- return {
372
- ...evidence,
373
- snippet: stripSearchSnippetMarkers(evidence.snippet) ?? evidence.snippet,
374
- };
375
- }
376
-
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)")}`;
382
- }
383
-
384
- function formatKeyHint(keybinding: Keybinding): string {
385
- return getKeybindings().getKeys(keybinding).join("/");
386
- }
387
-
388
- function validateSearchParams(params: SessionSearchToolParams): string | undefined {
389
- if (params.time?.after && !isValidIsoDateLike(params.time.after)) {
390
- return `Invalid time.after value: ${params.time.after}`;
391
- }
392
-
393
- if (params.time?.before && !isValidIsoDateLike(params.time.before)) {
394
- return `Invalid time.before value: ${params.time.before}`;
395
- }
396
-
397
- if (params.time?.after && params.time?.before) {
398
- const after = new Date(params.time.after);
399
- const before = new Date(params.time.before);
400
- if (after.getTime() > before.getTime()) {
401
- return `time.after must be less than or equal to time.before`;
402
- }
403
- }
404
-
405
- if (params.limit !== undefined && params.limit <= 0) {
406
- return `limit must be greater than 0`;
407
- }
408
-
409
- return undefined;
410
- }
411
-
412
- function isValidIsoDateLike(value: string): boolean {
413
- const date = new Date(value);
414
- return !Number.isNaN(date.getTime());
415
- }
416
-
417
- function formatError(error: unknown): string {
418
- return error instanceof Error && error.message.trim() ? error.message : String(error);
419
- }
@@ -1,26 +0,0 @@
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
- }