@bdsqqq/pi 0.0.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.
Files changed (99) hide show
  1. package/dist/chunk-DsIazq42.js +26 -0
  2. package/dist/command-palette-B97yKqne.js +406 -0
  3. package/dist/core/agents-md.d.ts +22 -0
  4. package/dist/core/agents-md.js +174 -0
  5. package/dist/core/box-chrome.d.ts +56 -0
  6. package/dist/core/box-chrome.js +207 -0
  7. package/dist/core/box-format.d.ts +82 -0
  8. package/dist/core/box-format.js +381 -0
  9. package/dist/core/config.d.ts +14 -0
  10. package/dist/core/config.js +201 -0
  11. package/dist/core/file-tracker.d.ts +65 -0
  12. package/dist/core/file-tracker.js +439 -0
  13. package/dist/core/github-api.d.ts +45 -0
  14. package/dist/core/github-api.js +101 -0
  15. package/dist/core/html-to-md.d.ts +5 -0
  16. package/dist/core/html-to-md.js +289 -0
  17. package/dist/core/interpolate.d.ts +44 -0
  18. package/dist/core/interpolate.js +475 -0
  19. package/dist/core/mutex.d.ts +8 -0
  20. package/dist/core/mutex.js +142 -0
  21. package/dist/core/output-buffer.d.ts +80 -0
  22. package/dist/core/output-buffer.js +517 -0
  23. package/dist/core/permissions.d.ts +19 -0
  24. package/dist/core/permissions.js +140 -0
  25. package/dist/core/pi-spawn.d.ts +62 -0
  26. package/dist/core/pi-spawn.js +244 -0
  27. package/dist/core/prompt-patch.d.ts +11 -0
  28. package/dist/core/prompt-patch.js +16 -0
  29. package/dist/core/show-renderer.d.ts +10 -0
  30. package/dist/core/show-renderer.js +39 -0
  31. package/dist/core/show.d.ts +29 -0
  32. package/dist/core/show.js +288 -0
  33. package/dist/core/sub-agent-render.d.ts +56 -0
  34. package/dist/core/sub-agent-render.js +579 -0
  35. package/dist/core/tool-cost.d.ts +17 -0
  36. package/dist/core/tool-cost.js +58 -0
  37. package/dist/core/tui.d.ts +9 -0
  38. package/dist/core/tui.js +33 -0
  39. package/dist/editor-BljciPdx.js +537 -0
  40. package/dist/extensions/bash.d.ts +12 -0
  41. package/dist/extensions/bash.js +261 -0
  42. package/dist/extensions/code-review.d.ts +14 -0
  43. package/dist/extensions/code-review.js +175 -0
  44. package/dist/extensions/command-palette.d.ts +6 -0
  45. package/dist/extensions/command-palette.js +2 -0
  46. package/dist/extensions/create-file.d.ts +7 -0
  47. package/dist/extensions/create-file.js +88 -0
  48. package/dist/extensions/edit-file.d.ts +7 -0
  49. package/dist/extensions/edit-file.js +395 -0
  50. package/dist/extensions/editor.d.ts +6 -0
  51. package/dist/extensions/editor.js +2 -0
  52. package/dist/extensions/finder.d.ts +13 -0
  53. package/dist/extensions/finder.js +108 -0
  54. package/dist/extensions/format-file.d.ts +11 -0
  55. package/dist/extensions/format-file.js +145 -0
  56. package/dist/extensions/github.d.ts +13 -0
  57. package/dist/extensions/github.js +527 -0
  58. package/dist/extensions/glob.d.ts +10 -0
  59. package/dist/extensions/glob.js +163 -0
  60. package/dist/extensions/grep.d.ts +13 -0
  61. package/dist/extensions/grep.js +356 -0
  62. package/dist/extensions/handoff.d.ts +6 -0
  63. package/dist/extensions/handoff.js +277 -0
  64. package/dist/extensions/librarian.d.ts +13 -0
  65. package/dist/extensions/librarian.js +113 -0
  66. package/dist/extensions/look-at.d.ts +13 -0
  67. package/dist/extensions/look-at.js +121 -0
  68. package/dist/extensions/ls.d.ts +8 -0
  69. package/dist/extensions/ls.js +87 -0
  70. package/dist/extensions/mermaid.d.ts +20 -0
  71. package/dist/extensions/mermaid.js +2 -0
  72. package/dist/extensions/oracle.d.ts +13 -0
  73. package/dist/extensions/oracle.js +143 -0
  74. package/dist/extensions/read-session.d.ts +13 -0
  75. package/dist/extensions/read-session.js +265 -0
  76. package/dist/extensions/read-web-page.d.ts +11 -0
  77. package/dist/extensions/read-web-page.js +234 -0
  78. package/dist/extensions/read.d.ts +23 -0
  79. package/dist/extensions/read.js +323 -0
  80. package/dist/extensions/search-sessions.d.ts +29 -0
  81. package/dist/extensions/search-sessions.js +426 -0
  82. package/dist/extensions/session-name.d.ts +6 -0
  83. package/dist/extensions/session-name.js +54 -0
  84. package/dist/extensions/skill.d.ts +7 -0
  85. package/dist/extensions/skill.js +232 -0
  86. package/dist/extensions/system-prompt.d.ts +6 -0
  87. package/dist/extensions/system-prompt.js +31 -0
  88. package/dist/extensions/task.d.ts +11 -0
  89. package/dist/extensions/task.js +114 -0
  90. package/dist/extensions/tool-harness.d.ts +6 -0
  91. package/dist/extensions/tool-harness.js +37 -0
  92. package/dist/extensions/undo-edit.d.ts +7 -0
  93. package/dist/extensions/undo-edit.js +127 -0
  94. package/dist/extensions/web-search.d.ts +12 -0
  95. package/dist/extensions/web-search.js +254 -0
  96. package/dist/extensions.d.ts +29 -0
  97. package/dist/extensions.js +29 -0
  98. package/dist/mermaid-C7xCVBrw.js +95296 -0
  99. package/package.json +67 -0
