@docyrus/docyrus 0.0.66 → 0.0.67
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/main.js +1 -1
- package/main.js.map +1 -1
- package/package.json +1 -1
- package/resources/pi-agent/extensions/context.ts +22 -16
- package/resources/pi-agent/extensions/control.ts +43 -27
- package/resources/pi-agent/extensions/knowledge.ts +42 -16
- package/resources/pi-agent/extensions/loop.ts +44 -27
- package/resources/pi-agent/extensions/plan.ts +79 -40
- package/resources/pi-agent/extensions/prompt-url-widget.ts +39 -18
- package/resources/pi-agent/extensions/review.ts +13 -2
- package/resources/pi-agent/shared/extensionLifecycle.ts +11 -0
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import { DynamicBorder, getAgentDir as piGetAgentDir, loadProjectContextFiles as
|
|
|
13
13
|
import { Container, Key, Text, matchesKey, type Component, type TUI } from "@mariozechner/pi-tui";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
16
17
|
|
|
17
18
|
function formatUsd(cost: number): string {
|
|
18
19
|
if (!Number.isFinite(cost) || cost <= 0) {return "$0.00";}
|
|
@@ -390,22 +391,27 @@ export default function contextExtension(pi: ExtensionAPI) {
|
|
|
390
391
|
};
|
|
391
392
|
|
|
392
393
|
pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
cachedLoadedSkills.
|
|
408
|
-
|
|
394
|
+
try {
|
|
395
|
+
// Only count successful reads.
|
|
396
|
+
if ((event as any).toolName !== "read") {return;}
|
|
397
|
+
if ((event as any).isError) {return;}
|
|
398
|
+
|
|
399
|
+
const input = (event as any).input as { path?: unknown } | undefined;
|
|
400
|
+
const p = typeof input?.path === "string" ? input.path : "";
|
|
401
|
+
if (!p) {return;}
|
|
402
|
+
|
|
403
|
+
ensureCaches(ctx);
|
|
404
|
+
const abs = normalizeReadPath(p, ctx.cwd);
|
|
405
|
+
const skillName = matchSkillForPath(abs);
|
|
406
|
+
if (!skillName) {return;}
|
|
407
|
+
|
|
408
|
+
if (!cachedLoadedSkills.has(skillName)) {
|
|
409
|
+
cachedLoadedSkills.add(skillName);
|
|
410
|
+
pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (error: unknown) {
|
|
414
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
409
415
|
}
|
|
410
416
|
});
|
|
411
417
|
|
|
@@ -52,6 +52,7 @@ import { promises as fs } from "node:fs";
|
|
|
52
52
|
import * as net from "node:net";
|
|
53
53
|
import * as os from "node:os";
|
|
54
54
|
import * as path from "node:path";
|
|
55
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
55
56
|
|
|
56
57
|
const CONTROL_FLAG = "session-control";
|
|
57
58
|
const CONTROL_TARGET_FLAG = "control-session";
|
|
@@ -1080,42 +1081,57 @@ export default function(pi: ExtensionAPI) {
|
|
|
1080
1081
|
};
|
|
1081
1082
|
|
|
1082
1083
|
pi.on("session_start", async(_event, ctx) => {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
cliSendHandled
|
|
1086
|
-
|
|
1084
|
+
try {
|
|
1085
|
+
await refreshServer(ctx);
|
|
1086
|
+
if (!cliSendHandled) {
|
|
1087
|
+
cliSendHandled = true;
|
|
1088
|
+
await maybeHandleStartupControlSend(pi, ctx);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
catch (error: unknown) {
|
|
1092
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1087
1093
|
}
|
|
1088
1094
|
});
|
|
1089
1095
|
|
|
1090
1096
|
pi.on("session_shutdown", async() => {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1097
|
+
try {
|
|
1098
|
+
if (state.aliasTimer) {
|
|
1099
|
+
clearInterval(state.aliasTimer);
|
|
1100
|
+
state.aliasTimer = null;
|
|
1101
|
+
}
|
|
1102
|
+
updateStatus(state.context, false);
|
|
1103
|
+
updateSessionEnv(state.context, false);
|
|
1104
|
+
await stopControlServer(state);
|
|
1105
|
+
}
|
|
1106
|
+
catch (error: unknown) {
|
|
1107
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1094
1108
|
}
|
|
1095
|
-
updateStatus(state.context, false);
|
|
1096
|
-
updateSessionEnv(state.context, false);
|
|
1097
|
-
await stopControlServer(state);
|
|
1098
1109
|
});
|
|
1099
1110
|
|
|
1100
1111
|
// Fire turn_end events to subscribers
|
|
1101
1112
|
pi.on("turn_end", (event: TurnEndEvent, ctx: ExtensionContext) => {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1113
|
+
try {
|
|
1114
|
+
if (state.turnEndSubscriptions.length === 0) {return;}
|
|
1115
|
+
|
|
1116
|
+
void syncAlias(state, ctx);
|
|
1117
|
+
const lastMessage = getLastAssistantMessage(ctx);
|
|
1118
|
+
const eventData = { message: lastMessage, turnIndex: event.turnIndex };
|
|
1119
|
+
|
|
1120
|
+
// Fire to all subscribers (one-shot)
|
|
1121
|
+
const subscriptions = [...state.turnEndSubscriptions];
|
|
1122
|
+
state.turnEndSubscriptions = [];
|
|
1123
|
+
|
|
1124
|
+
for (const sub of subscriptions) {
|
|
1125
|
+
writeEvent(sub.socket, {
|
|
1126
|
+
type: "event",
|
|
1127
|
+
event: "turn_end",
|
|
1128
|
+
data: eventData,
|
|
1129
|
+
subscriptionId: sub.subscriptionId,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch (error: unknown) {
|
|
1134
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1119
1135
|
}
|
|
1120
1136
|
});
|
|
1121
1137
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import { Box, Markdown, Text } from "@mariozechner/pi-tui";
|
|
13
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
13
14
|
|
|
14
15
|
const KNOWLEDGE_STATE_TYPE = "knowledge-session";
|
|
15
16
|
const KNOWLEDGE_WIDGET_KEY = "knowledge";
|
|
@@ -458,35 +459,60 @@ export default function(pi: ExtensionAPI) {
|
|
|
458
459
|
});
|
|
459
460
|
|
|
460
461
|
pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
|
|
461
|
-
|
|
462
|
+
try {
|
|
463
|
+
trackToolResult(pi, ctx, event);
|
|
464
|
+
}
|
|
465
|
+
catch (error: unknown) {
|
|
466
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
467
|
+
}
|
|
462
468
|
});
|
|
463
469
|
|
|
464
470
|
pi.on("session_start", async(_event, ctx) => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
471
|
+
try {
|
|
472
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
473
|
+
runCheck: false,
|
|
474
|
+
allowReminder: true,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch (error: unknown) {
|
|
478
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
479
|
+
}
|
|
469
480
|
});
|
|
470
481
|
|
|
471
482
|
pi.on("turn_end", async(_event: TurnEndEvent, ctx) => {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
483
|
+
try {
|
|
484
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
485
|
+
runCheck: false,
|
|
486
|
+
allowReminder: true,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
catch (error: unknown) {
|
|
490
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
491
|
+
}
|
|
476
492
|
});
|
|
477
493
|
|
|
478
494
|
pi.on("agent_end", async(_event, ctx) => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
ctx.
|
|
495
|
+
try {
|
|
496
|
+
const state = await refreshKnowledgeState(pi, ctx, {
|
|
497
|
+
runCheck: true,
|
|
498
|
+
allowReminder: true,
|
|
499
|
+
});
|
|
500
|
+
if (ctx.hasUI && state.commitWouldFail) {
|
|
501
|
+
ctx.ui.notify("Knowledge drift is severe or integrity checks failed. A commit would likely be blocked until knowledge is updated.", "warning");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (error: unknown) {
|
|
505
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
485
506
|
}
|
|
486
507
|
});
|
|
487
508
|
|
|
488
509
|
pi.on("session_shutdown", async(_event, ctx) => {
|
|
489
|
-
|
|
510
|
+
try {
|
|
511
|
+
updateKnowledgeWidget(ctx, defaultKnowledgeState());
|
|
512
|
+
}
|
|
513
|
+
catch (error: unknown) {
|
|
514
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
515
|
+
}
|
|
490
516
|
});
|
|
491
517
|
|
|
492
518
|
pi.registerMessageRenderer("knowledge-reminder", (message, { expanded }, theme) => {
|
|
@@ -12,6 +12,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
12
12
|
import { compact } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
14
14
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
15
16
|
|
|
16
17
|
type LoopMode = "tests" | "custom" | "self";
|
|
17
18
|
|
|
@@ -385,39 +386,50 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
385
386
|
});
|
|
386
387
|
|
|
387
388
|
pi.on("agent_end", async(event, ctx) => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
389
|
+
try {
|
|
390
|
+
if (!loopState.active) {return;}
|
|
391
|
+
|
|
392
|
+
if (ctx.hasUI && wasLastAssistantAborted(event.messages)) {
|
|
393
|
+
const confirm = await ctx.ui.confirm(
|
|
394
|
+
"Break active loop?",
|
|
395
|
+
"Operation aborted. Break out of the loop?",
|
|
396
|
+
);
|
|
397
|
+
if (confirm) {
|
|
398
|
+
breakLoop(ctx);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
398
401
|
}
|
|
399
|
-
}
|
|
400
402
|
|
|
401
|
-
|
|
403
|
+
triggerLoopPrompt(ctx);
|
|
404
|
+
}
|
|
405
|
+
catch (error: unknown) {
|
|
406
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
407
|
+
}
|
|
402
408
|
});
|
|
403
409
|
|
|
404
410
|
pi.on("session_before_compact", async(event, ctx) => {
|
|
405
|
-
if (!loopState.active || !loopState.mode || !ctx.model) {return;}
|
|
406
|
-
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
|
407
|
-
if (!apiKey) {return;}
|
|
408
|
-
|
|
409
|
-
const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
|
|
410
|
-
.filter(Boolean)
|
|
411
|
-
.join("\n\n");
|
|
412
|
-
|
|
413
411
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
412
|
+
if (!loopState.active || !loopState.mode || !ctx.model) {return;}
|
|
413
|
+
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
|
414
|
+
if (!apiKey) {return;}
|
|
415
|
+
|
|
416
|
+
const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join("\n\n");
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
|
|
422
|
+
return { compaction };
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (ctx.hasUI) {
|
|
425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
+
ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
420
429
|
}
|
|
430
|
+
}
|
|
431
|
+
catch (error: unknown) {
|
|
432
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
421
433
|
return;
|
|
422
434
|
}
|
|
423
435
|
});
|
|
@@ -440,6 +452,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
440
452
|
}
|
|
441
453
|
|
|
442
454
|
pi.on("session_start", async(_event, ctx) => {
|
|
443
|
-
|
|
455
|
+
try {
|
|
456
|
+
await restoreLoopState(ctx);
|
|
457
|
+
}
|
|
458
|
+
catch (error: unknown) {
|
|
459
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
460
|
+
}
|
|
444
461
|
});
|
|
445
462
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type IAskUserQuestion,
|
|
22
22
|
type IAskUserResponse,
|
|
23
23
|
} from "../shared/askUserProtocol";
|
|
24
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
24
25
|
|
|
25
26
|
const PLAN_OUTPUT_ROOT_SEGMENTS = [".docyrus", "plans"] as const;
|
|
26
27
|
const PLAN_STATE_TYPE = "plan-session";
|
|
@@ -1360,71 +1361,109 @@ export default function planExtension(pi: ExtensionAPI) {
|
|
|
1360
1361
|
});
|
|
1361
1362
|
|
|
1362
1363
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1364
|
+
try {
|
|
1365
|
+
const planState = getPlanState(ctx);
|
|
1366
|
+
const readOnlyState = getReadOnlyState(ctx);
|
|
1367
|
+
const overlays: string[] = [];
|
|
1366
1368
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1369
|
+
if (planState?.active) {
|
|
1370
|
+
overlays.push(buildPlanPromptOverlay(planState));
|
|
1371
|
+
}
|
|
1370
1372
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1373
|
+
if (readOnlyState?.active) {
|
|
1374
|
+
overlays.push(READONLY_MODE_SYSTEM_PROMPT);
|
|
1375
|
+
}
|
|
1374
1376
|
|
|
1375
|
-
|
|
1377
|
+
if (overlays.length === 0) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return {
|
|
1382
|
+
systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
catch (error: unknown) {
|
|
1386
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1376
1387
|
return;
|
|
1377
1388
|
}
|
|
1378
|
-
|
|
1379
|
-
return {
|
|
1380
|
-
systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
|
|
1381
|
-
};
|
|
1382
1389
|
});
|
|
1383
1390
|
|
|
1384
1391
|
pi.on("tool_call", (event, ctx) => {
|
|
1385
|
-
|
|
1392
|
+
try {
|
|
1393
|
+
return handleToolCallDuringPlan(event, ctx) ?? handleToolCallDuringReadOnly(event, ctx);
|
|
1394
|
+
}
|
|
1395
|
+
catch (error: unknown) {
|
|
1396
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1386
1399
|
});
|
|
1387
1400
|
|
|
1388
1401
|
pi.on("user_bash", (event, ctx) => {
|
|
1389
|
-
|
|
1402
|
+
try {
|
|
1403
|
+
return handleUserBashDuringPlan(event, ctx) ?? handleUserBashDuringReadOnly(event, ctx);
|
|
1404
|
+
}
|
|
1405
|
+
catch (error: unknown) {
|
|
1406
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1390
1409
|
});
|
|
1391
1410
|
|
|
1392
1411
|
pi.on("agent_end", async(event, ctx) => {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1412
|
+
try {
|
|
1413
|
+
const state = getPlanState(ctx);
|
|
1414
|
+
if (state?.active) {
|
|
1415
|
+
const text = extractLastAssistantText(event.messages ?? []);
|
|
1416
|
+
const askUserRequest = text ? parseAskUserRequestFromText(text) : undefined;
|
|
1417
|
+
if (askUserRequest && ctx.hasUI) {
|
|
1418
|
+
const response = await collectAskUserResponse(ctx, askUserRequest);
|
|
1419
|
+
if (response) {
|
|
1420
|
+
pi.sendUserMessage(formatAskUserResponsePrompt(response));
|
|
1421
|
+
}
|
|
1422
|
+
return;
|
|
1401
1423
|
}
|
|
1402
|
-
return;
|
|
1403
1424
|
}
|
|
1404
|
-
}
|
|
1405
1425
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1426
|
+
await writePlanArtifactFromEvent(event, ctx);
|
|
1427
|
+
await writeArchitectArtifactFromEvent(event, ctx);
|
|
1428
|
+
}
|
|
1429
|
+
catch (error: unknown) {
|
|
1430
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1431
|
+
}
|
|
1408
1432
|
});
|
|
1409
1433
|
|
|
1410
1434
|
pi.on("session_start", async(_event, ctx) => {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1435
|
+
try {
|
|
1436
|
+
await syncPlanState(pi, ctx);
|
|
1437
|
+
const readOnlyState = getReadOnlyState(ctx);
|
|
1438
|
+
currentReadOnlyState = readOnlyState;
|
|
1439
|
+
setReadOnlyWidget(ctx, readOnlyState);
|
|
1440
|
+
}
|
|
1441
|
+
catch (error: unknown) {
|
|
1442
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1443
|
+
}
|
|
1415
1444
|
});
|
|
1416
1445
|
|
|
1417
1446
|
pi.on("session_tree", async(_event, ctx) => {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1447
|
+
try {
|
|
1448
|
+
await syncPlanState(pi, ctx);
|
|
1449
|
+
const readOnlyState = getReadOnlyState(ctx);
|
|
1450
|
+
currentReadOnlyState = readOnlyState;
|
|
1451
|
+
setReadOnlyWidget(ctx, readOnlyState);
|
|
1452
|
+
}
|
|
1453
|
+
catch (error: unknown) {
|
|
1454
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1455
|
+
}
|
|
1422
1456
|
});
|
|
1423
1457
|
|
|
1424
1458
|
pi.on("session_shutdown", async(_event, ctx) => {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1459
|
+
try {
|
|
1460
|
+
currentPlanState = undefined;
|
|
1461
|
+
setPlanWidget(ctx, undefined);
|
|
1462
|
+
currentReadOnlyState = undefined;
|
|
1463
|
+
setReadOnlyWidget(ctx, undefined);
|
|
1464
|
+
}
|
|
1465
|
+
catch (error: unknown) {
|
|
1466
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
1467
|
+
}
|
|
1429
1468
|
});
|
|
1430
1469
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Container, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
3
4
|
|
|
4
5
|
const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
|
|
5
6
|
const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
|
|
@@ -92,20 +93,30 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
|
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
pi.on("before_agent_start", async(event, ctx) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
try {
|
|
97
|
+
if (!ctx.hasUI) {return;}
|
|
98
|
+
const match = extractPromptMatch(event.prompt);
|
|
99
|
+
if (!match) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setWidget(ctx, match);
|
|
104
|
+
applySessionName(ctx, match);
|
|
105
|
+
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
|
106
|
+
try {
|
|
107
|
+
const title = meta?.title?.trim();
|
|
108
|
+
const authorText = formatAuthor(meta?.author);
|
|
109
|
+
setWidget(ctx, match, title, authorText);
|
|
110
|
+
applySessionName(ctx, match, title);
|
|
111
|
+
}
|
|
112
|
+
catch (error: unknown) {
|
|
113
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error: unknown) {
|
|
118
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
99
119
|
}
|
|
100
|
-
|
|
101
|
-
setWidget(ctx, match);
|
|
102
|
-
applySessionName(ctx, match);
|
|
103
|
-
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
|
104
|
-
const title = meta?.title?.trim();
|
|
105
|
-
const authorText = formatAuthor(meta?.author);
|
|
106
|
-
setWidget(ctx, match, title, authorText);
|
|
107
|
-
applySessionName(ctx, match, title);
|
|
108
|
-
});
|
|
109
120
|
});
|
|
110
121
|
|
|
111
122
|
const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
|
|
@@ -141,14 +152,24 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
|
|
|
141
152
|
setWidget(ctx, match);
|
|
142
153
|
applySessionName(ctx, match);
|
|
143
154
|
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
try {
|
|
156
|
+
const title = meta?.title?.trim();
|
|
157
|
+
const authorText = formatAuthor(meta?.author);
|
|
158
|
+
setWidget(ctx, match, title, authorText);
|
|
159
|
+
applySessionName(ctx, match, title);
|
|
160
|
+
}
|
|
161
|
+
catch (error: unknown) {
|
|
162
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
163
|
+
}
|
|
148
164
|
});
|
|
149
165
|
};
|
|
150
166
|
|
|
151
167
|
pi.on("session_start", async(_event, ctx) => {
|
|
152
|
-
|
|
168
|
+
try {
|
|
169
|
+
rebuildFromSession(ctx);
|
|
170
|
+
}
|
|
171
|
+
catch (error: unknown) {
|
|
172
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
173
|
+
}
|
|
153
174
|
});
|
|
154
175
|
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "@mariozechner/pi-tui";
|
|
43
43
|
import path from "node:path";
|
|
44
44
|
import { promises as fs } from "node:fs";
|
|
45
|
+
import { isStaleExtensionError } from "../shared/extensionLifecycle";
|
|
45
46
|
|
|
46
47
|
// State to track fresh session review (where we branched from).
|
|
47
48
|
// Module-level state means only one review can be active at a time.
|
|
@@ -935,11 +936,21 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
935
936
|
}
|
|
936
937
|
|
|
937
938
|
pi.on("session_start", (_event, ctx) => {
|
|
938
|
-
|
|
939
|
+
try {
|
|
940
|
+
applyAllReviewState(ctx);
|
|
941
|
+
}
|
|
942
|
+
catch (error: unknown) {
|
|
943
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
944
|
+
}
|
|
939
945
|
});
|
|
940
946
|
|
|
941
947
|
pi.on("session_tree", (_event, ctx) => {
|
|
942
|
-
|
|
948
|
+
try {
|
|
949
|
+
applyAllReviewState(ctx);
|
|
950
|
+
}
|
|
951
|
+
catch (error: unknown) {
|
|
952
|
+
if (!isStaleExtensionError(error)) {throw error;}
|
|
953
|
+
}
|
|
943
954
|
});
|
|
944
955
|
|
|
945
956
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi invalidates the captured `pi` / `ctx` held by an extension factory closure
|
|
3
|
+
* after session replacement (`/new`, `/clear`, `/resume`, `/reload`, etc.).
|
|
4
|
+
* Async handlers that started before the replacement resume against the stale
|
|
5
|
+
* runtime and throw — the runner catches and rethrows via `emitError`, which
|
|
6
|
+
* surfaces as a visible TUI error. Use this helper to ignore only that one
|
|
7
|
+
* well-known error and let every other failure bubble.
|
|
8
|
+
*/
|
|
9
|
+
export function isStaleExtensionError(error: unknown): boolean {
|
|
10
|
+
return error instanceof Error && /stale after session replacement or reload/i.test(error.message);
|
|
11
|
+
}
|