@pi-archimedes/session-name 2.8.0 → 2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/session-name",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/danielcherubini/pi-archimedes.git"
@@ -18,17 +18,17 @@
18
18
  ".": "./src/index.ts"
19
19
  },
20
20
  "dependencies": {
21
- "@pi-archimedes/core": "2.8.0"
21
+ "@pi-archimedes/core": "2.9.0"
22
22
  },
23
23
  "peerDependencies": {
24
- "@earendil-works/pi-coding-agent": ">=0.1.0",
25
- "@earendil-works/pi-ai": ">=0.1.0",
26
- "@earendil-works/pi-tui": ">=0.1.0"
24
+ "@earendil-works/pi-coding-agent": ">=0.86.0",
25
+ "@earendil-works/pi-ai": ">=0.86.0",
26
+ "@earendil-works/pi-tui": ">=0.86.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@earendil-works/pi-ai": "^0.85.1",
30
- "@earendil-works/pi-coding-agent": "^0.85.1",
31
- "@earendil-works/pi-tui": "^0.85.1",
29
+ "@earendil-works/pi-ai": "^0.87.0",
30
+ "@earendil-works/pi-coding-agent": "^0.87.0",
31
+ "@earendil-works/pi-tui": "^0.87.0",
32
32
  "typescript": "^6.0.0"
33
33
  },
