pi-sessions 0.1.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/extensions/session-ask.ts +297 -0
  4. package/extensions/session-auto-title/command.ts +109 -0
  5. package/extensions/session-auto-title/context.ts +41 -0
  6. package/extensions/session-auto-title/controller.ts +298 -0
  7. package/extensions/session-auto-title/model.ts +61 -0
  8. package/extensions/session-auto-title/prompt.ts +54 -0
  9. package/extensions/session-auto-title/retitle.ts +641 -0
  10. package/extensions/session-auto-title/state.ts +69 -0
  11. package/extensions/session-auto-title/wizard.ts +583 -0
  12. package/extensions/session-auto-title.ts +209 -0
  13. package/extensions/session-handoff/extract.ts +278 -0
  14. package/extensions/session-handoff/metadata.ts +96 -0
  15. package/extensions/session-handoff/picker.ts +394 -0
  16. package/extensions/session-handoff/query.ts +318 -0
  17. package/extensions/session-handoff/refs.ts +151 -0
  18. package/extensions/session-handoff/review.ts +268 -0
  19. package/extensions/session-handoff.ts +203 -0
  20. package/extensions/session-hooks.ts +55 -0
  21. package/extensions/session-index.ts +159 -0
  22. package/extensions/session-search/extract.ts +997 -0
  23. package/extensions/session-search/hooks.ts +350 -0
  24. package/extensions/session-search/normalize.ts +170 -0
  25. package/extensions/session-search/reindex.ts +93 -0
  26. package/extensions/session-search.ts +390 -0
  27. package/extensions/shared/search-snippet.ts +40 -0
  28. package/extensions/shared/session-index/common.ts +222 -0
  29. package/extensions/shared/session-index/index.ts +5 -0
  30. package/extensions/shared/session-index/lineage.ts +417 -0
  31. package/extensions/shared/session-index/schema.ts +178 -0
  32. package/extensions/shared/session-index/search.ts +688 -0
  33. package/extensions/shared/session-index/store.ts +173 -0
  34. package/extensions/shared/session-ui.ts +15 -0
  35. package/extensions/shared/settings.ts +141 -0
  36. package/extensions/shared/time.ts +38 -0
  37. package/extensions/shared/typebox.ts +61 -0
  38. package/images/handoff.png +0 -0
  39. package/images/session-title.png +0 -0
  40. package/images/session_ask.png +0 -0
  41. package/images/session_picker.png +0 -0
  42. package/images/session_search.png +0 -0
  43. package/package.json +64 -0
