pi-anchor 0.2.4 → 0.3.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/src/tui.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
  import { AnchorStore, getTodayDateString } from './store.ts';
3
+ import { isAnchorTargetingWorkspace } from './matcher.ts';
3
4
  import type { Anchor } from './types.ts';
4
5
  import path from 'node:path';
5
6
 
@@ -57,6 +58,40 @@ export function groupAnchorsByQuadrant(anchors: Anchor[], now: number = Date.now
57
58
  return groups;
58
59
  }
59
60
 
61
+ const PRIORITY_RANK: Record<string, number> = { p0: 0, p1: 1, p2: 2 };
62
+
63
+ /**
64
+ * Sort unfinished anchors into a single compact, priority-driven engineering list:
65
+ * 1. Active before Sleeping (quiet)
66
+ * 2. Matching current workspace before other workspaces
67
+ * 3. Due Today / Overdue before open-ended
68
+ * 4. Priority (P0 -> P1 -> P2)
69
+ */
70
+ export function sortAnchorsForDisplay(anchors: Anchor[], cwd?: string, now: number = Date.now()): Anchor[] {
71
+ const todayStr = getTodayDateString(now);
72
+ return [...anchors].sort((a, b) => {
73
+ // 1. Active before sleeping
74
+ if (a.status !== b.status) {
75
+ return a.status === 'active' ? -1 : 1;
76
+ }
77
+ // 2. Current workspace match first
78
+ if (cwd) {
79
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
80
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
81
+ if (aMatch !== bMatch) return aMatch - bMatch;
82
+ }
83
+ // 3. Due today / overdue first
84
+ const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
85
+ const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
86
+ if (aDue !== bDue) return aDue - bDue;
87
+ // 4. Priority P0 -> P1 -> P2
88
+ const pA = PRIORITY_RANK[a.priority] ?? 1;
89
+ const pB = PRIORITY_RANK[b.priority] ?? 1;
90
+ if (pA !== pB) return pA - pB;
91
+ return a.createdAt - b.createdAt;
92
+ });
93
+ }
94
+
60
95
  const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
61
96
 
62
97
  /**
@@ -286,15 +321,23 @@ export function formatRemainingTtl(anchor: Anchor, now: number = Date.now()): st
286
321
  }
287
322
 
288
323
  /**
289
- * Format origin creation workspace folder (provenance: where the task was born/created)
324
+ * Format origin workspace / project label concisely
290
325
  */
291
326
  export function formatOrigin(anchor: Anchor | string): string {
292
327
  if (typeof anchor === 'object') {
293
- if (!anchor.cwd) return anchor.project || 'global';
294
- return path.basename(anchor.cwd) || 'global';
328
+ if (anchor.project && anchor.project !== 'global') {
329
+ const normProj = anchor.project.replace(/\\/g, '/').replace(/\/+$/, '');
330
+ if (normProj.includes('/')) {
331
+ const parts = normProj.split('/').filter(Boolean);
332
+ return parts.slice(-2).join('/');
333
+ }
334
+ return normProj;
335
+ }
336
+ if (!anchor.cwd) return 'global';
337
+ return path.basename(anchor.cwd.replace(/\\/g, '/')) || 'global';
295
338
  }
296
339
  if (!anchor) return 'global';
297
- return path.basename(anchor) || 'global';
340
+ return path.basename(anchor.replace(/\\/g, '/')) || 'global';
298
341
  }
299
342
 
300
343
  /**
@@ -372,7 +415,7 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
372
415
  }
373
416
 
374
417
  /**
375
- * Clean, columnar-aligned checklist with Target Date and Creation Time columns
418
+ * Clean, flat priority-sorted engineering checklist (includes both active and quiet/sleeping tasks so nothing is ever lost)
376
419
  */