34
34
  "pi": {
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { generateTitle } from "./index.js";
3
+
4
+ function createMockPi(sessionName: string | undefined = undefined) {
5
+ let currentName = sessionName;
6
+ return {
7
+ getSessionName: vi.fn(() => currentName),
8
+ setSessionName: vi.fn((name: string) => {
9
+ currentName = name;
10
+ }),
11
+ on: vi.fn(),
12
+ };
13
+ }
14
+
15
+ function createMockCtx(options: {
16
+ streamSimpleResult?: any;
17
+ branch?: any[];
18
+ model?: any;
19
+ hasConfiguredAuth?: boolean;
20
+ }) {
21
+ const defaultBranch = [
22
+ {
23
+ type: "message",
24
+ entry: undefined,
25
+ message: {
26
+ role: "user",
27
+ content: [{ type: "text", text: "How do I fix this bug?" }],
28
+ },
29
+ },
30
+ {
31
+ type: "message",
32
+ entry: undefined,
33
+ message: {
34
+ role: "assistant",
35
+ content: [{ type: "text", text: "Here is how you fix the bug." }],
36
+ },
37
+ },
38
+ ];
39
+
40
+ const defaultModel = { provider: "anthropic", id: "claude-3-5-sonnet" };
41
+
42
+ return {
43
+ sessionManager: {
44
+ getBranch: vi.fn(() => options.branch ?? defaultBranch),
45
+ getSessionFile: vi.fn(() => "/tmp/session.json"),
46
+ },
47
+ modelRegistry: {
48
+ getAll: vi.fn(() => [defaultModel]),
49
+ hasConfiguredAuth: vi.fn(() => options.hasConfiguredAuth ?? true),
50
+ streamSimple: vi.fn(() => ({
51
+ result: vi.fn().mockResolvedValue(
52
+ options.streamSimpleResult ?? {
53
+ stopReason: "stop",
54
+ content: [{ type: "text", text: "Bug Fix Discussion" }],
55
+ },
56
+ ),
57
+ })),
58
+ },
59
+ model: options.model ?? defaultModel,
60
+ };
61
+ }
62
+
63
+ describe("generateTitle", () => {
64
+ it("generates title, trims quotes and whitespace, and calls pi.setSessionName", async () => {
65
+ const pi = createMockPi();
66
+ const ctx = createMockCtx({
67
+ streamSimpleResult: {
68
+ stopReason: "stop",
69
+ content: [{ type: "text", text: ' "Fixing the Login Bug" \n' }],
70
+ },
71
+ });
72
+ const onSuccess = vi.fn();
73
+ const onFailure = vi.fn();
74
+
75
+ await generateTitle(pi as any, ctx as any, onSuccess, onFailure);
76
+
77
+ expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(1);
78
+ expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledWith(
79
+ ctx.model,
80
+ expect.objectContaining({
81
+ messages: [
82
+ expect.objectContaining({
83
+ role: "user",
84
+ content: [
85
+ expect.objectContaining({
86
+ type: "text",
87
+ text: expect.stringContaining("How do I fix this bug?"),
88
+ }),
89
+ ],
90
+ }),
91
+ ],
92
+ }),
93
+ expect.objectContaining({
94
+ reasoning: "minimal",
95
+ cacheRetention: "none",
96
+ sessionId: expect.any(String),
97
+ }),
98
+ );
99
+ expect(pi.setSessionName).toHaveBeenCalledWith("Fixing the Login Bug");
100
+ expect(onSuccess).toHaveBeenCalledTimes(1);
101
+ expect(onFailure).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it("triggers onFailure when streamSimple returns stopReason === 'error'", async () => {
105
+ const pi = createMockPi();
106
+ const ctx = createMockCtx({
107
+ streamSimpleResult: {
108
+ stopReason: "error",
109
+ content: [],
110
+ },
111
+ });
112
+ const onSuccess = vi.fn();
113
+ const onFailure = vi.fn();
114
+
115
+ await generateTitle(pi as any, ctx as any, onSuccess, onFailure);
116
+
117
+ expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(1);
118
+ expect(pi.setSessionName).not.toHaveBeenCalled();
119
+ expect(onSuccess).not.toHaveBeenCalled();
120
+ expect(onFailure).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it("does not call onFailure when streamSimple returns stopReason === 'aborted'", async () => {
124
+ const pi = createMockPi();
125
+ const ctx = createMockCtx({
126
+ streamSimpleResult: {
127
+ stopReason: "aborted",
128
+ content: [{ type: "text", text: "Partial Title" }],
129
+ },
130
+ });
131
+ const onSuccess = vi.fn();
132
+ const onFailure = vi.fn();
133
+
134
+ await generateTitle(pi as any, ctx as any, onSuccess, onFailure);
135
+
136
+ expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(1);
137
+ expect(pi.setSessionName).not.toHaveBeenCalled();
138
+ expect(onSuccess).not.toHaveBeenCalled();
139
+ expect(onFailure).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it("skips title generation without incrementing failCount if model lacks configured auth", async () => {
143
+ const pi = createMockPi();
144
+ const ctx = createMockCtx({
145
+ hasConfiguredAuth: false,
146
+ });
147
+ const onSuccess = vi.fn();
148
+ const onFailure = vi.fn();
149
+
150
+ await generateTitle(pi as any, ctx as any, onSuccess, onFailure);
151
+
152
+ expect(ctx.modelRegistry.hasConfiguredAuth).toHaveBeenCalledWith(ctx.model);
153
+ expect(ctx.modelRegistry.streamSimple).not.toHaveBeenCalled();
154
+ expect(pi.setSessionName).not.toHaveBeenCalled();
155
+ expect(onSuccess).not.toHaveBeenCalled();
156
+ expect(onFailure).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it("aborts without setting session name if race condition occurs (session name already set)", async () => {
160
+ let getSessionNameCallCount = 0;
161
+ const pi = {
162
+ getSessionName: vi.fn(() => {
163
+ getSessionNameCallCount++;
164
+ // First check in generateTitle before setting name returns existing name
165
+ // (Wait, step 8 checks if (pi.getSessionName()) return;)
166
+ return "Already Named";
167
+ }),
168
+ setSessionName: vi.fn(),
169
+ on: vi.fn(),
170
+ };
171
+ const ctx = createMockCtx({
172
+ streamSimpleResult: {
173
+ stopReason: "stop",
174
+ content: [{ type: "text", text: "New Title" }],
175
+ },
176
+ });
177
+ const onSuccess = vi.fn();
178
+ const onFailure = vi.fn();
179
+
180
+ await generateTitle(pi as any, ctx as any, onSuccess, onFailure);
181
+
182
+ expect(ctx.modelRegistry.streamSimple).toHaveBeenCalledTimes(1);
183
+ expect(pi.setSessionName).not.toHaveBeenCalled();
184
+ expect(onSuccess).not.toHaveBeenCalled();
185
+ expect(onFailure).not.toHaveBeenCalled();
186
+ });
187
+ });
package/src/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
- import type { ProviderHeaders } from "@earendil-works/pi-ai";
3
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
2
  import { loadConfig } from "@pi-archimedes/core/settings-io";
5
3
  import type { SettingItem } from "@earendil-works/pi-tui";
@@ -76,7 +74,7 @@ export function resolveModel<T extends { provider: string; id: string }>(
76
74
  * Generate and set a session title. Runs asynchronously without blocking
77
75
  * the agent_end handler so the UI stays responsive.
78
76
  */
79
- async function generateTitle(
77
+ export async function generateTitle(
80
78
  pi: ExtensionAPI,
81
79
  ctx: ExtensionContext,
82
80
  onSuccess: () => void,
@@ -151,31 +149,37 @@ async function generateTitle(
151
149
  const model = settingsModel ?? ctx.model;
152
150
  if (!model) return;
153
151
 
154
- // 4. Check auth
152
+ // 4. Check auth — skip without incrementing failCount if not configured
155
153
  if (!ctx.modelRegistry.hasConfiguredAuth(model)) return;
156
154
 
157
- // 5. Get API key and headers
158
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
159
- if (!auth.ok) return;
160
-
161
- // 6. Make API call
162
- const opts: { reasoning: "minimal"; cacheRetention: "none"; sessionId: string; apiKey?: string; headers?: ProviderHeaders } = {
163
- reasoning: "minimal",
164
- cacheRetention: "none",
165
- sessionId: crypto.randomUUID(),
166
- };
167
- if (auth.apiKey) opts.apiKey = auth.apiKey;
168
- if (auth.headers) opts.headers = auth.headers;
169
-
170
- const response = await complete(model, {
171
- messages: [
172
- {
173
- role: "user" as const,
174
- content: [{ type: "text" as const, text: titlePrompt }],
175
- timestamp: Date.now(),
176
- },
177
- ],
178
- }, opts);
155
+ // 5. Stream simple with built-in auth resolution
156
+ const stream = ctx.modelRegistry.streamSimple(
157
+ model,
158
+ {
159
+ messages: [
160
+ {
161
+ role: "user",
162
+ content: [{ type: "text", text: titlePrompt }],
163
+ timestamp: Date.now(),
164
+ },
165
+ ],
166
+ },
167
+ {
168
+ reasoning: "minimal",
169
+ cacheRetention: "none",
170
+ sessionId: crypto.randomUUID(),
171
+ },
172
+ );
173
+
174
+ const response = await stream.result();
175
+ // User/system cancellation is not a failure; don't burn the retry budget
176
+ if (response.stopReason === "aborted") {
177
+ return;
178
+ }
179
+ if (response.stopReason === "error") {
180
+ onFailure();
181
+ return;
182
+ }
179
183
 
180
184
  // 7. Extract and clean title
181
185
  const title = response.content