pi-sessions 0.2.2 → 0.3.1
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 +4 -1
- package/extensions/session-auto-title/command.ts +4 -3
- 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 +23 -10
- 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 +54 -16
- package/extensions/session-index.ts +2 -1
- package/extensions/shared/pi-mode.ts +5 -0
- 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";
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
runRetitlePlan,
|
|
27
26
|
} from "./session-auto-title/retitle.js";
|
|
28
27
|
import { showRetitleWizard } from "./session-auto-title/wizard.js";
|
|
28
|
+
import { isTuiMode } from "./shared/pi-mode.js";
|
|
29
29
|
import { loadSettings } from "./shared/settings.js";
|
|
30
30
|
|
|
31
31
|
export {
|
|
@@ -74,6 +74,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
74
74
|
ctx,
|
|
75
75
|
model,
|
|
76
76
|
invocation,
|
|
77
|
+
settings.autoTitle.prompt,
|
|
77
78
|
);
|
|
78
79
|
},
|
|
79
80
|
),
|
|
@@ -102,6 +103,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
102
103
|
existingPlan: result.plan,
|
|
103
104
|
getSessionEpoch: () => sessionEpoch,
|
|
104
105
|
notifyOnSuccess: false,
|
|
106
|
+
systemPrompt: settings.autoTitle.prompt,
|
|
105
107
|
})
|
|
106
108
|
.then((outcome) => {
|
|
107
109
|
if (outcome.ok) {
|
|
@@ -111,7 +113,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
|
|
|
111
113
|
const shouldNotify = controller.handleTitleFailed(ctx, outcome.failure);
|
|
112
114
|
if (shouldNotify && ctx.hasUI) {
|
|
113
115
|
ctx.ui.notify(
|
|
114
|
-
`Auto-title failed: ${
|
|
116
|
+
`Auto-title failed: ${outcome.failure.message}. Open /title for details.`,
|
|
115
117
|
"warning",
|
|
116
118
|
);
|
|
117
119
|
}
|
|
@@ -136,6 +138,7 @@ async function handleTitleInvocation(
|
|
|
136
138
|
ctx: ExtensionCommandContext,
|
|
137
139
|
model: Model<Api> | undefined,
|
|
138
140
|
invocation: RetitleCommandInvocation,
|
|
141
|
+
systemPrompt: string,
|
|
139
142
|
): Promise<RetitleCommandOutcome> {
|
|
140
143
|
const retitleOpts = {
|
|
141
144
|
pi,
|
|
@@ -143,6 +146,7 @@ async function handleTitleInvocation(
|
|
|
143
146
|
ctx,
|
|
144
147
|
model,
|
|
145
148
|
isManual: true,
|
|
149
|
+
systemPrompt,
|
|
146
150
|
getSessionEpoch: state.getSessionEpoch,
|
|
147
151
|
};
|
|
148
152
|
|
|
@@ -157,24 +161,32 @@ async function handleTitleInvocation(
|
|
|
157
161
|
};
|
|
158
162
|
|
|
159
163
|
if (invocation.kind === "open-pane") {
|
|
160
|
-
if (!ctx
|
|
164
|
+
if (!isTuiMode(ctx)) {
|
|
161
165
|
return retitleCurrentSession();
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch);
|
|
168
|
+
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, systemPrompt);
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
if (invocation.scope === "this") {
|
|
168
172
|
return runWithInFlightTracking(state, retitleCurrentSession);
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
if (ctx
|
|
172
|
-
return showRetitleWizard(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
if (isTuiMode(ctx) && !invocation.force) {
|
|
176
|
+
return showRetitleWizard(
|
|
177
|
+
pi,
|
|
178
|
+
state.controller,
|
|
179
|
+
ctx,
|
|
180
|
+
model,
|
|
181
|
+
state.getSessionEpoch,
|
|
182
|
+
systemPrompt,
|
|
183
|
+
{
|
|
184
|
+
initialInvocation: {
|
|
185
|
+
scope: invocation.scope,
|
|
186
|
+
mode: invocation.mode ?? "backfill",
|
|
187
|
+
},
|
|
176
188
|
},
|
|
177
|
-
|
|
189
|
+
);
|
|
178
190
|
}
|
|
179
191
|
|
|
180
192
|
const scan = await buildRetitleScopeScan(ctx, invocation.scope);
|
|
@@ -188,6 +200,7 @@ async function handleTitleInvocation(
|
|
|
188
200
|
scan,
|
|
189
201
|
mode,
|
|
190
202
|
state.getSessionEpoch,
|
|
203
|
+
systemPrompt,
|
|
191
204
|
);
|
|
192
205
|
notifyBulkRetitleResult(ctx, scan, mode, result);
|
|
193
206
|
return "success";
|