377
420
  export async function openAnchorDashboard(
378
421
  ctx: ExtensionContext,
@@ -380,66 +423,67 @@ export async function openAnchorDashboard(
380
423
  ): Promise<void> {
381
424
  if (!hasActiveUI(ctx)) return;
382
425
 
383
- const list = store.list({ status: 'active', cwd: ctx.cwd });
384
- if (list.length === 0) {
426
+ const allUnfinished = store
427
+ .list({ cwd: ctx.cwd, all: true })
428
+ .filter(a => a.status === 'active' || a.status === 'sleeping');
429
+
430
+ if (allUnfinished.length === 0) {
385
431
  ctx.ui.notify('⌖ No active anchors. Use /pin <task> to record.', 'info');
386
432
  return;
387
433
  }
388
434
 
389
- const groups = groupAnchorsByQuadrant(list);
390
- const sortedList = [
391
- ...groups.today,
392
- ...groups.upcoming,
393
- ...groups.habits,
394
- ...groups.backlog
395
- ];
435
+ const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
396
436
 
397
437
  const optionMap = new Map<string, Anchor>();
398
438
  const displayOptions: string[] = [];
399
439
  const termCols = process.stdout?.columns || 96;
400
440
  const isNarrow = termCols < 88;
401
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
441
+ const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
402
442
 
403
443
  for (let i = 0; i < sortedList.length; i++) {
404
444
  const a = sortedList[i];
405
445
  const num = a.id.replace(/^anc-/, '').padStart(2, '0');
406
- const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
446
+ const badge = a.status === 'sleeping'
447
+ ? padToWidth('[Quiet]', 10)
448
+ : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
407
449
  const origin = formatOrigin(a);
408
- const target = formatTargetDate(a);
450
+ const target = a.status === 'sleeping' ? 'Quiet (0 tok)' : formatTargetDate(a);
409
451
  const created = formatCreationTime(a.createdAt);
410
452
 
411
453
  const titleWithFiles = (a.files && a.files.length > 0)
412
454
  ? `${a.title} [${a.files.slice(0, 1).join(', ')}]`
413
455
  : a.title;
414
456
 
415
- // Clean tabular column alignment: ID, Category, Title, Project, Target, Created
416
457
  const colNum = `${num} `;
417
458
  const colTitle = padToWidth(titleWithFiles, titleWidth);
418
- const colOrigin = padToWidth(origin, 10);
419
- const colTarget = padToWidth(target, 12);
459
+ const colOrigin = padToWidth(origin, 12);
460
+ const colTarget = padToWidth(target, 14);
420
461
  const colCreated = created;
421
462
 
422
463
  const label = isNarrow
423
- ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd()
424
- : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
464
+ ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd()
465
+ : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
425
466
 
426
467
  optionMap.set(label, a);
427
468
  displayOptions.push(label);
428
469
  }
429
470
 
430
- const selected = await ctx.ui.select('⌖ Anchors (select to inspect or settle):', displayOptions);
471
+ const selected = await ctx.ui.select('⌖ Anchors (select to settle, wake, or inspect):', displayOptions);
431
472
  if (!selected) return;
432
473
 
433
474
  const anchor = optionMap.get(selected);
434
475
  if (!anchor) return;
435
476
 
477
+ const actions = [
478
+ '✓ Settle (Mark completed and archive)',
479
+ ...(anchor.status === 'sleeping' ? ['⚡ Wake up (Activate for current session)'] : []),
480
+ 'ℹ View details & associated files',
481
+ '↩ Cancel'
482
+ ];
483
+
436
484
  const action = await ctx.ui.select(
437
485
  `⌖ Anchor #${anchor.id}: "${anchor.title}"`,
438
- [
439
- '✓ Settle (Mark completed and archive)',
440
- 'ℹ View details & associated files',
441
- '↩ Cancel'
442
- ]
486
+ actions
443
487
  );
444
488
  if (!action || action.startsWith('↩')) return;
445
489
 
@@ -450,9 +494,13 @@ export async function openAnchorDashboard(
450
494
  : `⌖ Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
451
495
  ctx.ui.notify(successMsg, 'info');
452
496
  updateAnchorStatusBar(ctx, store);
497
+ } else if (action.startsWith('⚡')) {
498
+ store.touch(anchor.id);
499
+ ctx.ui.notify(`⌖ Woke up #${anchor.id}: "${anchor.title}"`, 'info');
500
+ updateAnchorStatusBar(ctx, store);
453
501
  } else if (action.startsWith('ℹ')) {
454
502
  const details = [
455
- `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
503
+ `#${anchor.id} [${anchor.priority.toUpperCase()} · ${anchor.status.toUpperCase()}] ${anchor.title}`,
456
504
  anchor.description ? `Description: ${anchor.description}` : null,
457
505
  anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
458
506
  anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(', ')}` : null,
package/src/types.ts CHANGED
@@ -87,19 +87,19 @@ export interface SettlementProposal {
87
87
  }
88
88
 
89
89
  export const EPHEMERAL_DECAY_POLICY: AnchorDecayPolicy = {
90
- activeDays: 1, // 24 hours active
91
- sleepDays: 1, // 24 hours sleeping
92
- graveyardDays: 2 // 48 hours total before auto-clearing
90
+ activeDays: 7, // 7 days active before quiet sleep
91
+ sleepDays: 30,
92
+ graveyardDays: 9999 // Never silently auto-delete unfinished tasks
93
93
  };
94
94
 
95
95
  export const DURABLE_DECAY_POLICY: AnchorDecayPolicy = {
96
- activeDays: 3,
96
+ activeDays: 7, // 7 days active before quiet sleep
97
97
  sleepDays: 30,
98
98
  graveyardDays: 9999 // Never permanently auto-dropped; preserved indefinitely
99
99
  };
100
100
 
101
101
  export const DEFAULT_DECAY_POLICY: AnchorDecayPolicy = {
102
- activeDays: 3,
103
- sleepDays: 7,
104
- graveyardDays: 14
102
+ activeDays: 7,
103
+ sleepDays: 30,
104
+ graveyardDays: 9999
105
105
  };