@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 +8 -8
- package/src/index.test.ts +187 -0
- package/src/index.ts +30 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/session-name",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
21
|
+
"@pi-archimedes/core": "2.9.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
25
|
-
"@earendil-works/pi-ai": ">=0.
|
|
26
|
-
"@earendil-works/pi-tui": ">=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.
|
|
30
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
31
|
-
"@earendil-works/pi-tui": "^0.
|
|
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.
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|