pi-anchor 0.1.0 → 0.2.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/dist/{chunk-NNYUD5O7.js → chunk-T62PHIQI.js} +33 -20
- package/dist/extension.cjs +128 -21
- package/dist/extension.js +96 -2
- package/dist/index.cjs +33 -20
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/context_injector.ts +14 -5
- package/src/extension.ts +102 -2
- package/src/pi-types.d.ts +2 -0
- package/src/tui.ts +31 -18
|
@@ -931,16 +931,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
931
931
|
}
|
|
932
932
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
933
933
|
}
|
|
934
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
934
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
935
935
|
let cwd;
|
|
936
936
|
let now = Date.now();
|
|
937
937
|
if (typeof cwdOrNow === "number") {
|
|
938
938
|
now = cwdOrNow;
|
|
939
939
|
} else if (typeof cwdOrNow === "string") {
|
|
940
940
|
cwd = cwdOrNow;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
941
|
+
}
|
|
942
|
+
if (typeof nowArg === "number") {
|
|
943
|
+
now = nowArg;
|
|
944
944
|
}
|
|
945
945
|
sweepStore(store, now);
|
|
946
946
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -968,7 +968,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
968
968
|
}
|
|
969
969
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
970
970
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
971
|
-
|
|
971
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
972
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
973
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
972
974
|
});
|
|
973
975
|
return [
|
|
974
976
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -977,6 +979,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
977
979
|
``,
|
|
978
980
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
979
981
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
982
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
980
983
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
981
984
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
982
985
|
`</active-anchors>`
|
|
@@ -1191,22 +1194,32 @@ function updateStartupBanner(ctx, store) {
|
|
|
1191
1194
|
}
|
|
1192
1195
|
const groups = groupAnchorsByQuadrant(active);
|
|
1193
1196
|
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1194
|
-
const lines = [
|
|
1195
|
-
`\u2316 Anchor \xB7 ${active.length} active commitments:`
|
|
1196
|
-
];
|
|
1197
1197
|
const topItems = sorted.slice(0, 3);
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1198
|
+
const factory = (_tui, theme) => ({
|
|
1199
|
+
render: (width) => {
|
|
1200
|
+
const maxTitleWidth = Math.max(16, width - 36);
|
|
1201
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1202
|
+
const lines = [
|
|
1203
|
+
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1204
|
+
];
|
|
1205
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
1206
|
+
const a = topItems[i];
|
|
1207
|
+
const num = (i + 1).toString().padStart(2, "0");
|
|
1208
|
+
const q = classifyAnchor(a);
|
|
1209
|
+
const tgt = formatTargetDate(a);
|
|
1210
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1211
|
+
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1212
|
+
}
|
|
1213
|
+
if (sorted.length > 3) {
|
|
1214
|
+
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1215
|
+
}
|
|
1216
|
+
return lines;
|
|
1217
|
+
},
|
|
1218
|
+
invalidate: () => {
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1222
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
|
|
1210
1223
|
}
|
|
1211
1224
|
async function openAnchorDashboard(ctx, store) {
|
|
1212
1225
|
if (!ctx.hasUI || !ctx.ui) return;
|
package/dist/extension.cjs
CHANGED
|
@@ -965,16 +965,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
965
965
|
}
|
|
966
966
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
967
967
|
}
|
|
968
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
968
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
969
969
|
let cwd;
|
|
970
970
|
let now = Date.now();
|
|
971
971
|
if (typeof cwdOrNow === "number") {
|
|
972
972
|
now = cwdOrNow;
|
|
973
973
|
} else if (typeof cwdOrNow === "string") {
|
|
974
974
|
cwd = cwdOrNow;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
975
|
+
}
|
|
976
|
+
if (typeof nowArg === "number") {
|
|
977
|
+
now = nowArg;
|
|
978
978
|
}
|
|
979
979
|
sweepStore(store, now);
|
|
980
980
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -1002,7 +1002,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1002
1002
|
}
|
|
1003
1003
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
1004
1004
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
1005
|
-
|
|
1005
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
1006
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
1007
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
1006
1008
|
});
|
|
1007
1009
|
return [
|
|
1008
1010
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -1011,6 +1013,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1011
1013
|
``,
|
|
1012
1014
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
1013
1015
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
1016
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
1014
1017
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
1015
1018
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
1016
1019
|
`</active-anchors>`
|
|
@@ -1205,22 +1208,32 @@ function updateStartupBanner(ctx, store) {
|
|
|
1205
1208
|
}
|
|
1206
1209
|
const groups = groupAnchorsByQuadrant(active);
|
|
1207
1210
|
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1208
|
-
const lines = [
|
|
1209
|
-
`\u2316 Anchor \xB7 ${active.length} active commitments:`
|
|
1210
|
-
];
|
|
1211
1211
|
const topItems = sorted.slice(0, 3);
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1212
|
+
const factory = (_tui, theme) => ({
|
|
1213
|
+
render: (width) => {
|
|
1214
|
+
const maxTitleWidth = Math.max(16, width - 36);
|
|
1215
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1216
|
+
const lines = [
|
|
1217
|
+
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1218
|
+
];
|
|
1219
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
1220
|
+
const a = topItems[i];
|
|
1221
|
+
const num = (i + 1).toString().padStart(2, "0");
|
|
1222
|
+
const q = classifyAnchor(a);
|
|
1223
|
+
const tgt = formatTargetDate(a);
|
|
1224
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1225
|
+
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (sorted.length > 3) {
|
|
1228
|
+
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1229
|
+
}
|
|
1230
|
+
return lines;
|
|
1231
|
+
},
|
|
1232
|
+
invalidate: () => {
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1236
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
|
|
1224
1237
|
}
|
|
1225
1238
|
async function openAnchorDashboard(ctx, store) {
|
|
1226
1239
|
if (!ctx.hasUI || !ctx.ui) return;
|
|
@@ -1267,18 +1280,52 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1267
1280
|
|
|
1268
1281
|
// src/extension.ts
|
|
1269
1282
|
var import_node_child_process2 = require("child_process");
|
|
1283
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
1284
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1270
1285
|
var MUTATION_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
1271
1286
|
function extension_default(pi) {
|
|
1272
1287
|
const store = new AnchorStore();
|
|
1273
1288
|
const observer = new SessionTouchObserver();
|
|
1274
1289
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
1290
|
+
const preflightFailures = /* @__PURE__ */ new Map();
|
|
1275
1291
|
pi.on("session_start", async (event, ctx) => {
|
|
1276
1292
|
observer.clear();
|
|
1277
1293
|
annotatedThisSession.clear();
|
|
1294
|
+
preflightFailures.clear();
|
|
1278
1295
|
const sweep = sweepStore(store);
|
|
1279
1296
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
1280
1297
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
|
|
1281
1298
|
}
|
|
1299
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1300
|
+
for (const a of activeAnchors) {
|
|
1301
|
+
if (a.verifyCommand) {
|
|
1302
|
+
try {
|
|
1303
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
1304
|
+
if (res.success) {
|
|
1305
|
+
store.settle(a.id, {
|
|
1306
|
+
settledBy: "verification-test",
|
|
1307
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
1308
|
+
});
|
|
1309
|
+
ctx.ui.notify(`\u2693 \u7269\u7406\u9A8C\u8BC1\u901A\u8FC7\uFF01\u951A\u70B9 #${a.id} ("${a.title}") \u5DF2\u81EA\u52A8\u7ED3\u6848\u5F52\u6863\u3002`, "info");
|
|
1310
|
+
} else {
|
|
1311
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
1312
|
+
}
|
|
1313
|
+
} catch (_) {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
try {
|
|
1318
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1319
|
+
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1320
|
+
if (import_node_fs3.default.existsSync(pendingPath)) {
|
|
1321
|
+
const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
|
|
1322
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
|
|
1323
|
+
ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
|
|
1324
|
+
}
|
|
1325
|
+
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1326
|
+
}
|
|
1327
|
+
} catch (_) {
|
|
1328
|
+
}
|
|
1282
1329
|
updateAnchorStatusBar(ctx, store);
|
|
1283
1330
|
if (event.reason !== "resume") {
|
|
1284
1331
|
updateStartupBanner(ctx, store);
|
|
@@ -1294,7 +1341,7 @@ function extension_default(pi) {
|
|
|
1294
1341
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
1295
1342
|
const isColdStart = messageTurns.length <= 1;
|
|
1296
1343
|
if (isColdStart) {
|
|
1297
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
1344
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
|
|
1298
1345
|
if (contextSnippet) {
|
|
1299
1346
|
return {
|
|
1300
1347
|
systemPrompt: `${event.systemPrompt}
|
|
@@ -1426,6 +1473,42 @@ Mark as completed and archive?`
|
|
|
1426
1473
|
verifyCommand: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional shell command for automated physical verification" })),
|
|
1427
1474
|
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
|
|
1428
1475
|
}),
|
|
1476
|
+
renderCall(args, theme) {
|
|
1477
|
+
const action = args.action || "list";
|
|
1478
|
+
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
1479
|
+
if (action === "pin" && args.title) {
|
|
1480
|
+
const due = args.targetDate ? ` ${theme.fg("dim", `[${args.targetDate}]`)}` : "";
|
|
1481
|
+
const files = args.files && args.files.length > 0 ? ` ${theme.fg("muted", `(${args.files.slice(0, 2).join(", ")})`)}` : "";
|
|
1482
|
+
title += ` "${args.title}"${due}${files}`;
|
|
1483
|
+
} else if (action === "settle" && args.id) {
|
|
1484
|
+
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
1485
|
+
} else if (action === "touch" && args.id) {
|
|
1486
|
+
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
1487
|
+
} else if (action === "list") {
|
|
1488
|
+
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
render: () => [title],
|
|
1492
|
+
invalidate: () => {
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
},
|
|
1496
|
+
renderResult(result, _options, theme) {
|
|
1497
|
+
if (result.isError) {
|
|
1498
|
+
const errText = result.content?.[0]?.text || "Execution failed";
|
|
1499
|
+
return {
|
|
1500
|
+
render: () => [theme.fg("error", `\u2717 ${errText}`)],
|
|
1501
|
+
invalidate: () => {
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const text = result.content?.[0]?.text || "OK";
|
|
1506
|
+
return {
|
|
1507
|
+
render: () => [theme.fg("success", `\u2714 ${text}`)],
|
|
1508
|
+
invalidate: () => {
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
},
|
|
1429
1512
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1430
1513
|
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
1431
1514
|
if (params.action === "pin") {
|
|
@@ -1557,4 +1640,28 @@ Mark as completed and archive?`
|
|
|
1557
1640
|
updateAnchorStatusBar(ctx, store);
|
|
1558
1641
|
}
|
|
1559
1642
|
});
|
|
1643
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1644
|
+
if (event.reason === "quit") {
|
|
1645
|
+
try {
|
|
1646
|
+
const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
1647
|
+
cwd: ctx.cwd,
|
|
1648
|
+
encoding: "utf-8",
|
|
1649
|
+
timeout: 2e3,
|
|
1650
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1651
|
+
}).trim();
|
|
1652
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1653
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
1654
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1655
|
+
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1656
|
+
const data = {
|
|
1657
|
+
cwd: ctx.cwd,
|
|
1658
|
+
timestamp: Date.now(),
|
|
1659
|
+
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1660
|
+
};
|
|
1661
|
+
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1662
|
+
}
|
|
1663
|
+
} catch (_) {
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1560
1667
|
}
|
package/dist/extension.js
CHANGED
|
@@ -11,23 +11,57 @@ import {
|
|
|
11
11
|
sweepStore,
|
|
12
12
|
updateAnchorStatusBar,
|
|
13
13
|
updateStartupBanner
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-T62PHIQI.js";
|
|
15
15
|
|
|
16
16
|
// src/extension.ts
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
18
|
import { execSync } from "child_process";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
19
21
|
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
20
22
|
function extension_default(pi) {
|
|
21
23
|
const store = new AnchorStore();
|
|
22
24
|
const observer = new SessionTouchObserver();
|
|
23
25
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
26
|
+
const preflightFailures = /* @__PURE__ */ new Map();
|
|
24
27
|
pi.on("session_start", async (event, ctx) => {
|
|
25
28
|
observer.clear();
|
|
26
29
|
annotatedThisSession.clear();
|
|
30
|
+
preflightFailures.clear();
|
|
27
31
|
const sweep = sweepStore(store);
|
|
28
32
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
29
33
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
|
|
30
34
|
}
|
|
35
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
36
|
+
for (const a of activeAnchors) {
|
|
37
|
+
if (a.verifyCommand) {
|
|
38
|
+
try {
|
|
39
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
40
|
+
if (res.success) {
|
|
41
|
+
store.settle(a.id, {
|
|
42
|
+
settledBy: "verification-test",
|
|
43
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
44
|
+
});
|
|
45
|
+
ctx.ui.notify(`\u2693 \u7269\u7406\u9A8C\u8BC1\u901A\u8FC7\uFF01\u951A\u70B9 #${a.id} ("${a.title}") \u5DF2\u81EA\u52A8\u7ED3\u6848\u5F52\u6863\u3002`, "info");
|
|
46
|
+
} else {
|
|
47
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
55
|
+
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
56
|
+
if (fs.existsSync(pendingPath)) {
|
|
57
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, "utf-8"));
|
|
58
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
|
|
59
|
+
ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
|
|
60
|
+
}
|
|
61
|
+
fs.unlinkSync(pendingPath);
|
|
62
|
+
}
|
|
63
|
+
} catch (_) {
|
|
64
|
+
}
|
|
31
65
|
updateAnchorStatusBar(ctx, store);
|
|
32
66
|
if (event.reason !== "resume") {
|
|
33
67
|
updateStartupBanner(ctx, store);
|
|
@@ -43,7 +77,7 @@ function extension_default(pi) {
|
|
|
43
77
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
44
78
|
const isColdStart = messageTurns.length <= 1;
|
|
45
79
|
if (isColdStart) {
|
|
46
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
80
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
|
|
47
81
|
if (contextSnippet) {
|
|
48
82
|
return {
|
|
49
83
|
systemPrompt: `${event.systemPrompt}
|
|
@@ -175,6 +209,42 @@ Mark as completed and archive?`
|
|
|
175
209
|
verifyCommand: Type.Optional(Type.String({ description: "Optional shell command for automated physical verification" })),
|
|
176
210
|
id: Type.Optional(Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
|
|
177
211
|
}),
|
|
212
|
+
renderCall(args, theme) {
|
|
213
|
+
const action = args.action || "list";
|
|
214
|
+
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
215
|
+
if (action === "pin" && args.title) {
|
|
216
|
+
const due = args.targetDate ? ` ${theme.fg("dim", `[${args.targetDate}]`)}` : "";
|
|
217
|
+
const files = args.files && args.files.length > 0 ? ` ${theme.fg("muted", `(${args.files.slice(0, 2).join(", ")})`)}` : "";
|
|
218
|
+
title += ` "${args.title}"${due}${files}`;
|
|
219
|
+
} else if (action === "settle" && args.id) {
|
|
220
|
+
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
221
|
+
} else if (action === "touch" && args.id) {
|
|
222
|
+
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
223
|
+
} else if (action === "list") {
|
|
224
|
+
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
render: () => [title],
|
|
228
|
+
invalidate: () => {
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
renderResult(result, _options, theme) {
|
|
233
|
+
if (result.isError) {
|
|
234
|
+
const errText = result.content?.[0]?.text || "Execution failed";
|
|
235
|
+
return {
|
|
236
|
+
render: () => [theme.fg("error", `\u2717 ${errText}`)],
|
|
237
|
+
invalidate: () => {
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const text = result.content?.[0]?.text || "OK";
|
|
242
|
+
return {
|
|
243
|
+
render: () => [theme.fg("success", `\u2714 ${text}`)],
|
|
244
|
+
invalidate: () => {
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
},
|
|
178
248
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
179
249
|
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
180
250
|
if (params.action === "pin") {
|
|
@@ -306,6 +376,30 @@ Mark as completed and archive?`
|
|
|
306
376
|
updateAnchorStatusBar(ctx, store);
|
|
307
377
|
}
|
|
308
378
|
});
|
|
379
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
380
|
+
if (event.reason === "quit") {
|
|
381
|
+
try {
|
|
382
|
+
const gitStatus = execSync("git status --porcelain", {
|
|
383
|
+
cwd: ctx.cwd,
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
timeout: 2e3,
|
|
386
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
387
|
+
}).trim();
|
|
388
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
389
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
390
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
391
|
+
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
392
|
+
const data = {
|
|
393
|
+
cwd: ctx.cwd,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
396
|
+
};
|
|
397
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
398
|
+
}
|
|
399
|
+
} catch (_) {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
309
403
|
}
|
|
310
404
|
export {
|
|
311
405
|
extension_default as default
|
package/dist/index.cjs
CHANGED
|
@@ -1038,22 +1038,32 @@ function updateStartupBanner(ctx, store) {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
const groups = groupAnchorsByQuadrant(active);
|
|
1040
1040
|
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1041
|
-
const lines = [
|
|
1042
|
-
`\u2316 Anchor \xB7 ${active.length} active commitments:`
|
|
1043
|
-
];
|
|
1044
1041
|
const topItems = sorted.slice(0, 3);
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1042
|
+
const factory = (_tui, theme) => ({
|
|
1043
|
+
render: (width) => {
|
|
1044
|
+
const maxTitleWidth = Math.max(16, width - 36);
|
|
1045
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1046
|
+
const lines = [
|
|
1047
|
+
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1048
|
+
];
|
|
1049
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
1050
|
+
const a = topItems[i];
|
|
1051
|
+
const num = (i + 1).toString().padStart(2, "0");
|
|
1052
|
+
const q = classifyAnchor(a);
|
|
1053
|
+
const tgt = formatTargetDate(a);
|
|
1054
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1055
|
+
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1056
|
+
}
|
|
1057
|
+
if (sorted.length > 3) {
|
|
1058
|
+
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1059
|
+
}
|
|
1060
|
+
return lines;
|
|
1061
|
+
},
|
|
1062
|
+
invalidate: () => {
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1066
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
|
|
1057
1067
|
}
|
|
1058
1068
|
async function openAnchorDashboard(ctx, store) {
|
|
1059
1069
|
if (!ctx.hasUI || !ctx.ui) return;
|
|
@@ -1276,16 +1286,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
1276
1286
|
}
|
|
1277
1287
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
1278
1288
|
}
|
|
1279
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
1289
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
1280
1290
|
let cwd;
|
|
1281
1291
|
let now = Date.now();
|
|
1282
1292
|
if (typeof cwdOrNow === "number") {
|
|
1283
1293
|
now = cwdOrNow;
|
|
1284
1294
|
} else if (typeof cwdOrNow === "string") {
|
|
1285
1295
|
cwd = cwdOrNow;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof nowArg === "number") {
|
|
1298
|
+
now = nowArg;
|
|
1289
1299
|
}
|
|
1290
1300
|
sweepStore(store, now);
|
|
1291
1301
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -1313,7 +1323,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1313
1323
|
}
|
|
1314
1324
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
1315
1325
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
1316
|
-
|
|
1326
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
1327
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
1328
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
1317
1329
|
});
|
|
1318
1330
|
return [
|
|
1319
1331
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -1322,6 +1334,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1322
1334
|
``,
|
|
1323
1335
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
1324
1336
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
1337
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
1325
1338
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
1326
1339
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
1327
1340
|
`</active-anchors>`
|
package/dist/index.d.cts
CHANGED
|
@@ -397,7 +397,7 @@ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): strin
|
|
|
397
397
|
* Only active anchors for the current project context are injected.
|
|
398
398
|
* From turn 2 onwards, this returns empty string (0 tokens).
|
|
399
399
|
*/
|
|
400
|
-
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
|
|
400
|
+
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number, preflightFailures?: Map<string, string>): string;
|
|
401
401
|
/** Legacy alias for backwards compatibility */
|
|
402
402
|
declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
|
|
403
403
|
|
package/dist/index.d.ts
CHANGED
|
@@ -397,7 +397,7 @@ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): strin
|
|
|
397
397
|
* Only active anchors for the current project context are injected.
|
|
398
398
|
* From turn 2 onwards, this returns empty string (0 tokens).
|
|
399
399
|
*/
|
|
400
|
-
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
|
|
400
|
+
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number, preflightFailures?: Map<string, string>): string;
|
|
401
401
|
/** Legacy alias for backwards compatibility */
|
|
402
402
|
declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
|
|
403
403
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/context_injector.ts
CHANGED
|
@@ -89,7 +89,8 @@ export function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): string
|
|
|
89
89
|
export function renderColdStartAnchorsContext(
|
|
90
90
|
store: AnchorStore,
|
|
91
91
|
cwdOrNow?: string | number,
|
|
92
|
-
nowArg?: number
|
|
92
|
+
nowArg?: number,
|
|
93
|
+
preflightFailures?: Map<string, string>
|
|
93
94
|
): string {
|
|
94
95
|
let cwd: string | undefined;
|
|
95
96
|
let now = Date.now();
|
|
@@ -98,9 +99,9 @@ export function renderColdStartAnchorsContext(
|
|
|
98
99
|
now = cwdOrNow;
|
|
99
100
|
} else if (typeof cwdOrNow === 'string') {
|
|
100
101
|
cwd = cwdOrNow;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
}
|
|
103
|
+
if (typeof nowArg === 'number') {
|
|
104
|
+
now = nowArg;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// 1. Run opportunistic sweep to ensure expired anchors don't leak into context
|
|
@@ -137,7 +138,14 @@ export function renderColdStartAnchorsContext(
|
|
|
137
138
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1000));
|
|
138
139
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : '';
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
142
|
+
const verifyHint = failureSnippet
|
|
143
|
+
? ` [⚡ 物理验证失败: ${failureSnippet.replace(/\s+/g, ' ').slice(-100)}]`
|
|
144
|
+
: a.verifyCommand
|
|
145
|
+
? ` [verify: ${a.verifyCommand}]`
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
return `• #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
141
149
|
});
|
|
142
150
|
|
|
143
151
|
return [
|
|
@@ -147,6 +155,7 @@ export function renderColdStartAnchorsContext(
|
|
|
147
155
|
``,
|
|
148
156
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
149
157
|
`- If the user opens the session with a casual greeting or continuation ("hi", "在吗", "继续", "早上好"), naturally prioritize items tagged [今日聚焦·Due Today] in a friendly assistant tone (e.g. "嗨!今天规划的【...】现在处理还是看别的?").`,
|
|
158
|
+
`- If an anchor has [⚡ 物理验证失败], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
150
159
|
`- If the user issues a direct, concrete new instruction (e.g. "帮我查下这个报错"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
151
160
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
152
161
|
`</active-anchors>`
|
package/src/extension.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizePath, findMatchedAnchors } from './matcher.ts';
|
|
|
8
8
|
import { sweepStore } from './decay.ts';
|
|
9
9
|
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner } from './tui.ts';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
|
|
13
14
|
const MUTATION_TOOLS = new Set(['edit', 'write', 'patch', 'apply_diff', 'create_file', 'modify']);
|
|
@@ -17,17 +18,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
18
|
const store = new AnchorStore();
|
|
18
19
|
const observer = new SessionTouchObserver();
|
|
19
20
|
const annotatedThisSession = new Set<string>();
|
|
21
|
+
const preflightFailures = new Map<string, string>();
|
|
20
22
|
|
|
21
|
-
// 1. Session start: sweep stale tasks,
|
|
23
|
+
// 1. Session start: sweep stale tasks, run physical pre-flight tests, update TUI status bar
|
|
22
24
|
pi.on('session_start', async (event: any, ctx: ExtensionContext) => {
|
|
23
25
|
observer.clear();
|
|
24
26
|
annotatedThisSession.clear();
|
|
27
|
+
preflightFailures.clear();
|
|
28
|
+
|
|
25
29
|
const sweep = sweepStore(store);
|
|
26
30
|
|
|
27
31
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
28
32
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} 个非活跃任务已进入休眠`, 'info');
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// Physical Pre-flight Verification: auto-settle passing tasks or record failing output
|
|
36
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter(a => a.status === 'active');
|
|
37
|
+
for (const a of activeAnchors) {
|
|
38
|
+
if (a.verifyCommand) {
|
|
39
|
+
try {
|
|
40
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
41
|
+
if (res.success) {
|
|
42
|
+
store.settle(a.id, {
|
|
43
|
+
settledBy: 'verification-test',
|
|
44
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
45
|
+
});
|
|
46
|
+
ctx.ui.notify(`⚓ 物理验证通过!锚点 #${a.id} ("${a.title}") 已自动结案归档。`, 'info');
|
|
47
|
+
} else {
|
|
48
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
49
|
+
}
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exit Guard notification check
|
|
55
|
+
try {
|
|
56
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
57
|
+
const pendingPath = path.join(home, '.anchor', 'pending_exit.json');
|
|
58
|
+
if (fs.existsSync(pendingPath)) {
|
|
59
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
60
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1000) {
|
|
61
|
+
ctx.ui.notify(`⌖ [Anchor 退出守护] 检测到上次异常退出且有 ${pending.modifiedCount} 个未提交改动,可用 anchor(action="pin") 固化任务。`, 'info');
|
|
62
|
+
}
|
|
63
|
+
fs.unlinkSync(pendingPath);
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
|
|
31
67
|
updateAnchorStatusBar(ctx, store);
|
|
32
68
|
|
|
33
69
|
// Only render startup banner on initial startup or new session, not on mid-session resume
|
|
@@ -50,7 +86,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
86
|
const isColdStart = messageTurns.length <= 1;
|
|
51
87
|
|
|
52
88
|
if (isColdStart) {
|
|
53
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
89
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, undefined, preflightFailures);
|
|
54
90
|
if (contextSnippet) {
|
|
55
91
|
return {
|
|
56
92
|
systemPrompt: `${event.systemPrompt}\n\n${contextSnippet}`
|
|
@@ -212,6 +248,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
212
248
|
verifyCommand: Type.Optional(Type.String({ description: 'Optional shell command for automated physical verification' })),
|
|
213
249
|
id: Type.Optional(Type.String({ description: 'Anchor ID, e.g. anc-1 (for settle or touch)' }))
|
|
214
250
|
}),
|
|
251
|
+
renderCall(args: any, theme: any) {
|
|
252
|
+
const action = args.action || 'list';
|
|
253
|
+
let title = theme.fg('toolTitle', theme.bold('⌖ anchor ')) + theme.fg('accent', action);
|
|
254
|
+
|
|
255
|
+
if (action === 'pin' && args.title) {
|
|
256
|
+
const due = args.targetDate ? ` ${theme.fg('dim', `[${args.targetDate}]`)}` : '';
|
|
257
|
+
const files = (args.files && args.files.length > 0)
|
|
258
|
+
? ` ${theme.fg('muted', `(${args.files.slice(0, 2).join(', ')})`)}`
|
|
259
|
+
: '';
|
|
260
|
+
title += ` "${args.title}"${due}${files}`;
|
|
261
|
+
} else if (action === 'settle' && args.id) {
|
|
262
|
+
title += ` ${theme.fg('dim', `#${args.id}`)}`;
|
|
263
|
+
} else if (action === 'touch' && args.id) {
|
|
264
|
+
title += ` ${theme.fg('dim', `#${args.id}`)}`;
|
|
265
|
+
} else if (action === 'list') {
|
|
266
|
+
if (args.priority) title += ` ${theme.fg('muted', `[${args.priority}]`)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
render: () => [title],
|
|
271
|
+
invalidate: () => {}
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
renderResult(result: any, _options: any, theme: any) {
|
|
275
|
+
if (result.isError) {
|
|
276
|
+
const errText = result.content?.[0]?.text || 'Execution failed';
|
|
277
|
+
return {
|
|
278
|
+
render: () => [theme.fg('error', `✗ ${errText}`)],
|
|
279
|
+
invalidate: () => {}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const text = result.content?.[0]?.text || 'OK';
|
|
284
|
+
return {
|
|
285
|
+
render: () => [theme.fg('success', `✔ ${text}`)],
|
|
286
|
+
invalidate: () => {}
|
|
287
|
+
};
|
|
288
|
+
},
|
|
215
289
|
async execute(_toolCallId: string, params: any, _signal?: AbortSignal, _onUpdate?: (partial: any) => void, ctx?: ExtensionContext) {
|
|
216
290
|
if (!ctx) return { content: [{ type: 'text', text: 'Error: context required' }], isError: true };
|
|
217
291
|
if (params.action === 'pin') {
|
|
@@ -360,4 +434,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
360
434
|
updateAnchorStatusBar(ctx, store);
|
|
361
435
|
}
|
|
362
436
|
});
|
|
437
|
+
|
|
438
|
+
// 7. Exit Guard: detect uncommitted changes on abrupt session shutdown
|
|
439
|
+
pi.on('session_shutdown', async (event: any, ctx: ExtensionContext) => {
|
|
440
|
+
if (event.reason === 'quit') {
|
|
441
|
+
try {
|
|
442
|
+
const gitStatus = execSync('git status --porcelain', {
|
|
443
|
+
cwd: ctx.cwd,
|
|
444
|
+
encoding: 'utf-8',
|
|
445
|
+
timeout: 2000,
|
|
446
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
447
|
+
}).trim();
|
|
448
|
+
|
|
449
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter(a => a.status === 'active');
|
|
450
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
451
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
452
|
+
const pendingPath = path.join(home, '.anchor', 'pending_exit.json');
|
|
453
|
+
const data = {
|
|
454
|
+
cwd: ctx.cwd,
|
|
455
|
+
timestamp: Date.now(),
|
|
456
|
+
modifiedCount: gitStatus.split('\n').filter(Boolean).length
|
|
457
|
+
};
|
|
458
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
459
|
+
}
|
|
460
|
+
} catch (_) {}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
363
463
|
}
|
package/src/pi-types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ declare module '@earendil-works/pi-coding-agent' {
|
|
|
18
18
|
promptSnippet?: string;
|
|
19
19
|
promptGuidelines?: string[];
|
|
20
20
|
parameters: unknown;
|
|
21
|
+
renderCall?: (args: any, theme: any, context?: any) => any;
|
|
22
|
+
renderResult?: (result: any, options: any, theme: any, context?: any) => any;
|
|
21
23
|
execute: (
|
|
22
24
|
toolCallId: string,
|
|
23
25
|
params: any,
|
package/src/tui.ts
CHANGED
|
@@ -298,26 +298,39 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
|
|
|
298
298
|
|
|
299
299
|
const groups = groupAnchorsByQuadrant(active);
|
|
300
300
|
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
301
|
-
|
|
302
|
-
const lines: string[] = [
|
|
303
|
-
`⌖ Anchor · ${active.length} active commitments:`
|
|
304
|
-
];
|
|
305
|
-
|
|
306
301
|
const topItems = sorted.slice(0, 3);
|
|
307
|
-
for (let i = 0; i < topItems.length; i++) {
|
|
308
|
-
const a = topItems[i];
|
|
309
|
-
const num = (i + 1).toString().padStart(2, '0');
|
|
310
|
-
const q = classifyAnchor(a);
|
|
311
|
-
const tgt = formatTargetDate(a);
|
|
312
|
-
const titlePadded = a.title.length > 34 ? a.title.slice(0, 32) + '..' : a.title;
|
|
313
|
-
lines.push(` • [${q}] ${num} ${titlePadded} (${tgt})`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (sorted.length > 3) {
|
|
317
|
-
lines.push(` (+${sorted.length - 3} more · run /anchor to inspect)`);
|
|
318
|
-
}
|
|
319
302
|
|
|
320
|
-
|
|
303
|
+
const factory = (_tui?: any, theme?: any) => ({
|
|
304
|
+
render: (width: number) => {
|
|
305
|
+
const maxTitleWidth = Math.max(16, width - 36);
|
|
306
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c: string, text: string) => text;
|
|
307
|
+
|
|
308
|
+
const lines: string[] = [
|
|
309
|
+
fg('accent', `⌖ Anchor · ${active.length} active commitments:`)
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
313
|
+
const a = topItems[i];
|
|
314
|
+
const num = (i + 1).toString().padStart(2, '0');
|
|
315
|
+
const q = classifyAnchor(a);
|
|
316
|
+
const tgt = formatTargetDate(a);
|
|
317
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
318
|
+
lines.push(` • ${fg('muted', `[${q}]`)} ${fg('dim', num)} ${titlePadded} ${fg('dim', `(${tgt})`)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (sorted.length > 3) {
|
|
322
|
+
lines.push(fg('dim', ` (+${sorted.length - 3} more · run /anchor to inspect)`));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return lines;
|
|
326
|
+
},
|
|
327
|
+
invalidate: () => {}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Reactive component factory in real TUI mode, static string[] in mock tests
|
|
331
|
+
const content = (ctx as any).ui?.theme ? factory : factory().render(80);
|
|
332
|
+
|
|
333
|
+
ctx.ui.setWidget('anchor-startup', content as any, { placement: 'aboveEditor' });
|
|
321
334
|
}
|
|
322
335
|
|
|
323
336
|
/**
|