pi-anchor 0.2.2 → 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/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(`Anchor: ${sweep.transitionedToSleeping.length} 个非活跃任务已进入休眠`, 'info');
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(`⚓ 物理验证通过!锚点 #${a.id} ("${a.title}") 已自动结案归档。`, 'info');
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 /pin
489
+ // 6. User Slash Commands: /anchor, /pin, and /done
420
490
  pi.registerCommand('anchor', {
421
- description: 'Inspect and settle active anchors',
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 input = (args || '').trim();
493
+ const parsed = parseAnchorCommandInput(args);
424
494
 
425
495
  // Case 1: View active anchors (clean list, enter to complete)
426
- if (!input || input === 'list' || input === 'ls') {
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 (input === 'undo') {
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: Complete directly by ID (e.g. /anchor anc-1)
444
- const targetId = input.startsWith('#') ? input.slice(1) : input;
445
- const item = store.get(targetId);
446
- if (item) {
447
- store.settle(targetId, { settledBy: 'manual-command' });
448
- ctx.ui.notify(`⌖ Settled: "${item.title}"`, 'info');
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: Freeform text directly creates an anchor
454
- const anc = store.create({ title: input, cwd: ctx.cwd });
455
- ctx.ui.notify(`⌖ Pinned #${anc.id}: "${anc.title}"`, 'info');
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: 'Quickly pin a task across sessions',
551
+ description: 'Pin a task across sessions (e.g. /pin Refactor auth --due tomorrow)',
463
552
  handler: async (args: string, ctx: ExtensionContext) => {
464
- const title = (args || '').trim();
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({ title, cwd: ctx.cwd });
470
- ctx.ui.notify(`⌖ Pinned #${anc.id}: "${anc.title}"`, 'info');
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
  });