pi-sessions 0.2.1 → 0.3.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 (30) hide show
  1. package/README.md +2 -1
  2. package/extensions/session-ask.ts +4 -4
  3. package/extensions/session-auto-title/command.ts +2 -2
  4. package/extensions/session-auto-title/context.ts +1 -1
  5. package/extensions/session-auto-title/controller.ts +3 -16
  6. package/extensions/session-auto-title/generate.ts +256 -0
  7. package/extensions/session-auto-title/model.ts +2 -2
  8. package/extensions/session-auto-title/retitle.ts +41 -377
  9. package/extensions/session-auto-title/state.ts +2 -2
  10. package/extensions/session-auto-title/wizard.ts +12 -16
  11. package/extensions/session-auto-title.ts +22 -10
  12. package/extensions/session-handoff/extract.ts +33 -16
  13. package/extensions/session-handoff/metadata.ts +7 -2
  14. package/extensions/session-handoff/picker.ts +2 -2
  15. package/extensions/session-handoff/review.ts +3 -3
  16. package/extensions/session-handoff/spawn.ts +27 -6
  17. package/extensions/session-handoff.ts +54 -17
  18. package/extensions/session-hooks.ts +1 -1
  19. package/extensions/session-index.ts +2 -2
  20. package/extensions/session-search/extract.ts +4 -4
  21. package/extensions/session-search/hooks.ts +2 -2
  22. package/extensions/session-search/reindex.ts +1 -1
  23. package/extensions/session-search.ts +3 -3
  24. package/extensions/shared/session-index/common.ts +1 -1
  25. package/extensions/shared/session-index/lineage.ts +1 -1
  26. package/extensions/shared/session-index/search.ts +1 -1
  27. package/extensions/shared/settings.ts +12 -3
  28. package/extensions/shared/typebox.ts +4 -4
  29. package/package.json +18 -13
  30. package/extensions/session-auto-title/prompt.ts +0 -54
package/README.md CHANGED
@@ -163,7 +163,8 @@ To change auto-titling settings, edit `~/.pi/agent/settings.json`:
163
163
  "sessions": {
164
164
  "autoTitle": {
165
165
  "refreshTurns": 4,
166
- "model": "anthropic/claude-haiku-4-5"
166
+ "model": "anthropic/claude-haiku-4-5",
167
+ "prompt": "Custom prompt that overrides the default."
167
168
  }
168
169
  }
169
170
  }
@@ -1,7 +1,7 @@
1
- import { complete, type Message } from "@mariozechner/pi-ai";
2
- import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
3
- import { Text } from "@mariozechner/pi-tui";
4
- import { Type } from "@sinclair/typebox";
1
+ import { complete, type Message } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { Type } from "typebox";
5
5
  import { type RenderedSessionTree, renderSessionTreeMarkdown } from "./session-search/extract.js";
