pi-anchor 0.2.3 → 0.2.4
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 +16 -5
- package/README_zh.md +16 -5
- package/assets/hero-en.svg +108 -0
- package/assets/hero-zh.svg +108 -0
- package/assets/hero.svg +108 -0
- package/bin/anchor.js +4 -4
- package/dist/extension.cjs +114 -19
- package/dist/extension.d.cts +16 -1
- package/dist/extension.d.ts +16 -1
- package/dist/extension.js +111 -19
- package/dist/index.d.cts +3 -209
- package/dist/index.d.ts +3 -209
- package/dist/store-BNKjzhS_.d.cts +209 -0
- package/dist/store-BNKjzhS_.d.ts +209 -0
- package/package.json +21 -5
- package/src/extension.ts +137 -24
package/src/extension.ts
CHANGED
|
@@ -1,18 +1,88 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { Type } from '@sinclair/typebox';
|
|
3
|
-
import { AnchorStore } from './store.ts';
|
|
3
|
+
import { AnchorStore, detectTargetDate } from './store.ts';
|
|
4
4
|
import { SessionTouchObserver } from './observer.ts';
|
|
5
5
|
import { renderColdStartAnchorsContext, renderActiveAnchorsContext, makeSafeTaskAnnotation } from './context_injector.ts';
|
|
6
6
|
import { generateSettlementProposals, runPhysicalVerification } from './settlement.ts';
|
|
7
7
|
import { normalizePath, isSameOrSubPath, findMatchedAnchors } from './matcher.ts';
|
|
8
8
|
import { sweepStore } from './decay.ts';
|
|
9
|
-
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner, truncateToWidth, hasActiveUI } from './tui.ts';
|
|
9
|
+
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner, truncateToWidth, hasActiveUI, groupAnchorsByQuadrant } from './tui.ts';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
14
|
const MUTATION_TOOLS = new Set(['edit', 'write', 'patch', 'apply_diff', 'create_file', 'modify']);
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Parse inline `--due <date>` / `-d <date>` flags and `done`/`rm`/`close`/`settle` subcommands
|
|
18
|
+
* for `/anchor` and `/pin` slash commands.
|
|
19
|
+
*/
|
|
20
|
+
export function parseAnchorCommandInput(raw: string, now: number = Date.now()): {
|
|
21
|
+
mode: 'dashboard' | 'undo' | 'settle' | 'pin';
|
|
22
|
+
targetId?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
targetDate?: string;
|
|
25
|
+
} {
|
|
26
|
+
const trimmed = (raw || '').trim();
|
|
27
|
+
if (!trimmed || trimmed === 'list' || trimmed === 'ls') {
|
|
28
|
+
return { mode: 'dashboard' };
|
|
29
|
+
}
|
|
30
|
+
if (trimmed === 'undo') {
|
|
31
|
+
return { mode: 'undo' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Match explicit settle subcommands: `/anchor done 1`, `/anchor rm anc-2`, `/anchor settle #1`
|
|
35
|
+
const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
|
|
36
|
+
if (settleMatch) {
|
|
37
|
+
return {
|
|
38
|
+
mode: 'settle',
|
|
39
|
+
targetId: settleMatch[1].trim().replace(/^#/, '')
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract optional `--due <val>` or `-d <val>` flag
|
|
44
|
+
let targetDate: string | undefined;
|
|
45
|
+
const tokens = trimmed.split(/\s+/);
|
|
46
|
+
const dueIdx = tokens.findIndex(t => t === '--due' || t === '-d');
|
|
47
|
+
if (dueIdx !== -1 && tokens[dueIdx + 1]) {
|
|
48
|
+
const rawDue = tokens[dueIdx + 1];
|
|
49
|
+
targetDate = detectTargetDate(rawDue, now) || rawDue;
|
|
50
|
+
tokens.splice(dueIdx, 2);
|
|
51
|
+
}
|
|
52
|
+
const cleanTitle = tokens.join(' ').trim();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
mode: 'pin',
|
|
56
|
+
title: cleanTitle || trimmed,
|
|
57
|
+
targetDate
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve either an explicit anchor ID ('anc-2', '2') or a 1-based visual dashboard index ('01', '1')
|
|
63
|
+
*/
|
|
64
|
+
export function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined {
|
|
65
|
+
const clean = idOrIndex.trim().replace(/^#/, '');
|
|
66
|
+
// 1. Check 1-based sequential index from the active quadrant-sorted list first when user passes a plain number
|
|
67
|
+
if (/^\d+$/.test(clean)) {
|
|
68
|
+
const seqIndex = parseInt(clean, 10);
|
|
69
|
+
const active = store.list({ status: 'active', cwd });
|
|
70
|
+
const groups = groupAnchorsByQuadrant(active);
|
|
71
|
+
const sortedActive = [
|
|
72
|
+
...groups.today,
|
|
73
|
+
...groups.upcoming,
|
|
74
|
+
...groups.habits,
|
|
75
|
+
...groups.backlog
|
|
76
|
+
];
|
|
77
|
+
if (seqIndex >= 1 && seqIndex <= sortedActive.length) {
|
|
78
|
+
return sortedActive[seqIndex - 1].id;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 2. Fallback to direct ID lookup in store
|
|
82
|
+
const direct = store.get(clean);
|
|
83
|
+
return direct?.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
16
86
|
export default function (pi: ExtensionAPI) {
|
|
17
87
|
// Global authoritative store in ~/.anchor/ (0 workspace clutter)
|
|
18
88
|
const store = new AnchorStore();
|
|
@@ -29,7 +99,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
29
99
|
const sweep = sweepStore(store);
|
|
30
100
|
|
|
31
101
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
32
|
-
ctx.ui.notify(
|
|
102
|
+
ctx.ui.notify(`⌖ Anchor: ${sweep.transitionedToSleeping.length} inactive tasks transitioned to sleep`, 'info');
|
|
33
103
|
}
|
|
34
104
|
|
|
35
105
|
// Physical Pre-flight Verification: auto-settle passing tasks or record failing output (scoped strictly to current cwd)
|
|
@@ -43,7 +113,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
113
|
settledBy: 'verification-test',
|
|
44
114
|
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
45
115
|
});
|
|
46
|
-
ctx.ui.notify(
|
|
116
|
+
ctx.ui.notify(`⌖ Verification passed! Auto-settled #${a.id}: "${a.title}"`, 'info');
|
|
47
117
|
} else {
|
|
48
118
|
preflightFailures.set(a.id, res.output.slice(-250));
|
|
49
119
|
}
|
|
@@ -416,20 +486,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
416
486
|
}
|
|
417
487
|
});
|
|
418
488
|
|
|
419
|
-
// 6. User Slash Commands: /anchor and /
|
|
489
|
+
// 6. User Slash Commands: /anchor, /pin, and /done
|
|
420
490
|
pi.registerCommand('anchor', {
|
|
421
|
-
description: 'Inspect
|
|
491
|
+
description: 'Inspect, pin, or settle active anchors (e.g. /anchor, /anchor done 1, /anchor undo)',
|
|
422
492
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
423
|
-
const
|
|
493
|
+
const parsed = parseAnchorCommandInput(args);
|
|
424
494
|
|
|
425
495
|
// Case 1: View active anchors (clean list, enter to complete)
|
|
426
|
-
if (
|
|
496
|
+
if (parsed.mode === 'dashboard') {
|
|
427
497
|
await openAnchorDashboard(ctx, store);
|
|
428
498
|
return;
|
|
429
499
|
}
|
|
430
500
|
|
|
431
501
|
// Case 2: Revert settlement
|
|
432
|
-
if (
|
|
502
|
+
if (parsed.mode === 'undo') {
|
|
433
503
|
try {
|
|
434
504
|
const restored = store.undoSettle();
|
|
435
505
|
ctx.ui.notify(`⌖ Restored #${restored.id}: "${restored.title}"`, 'info');
|
|
@@ -440,34 +510,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
440
510
|
return;
|
|
441
511
|
}
|
|
442
512
|
|
|
443
|
-
// Case 3:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
513
|
+
// Case 3: Explicit settle subcommand (`/anchor done 1` or `/anchor done anc-1`)
|
|
514
|
+
if (parsed.mode === 'settle' && parsed.targetId) {
|
|
515
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
516
|
+
if (!resolvedId) {
|
|
517
|
+
ctx.ui.notify(`⌖ Anchor not found: ${parsed.targetId}`, 'error');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const settled = store.settle(resolvedId, { settledBy: 'manual-command' });
|
|
521
|
+
ctx.ui.notify(`⌖ Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, 'info');
|
|
449
522
|
updateAnchorStatusBar(ctx, store);
|
|
450
523
|
return;
|
|
451
524
|
}
|
|
452
525
|
|
|
453
|
-
// Case 4:
|
|
454
|
-
const
|
|
455
|
-
|
|
526
|
+
// Case 4: Direct ID completion if input matches an existing ID (e.g. `/anchor anc-1` or `/anchor #1`)
|
|
527
|
+
const rawTrimmed = (args || '').trim();
|
|
528
|
+
if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
|
|
529
|
+
const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
|
|
530
|
+
if (resolvedId) {
|
|
531
|
+
const settled = store.settle(resolvedId, { settledBy: 'manual-command' });
|
|
532
|
+
ctx.ui.notify(`⌖ Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, 'info');
|
|
533
|
+
updateAnchorStatusBar(ctx, store);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Case 5: Freeform text creates an anchor (supports `--due <date>`)
|
|
539
|
+
const anc = store.create({
|
|
540
|
+
title: parsed.title || rawTrimmed,
|
|
541
|
+
targetDate: parsed.targetDate,
|
|
542
|
+
cwd: ctx.cwd
|
|
543
|
+
});
|
|
544
|
+
ctx.ui.notify(`⌖ Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ''}`, 'info');
|
|
456
545
|
updateAnchorStatusBar(ctx, store);
|
|
457
546
|
}
|
|
458
547
|
});
|
|
459
548
|
|
|
460
|
-
// Alias /pin to instant-add
|
|
549
|
+
// Alias /pin to instant-add (supports `--due <date>`)
|
|
461
550
|
pi.registerCommand('pin', {
|
|
462
|
-
description: '
|
|
551
|
+
description: 'Pin a task across sessions (e.g. /pin Refactor auth --due tomorrow)',
|
|
463
552
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
464
|
-
const
|
|
465
|
-
if (!title) {
|
|
553
|
+
const parsed = parseAnchorCommandInput(args);
|
|
554
|
+
if (parsed.mode === 'dashboard' || !parsed.title) {
|
|
466
555
|
await openAnchorDashboard(ctx, store);
|
|
467
556
|
return;
|
|
468
557
|
}
|
|
469
|
-
const anc = store.create({
|
|
470
|
-
|
|
558
|
+
const anc = store.create({
|
|
559
|
+
title: parsed.title,
|
|
560
|
+
targetDate: parsed.targetDate,
|
|
561
|
+
cwd: ctx.cwd
|
|
562
|
+
});
|
|
563
|
+
ctx.ui.notify(`⌖ Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ''}`, 'info');
|
|
564
|
+
updateAnchorStatusBar(ctx, store);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Alias /done for 1-step completion by display number or ID (e.g. `/done 1`)
|
|
569
|
+
pi.registerCommand('done', {
|
|
570
|
+
description: 'Complete an active anchor by row number or ID (e.g. /done 1)',
|
|
571
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
572
|
+
const target = (args || '').trim();
|
|
573
|
+
if (!target) {
|
|
574
|
+
await openAnchorDashboard(ctx, store);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
578
|
+
if (!resolvedId) {
|
|
579
|
+
ctx.ui.notify(`⌖ Anchor not found: ${target}`, 'error');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const settled = store.settle(resolvedId, { settledBy: 'manual-command' });
|
|
583
|
+
ctx.ui.notify(`⌖ Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, 'info');
|
|
471
584
|
updateAnchorStatusBar(ctx, store);
|
|
472
585
|
}
|
|
473
586
|
});
|