@@ -0,0 +1,298 @@
1
+ import { buildSessionContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { AutoTitleSettings } from "../shared/settings.js";
3
+ import {
4
+ type AutoTitleMode,
5
+ type AutoTitlePersistedState,
6
+ type AutoTitleTrigger,
7
+ createAutoTitleState,
8
+ getLatestAutoTitleState,
9
+ } from "./state.js";
10
+
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
+ export interface SessionAutoTitleStateSnapshot {
26
+ currentSessionFile: string | undefined;
27
+ mode: AutoTitleMode;
28
+ lastAutoTitle: string | undefined;
29
+ lastAppliedUserTurnCount: number | undefined;
30
+ lastTrigger: AutoTitleTrigger | undefined;
31
+ lastFailure: AutoTitleFailure | undefined;
32
+ }
33
+
34
+ export interface AutoTitleTriggerPlan {
35
+ reason: AutoTitleTrigger;
36
+ userTurnCount: number;
37
+ currentTitle: string | undefined;
38
+ }
39
+
40
+ export interface AutoTitleTurnEndResult {
41
+ plan: AutoTitleTriggerPlan | undefined;
42
+ persistedState: AutoTitlePersistedState | undefined;
43
+ }
44
+
45
+ export interface AutoRetitleStatus {
46
+ mode: AutoTitleMode;
47
+ userTurnCount: number;
48
+ turnsUntilAutoRetitle: number | undefined;
49
+ }
50
+
51
+ export interface SessionAutoTitleController {
52
+ getState(): SessionAutoTitleStateSnapshot;
53
+ getAutoRetitleStatus(ctx: ExtensionContext): AutoRetitleStatus;
54
+ getLastFailure(ctx: ExtensionContext): AutoTitleFailure | undefined;
55
+ handleSessionStart(ctx: ExtensionContext): AutoTitlePersistedState | undefined;
56
+ handleSessionShutdown(): void;
57
+ handleTurnEnd(ctx: ExtensionContext): AutoTitleTurnEndResult;
58
+ handleManualRetitle(ctx: ExtensionContext): AutoTitleTriggerPlan | undefined;
59
+ handleTitleApplied(title: string, plan: AutoTitleTriggerPlan): AutoTitlePersistedState;
60
+ handleTitleFailed(ctx: ExtensionContext, failure: AutoTitleFailure): boolean;
61
+ }
62
+
63
+ interface ControllerState {
64
+ currentSessionFile: string | undefined;
65
+ persistedState: AutoTitlePersistedState;
66
+ lastFailure: AutoTitleFailure | undefined;
67
+ lastNotifiedFailureKey: string | undefined;
68
+ snapshot(): SessionAutoTitleStateSnapshot;
69
+ restore(ctx: ExtensionContext): void;
70
+ clear(): void;
71
+ ensureAttached(ctx: ExtensionContext): void;
72
+ }
73
+
74
+ export function createSessionAutoTitleController(
75
+ settings: AutoTitleSettings,
76
+ ): SessionAutoTitleController {
77
+ const state = createControllerState();
78
+
79
+ return {
80
+ getState() {
81
+ return state.snapshot();
82
+ },
83
+ getAutoRetitleStatus(ctx) {
84
+ state.ensureAttached(ctx);
85
+ pauseForManualOverrideIfNeeded(state, ctx);
86
+
87
+ const userTurnCount = countUserTurns(
88
+ buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId())
89
+ .messages,
90
+ );
91
+ if (state.persistedState.mode === "paused_manual") {
92
+ return {
93
+ mode: state.persistedState.mode,
94
+ userTurnCount,
95
+ turnsUntilAutoRetitle: undefined,
96
+ };
97
+ }
98
+
99
+ const status = resolveAutoRetitleStatus(
100
+ settings.refreshTurns,
101
+ userTurnCount,
102
+ ctx.sessionManager.getSessionName(),
103
+ state.persistedState,
104
+ );
105
+ return {
106
+ mode: state.persistedState.mode,
107
+ userTurnCount,
108
+ turnsUntilAutoRetitle: status.turnsUntilAutoRetitle,
109
+ };
110
+ },
111
+ getLastFailure(ctx) {
112
+ state.ensureAttached(ctx);
113
+ return state.lastFailure;
114
+ },
115
+ handleSessionStart(ctx) {
116
+ state.restore(ctx);
117
+ return pauseForManualOverrideIfNeeded(state, ctx);
118
+ },
119
+ handleSessionShutdown() {
120
+ state.clear();
121
+ },
122
+ handleTurnEnd(ctx) {
123
+ state.ensureAttached(ctx);
124
+ const persistedState = pauseForManualOverrideIfNeeded(state, ctx);
125
+ if (state.persistedState.mode === "paused_manual") {
126
+ return { plan: undefined, persistedState };
127
+ }
128
+
129
+ const sessionContext = buildSessionContext(
130
+ ctx.sessionManager.getEntries(),
131
+ ctx.sessionManager.getLeafId(),
132
+ );
133
+ const userTurnCount = countUserTurns(sessionContext.messages);
134
+ const status = resolveAutoRetitleStatus(
135
+ settings.refreshTurns,
136
+ userTurnCount,
137
+ ctx.sessionManager.getSessionName(),
138
+ state.persistedState,
139
+ );
140
+ if (!status.reason) {
141
+ return { plan: undefined, persistedState };
142
+ }
143
+
144
+ return {
145
+ plan: buildTriggerPlan(ctx, status.reason, userTurnCount),
146
+ persistedState,
147
+ };
148
+ },
149
+ handleManualRetitle(ctx) {
150
+ state.ensureAttached(ctx);
151
+ return buildTriggerPlan(
152
+ ctx,
153
+ "manual",
154
+ countUserTurns(
155
+ buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId())
156
+ .messages,
157
+ ),
158
+ );
159
+ },
160
+ handleTitleApplied(title, plan) {
161
+ state.lastFailure = undefined;
162
+ state.lastNotifiedFailureKey = undefined;
163
+ state.persistedState = createAutoTitleState({
164
+ mode: "active",
165
+ lastAutoTitle: title,
166
+ lastAppliedUserTurnCount: plan.userTurnCount,
167
+ lastTrigger: plan.reason,
168
+ });
169
+ return state.persistedState;
170
+ },
171
+ handleTitleFailed(ctx, failure) {
172
+ state.ensureAttached(ctx);
173
+ state.lastFailure = failure;
174
+
175
+ const failureKey = formatFailureKey(failure);
176
+ if (failureKey === state.lastNotifiedFailureKey) {
177
+ return false;
178
+ }
179
+
180
+ state.lastNotifiedFailureKey = failureKey;
181
+ return true;
182
+ },
183
+ };
184
+ }
185
+
186
+ function createControllerState(): ControllerState {
187
+ return {
188
+ currentSessionFile: undefined,
189
+ persistedState: createAutoTitleState(),
190
+ lastFailure: undefined,
191
+ lastNotifiedFailureKey: undefined,
192
+ snapshot() {
193
+ return {
194
+ currentSessionFile: this.currentSessionFile,
195
+ mode: this.persistedState.mode,
196
+ lastAutoTitle: this.persistedState.lastAutoTitle,
197
+ lastAppliedUserTurnCount: this.persistedState.lastAppliedUserTurnCount,
198
+ lastTrigger: this.persistedState.lastTrigger,
199
+ lastFailure: this.lastFailure,
200
+ };
201
+ },
202
+ restore(ctx) {
203
+ this.currentSessionFile = ctx.sessionManager.getSessionFile();
204
+ this.persistedState =
205
+ getLatestAutoTitleState(ctx.sessionManager.getBranch()) ?? createAutoTitleState();
206
+ this.lastFailure = undefined;
207
+ this.lastNotifiedFailureKey = undefined;
208
+ },
209
+ clear() {
210
+ this.currentSessionFile = undefined;
211
+ this.persistedState = createAutoTitleState();
212
+ this.lastFailure = undefined;
213
+ this.lastNotifiedFailureKey = undefined;
214
+ },
215
+ ensureAttached(ctx) {
216
+ if (ctx.sessionManager.getSessionFile() === this.currentSessionFile) {
217
+ return;
218
+ }
219
+
220
+ this.restore(ctx);
221
+ },
222
+ };
223
+ }
224
+
225
+ function pauseForManualOverrideIfNeeded(
226
+ state: ControllerState,
227
+ ctx: ExtensionContext,
228
+ ): AutoTitlePersistedState | undefined {
229
+ if (state.persistedState.mode === "paused_manual") {
230
+ return undefined;
231
+ }
232
+
233
+ const currentTitle = ctx.sessionManager.getSessionName();
234
+ const lastAutoTitle = state.persistedState.lastAutoTitle;
235
+ const titleMatchesOrBothEmpty =
236
+ lastAutoTitle === undefined ? !currentTitle : currentTitle === lastAutoTitle;
237
+ if (titleMatchesOrBothEmpty) {
238
+ return undefined;
239
+ }
240
+
241
+ state.persistedState = createAutoTitleState({
242
+ mode: "paused_manual",
243
+ ...(lastAutoTitle && { lastAutoTitle }),
244
+ ...(state.persistedState.lastAppliedUserTurnCount && {
245
+ lastAppliedUserTurnCount: state.persistedState.lastAppliedUserTurnCount,
246
+ }),
247
+ ...(state.persistedState.lastTrigger && { lastTrigger: state.persistedState.lastTrigger }),
248
+ });
249
+ return state.persistedState;
250
+ }
251
+
252
+ function resolveAutoRetitleStatus(
253
+ refreshTurns: number,
254
+ userTurnCount: number,
255
+ currentTitle: string | undefined,
256
+ persistedState: AutoTitlePersistedState,
257
+ ): { reason: AutoTitleTrigger | undefined; turnsUntilAutoRetitle: number } {
258
+ if (!currentTitle && !persistedState.lastAutoTitle && userTurnCount === 1) {
259
+ return {
260
+ reason: "initial",
261
+ turnsUntilAutoRetitle: 0,
262
+ };
263
+ }
264
+
265
+ const lastAppliedUserTurnCount = persistedState.lastAppliedUserTurnCount ?? 0;
266
+ const turnsSinceLastRetitle = userTurnCount - lastAppliedUserTurnCount;
267
+ if (turnsSinceLastRetitle < refreshTurns) {
268
+ return {
269
+ reason: undefined,
270
+ turnsUntilAutoRetitle: refreshTurns - turnsSinceLastRetitle,
271
+ };
272
+ }
273
+
274
+ return {
275
+ reason: "periodic",
276
+ turnsUntilAutoRetitle: 0,
277
+ };
278
+ }
279
+
280
+ function buildTriggerPlan(
281
+ ctx: ExtensionContext,
282
+ reason: AutoTitleTrigger,
283
+ userTurnCount: number,
284
+ ): AutoTitleTriggerPlan {
285
+ return {
286
+ reason,
287
+ userTurnCount,
288
+ currentTitle: ctx.sessionManager.getSessionName(),
289
+ };
290
+ }
291
+
292
+ function countUserTurns(messages: Array<{ role: string }>): number {
293
+ return messages.filter((message) => message.role === "user").length;
294
+ }
295
+
296
+ function formatFailureKey(failure: AutoTitleFailure): string {
297
+ return `${failure.model}\n${failure.status ?? ""}\n${failure.message}`;
298
+ }
@@ -0,0 +1,61 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import { ModelReference } from "../shared/settings.js";
4
+
5
+ const DEFAULT_AUTO_TITLE_FALLBACK_MODELS: readonly ModelReference[] = [
6
+ new ModelReference("google", "gemini-flash-lite-latest"),
7
+ new ModelReference("anthropic", "claude-haiku-4-5"),
8
+ new ModelReference("openai", "gpt-5.4-mini"),
9
+ ] as const;
10
+
11
+ export type AutoTitleModelSource = "configured" | "fallback" | "current";
12
+
13
+ export interface AutoTitleModelResolution {
14
+ model: Model<Api>;
15
+ source: AutoTitleModelSource;
16
+ }
17
+
18
+ export function resolveAutoTitleModel(
19
+ ctx: ExtensionContext,
20
+ configuredModel: ModelReference | undefined,
21
+ ): AutoTitleModelResolution | undefined {
22
+ const availableModels = ctx.modelRegistry.getAvailable();
23
+
24
+ if (configuredModel) {
25
+ const configuredMatch = findMatchingModel(availableModels, configuredModel);
26
+ if (configuredMatch) {
27
+ return {
28
+ model: configuredMatch,
29
+ source: "configured",
30
+ };
31
+ }
32
+ }
33
+
34
+ for (const fallbackReference of DEFAULT_AUTO_TITLE_FALLBACK_MODELS) {
35
+ const fallbackMatch = findMatchingModel(availableModels, fallbackReference);
36
+ if (fallbackMatch) {
37
+ return {
38
+ model: fallbackMatch,
39
+ source: "fallback",
40
+ };
41
+ }
42
+ }
43
+
44
+ if (!ctx.model) {
45
+ return undefined;
46
+ }
47
+
48
+ return {
49
+ model: ctx.model,
50
+ source: "current",
51
+ };
52
+ }
53
+
54
+ function findMatchingModel(
55
+ availableModels: Model<Api>[],
56
+ reference: ModelReference,
57
+ ): Model<Api> | undefined {
58
+ return availableModels.find(
59
+ (model) => model.provider === reference.provider && model.id === reference.modelId,
60
+ );
61
+ }
@@ -0,0 +1,54 @@
1
+ import type { AutoTitleContext } from "./context.js";
2
+ import type { AutoTitleTrigger } from "./state.js";
3
+
4
+ const AUTO_TITLE_CHAR_MAX = 120;
5
+
6
+ export const AUTO_TITLE_SYSTEM_PROMPT = `You generate concrete coding session titles from the conversation provided.
7
+
8
+ Return title text only.
9
+
10
+ Rules:
11
+ - Prefer 3-15 words.
12
+ - Maximum 120 characters.
13
+ - Use the full conversation, while letting the most recent work refine the title when needed.
14
+ - Describe the current task, bug, feature, or investigation on the active branch.
15
+ - Mention specific subsystem or file only when it improves clarity.
16
+ - No quotes, markdown, emojis, prefixes, or explanations.
17
+ - No trailing punctuation.
18
+ - Avoid generic titles like Coding help or Working on project.`;
19
+
20
+ export function buildAutoTitlePrompt(context: AutoTitleContext, trigger: AutoTitleTrigger): string {
21
+ const sections = [
22
+ ["## Trigger", trigger],
23
+ ["## Current Title", context.currentTitle ?? "(none)"],
24
+ ["## Counts", formatCounts(context)],
25
+ ["## Conversation", context.conversationText || "(none)"],
26
+ ];
27
+
28
+ if (context.cwd) {
29
+ sections.unshift(["## Cwd", context.cwd]);
30
+ }
31
+
32
+ return sections.map(([heading, body]) => `${heading}\n${body}`).join("\n\n");
33
+ }
34
+
35
+ export function normalizeGeneratedAutoTitle(value: string): string | undefined {
36
+ const withoutQuotes = value.trim().replace(/^["'`]+|["'`]+$/g, "");
37
+ const collapsed = withoutQuotes
38
+ .replace(/\s+/g, " ")
39
+ .replace(/[.!?]+$/g, "")
40
+ .trim();
41
+ if (!collapsed) {
42
+ return undefined;
43
+ }
44
+
45
+ const truncated = collapsed.slice(0, AUTO_TITLE_CHAR_MAX).trim();
46
+ return truncated || undefined;
47
+ }
48
+
49
+ function formatCounts(context: AutoTitleContext): string {
50
+ return [
51
+ `user_turns: ${context.userTurnCount}`,
52
+ `assistant_turns: ${context.assistantTurnCount}`,
53
+ ].join("\n");
54
+ }