6
6
  import {
7
7
  getIndexStatus,
@@ -1,5 +1,5 @@
1
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import type { AutocompleteItem } from "@mariozechner/pi-tui";
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
3
 
4
4
  export const TITLE_USAGE = "Usage: /title [this|folder|pi] [-f]";
5
5
 
@@ -3,7 +3,7 @@ import {
3
3
  convertToLlm,
4
4
  type SessionEntry,
5
5
  serializeConversation,
6
- } from "@mariozechner/pi-coding-agent";
6
+ } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  export interface AutoTitleContext {
9
9
  cwd: string | undefined;
@@ -1,5 +1,6 @@
1
- import { buildSessionContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import { buildSessionContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutoTitleSettings } from "../shared/settings.js";
3
+ import type { AutoTitleFailure } from "./generate.js";
3
4
  import {
4
5
  type AutoTitleMode,
5
6
  type AutoTitlePersistedState,
@@ -8,20 +9,6 @@ import {
8
9
  getLatestAutoTitleState,
9
10
  } from "./state.js";
10
11
 
11
- export interface AutoTitleFailure {
12
- at: string;
13
- trigger: AutoTitleTrigger;
14
- model: string;
15
- message: string;
16
- status?: number;
17
- }
18
-
19
- export function formatAutoTitleFailureSummary(failure: AutoTitleFailure): string {
20
- return failure.status === undefined
21
- ? failure.message
22
- : `HTTP ${failure.status} · ${failure.message}`;
23
- }
24
-
25
12
  export interface SessionAutoTitleStateSnapshot {
26
13
  currentSessionFile: string | undefined;
27
14
  mode: AutoTitleMode;
@@ -294,5 +281,5 @@ function countUserTurns(messages: Array<{ role: string }>): number {
294
281
  }
295
282
 
296
283
  function formatFailureKey(failure: AutoTitleFailure): string {
297
- return `${failure.model}\n${failure.status ?? ""}\n${failure.message}`;
284
+ return `${failure.model}\n${failure.message}`;
298
285
  }
@@ -0,0 +1,256 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ type Api,
6
+ completeSimple,
7
+ type Model,
8
+ type TextContent,
9
+ type UserMessage,
10
+ } from "@earendil-works/pi-ai";
11
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import type { AutoTitleContext } from "./context.js";
13
+ import type { AutoTitleTrigger } from "./state.js";
14
+
15
+ const AUTO_TITLE_REQUEST_TIMEOUT_MS = 15_000;
16
+ const AUTO_TITLE_MAX_TOKENS = 64;
17
+ const AUTO_TITLE_CHAR_MAX = 80;
18
+
19
+ export interface AutoTitleFailure {
20
+ at: string;
21
+ trigger: AutoTitleTrigger;
22
+ model: string;
23
+ message: string;
24
+ }
25
+
26
+ export type AutoTitleGenerationResult =
27
+ | {
28
+ ok: true;
29
+ title: string;
30
+ }
31
+ | {
32
+ ok: false;
33
+ failure: AutoTitleFailure;
34
+ };
35
+
36
+ export async function generateAutoTitle(
37
+ ctx: ExtensionContext,
38
+ model: Model<Api>,
39
+ context: AutoTitleContext,
40
+ trigger: AutoTitleTrigger,
41
+ systemPrompt: string,
42
+ ): Promise<AutoTitleGenerationResult> {
43
+ if (!context.conversationText) {
44
+ return {
45
+ ok: false,
46
+ failure: createAutoTitleFailure(
47
+ trigger,
48
+ model,
49
+ "No conversation available for auto-title generation.",
50
+ ),
51
+ };
52
+ }
53
+
54
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
55
+ if (!auth.ok) {
56
+ return {
57
+ ok: false,
58
+ failure: createAutoTitleFailure(trigger, model, "Failed to authenticate auto-title model."),
59
+ };
60
+ }
61
+
62
+ const shouldPreserveTitle = trigger === "periodic" && Boolean(context.currentTitle?.trim());
63
+ const resolvedSystemPrompt = buildAutoTitleSystemPrompt(systemPrompt, shouldPreserveTitle);
64
+ const userPrompt = buildAutoTitlePrompt(context, resolvedSystemPrompt, shouldPreserveTitle);
65
+ const message: UserMessage = {
66
+ role: "user",
67
+ content: [{ type: "text", text: userPrompt }],
68
+ timestamp: Date.now(),
69
+ };
70
+
71
+ const abortController = new AbortController();
72
+ const timeoutId = setTimeout(() => abortController.abort(), AUTO_TITLE_REQUEST_TIMEOUT_MS);
73
+
74
+ try {
75
+ const requestContext = {
76
+ systemPrompt: resolvedSystemPrompt,
77
+ messages: [message],
78
+ };
79
+ writeAutoTitleDebugRequest(model, trigger, requestContext);
80
+ const response = await completeSimple(model, requestContext, {
81
+ ...(auth.apiKey && { apiKey: auth.apiKey }),
82
+ ...(auth.headers && { headers: auth.headers }),
83
+ maxTokens: AUTO_TITLE_MAX_TOKENS,
84
+ signal: abortController.signal,
85
+ });
86
+ writeAutoTitleDebugResponse(model, trigger, response);
87
+
88
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
89
+ const fallbackMessage =
90
+ response.stopReason === "aborted" ? "Request was aborted." : "Provider returned an error.";
91
+ return {
92
+ ok: false,
93
+ failure: createAutoTitleFailure(trigger, model, response.errorMessage || fallbackMessage),
94
+ };
95
+ }
96
+
97
+ const normalizedTitle = normalizeGeneratedAutoTitle(extractResponseText(response.content));
98
+ if (!normalizedTitle) {
99
+ return {
100
+ ok: false,
101
+ failure: createAutoTitleFailure(trigger, model, "Model returned an empty title."),
102
+ };
103
+ }
104
+
105
+ return {
106
+ ok: true,
107
+ title: normalizedTitle,
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ ok: false,
112
+ failure: createAutoTitleFailure(trigger, model, extractErrorMessage(error)),
113
+ };
114
+ } finally {
115
+ clearTimeout(timeoutId);
116
+ }
117
+ }
118
+
119
+ export function createAutoTitleFailure(
120
+ trigger: AutoTitleTrigger,
121
+ model: Model<Api> | undefined,
122
+ message: string,
123
+ ): AutoTitleFailure {
124
+ return {
125
+ at: new Date().toISOString(),
126
+ trigger,
127
+ model: formatModelLabel(model),
128
+ message,
129
+ };
130
+ }
131
+
132
+ function buildAutoTitleSystemPrompt(systemPrompt: string, shouldPreserveTitle: boolean): string {
133
+ if (!shouldPreserveTitle) {
134
+ return systemPrompt;
135
+ }
136
+
137
+ return `${systemPrompt}\n\nPreserve the current title unless the conversation has meaningfully shifted.`;
138
+ }
139
+
140
+ function buildAutoTitlePrompt(
141
+ context: AutoTitleContext,
142
+ titleInstructions: string,
143
+ shouldPreserveTitle: boolean,
144
+ ): string {
145
+ const sections = ["Generate the title from this session context.", "<session_context>"];
146
+
147
+ if (context.cwd) {
148
+ sections.push(`<cwd>${context.cwd}</cwd>`);
149
+ }
150
+
151
+ sections.push(`<counts>\n${formatCounts(context)}\n</counts>`);
152
+
153
+ if (shouldPreserveTitle) {
154
+ sections.push(`<current_title>${context.currentTitle ?? ""}</current_title>`);
155
+ }
156
+
157
+ sections.push(`<conversation>\n${context.conversationText || "(none)"}\n</conversation>`);
158
+ sections.push("</session_context>");
159
+ sections.push(`<title_instructions>\n${titleInstructions}\n</title_instructions>`);
160
+
161
+ return sections.join("\n\n");
162
+ }
163
+
164
+ function normalizeGeneratedAutoTitle(value: string): string | undefined {
165
+ const withoutQuotes = value.trim().replace(/^["'`]+|["'`]+$/g, "");
166
+ const collapsed = withoutQuotes
167
+ .replace(/\s+/g, " ")
168
+ .replace(/[.!?]+$/g, "")
169
+ .trim();
170
+ if (!collapsed) {
171
+ return undefined;
172
+ }
173
+
174
+ const truncated = collapsed.slice(0, AUTO_TITLE_CHAR_MAX).trim();
175
+ return truncated || undefined;
176
+ }
177
+
178
+ function writeAutoTitleDebugRequest(
179
+ model: Model<Api>,
180
+ trigger: AutoTitleTrigger,
181
+ request: unknown,
182
+ ): void {
183
+ writeAutoTitleDebugEntry({
184
+ phase: "request",
185
+ trigger,
186
+ model: formatModelLabel(model),
187
+ request,
188
+ });
189
+ }
190
+
191
+ function writeAutoTitleDebugResponse(
192
+ model: Model<Api>,
193
+ trigger: AutoTitleTrigger,
194
+ response: unknown,
195
+ ): void {
196
+ writeAutoTitleDebugEntry({
197
+ phase: "response",
198
+ trigger,
199
+ model: formatModelLabel(model),
200
+ response,
201
+ });
202
+ }
203
+
204
+ function writeAutoTitleDebugEntry(entry: Record<string, unknown>): void {
205
+ try {
206
+ const dir = join(homedir(), ".pi", "agent", "pi-sessions");
207
+ mkdirSync(dir, { recursive: true });
208
+ appendFileSync(
209
+ join(dir, "auto-title-debug.jsonl"),
210
+ `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
211
+ );
212
+ } catch {
213
+ // Debug logging must not affect title generation.
214
+ }
215
+ }
216
+
217
+ function extractResponseText(content: unknown[]): string {
218
+ return content
219
+ .filter(
220
+ (part): part is TextContent =>
221
+ isObject(part) && part.type === "text" && typeof part.text === "string",
222
+ )
223
+ .map((part) => part.text)
224
+ .join("\n");
225
+ }
226
+
227
+ function formatCounts(context: AutoTitleContext): string {
228
+ return [
229
+ `user_turns: ${context.userTurnCount}`,
230
+ `assistant_turns: ${context.assistantTurnCount}`,
231
+ ].join("\n");
232
+ }
233
+
234
+ function formatModelLabel(model: Model<Api> | undefined): string {
235
+ if (!model) {
236
+ return "(no model resolved)";
237
+ }
238
+
239
+ return `${model.provider}/${model.id}`;
240
+ }
241
+
242
+ function extractErrorMessage(error: unknown): string {
243
+ if (error instanceof Error && error.message.trim()) {
244
+ return error.message.trim();
245
+ }
246
+
247
+ if (typeof error === "string" && error.trim()) {
248
+ return error.trim();
249
+ }
250
+
251
+ return "Unknown provider error.";
252
+ }
253
+
254
+ function isObject(value: unknown): value is Record<string, unknown> {
255
+ return typeof value === "object" && value !== null;
256
+ }
@@ -1,5 +1,5 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
2
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { ModelReference } from "../shared/settings.js";
4
4
 
5
5
  const DEFAULT_AUTO_TITLE_FALLBACK_MODELS: readonly ModelReference[] = [