@@ -0,0 +1,426 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed } from "../core/box-format.js";
3
+ import { getExtensionConfig } from "../core/config.js";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ import { Text } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import * as os from "node:os";
10
+ //#region packages/extensions/search-sessions/index.ts
11
+ /**
12
+ * search_sessions tool — find session branches by keyword, file, or date.
13
+ *
14
+ * a session is a tree of entries (messages, tool calls, branches).
15
+ * the searchable unit is a BRANCH — a root-to-leaf path through the tree.
16
+ * each branch has its own files-touched set, message chain, and timestamp range.
17
+ *
18
+ * pipeline: glob session files → optional rg keyword pre-filter → parse JSONL →
19
+ * enumerate branches → filter by params → return sorted results.
20
+ */
21
+ const CONFIG_DEFAULTS = {
22
+ maxResults: 50,
23
+ sessionsDir: path.join(os.homedir(), ".pi", "agent", "sessions"),
24
+ rgTimeoutMs: 1e4
25
+ };
26
+ /** per-block excerpts for collapsed display — first 5 visual lines */
27
+ const COLLAPSED_EXCERPTS = [{
28
+ focus: "head",
29
+ context: 5
30
+ }];
31
+ function isTextContent(part) {
32
+ return part.type === "text" && typeof part.text === "string";
33
+ }
34
+ /** tool argument keys that contain file paths */
35
+ const PATH_KEYS = [
36
+ "path",
37
+ "filePath",
38
+ "file_path"
39
+ ];
40
+ function extractFilePaths(args) {
41
+ if (!args) return [];
42
+ const paths = [];
43
+ for (const key of PATH_KEYS) if (typeof args[key] === "string") paths.push(args[key]);
44
+ return paths;
45
+ }
46
+ /** extract file paths from free text — @-mentions and absolute paths */
47
+ function extractFilePathsFromText(text) {
48
+ const paths = [];
49
+ for (const m of text.matchAll(/@([\w./-]+\/[\w./-]+)/g)) if (m[1]) paths.push(m[1]);
50
+ for (const m of text.matchAll(/(?:^|\s)(\/[\w./-]+)/gm)) if (m[1]) paths.push(m[1]);
51
+ return paths;
52
+ }
53
+ function parseSessionFile(filePath) {
54
+ let raw;
55
+ try {
56
+ raw = fs.readFileSync(filePath, "utf-8");
57
+ } catch {
58
+ return {
59
+ header: null,
60
+ entries: [],
61
+ sessionName: ""
62
+ };
63
+ }
64
+ const lines = raw.split("\n").filter((l) => l.trim());
65
+ let header = null;
66
+ const entries = [];
67
+ let sessionName = "";
68
+ for (const line of lines) try {
69
+ const entry = JSON.parse(line);
70
+ if (entry.type === "session") header = entry;
71
+ else if (entry.type === "session_info" && entry.name) sessionName = entry.name;
72
+ if (entry.id) entries.push(entry);
73
+ } catch {}
74
+ return {
75
+ header,
76
+ entries,
77
+ sessionName
78
+ };
79
+ }
80
+ function enumerateBranches(header, entries, sessionName, filePath) {
81
+ const parentSessionPath = header.parentSession;
82
+ const children = /* @__PURE__ */ new Map();
83
+ for (const e of entries) {
84
+ const parent = e.parentId ?? null;
85
+ if (!children.has(parent)) children.set(parent, []);
86
+ children.get(parent).push(e);
87
+ }
88
+ const hasChildren = /* @__PURE__ */ new Set();
89
+ for (const e of entries) if (e.parentId) hasChildren.add(e.parentId);
90
+ const leaves = entries.filter((e) => !hasChildren.has(e.id));
91
+ const byId = /* @__PURE__ */ new Map();
92
+ for (const e of entries) byId.set(e.id, e);
93
+ const branches = [];
94
+ for (const leaf of leaves) {
95
+ if (leaf.type === "session" || leaf.type === "model_change" || leaf.type === "thinking_level_change") {
96
+ let hasMsgs = false;
97
+ let cur = leaf;
98
+ while (cur) {
99
+ if (cur.type === "message") {
100
+ hasMsgs = true;
101
+ break;
102
+ }
103
+ cur = cur.parentId ? byId.get(cur.parentId) : void 0;
104
+ }
105
+ if (!hasMsgs) continue;
106
+ }
107
+ const chain = [];
108
+ let current = leaf;
109
+ while (current) {
110
+ chain.unshift(current);
111
+ current = current.parentId ? byId.get(current.parentId) : void 0;
112
+ }
113
+ const files = /* @__PURE__ */ new Set();
114
+ const models = /* @__PURE__ */ new Set();
115
+ const textChunks = [];
116
+ let messageCount = 0;
117
+ let firstUserMessage = "";
118
+ for (const entry of chain) {
119
+ if (entry.type === "model_change" && typeof entry.modelId === "string") models.add(entry.modelId);
120
+ if (entry.type === "message") {
121
+ const msg = entry.message;
122
+ if (!msg) continue;
123
+ messageCount++;
124
+ if (msg.role === "user") {
125
+ for (const part of msg.content || []) if (isTextContent(part) && part.text) {
126
+ if (!firstUserMessage) firstUserMessage = part.text.slice(0, 200);
127
+ textChunks.push(part.text);
128
+ for (const p of extractFilePathsFromText(part.text)) files.add(p);
129
+ }
130
+ }
131
+ if (msg.role === "assistant" && Array.isArray(msg.content)) for (const part of msg.content) {
132
+ if (isTextContent(part) && part.text) textChunks.push(part.text);
133
+ if (part.type === "toolCall" && part.arguments) for (const p of extractFilePaths(part.arguments)) files.add(p);
134
+ }
135
+ }
136
+ }
137
+ const timestamps = chain.map((e) => e.timestamp).filter(Boolean).sort();
138
+ branches.push({
139
+ sessionId: header.id,
140
+ sessionName,
141
+ leafId: leaf.id,
142
+ workspace: header.cwd,
143
+ filePath,
144
+ timestampStart: timestamps[0] || header.timestamp,
145
+ timestampEnd: timestamps[timestamps.length - 1] || header.timestamp,
146
+ filesTouched: [...files],
147
+ models: [...models],
148
+ messageCount,
149
+ firstUserMessage,
150
+ searchableText: textChunks.join("\n"),
151
+ parentSessionPath
152
+ });
153
+ }
154
+ return branches;
155
+ }
156
+ function matchesKeyword(branch, keyword) {
157
+ const lower = keyword.toLowerCase();
158
+ if (branch.sessionName.toLowerCase().includes(lower)) return true;
159
+ if (branch.searchableText.toLowerCase().includes(lower)) return true;
160
+ return false;
161
+ }
162
+ function matchesFile(branch, fileQuery) {
163
+ const lower = fileQuery.toLowerCase();
164
+ return branch.filesTouched.some((f) => f.toLowerCase().includes(lower));
165
+ }
166
+ function parseDate(dateStr) {
167
+ const relMatch = dateStr.match(/^(\d+)([dw])$/);
168
+ if (relMatch && relMatch[1] && relMatch[2]) {
169
+ const n = parseInt(relMatch[1], 10);
170
+ const unit = relMatch[2];
171
+ const now = /* @__PURE__ */ new Date();
172
+ if (unit === "d") now.setDate(now.getDate() - n);
173
+ else if (unit === "w") now.setDate(now.getDate() - n * 7);
174
+ return now;
175
+ }
176
+ const d = new Date(dateStr);
177
+ return isNaN(d.getTime()) ? null : d;
178
+ }
179
+ function matchesDateRange(branch, after, before) {
180
+ const branchEnd = new Date(branch.timestampEnd);
181
+ const branchStart = new Date(branch.timestampStart);
182
+ if (after) {
183
+ const afterDate = parseDate(after);
184
+ if (afterDate && branchEnd < afterDate) return false;
185
+ }
186
+ if (before) {
187
+ const beforeDate = parseDate(before);
188
+ if (beforeDate && branchStart > beforeDate) return false;
189
+ }
190
+ return true;
191
+ }
192
+ function rgFilterFiles(keyword, sessionsDir, timeoutMs) {
193
+ try {
194
+ const result = execSync(`rg -l -i ${JSON.stringify(keyword)} ${JSON.stringify(sessionsDir)}`, {
195
+ stdio: [
196
+ "ignore",
197
+ "pipe",
198
+ "ignore"
199
+ ],
200
+ timeout: timeoutMs
201
+ }).toString().trim();
202
+ if (!result) return /* @__PURE__ */ new Set();
203
+ return new Set(result.split("\n").filter(Boolean));
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ function formatBranchResults(branches) {
209
+ if (branches.length === 0) return {
210
+ text: "(no matching sessions found)",
211
+ headerLineIndices: []
212
+ };
213
+ const lines = [];
214
+ const headerLineIndices = [];
215
+ lines.push(`found ${branches.length} matching branch${branches.length !== 1 ? "es" : ""}:`);
216
+ lines.push("");
217
+ for (let i = 0; i < branches.length; i++) {
218
+ const b = branches[i];
219
+ if (!b) continue;
220
+ const dateStr = new Date(b.timestampEnd).toLocaleDateString("en-US", {
221
+ weekday: "short",
222
+ year: "numeric",
223
+ month: "short",
224
+ day: "numeric"
225
+ });
226
+ const timeStr = new Date(b.timestampEnd).toLocaleTimeString("en-US", {
227
+ hour: "2-digit",
228
+ minute: "2-digit"
229
+ });
230
+ headerLineIndices.push(lines.length);
231
+ lines.push(`### ${i + 1}. ${b.sessionName || "(unnamed)"}`);
232
+ lines.push(`session: ${b.sessionId} / branch: ${b.leafId}`);
233
+ if (b.parentSessionPath) {
234
+ const parentId = b.parentSessionPath.split("/").pop()?.replace(/\.jsonl$/, "")?.split("_")[1] || b.parentSessionPath;
235
+ lines.push(`forked from: ${parentId}`);
236
+ }
237
+ lines.push(`${dateStr} ${timeStr} — ${b.messageCount} messages`);
238
+ if (b.models.length > 0) lines.push(`models: ${b.models.join(", ")}`);
239
+ if (b.filesTouched.length > 0) {
240
+ const shown = b.filesTouched.slice(0, 10);
241
+ lines.push(`files: ${shown.join(", ")}${b.filesTouched.length > 10 ? ` (+${b.filesTouched.length - 10} more)` : ""}`);
242
+ }
243
+ if (b.firstUserMessage) {
244
+ const preview = b.firstUserMessage.length > 150 ? `${b.firstUserMessage.slice(0, 150)}...` : b.firstUserMessage;
245
+ lines.push(`> ${preview}`);
246
+ }
247
+ if (i < branches.length - 1) lines.push("");
248
+ }
249
+ return {
250
+ text: lines.join("\n"),
251
+ headerLineIndices
252
+ };
253
+ }
254
+ /** convert branch results to BoxSections for box-format rendering */
255
+ function branchesToSections(branches) {
256
+ return branches.map((b, i) => {
257
+ const dateStr = new Date(b.timestampEnd).toLocaleDateString("en-US", {
258
+ weekday: "short",
259
+ year: "numeric",
260
+ month: "short",
261
+ day: "numeric"
262
+ });
263
+ const timeStr = new Date(b.timestampEnd).toLocaleTimeString("en-US", {
264
+ hour: "2-digit",
265
+ minute: "2-digit"
266
+ });
267
+ const lines = [];
268
+ lines.push({
269
+ text: `session: ${b.sessionId} / branch: ${b.leafId}`,
270
+ highlight: true
271
+ });
272
+ if (b.parentSessionPath) {
273
+ const parentId = b.parentSessionPath.split("/").pop()?.replace(/\.jsonl$/, "")?.split("_")[1] || b.parentSessionPath;
274
+ lines.push({
275
+ text: `forked from: ${parentId}`,
276
+ highlight: true
277
+ });
278
+ }
279
+ lines.push({
280
+ text: `${dateStr} ${timeStr} — ${b.messageCount} messages`,
281
+ highlight: true
282
+ });
283
+ if (b.models.length > 0) lines.push({
284
+ text: `models: ${b.models.join(", ")}`,
285
+ highlight: true
286
+ });
287
+ if (b.filesTouched.length > 0) {
288
+ const shown = b.filesTouched.slice(0, 10);
289
+ lines.push({
290
+ text: `files: ${shown.join(", ")}${b.filesTouched.length > 10 ? ` (+${b.filesTouched.length - 10} more)` : ""}`,
291
+ highlight: true
292
+ });
293
+ }
294
+ if (b.firstUserMessage) {
295
+ const preview = b.firstUserMessage.length > 150 ? `${b.firstUserMessage.slice(0, 150)}...` : b.firstUserMessage;
296
+ lines.push({
297
+ text: `> ${preview}`,
298
+ highlight: false
299
+ });
300
+ }
301
+ return {
302
+ header: b.sessionName || `session ${i + 1}`,
303
+ blocks: [{ lines }]
304
+ };
305
+ });
306
+ }
307
+ function createSearchSessionsTool(config = CONFIG_DEFAULTS) {
308
+ return {
309
+ name: "search_sessions",
310
+ label: "Search Sessions",
311
+ description: "Search pi session history by keyword, file path, or date range.\n\nSessions are trees with branches. Each branch (root-to-leaf path) is a separate search result with its own files-touched set and message chain.\n\nScoped to current workspace by default. Use `all_workspaces: true` to search everywhere.\n\nUse `read_session` to extract detailed content from a specific session.\n\nWHEN TO USE:\n- \"Find the session where I worked on X\"\n- \"What session touched this file?\"\n- \"Show recent sessions\"\n- Looking up prior context from past conversations\n\nWHEN NOT TO USE:\n- Current session context (already available)\n- Git history (use git log)",
312
+ parameters: Type.Object({
313
+ keyword: Type.Optional(Type.String({ description: "Text to search for in session names, user messages, and assistant responses." })),
314
+ file: Type.Optional(Type.String({ description: "File path (partial match) to find sessions that touched this file." })),
315
+ after: Type.Optional(Type.String({ description: "Only return sessions after this date. ISO date (2026-02-20) or relative (7d, 2w)." })),
316
+ before: Type.Optional(Type.String({ description: "Only return sessions before this date. ISO date or relative." })),
317
+ workspace: Type.Optional(Type.String({ description: "Filter by workspace path (partial match against session cwd). Defaults to current workspace." })),
318
+ all_workspaces: Type.Optional(Type.Boolean({ description: "Search across all workspaces instead of just the current one." }))
319
+ }),
320
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
321
+ const p = params;
322
+ if (!fs.existsSync(config.sessionsDir)) return { content: [{
323
+ type: "text",
324
+ text: "(no sessions directory found)"
325
+ }] };
326
+ let sessionFiles = [];
327
+ try {
328
+ const walkDir = (dir) => {
329
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
330
+ const full = path.join(dir, entry.name);
331
+ if (entry.isDirectory()) walkDir(full);
332
+ else if (entry.name.endsWith(".jsonl")) sessionFiles.push(full);
333
+ }
334
+ };
335
+ walkDir(config.sessionsDir);
336
+ } catch {
337
+ return {
338
+ content: [{
339
+ type: "text",
340
+ text: "(could not read sessions directory)"
341
+ }],
342
+ isError: true
343
+ };
344
+ }
345
+ if (sessionFiles.length === 0) return { content: [{
346
+ type: "text",
347
+ text: "(no sessions found)"
348
+ }] };
349
+ if (p.keyword) {
350
+ const matches = rgFilterFiles(p.keyword, config.sessionsDir, config.rgTimeoutMs);
351
+ if (matches !== null) sessionFiles = sessionFiles.filter((f) => matches.has(f));
352
+ }
353
+ if (p.after || p.before) sessionFiles = sessionFiles.filter((f) => {
354
+ const tsMatch = path.basename(f).match(/^(\d{4}-\d{2}-\d{2})T/);
355
+ if (!tsMatch || !tsMatch[1]) return true;
356
+ const fileDate = new Date(tsMatch[1]);
357
+ if (p.after) {
358
+ const afterDate = parseDate(p.after);
359
+ if (afterDate && fileDate < afterDate) return false;
360
+ }
361
+ if (p.before) {
362
+ const beforeDate = parseDate(p.before);
363
+ if (beforeDate && fileDate > beforeDate) return false;
364
+ }
365
+ return true;
366
+ });
367
+ const allBranches = [];
368
+ const workspaceFilter = p.all_workspaces ? void 0 : p.workspace || ctx.cwd;
369
+ for (const file of sessionFiles) {
370
+ const { header, entries, sessionName } = parseSessionFile(file);
371
+ if (!header) continue;
372
+ if (workspaceFilter && !header.cwd.toLowerCase().includes(workspaceFilter.toLowerCase())) continue;
373
+ const branches = enumerateBranches(header, entries, sessionName, file);
374
+ allBranches.push(...branches);
375
+ }
376
+ let filtered = allBranches;
377
+ if (p.keyword) filtered = filtered.filter((b) => matchesKeyword(b, p.keyword));
378
+ if (p.file) filtered = filtered.filter((b) => matchesFile(b, p.file));
379
+ if (p.after || p.before) filtered = filtered.filter((b) => matchesDateRange(b, p.after, p.before));
380
+ filtered.sort((a, b) => new Date(b.timestampEnd).getTime() - new Date(a.timestampEnd).getTime());
381
+ const shown = filtered.length > config.maxResults ? [...filtered.slice(0, Math.floor(config.maxResults / 2)), ...filtered.slice(-Math.floor(config.maxResults / 2))] : filtered;
382
+ const truncated = filtered.length > config.maxResults ? filtered.length - shown.length : 0;
383
+ const { text: output } = formatBranchResults(shown);
384
+ const resultSections = branchesToSections(shown);
385
+ return {
386
+ content: [{
387
+ type: "text",
388
+ text: output
389
+ }],
390
+ details: {
391
+ resultSections,
392
+ truncated
393
+ }
394
+ };
395
+ },
396
+ renderCall(args, theme) {
397
+ const parts = [];
398
+ if (args.keyword) parts.push(args.keyword);
399
+ if (args.file) parts.push(`file:${args.file}`);
400
+ if (args.after) parts.push(`after:${args.after}`);
401
+ if (args.before) parts.push(`before:${args.before}`);
402
+ if (args.workspace) parts.push(`ws:${args.workspace}`);
403
+ const preview = parts.join(" ") || "...";
404
+ return new Text(theme.fg("toolTitle", theme.bold("search_sessions ")) + theme.fg("dim", preview), 0, 0);
405
+ },
406
+ renderResult(result, { expanded }, _theme) {
407
+ const sections = result.details?.resultSections;
408
+ if (!sections?.length) return new Text(result.content?.[0]?.text ?? "(no output)", 0, 0);
409
+ const truncated = result.details?.truncated ?? 0;
410
+ const notices = truncated > 0 ? [`${truncated} sessions omitted`] : void 0;
411
+ return boxRendererWindowed(() => sections, {
412
+ collapsed: {
413
+ maxSections: 3,
414
+ excerpts: COLLAPSED_EXCERPTS
415
+ },
416
+ expanded: {}
417
+ }, notices, expanded);
418
+ }
419
+ };
420
+ }
421
+ function search_sessions_default(pi) {
422
+ const cfg = getExtensionConfig("@bds_pi/search-sessions", CONFIG_DEFAULTS);
423
+ pi.registerTool(withPromptPatch(createSearchSessionsTool(cfg)));
424
+ }
425
+ //#endregion
426
+ export { createSearchSessionsTool, search_sessions_default as default };
@@ -0,0 +1,6 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/session-name/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): void;
5
+ //#endregion
6
+ export { export_default as default };
@@ -0,0 +1,54 @@
1
+ import { getExtensionConfig } from "../core/config.js";
2
+ import { complete } from "@mariozechner/pi-ai";
3
+ //#region packages/extensions/session-name/index.ts
4
+ /**
5
+ * auto session naming — generates a short title from the first user message.
6
+ *
7
+ * fires on the `input` event so the name appears while the agent is still
8
+ * thinking. uses haiku for speed/cost. only names the session once.
9
+ */
10
+ const CONFIG_DEFAULTS = { model: {
11
+ provider: "openrouter",
12
+ id: "anthropic/claude-haiku-4.5"
13
+ } };
14
+ function session_name_default(pi) {
15
+ const cfg = getExtensionConfig("@bds_pi/session-name", CONFIG_DEFAULTS);
16
+ let named = false;
17
+ pi.on("input", async (event, ctx) => {
18
+ if (named) return;
19
+ if (pi.getSessionName()) {
20
+ named = true;
21
+ return;
22
+ }
23
+ const text = event.text.trim();
24
+ if (text.startsWith("/") || text.length < 10) return;
25
+ named = true;
26
+ const model = ctx.modelRegistry.find(cfg.model.provider, cfg.model.id) ?? ctx.model;
27
+ if (!model) return;
28
+ generateName(model, ctx.modelRegistry, text).then((name) => {
29
+ if (name) pi.setSessionName(name);
30
+ }).catch(() => {});
31
+ });
32
+ pi.on("session_switch", async () => {
33
+ named = false;
34
+ });
35
+ }
36
+ async function generateName(model, registry, userMessage) {
37
+ const apiKey = await registry.getApiKey(model);
38
+ if (!apiKey) return null;
39
+ const response = await complete(model, { messages: [{
40
+ role: "user",
41
+ content: [{
42
+ type: "text",
43
+ text: `Generate a 3-5 word title for a coding session that starts with this message. Return ONLY the title, no quotes, no punctuation, no explanation. Lowercase.\n\n${userMessage.slice(0, 500)}`
44
+ }],
45
+ timestamp: Date.now()
46
+ }] }, {
47
+ apiKey,
48
+ maxTokens: 20
49
+ });
50
+ if (response.stopReason === "aborted") return null;
51
+ return response.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim() || null;
52
+ }
53
+ //#endregion
54
+ export { session_name_default as default };
@@ -0,0 +1,7 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/skill/index.d.ts
4
+ declare function createSkillTool(): ToolDefinition;
5
+ declare function export_default(pi: ExtensionAPI): void;
6
+ //#endregion
7
+ export { createSkillTool, export_default as default };