pi-sessions 0.2.2 → 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.
- package/README.md +2 -1
- package/extensions/session-auto-title/controller.ts +2 -15
- package/extensions/session-auto-title/generate.ts +256 -0
- package/extensions/session-auto-title/retitle.ts +39 -375
- package/extensions/session-auto-title/wizard.ts +8 -12
- package/extensions/session-auto-title.ts +20 -8
- package/extensions/session-handoff/extract.ts +29 -12
- package/extensions/session-handoff/metadata.ts +5 -0
- package/extensions/session-handoff/spawn.ts +26 -5
- package/extensions/session-handoff.ts +51 -14
- package/extensions/shared/settings.ts +9 -0
- package/package.json +9 -9
- package/extensions/session-auto-title/prompt.ts +0 -54
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Api,
|
|
3
|
-
completeSimple,
|
|
4
|
-
type Model,
|
|
5
|
-
type TextContent,
|
|
6
|
-
type UserMessage,
|
|
7
|
-
} from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
8
2
|
import type {
|
|
9
3
|
ExtensionAPI,
|
|
10
4
|
ExtensionCommandContext,
|
|
@@ -13,27 +7,19 @@ import type {
|
|
|
13
7
|
} from "@earendil-works/pi-coding-agent";
|
|
14
8
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
15
9
|
import type { RetitleMode, RetitleScope } from "./command.js";
|
|
16
|
-
import {
|
|
17
|
-
import type {
|
|
18
|
-
AutoTitleFailure,
|
|
19
|
-
AutoTitleTriggerPlan,
|
|
20
|
-
SessionAutoTitleController,
|
|
21
|
-
} from "./controller.js";
|
|
10
|
+
import { buildAutoTitleContext } from "./context.js";
|
|
11
|
+
import type { AutoTitleTriggerPlan, SessionAutoTitleController } from "./controller.js";
|
|
22
12
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from "./
|
|
13
|
+
type AutoTitleGenerationResult,
|
|
14
|
+
createAutoTitleFailure,
|
|
15
|
+
generateAutoTitle,
|
|
16
|
+
} from "./generate.js";
|
|
27
17
|
import {
|
|
28
18
|
AUTO_TITLE_STATE_CUSTOM_TYPE,
|
|
29
19
|
type AutoTitlePersistedState,
|
|
30
|
-
type AutoTitleTrigger,
|
|
31
20
|
createAutoTitleState,
|
|
32
21
|
} from "./state.js";
|
|
33
22
|
|
|
34
|
-
const AUTO_TITLE_REQUEST_TIMEOUT_MS = 15_000;
|
|
35
|
-
const AUTO_TITLE_MAX_TOKENS = 64;
|
|
36
|
-
|
|
37
23
|
export interface RetitleScopeScan {
|
|
38
24
|
scope: Exclude<RetitleScope, "this">;
|
|
39
25
|
sessions: SessionInfo[];
|
|
@@ -71,6 +57,7 @@ export async function runBulkRetitle(
|
|
|
71
57
|
scan: RetitleScopeScan,
|
|
72
58
|
mode: RetitleMode,
|
|
73
59
|
getSessionEpoch: () => number,
|
|
60
|
+
systemPrompt: string,
|
|
74
61
|
): Promise<BulkRetitleResult> {
|
|
75
62
|
const result: BulkRetitleResult = {
|
|
76
63
|
attempted: 0,
|
|
@@ -97,6 +84,7 @@ export async function runBulkRetitle(
|
|
|
97
84
|
ctx,
|
|
98
85
|
model,
|
|
99
86
|
isManual: true,
|
|
87
|
+
systemPrompt,
|
|
100
88
|
getSessionEpoch,
|
|
101
89
|
notifyOnSuccess: false,
|
|
102
90
|
});
|
|
@@ -104,7 +92,7 @@ export async function runBulkRetitle(
|
|
|
104
92
|
continue;
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
const outcome = await retitleStoredSession(ctx, model, session.path);
|
|
95
|
+
const outcome = await retitleStoredSession(ctx, model, session.path, systemPrompt);
|
|
108
96
|
result[outcome] += 1;
|
|
109
97
|
}
|
|
110
98
|
|
|
@@ -176,23 +164,17 @@ export interface RetitlePlanOptions {
|
|
|
176
164
|
ctx: ExtensionContext;
|
|
177
165
|
model: Model<Api> | undefined;
|
|
178
166
|
isManual: boolean;
|
|
167
|
+
systemPrompt: string;
|
|
179
168
|
existingPlan?: AutoTitleTriggerPlan;
|
|
180
169
|
getSessionEpoch?: () => number;
|
|
181
170
|
notifyOnSuccess?: boolean;
|
|
182
171
|
}
|
|
183
172
|
|
|
184
|
-
export
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
| {
|
|
190
|
-
ok: false;
|
|
191
|
-
failure: AutoTitleFailure;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
export async function runRetitlePlan(options: RetitlePlanOptions): Promise<RetitlePlanResult> {
|
|
195
|
-
const { pi, controller, ctx, model, isManual, existingPlan, getSessionEpoch } = options;
|
|
173
|
+
export async function runRetitlePlan(
|
|
174
|
+
options: RetitlePlanOptions,
|
|
175
|
+
): Promise<AutoTitleGenerationResult> {
|
|
176
|
+
const { pi, controller, ctx, model, isManual, systemPrompt, existingPlan, getSessionEpoch } =
|
|
177
|
+
options;
|
|
196
178
|
const notifyOnSuccess = options.notifyOnSuccess ?? isManual;
|
|
197
179
|
|
|
198
180
|
const plan = existingPlan ?? controller.handleManualRetitle(ctx);
|
|
@@ -215,7 +197,21 @@ export async function runRetitlePlan(options: RetitlePlanOptions): Promise<Retit
|
|
|
215
197
|
}
|
|
216
198
|
|
|
217
199
|
const currentEpoch = getSessionEpoch?.();
|
|
218
|
-
const
|
|
200
|
+
const titleContext = buildAutoTitleContext(
|
|
201
|
+
ctx.sessionManager.getEntries(),
|
|
202
|
+
ctx.sessionManager.getLeafId(),
|
|
203
|
+
{
|
|
204
|
+
cwd: ctx.cwd,
|
|
205
|
+
currentTitle: plan.currentTitle,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
const generatedTitle = await generateAutoTitle(
|
|
209
|
+
ctx,
|
|
210
|
+
model,
|
|
211
|
+
titleContext,
|
|
212
|
+
plan.reason,
|
|
213
|
+
systemPrompt,
|
|
214
|
+
);
|
|
219
215
|
if (!generatedTitle.ok) {
|
|
220
216
|
return generatedTitle;
|
|
221
217
|
}
|
|
@@ -260,6 +256,7 @@ async function retitleStoredSession(
|
|
|
260
256
|
ctx: ExtensionContext,
|
|
261
257
|
model: Model<Api>,
|
|
262
258
|
sessionPath: string,
|
|
259
|
+
systemPrompt: string,
|
|
263
260
|
): Promise<"retitled" | "unchanged" | "failed"> {
|
|
264
261
|
const sessionManager = SessionManager.open(sessionPath);
|
|
265
262
|
const currentTitle = sessionManager.getSessionName();
|
|
@@ -276,7 +273,13 @@ async function retitleStoredSession(
|
|
|
276
273
|
userTurnCount: titleContext.userTurnCount,
|
|
277
274
|
currentTitle,
|
|
278
275
|
};
|
|
279
|
-
const generatedTitle = await
|
|
276
|
+
const generatedTitle = await generateAutoTitle(
|
|
277
|
+
ctx,
|
|
278
|
+
model,
|
|
279
|
+
titleContext,
|
|
280
|
+
plan.reason,
|
|
281
|
+
systemPrompt,
|
|
282
|
+
);
|
|
280
283
|
if (!generatedTitle.ok) {
|
|
281
284
|
return "failed";
|
|
282
285
|
}
|
|
@@ -300,342 +303,3 @@ async function retitleStoredSession(
|
|
|
300
303
|
function hasSessionTitle(name: string | undefined): boolean {
|
|
301
304
|
return Boolean(name?.trim());
|
|
302
305
|
}
|
|
303
|
-
|
|
304
|
-
async function generateAutoTitle(
|
|
305
|
-
ctx: ExtensionContext,
|
|
306
|
-
plan: AutoTitleTriggerPlan,
|
|
307
|
-
model: Model<Api>,
|
|
308
|
-
): Promise<RetitlePlanResult> {
|
|
309
|
-
const titleContext = buildAutoTitleContext(
|
|
310
|
-
ctx.sessionManager.getEntries(),
|
|
311
|
-
ctx.sessionManager.getLeafId(),
|
|
312
|
-
{
|
|
313
|
-
cwd: ctx.cwd,
|
|
314
|
-
currentTitle: plan.currentTitle,
|
|
315
|
-
},
|
|
316
|
-
);
|
|
317
|
-
return generateAutoTitleFromContext(ctx, plan, model, titleContext);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function generateAutoTitleFromContext(
|
|
321
|
-
ctx: ExtensionContext,
|
|
322
|
-
plan: AutoTitleTriggerPlan,
|
|
323
|
-
model: Model<Api>,
|
|
324
|
-
titleContext: AutoTitleContext,
|
|
325
|
-
): Promise<RetitlePlanResult> {
|
|
326
|
-
if (!titleContext.conversationText) {
|
|
327
|
-
return {
|
|
328
|
-
ok: false,
|
|
329
|
-
failure: createAutoTitleFailure(
|
|
330
|
-
plan.reason,
|
|
331
|
-
model,
|
|
332
|
-
"No conversation available for auto-title generation.",
|
|
333
|
-
),
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
338
|
-
if (!auth.ok) {
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
failure: createAutoTitleFailure(
|
|
342
|
-
plan.reason,
|
|
343
|
-
model,
|
|
344
|
-
"Failed to authenticate auto-title model.",
|
|
345
|
-
),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const message: UserMessage = {
|
|
350
|
-
role: "user",
|
|
351
|
-
content: [{ type: "text", text: buildAutoTitlePrompt(titleContext, plan.reason) }],
|
|
352
|
-
timestamp: Date.now(),
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const abortController = new AbortController();
|
|
356
|
-
const timeoutId = setTimeout(() => abortController.abort(), AUTO_TITLE_REQUEST_TIMEOUT_MS);
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const response = await completeSimple(
|
|
360
|
-
model,
|
|
361
|
-
{
|
|
362
|
-
systemPrompt: AUTO_TITLE_SYSTEM_PROMPT,
|
|
363
|
-
messages: [message],
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
...(auth.apiKey && { apiKey: auth.apiKey }),
|
|
367
|
-
...(auth.headers && { headers: auth.headers }),
|
|
368
|
-
maxTokens: AUTO_TITLE_MAX_TOKENS,
|
|
369
|
-
signal: abortController.signal,
|
|
370
|
-
},
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
374
|
-
const fallbackMessage =
|
|
375
|
-
response.stopReason === "aborted" ? "Request was aborted." : "Provider returned an error.";
|
|
376
|
-
const failureDetails = extractFailureDetails(
|
|
377
|
-
response.errorMessage || fallbackMessage,
|
|
378
|
-
response,
|
|
379
|
-
);
|
|
380
|
-
return {
|
|
381
|
-
ok: false,
|
|
382
|
-
failure: createAutoTitleFailure(
|
|
383
|
-
plan.reason,
|
|
384
|
-
model,
|
|
385
|
-
failureDetails.message,
|
|
386
|
-
failureDetails.status,
|
|
387
|
-
),
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const responseText = response.content
|
|
392
|
-
.filter((part): part is TextContent => part.type === "text")
|
|
393
|
-
.map((part) => part.text)
|
|
394
|
-
.join("\n");
|
|
395
|
-
const normalizedTitle = normalizeGeneratedAutoTitle(responseText);
|
|
396
|
-
if (!normalizedTitle) {
|
|
397
|
-
return {
|
|
398
|
-
ok: false,
|
|
399
|
-
failure: createAutoTitleFailure(plan.reason, model, "Model returned an empty title."),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
ok: true,
|
|
405
|
-
title: normalizedTitle,
|
|
406
|
-
};
|
|
407
|
-
} catch (error) {
|
|
408
|
-
const failureDetails = extractFailureDetails(error);
|
|
409
|
-
return {
|
|
410
|
-
ok: false,
|
|
411
|
-
failure: createAutoTitleFailure(
|
|
412
|
-
plan.reason,
|
|
413
|
-
model,
|
|
414
|
-
failureDetails.message,
|
|
415
|
-
failureDetails.status,
|
|
416
|
-
),
|
|
417
|
-
};
|
|
418
|
-
} finally {
|
|
419
|
-
clearTimeout(timeoutId);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function createAutoTitleFailure(
|
|
424
|
-
trigger: AutoTitleTrigger,
|
|
425
|
-
model: Model<Api> | undefined,
|
|
426
|
-
message: string,
|
|
427
|
-
status?: number,
|
|
428
|
-
): AutoTitleFailure {
|
|
429
|
-
return {
|
|
430
|
-
at: new Date().toISOString(),
|
|
431
|
-
trigger,
|
|
432
|
-
model: formatModelLabel(model),
|
|
433
|
-
message,
|
|
434
|
-
...(status !== undefined ? { status } : {}),
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function formatModelLabel(model: Model<Api> | undefined): string {
|
|
439
|
-
if (!model) {
|
|
440
|
-
return "(no model resolved)";
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return `${model.provider}/${model.id}`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function extractFailureDetails(
|
|
447
|
-
primary: unknown,
|
|
448
|
-
secondary?: unknown,
|
|
449
|
-
): { message: string; status?: number } {
|
|
450
|
-
const structured =
|
|
451
|
-
parseStructuredProviderError(primary) ??
|
|
452
|
-
parseStructuredProviderError(secondary) ??
|
|
453
|
-
parseEmbeddedErrorFromObject(primary);
|
|
454
|
-
if (structured) {
|
|
455
|
-
return structured;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const message =
|
|
459
|
-
extractStringMessage(primary) || extractStringMessage(secondary) || "Unknown provider error.";
|
|
460
|
-
const status = extractStatus(primary) ?? extractStatus(secondary);
|
|
461
|
-
return {
|
|
462
|
-
message,
|
|
463
|
-
...(status !== undefined ? { status } : {}),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function parseEmbeddedErrorFromObject(
|
|
468
|
-
value: unknown,
|
|
469
|
-
): { message: string; status?: number } | undefined {
|
|
470
|
-
if (!isObject(value) && !(value instanceof Error)) {
|
|
471
|
-
return undefined;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const embedded = extractStringMessage(value);
|
|
475
|
-
return embedded ? parseStructuredProviderError(embedded) : undefined;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function parseStructuredProviderError(
|
|
479
|
-
value: unknown,
|
|
480
|
-
): { message: string; status?: number } | undefined {
|
|
481
|
-
const json = parseJsonObjectCandidate(value);
|
|
482
|
-
if (!json) {
|
|
483
|
-
return undefined;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const topError = isObject(json.error) ? json.error : undefined;
|
|
487
|
-
const status =
|
|
488
|
-
readNumericStatus(json) ??
|
|
489
|
-
readNumericStatus(topError) ??
|
|
490
|
-
(typeof topError?.code === "number" ? topError.code : undefined);
|
|
491
|
-
const rawMessage =
|
|
492
|
-
readString(topError?.message) ?? readString(json.message) ?? readString(json.errorMessage);
|
|
493
|
-
if (!rawMessage) {
|
|
494
|
-
return undefined;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const labels = collectProviderErrorLabels(json, topError);
|
|
498
|
-
return {
|
|
499
|
-
message: formatStructuredErrorMessage(labels, rawMessage),
|
|
500
|
-
...(status !== undefined ? { status } : {}),
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function collectProviderErrorLabels(
|
|
505
|
-
root: Record<string, unknown>,
|
|
506
|
-
nestedError: Record<string, unknown> | undefined,
|
|
507
|
-
): string[] {
|
|
508
|
-
const labels: string[] = [];
|
|
509
|
-
const candidates = [
|
|
510
|
-
readString(nestedError?.status),
|
|
511
|
-
readString(nestedError?.type),
|
|
512
|
-
readString(nestedError?.code),
|
|
513
|
-
readString(root.status),
|
|
514
|
-
readString(root.type),
|
|
515
|
-
readString(root.code),
|
|
516
|
-
];
|
|
517
|
-
|
|
518
|
-
for (const candidate of candidates) {
|
|
519
|
-
if (!candidate) {
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const normalized = candidate.trim();
|
|
524
|
-
if (!normalized || normalized === "error") {
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (!labels.includes(normalized)) {
|
|
529
|
-
labels.push(normalized);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return labels;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function formatStructuredErrorMessage(labels: string[], rawMessage: string): string {
|
|
537
|
-
const cleanedMessage = stripRedundantErrorPrefix(rawMessage);
|
|
538
|
-
return labels.length > 0 ? `${labels.join(" · ")} · ${cleanedMessage}` : cleanedMessage;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function stripRedundantErrorPrefix(message: string): string {
|
|
542
|
-
return message
|
|
543
|
-
.replace(/^Unauthorized:\s*/i, "")
|
|
544
|
-
.replace(/^Authentication (?:error|failed):\s*/i, "")
|
|
545
|
-
.trim();
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function parseJsonObjectCandidate(value: unknown): Record<string, unknown> | undefined {
|
|
549
|
-
if (isObject(value)) {
|
|
550
|
-
return value;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const text = extractStringMessage(value);
|
|
554
|
-
if (!text) {
|
|
555
|
-
return undefined;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const startIndex = text.indexOf("{");
|
|
559
|
-
const endIndex = text.lastIndexOf("}");
|
|
560
|
-
if (startIndex < 0 || endIndex <= startIndex) {
|
|
561
|
-
return undefined;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
const parsed = JSON.parse(text.slice(startIndex, endIndex + 1));
|
|
566
|
-
return isObject(parsed) ? parsed : undefined;
|
|
567
|
-
} catch {
|
|
568
|
-
return undefined;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function extractStringMessage(value: unknown): string | undefined {
|
|
573
|
-
if (typeof value === "string" && value.trim()) {
|
|
574
|
-
return value.trim();
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (value instanceof Error && value.message.trim()) {
|
|
578
|
-
return value.message.trim();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (!isObject(value)) {
|
|
582
|
-
return undefined;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const directMessage = readString(value.message) ?? readString(value.errorMessage);
|
|
586
|
-
if (directMessage) {
|
|
587
|
-
return directMessage;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return undefined;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function extractStatus(error: unknown): number | undefined {
|
|
594
|
-
const candidates = [
|
|
595
|
-
error,
|
|
596
|
-
isObject(error) ? error.error : undefined,
|
|
597
|
-
isObject(error) ? error.response : undefined,
|
|
598
|
-
isObject(error) && isObject(error.error) ? error.error.response : undefined,
|
|
599
|
-
parseJsonObjectCandidate(error),
|
|
600
|
-
];
|
|
601
|
-
|
|
602
|
-
for (const candidate of candidates) {
|
|
603
|
-
const status = readNumericStatus(candidate);
|
|
604
|
-
if (status !== undefined) {
|
|
605
|
-
return status;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return undefined;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function readNumericStatus(value: unknown): number | undefined {
|
|
613
|
-
if (!isObject(value)) {
|
|
614
|
-
return undefined;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const directStatus = value.status;
|
|
618
|
-
if (typeof directStatus === "number") {
|
|
619
|
-
return directStatus;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const statusCode = value.statusCode;
|
|
623
|
-
if (typeof statusCode === "number") {
|
|
624
|
-
return statusCode;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const code = value.code;
|
|
628
|
-
if (typeof code === "number") {
|
|
629
|
-
return code;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return undefined;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function readString(value: unknown): string | undefined {
|
|
636
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
640
|
-
return typeof value === "object" && value !== null;
|
|
641
|
-
}
|
|
@@ -8,12 +8,8 @@ import {
|
|
|
8
8
|
import type { Focusable, TUI } from "@earendil-works/pi-tui";
|
|
9
9
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { RetitleCommandOutcome, RetitleMode, RetitleScope } from "./command.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
type AutoTitleFailure,
|
|
14
|
-
formatAutoTitleFailureSummary,
|
|
15
|
-
type SessionAutoTitleController,
|
|
16
|
-
} from "./controller.js";
|
|
11
|
+
import type { AutoRetitleStatus, SessionAutoTitleController } from "./controller.js";
|
|
12
|
+
import type { AutoTitleFailure } from "./generate.js";
|
|
17
13
|
import {
|
|
18
14
|
buildBulkRetitleMessage,
|
|
19
15
|
buildRetitleScopeScan,
|
|
@@ -60,6 +56,7 @@ export async function showRetitleWizard(
|
|
|
60
56
|
ctx: ExtensionCommandContext,
|
|
61
57
|
model: Model<Api> | undefined,
|
|
62
58
|
getSessionEpoch: () => number,
|
|
59
|
+
systemPrompt: string,
|
|
63
60
|
options?: RetitleWizardOptions,
|
|
64
61
|
): Promise<RetitleCommandOutcome> {
|
|
65
62
|
return ctx.ui.custom<RetitleCommandOutcome>(
|
|
@@ -72,6 +69,7 @@ export async function showRetitleWizard(
|
|
|
72
69
|
ctx,
|
|
73
70
|
model,
|
|
74
71
|
getSessionEpoch,
|
|
72
|
+
systemPrompt,
|
|
75
73
|
done,
|
|
76
74
|
options,
|
|
77
75
|
),
|
|
@@ -100,6 +98,7 @@ class RetitleWizardPanel implements Focusable {
|
|
|
100
98
|
private readonly ctx: ExtensionCommandContext,
|
|
101
99
|
private readonly model: Model<Api> | undefined,
|
|
102
100
|
private readonly getSessionEpoch: () => number,
|
|
101
|
+
private readonly systemPrompt: string,
|
|
103
102
|
private readonly done: (result: RetitleCommandOutcome) => void,
|
|
104
103
|
options?: RetitleWizardOptions,
|
|
105
104
|
) {
|
|
@@ -250,6 +249,7 @@ class RetitleWizardPanel implements Focusable {
|
|
|
250
249
|
ctx: this.ctx,
|
|
251
250
|
model: this.model,
|
|
252
251
|
isManual: true,
|
|
252
|
+
systemPrompt: this.systemPrompt,
|
|
253
253
|
getSessionEpoch: this.getSessionEpoch,
|
|
254
254
|
});
|
|
255
255
|
if (result.ok) {
|
|
@@ -309,6 +309,7 @@ class RetitleWizardPanel implements Focusable {
|
|
|
309
309
|
scan,
|
|
310
310
|
mode,
|
|
311
311
|
this.getSessionEpoch,
|
|
312
|
+
this.systemPrompt,
|
|
312
313
|
);
|
|
313
314
|
notifyBulkRetitleResult(this.ctx, scan, mode, result);
|
|
314
315
|
this.done(
|
|
@@ -468,11 +469,7 @@ class RetitleWizardPanel implements Focusable {
|
|
|
468
469
|
return this.renderRow(innerWidth, left);
|
|
469
470
|
}
|
|
470
471
|
|
|
471
|
-
const summaryText = truncateToWidth(
|
|
472
|
-
formatAutoTitleFailureSummary(failure),
|
|
473
|
-
maxSummaryWidth,
|
|
474
|
-
"…",
|
|
475
|
-
);
|
|
472
|
+
const summaryText = truncateToWidth(failure.message, maxSummaryWidth, "…");
|
|
476
473
|
const right = ` ${this.theme.fg("dim", summaryText)}`;
|
|
477
474
|
const gap = Math.max(1, innerWidth - visibleWidth(left) - visibleWidth(right));
|
|
478
475
|
return this.renderRow(innerWidth, `${left}${" ".repeat(gap)}${right}`);
|
|
@@ -523,7 +520,6 @@ function formatAutoRetitleStatusLine(theme: Theme, status: AutoRetitleStatus): s
|
|
|
523
520
|
function formatFailureClipboardText(failure: AutoTitleFailure): string {
|
|
524
521
|
const lines = [
|
|
525
522
|
`model: ${failure.model}`,
|
|
526
|
-
...(failure.status !== undefined ? [`status: ${failure.status}`] : []),
|
|
527
523
|
`trigger: ${failure.trigger}`,
|
|
528
524
|
`time: ${failure.at}`,
|
|
529
525
|
`error: ${failure.message}`,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "./session-auto-title/command.js";
|
|
15
15
|
import {
|
|
16
16
|
createSessionAutoTitleController,
|
|
17
|
-
formatAutoTitleFailureSummary,
|
|
18
17
|
type SessionAutoTitleController,
|
|
19
18
|
} from "./session-auto-title/controller.js";
|
|
20
19
|
import { resolveAutoTitleModel } from "./session-auto-title/model.js";
|
|
@@ -74,6 +73,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
74
73
|
ctx,
|
|
75
74
|
model,
|
|
76
75
|
invocation,
|
|
76
|
+
settings.autoTitle.prompt,
|
|
77
77
|
);
|
|
78
78
|
},
|
|
79
79
|
),
|
|
@@ -102,6 +102,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
102
102
|
existingPlan: result.plan,
|
|
103
103
|
getSessionEpoch: () => sessionEpoch,
|
|
104
104
|
notifyOnSuccess: false,
|
|
105
|
+
systemPrompt: settings.autoTitle.prompt,
|
|
105
106
|
})
|
|
106
107
|
.then((outcome) => {
|
|
107
108
|
if (outcome.ok) {
|
|
@@ -111,7 +112,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
111
112
|
const shouldNotify = controller.handleTitleFailed(ctx, outcome.failure);
|
|
112
113
|
if (shouldNotify && ctx.hasUI) {
|
|
113
114
|
ctx.ui.notify(
|
|
114
|
-
`Auto-title failed: ${
|
|
115
|
+
`Auto-title failed: ${outcome.failure.message}. Open /title for details.`,
|
|
115
116
|
"warning",
|
|
116
117
|
);
|
|
117
118
|
}
|
|
@@ -136,6 +137,7 @@ async function handleTitleInvocation(
|
|
|
136
137
|
ctx: ExtensionCommandContext,
|
|
137
138
|
model: Model<Api> | undefined,
|
|
138
139
|
invocation: RetitleCommandInvocation,
|
|
140
|
+
systemPrompt: string,
|
|
139
141
|
): Promise<RetitleCommandOutcome> {
|
|
140
142
|
const retitleOpts = {
|
|
141
143
|
pi,
|
|
@@ -143,6 +145,7 @@ async function handleTitleInvocation(
|
|
|
143
145
|
ctx,
|
|
144
146
|
model,
|
|
145
147
|
isManual: true,
|
|
148
|
+
systemPrompt,
|
|
146
149
|
getSessionEpoch: state.getSessionEpoch,
|
|
147
150
|
};
|
|
148
151
|
|
|
@@ -161,7 +164,7 @@ async function handleTitleInvocation(
|
|
|
161
164
|
return retitleCurrentSession();
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch);
|
|
167
|
+
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, systemPrompt);
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
if (invocation.scope === "this") {
|
|
@@ -169,12 +172,20 @@ async function handleTitleInvocation(
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
if (ctx.hasUI && !invocation.force) {
|
|
172
|
-
return showRetitleWizard(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
return showRetitleWizard(
|
|
176
|
+
pi,
|
|
177
|
+
state.controller,
|
|
178
|
+
ctx,
|
|
179
|
+
model,
|
|
180
|
+
state.getSessionEpoch,
|
|
181
|
+
systemPrompt,
|
|
182
|
+
{
|
|
183
|
+
initialInvocation: {
|
|
184
|
+
scope: invocation.scope,
|
|
185
|
+
mode: invocation.mode ?? "backfill",
|
|
186
|
+
},
|
|
176
187
|
},
|
|
177
|
-
|
|
188
|
+
);
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
const scan = await buildRetitleScopeScan(ctx, invocation.scope);
|
|
@@ -188,6 +199,7 @@ async function handleTitleInvocation(
|
|
|
188
199
|
scan,
|
|
189
200
|
mode,
|
|
190
201
|
state.getSessionEpoch,
|
|
202
|
+
systemPrompt,
|
|
191
203
|
);
|
|
192
204
|
notifyBulkRetitleResult(ctx, scan, mode, result);
|
|
193
205
|
return "success";
|