pi-anchor 0.3.0 → 0.4.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,115 +1,16 @@
1
1
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
- import { AnchorStore, getTodayDateString } from './store.ts';
3
- import { isAnchorTargetingWorkspace } from './matcher.ts';
2
+ import { AnchorStore, isAnchorTargetingWorkspace } from './store.ts';
4
3
  import type { Anchor } from './types.ts';
5
4
  import path from 'node:path';
6
5
 
7
- export type AnchorQuadrant = 'Today' | 'Upcoming' | 'Habits' | 'Backlog';
8
-
9
- export interface GroupedAnchors {
10
- today: Anchor[];
11
- upcoming: Anchor[];
12
- habits: Anchor[];
13
- backlog: Anchor[];
14
- }
15
-
16
- /**
17
- * Classify anchor into one of four cognitive quadrants:
18
- * - 'Today': Due today or overdue
19
- * - 'Upcoming': Due in the future (tomorrow, in 2d, specific dates)
20
- * - 'Habits': Daily recurring habits
21
- * - 'Backlog': Open-ended long-term vision
22
- */
23
- export function classifyAnchor(anchor: Anchor, now: number = Date.now()): AnchorQuadrant {
24
- if (anchor.recurrence === 'daily') {
25
- return 'Habits';
26
- }
27
- if (!anchor.targetDate) {
28
- return 'Backlog';
29
- }
30
-
31
- const todayStr = getTodayDateString(now);
32
- if (anchor.targetDate <= todayStr) {
33
- return 'Today';
34
- }
35
-
36
- return 'Upcoming';
37
- }
38
-
39
- /**
40
- * Group active anchors by cognitive quadrant
41
- */
42
- export function groupAnchorsByQuadrant(anchors: Anchor[], now: number = Date.now()): GroupedAnchors {
43
- const groups: GroupedAnchors = {
44
- today: [],
45
- upcoming: [],
46
- habits: [],
47
- backlog: []
48
- };
49
-
50
- for (const a of anchors) {
51
- const q = classifyAnchor(a, now);
52
- if (q === 'Today') groups.today.push(a);
53
- else if (q === 'Upcoming') groups.upcoming.push(a);
54
- else if (q === 'Habits') groups.habits.push(a);
55
- else groups.backlog.push(a);
56
- }
57
-
58
- return groups;
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
-
95
6
  const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
96
7
 
97
- /**
98
- * Strip ANSI escape codes from string
99
- */
100
8
  export function stripAnsi(str: string): string {
101
9
  return str.replace(ANSI_REGEX, '');
102
10
  }
103
11
 
104
12
  /**
105
- * Calculate display width in terminal columns, accounting for:
106
- * - ANSI escape codes (width 0)
107
- * - CJK ideographs & fullwidth forms (width 2)
108
- * - Japanese Kana (width 2)
109
- * - Korean Hangul syllables & Jamo (width 2)
110
- * - Emojis & Pictographs (width 2)
111
- * - Zero-width characters & joiners (width 0)
112
- * - Standard ASCII (width 1)
13
+ * Calculate visual terminal column width (CJK/Kana/Hangul/Emoji = 2 cells, ASCII = 1 cell)
113
14
  */
114
15
  export function getDisplayWidth(str: string): number {
115
16
  const clean = stripAnsi(str);
@@ -117,8 +18,6 @@ export function getDisplayWidth(str: string): number {
117
18
 
118
19
  for (const char of clean) {
119
20
  const code = char.codePointAt(0) || 0;
120
-
121
- // Zero-width characters (ZWJ, variation selectors, combining marks)
122
21
  if (
123
22
  code === 0x200d ||
124
23
  code === 0xfe0f ||
@@ -129,7 +28,6 @@ export function getDisplayWidth(str: string): number {
129
28
  continue;
130
29
  }
131
30
 
132
- // Double-width characters: CJK, Hangul, Kana, Emojis
133
31
  if (
134
32
  (code >= 0x4e00 && code <= 0x9fff) ||
135
33
  (code >= 0x3400 && code <= 0x4dbf) ||
@@ -166,9 +64,6 @@ export function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is Ex
166
64
  }
167
65
  }
168
66
 
169
- /**
170
- * Truncate string to target terminal visual width with ellipsis
171
- */
172
67
  export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…'): string {
173
68
  if (maxWidth <= 0) return '';
174
69
  const singleLine = str.replace(/[\r\n\t]+/g, ' ');
@@ -197,23 +92,17 @@ export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…')
197
92
  const fullChar = singleLine.slice(i, i + charLen);
198
93
  const charWidth = getDisplayWidth(fullChar);
199
94
 
200
- if (accumWidth + charWidth > target) {
201
- break;
202
- }
95
+ if (accumWidth + charWidth > target) break;
203
96
  accumulated += fullChar;
204
97
  accumWidth += charWidth;
205
98
  i += charLen;
206
99
  }
207
100
 
208
- // Invariant 11: Guard against trailing open brackets right before ellipsis (e.g. "Fix auth (…" -> "Fix auth…")
209
- accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, '');
210
-
101
+ // Invariant 11: Strip any trailing unclosed parenthesis/bracket fragment right before ellipsis
102
+ accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, '');
211
103
  return accumulated + ellipsis + (singleLine.includes('\x1b') ? '\x1b[0m' : '');
212
104
  }
213
105
 
214
- /**
215
- * Pad and/or truncate string to exact visual column width, eliminating column tearing
216
- */
217
106
  export function padToWidth(str: string, targetWidth: number): string {
218
107
  const truncated = truncateToWidth(str, targetWidth);
219
108
  const current = getDisplayWidth(truncated);
@@ -222,147 +111,84 @@ export function padToWidth(str: string, targetWidth: number): string {
222
111
  }
223
112
 
224
113
  /**
225
- * Format expected completion date in human-intuitive terms:
226
- * - 'Daily' for daily recurring habits
227
- * - 'Today' if targetDate is today
228
- * - 'Tomorrow' if targetDate is tomorrow
229
- * - 'In 2d' / 'In 3d' for near-term dates
230
- * - 'MM-DD' for future dates
231
- * - 'Overdue' if targetDate is past
232
- * - 'Someday' for open-ended architecture/long-term vision
114
+ * Format relative sticky-note age cleanly: 'just now', '15m ago', '3h ago', '2d ago'
233
115
  */
234
- export function formatTargetDate(anchor: Anchor, now: number = Date.now()): string {
235
- if (anchor.recurrence === 'daily') {
236
- return 'Daily';
237
- }
238
- if (!anchor.targetDate) {
239
- return 'Someday';
240
- }
241
-
242
- const todayStr = getTodayDateString(now);
243
- if (anchor.targetDate === todayStr) {
244
- return 'Today';
245
- }
246
-
247
- const MS_DAY = 24 * 60 * 60 * 1000;
248
- const tomorrowStr = getTodayDateString(now + MS_DAY);
249
- if (anchor.targetDate === tomorrowStr) {
250
- return 'Tomorrow';
251
- }
252
-
253
- const in2dStr = getTodayDateString(now + 2 * MS_DAY);
254
- if (anchor.targetDate === in2dStr) {
255
- return 'In 2d';
256
- }
257
-
258
- const in3dStr = getTodayDateString(now + 3 * MS_DAY);
259
- if (anchor.targetDate === in3dStr) {
260
- return 'In 3d';
261
- }
262
-
263
- if (anchor.targetDate < todayStr) {
264
- return 'Overdue';
265
- }
266
-
267
- const parts = anchor.targetDate.split('-');
268
- if (parts.length === 3) {
269
- return `${parts[1]}-${parts[2]}`;
270
- }
271
-
272
- return anchor.targetDate;
116
+ export function formatRelativeAge(timestamp: number, now: number = Date.now()): string {
117
+ const diffSec = Math.max(0, Math.floor((now - timestamp) / 1000));
118
+ if (diffSec < 60) return 'just now';
119
+ const diffMin = Math.floor(diffSec / 60);
120
+ if (diffMin < 60) return `${diffMin}m ago`;
121
+ const diffHours = Math.floor(diffMin / 60);
122
+ if (diffHours < 24) return `${diffHours}h ago`;
123
+ const diffDays = Math.floor(diffHours / 24);
124
+ return `${diffDays}d ago`;
273
125
  }
274
126
 
275
127
  /**
276
- * Format creation timestamp as concise date/time:
277
- * - 'Today HH:MM' if created today
278
- * - 'Yesterday HH:MM' if created yesterday
279
- * - 'MM-DD HH:MM' if created this year
280
- * - 'YYYY-MM-DD' if older
128
+ * Format workspace & first file context concisely (e.g. 'anchor · src/auth.ts' or 'College/优化')
281
129
  */
282
- export function formatCreationTime(timestamp: number, now: number = Date.now()): string {
283
- const d = new Date(timestamp);
284
- const nowD = new Date(now);
285
-
286
- const pad = (n: number) => String(n).padStart(2, '0');
287
- const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
288
-
289
- const todayStr = getTodayDateString(now);
290
- const createdDayStr = getTodayDateString(timestamp);
291
-
292
- if (createdDayStr === todayStr) {
293
- return `Today ${timeStr}`;
130
+ export function formatOrigin(anchor: Anchor | string): string {
131
+ if (typeof anchor === 'string') {
132
+ if (!anchor) return 'global';
133
+ return path.basename(anchor.replace(/\\/g, '/')) || 'global';
294
134
  }
295
135
 
296
- const yesterdayStr = getTodayDateString(now - 24 * 60 * 60 * 1000);
297
- if (createdDayStr === yesterdayStr) {
298
- return `Yesterday ${timeStr}`;
136
+ let proj = 'global';
137
+ if (anchor.project && anchor.project !== 'global') {
138
+ const normProj = anchor.project.replace(/\\/g, '/').replace(/\/+$/, '');
139
+ if (normProj.includes('/')) {
140
+ const parts = normProj.split('/').filter(Boolean);
141
+ proj = parts.slice(-2).join('/');
142
+ } else {
143
+ proj = normProj;
144
+ }
145
+ } else if (anchor.cwd) {
146
+ proj = path.basename(anchor.cwd.replace(/\\/g, '/')) || 'global';
299
147
  }
300
148
 
301
- const mmdd = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
302
- if (d.getFullYear() === nowD.getFullYear()) {
303
- return `${mmdd} ${timeStr}`;
149
+ if (anchor.files && anchor.files.length > 0) {
150
+ const shortFile = path.basename(anchor.files[0].replace(/\\/g, '/'));
151
+ if (shortFile && !proj.endsWith(shortFile)) {
152
+ return `${proj} · ${shortFile}`;
153
+ }
304
154
  }
305
-
306
- return `${d.getFullYear()}-${mmdd}`;
307
- }
308
-
309
- /**
310
- * Backward compatibility alias
311
- */
312
- export function formatRelativeTime(timestamp: number, now: number = Date.now()): string {
313
- return formatCreationTime(timestamp, now);
314
- }
315
-
316
- /**
317
- * Backward compatibility alias (now returns target date representation)
318
- */
319
- export function formatRemainingTtl(anchor: Anchor, now: number = Date.now()): string | undefined {
320
- return formatTargetDate(anchor, now);
155
+ return proj;
321
156
  }
322
157
 
323
158
  /**
324
- * Format origin workspace / project label concisely
159
+ * Sort sticky notes naturally:
160
+ * 1. Active notes before quiet (>7d untouched) notes
161
+ * 2. Notes belonging to current workspace first
162
+ * 3. Most recently touched first
325
163
  */
326
- export function formatOrigin(anchor: Anchor | string): string {
327
- if (typeof anchor === 'object') {
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;
164
+ export function sortAnchorsForDisplay(anchors: Anchor[], cwd?: string): Anchor[] {
165
+ return [...anchors].sort((a, b) => {
166
+ if (a.status !== b.status) {
167
+ return a.status === 'active' ? -1 : 1;
335
168
  }
336
- if (!anchor.cwd) return 'global';
337
- return path.basename(anchor.cwd.replace(/\\/g, '/')) || 'global';
338
- }
339
- if (!anchor) return 'global';
340
- return path.basename(anchor.replace(/\\/g, '/')) || 'global';
169
+ if (cwd) {
170
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
171
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
172
+ if (aMatch !== bMatch) return aMatch - bMatch;
173
+ }
174
+ return b.lastTouchedAt - a.lastTouchedAt;
175
+ });
341
176
  }
342
177
 
343
178
  /**
344
- * Update footer status bar capsule: `⌖ 1`
179
+ * Update status bar badge: `⌖ 2` (invisible when 0 active notes)
345
180
  */
346
181
  export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void {
347
182
  if (!hasActiveUI(ctx)) return;
348
-
349
183
  try {
350
184
  const active = store.list({ status: 'active', cwd: ctx.cwd });
351
- if (active.length === 0) {
352
- ctx.ui.setStatus('anchor', undefined);
353
- return;
354
- }
355
-
356
- ctx.ui.setStatus('anchor', `⌖ ${active.length}`);
357
- } catch {
358
- // Ignore stale context errors during session replacement
359
- }
185
+ ctx.ui.setStatus('anchor', active.length > 0 ? `⌖ ${active.length}` : undefined);
186
+ } catch {}
360
187
  }
361
188
 
362
189
  /**
363
- * Render lightweight startup banner widget above the editor.
364
- * Features 3-4 lines with quadrant badges and target date hints.
365
- * Automatically cleared on agent_start (when user sends first message).
190
+ * Render clean sticky-note card above editor on session startup.
191
+ * Automatically disappears as soon as the user sends their first message.
366
192
  */
367
193
  export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void {
368
194
  if (!hasActiveUI(ctx)) return;
@@ -374,30 +200,34 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
374
200
  return;
375
201
  }
376
202
 
377
- const groups = groupAnchorsByQuadrant(active);
378
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
203
+ const sorted = sortAnchorsForDisplay(active, ctx.cwd);
379
204
  const topItems = sorted.slice(0, 3);
380
205
 
381
206
  const factory = (_tui?: any, theme?: any) => ({
382
207
  render: (width: number) => {
383
- const maxTitleWidth = Math.max(12, width - 36);
208
+ const maxTitleWidth = Math.max(14, width - 34);
384
209
  const fg = theme?.fg ? theme.fg.bind(theme) : (_c: string, text: string) => text;
210
+ const label = active.length === 1 ? '1 note' : `${active.length} notes`;
385
211
 
386
212
  const lines: string[] = [
387
- truncateToWidth(fg('accent', `⌖ Anchor · ${active.length} active commitments:`), width)
213
+ truncateToWidth(fg('accent', `⌖ ${label}`), width)
388
214
  ];
389
215
 
390
- for (let i = 0; i < topItems.length; i++) {
391
- const a = topItems[i];
216
+ for (const a of topItems) {
392
217
  const num = a.id.replace(/^anc-/, '').padStart(2, '0');
393
- const q = classifyAnchor(a);
394
- const tgt = formatTargetDate(a);
395
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
396
- lines.push(truncateToWidth(` • ${fg('muted', `[${q}]`)} ${fg('dim', num)} ${titlePadded} ${fg('dim', `(${tgt})`)}`, width));
218
+ const origin = formatOrigin(a);
219
+ const age = formatRelativeAge(a.createdAt);
220
+ const titleClipped = truncateToWidth(a.title, maxTitleWidth);
221
+ lines.push(
222
+ truncateToWidth(
223
+ ` ${fg('dim', num)} ${titleClipped} ${fg('muted', `· ${origin}`)} ${fg('dim', `(${age})`)}`,
224
+ width
225
+ )
226
+ );
397
227
  }
398
228
 
399
229
  if (sorted.length > 3) {
400
- lines.push(truncateToWidth(fg('dim', ` (+${sorted.length - 3} more · run /anchor to inspect)`), width));
230
+ lines.push(truncateToWidth(fg('dim', ` +${sorted.length - 3} more · /anchor`), width));
401
231
  }
402
232
 
403
233
  return lines;
@@ -405,107 +235,82 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
405
235
  invalidate: () => {}
406
236
  });
407
237
 
408
- // Reactive component factory in real TUI mode, static string[] in mock tests
409
238
  const content = (ctx as any).ui?.theme ? factory : factory().render(80);
410
-
411
239
  ctx.ui.setWidget('anchor-startup', content as any, { placement: 'aboveEditor' });
412
- } catch {
413
- // Ignore stale context errors during session replacement
414
- }
240
+ } catch {}
415
241
  }
416
242
 
417
243
  /**
418
- * Clean, flat priority-sorted engineering checklist (includes both active and quiet/sleeping tasks so nothing is ever lost)
244
+ * Interactive `/anchor` sticky-note board
419
245
  */
420
- export async function openAnchorDashboard(
421
- ctx: ExtensionContext,
422
- store: AnchorStore
423
- ): Promise<void> {
246
+ export async function openAnchorDashboard(ctx: ExtensionContext, store: AnchorStore): Promise<void> {
424
247
  if (!hasActiveUI(ctx)) return;
425
248
 
426
- const allUnfinished = store
249
+ const allNotes = store
427
250
  .list({ cwd: ctx.cwd, all: true })
428
251
  .filter(a => a.status === 'active' || a.status === 'sleeping');
429
252
 
430
- if (allUnfinished.length === 0) {
431
- ctx.ui.notify('⌖ No active anchors. Use /pin <task> to record.', 'info');
253
+ if (allNotes.length === 0) {
254
+ ctx.ui.notify('⌖ No sticky notes. Use /pin <note> to stick one.', 'info');
432
255
  return;
433
256
  }
434
257
 
435
- const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
436
-
258
+ const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
437
259
  const optionMap = new Map<string, Anchor>();
438
260
  const displayOptions: string[] = [];
439
261
  const termCols = process.stdout?.columns || 96;
440
- const isNarrow = termCols < 88;
441
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
262
+ const isNarrow = termCols < 84;
263
+ const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
442
264
 
443
- for (let i = 0; i < sortedList.length; i++) {
444
- const a = sortedList[i];
265
+ for (const a of sorted) {
445
266
  const num = a.id.replace(/^anc-/, '').padStart(2, '0');
446
- const badge = a.status === 'sleeping'
447
- ? padToWidth('[Quiet]', 10)
448
- : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
449
267
  const origin = formatOrigin(a);
450
- const target = a.status === 'sleeping' ? 'Quiet (0 tok)' : formatTargetDate(a);
451
- const created = formatCreationTime(a.createdAt);
452
-
453
- const titleWithFiles = (a.files && a.files.length > 0)
454
- ? `${a.title} [${a.files.slice(0, 1).join(', ')}]`
455
- : a.title;
268
+ const age = a.status === 'sleeping'
269
+ ? `${formatRelativeAge(a.createdAt)} · quiet`
270
+ : formatRelativeAge(a.createdAt);
456
271
 
457
272
  const colNum = `${num} `;
458
- const colTitle = padToWidth(titleWithFiles, titleWidth);
459
- const colOrigin = padToWidth(origin, 12);
460
- const colTarget = padToWidth(target, 14);
461
- const colCreated = created;
273
+ const colTitle = padToWidth(a.title, titleWidth);
274
+ const colOrigin = padToWidth(origin, 22);
462
275
 
463
276
  const label = isNarrow
464
- ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd()
465
- : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
277
+ ? `${colNum}${colTitle} ${age}`.trimEnd()
278
+ : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
466
279
 
467
280
  optionMap.set(label, a);
468
281
  displayOptions.push(label);
469
282
  }
470
283
 
471
- const selected = await ctx.ui.select('⌖ Anchors (select to settle, wake, or inspect):', displayOptions);
284
+ const selected = await ctx.ui.select('⌖ Notes (select a note):', displayOptions);
472
285
  if (!selected) return;
473
286
 
474
287
  const anchor = optionMap.get(selected);
475
288
  if (!anchor) return;
476
289
 
477
290
  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'
291
+ '✓ Done (tear off note)',
292
+ ...(anchor.status === 'sleeping' ? ['⚡ Wake up (mark active)'] : []),
293
+ 'ℹ Read full note',
294
+ '↩ Back'
482
295
  ];
483
296
 
484
- const action = await ctx.ui.select(
485
- `⌖ Anchor #${anchor.id}: "${anchor.title}"`,
486
- actions
487
- );
297
+ const action = await ctx.ui.select(`⌖ #${anchor.id.replace(/^anc-/, '')}: "${anchor.title}"`, actions);
488
298
  if (!action || action.startsWith('↩')) return;
489
299
 
490
300
  if (action.startsWith('✓')) {
491
- store.settle(anchor.id, { settledBy: 'manual-command' });
492
- const successMsg = anchor.recurrence === 'daily'
493
- ? `⌖ Completed for today: "${anchor.title}" (resets tomorrow)`
494
- : `⌖ Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
495
- ctx.ui.notify(successMsg, 'info');
301
+ store.settle(anchor.id, { settledBy: 'manual' });
302
+ ctx.ui.notify(`⌖ Torn off #${anchor.id.replace(/^anc-/, '')}: "${anchor.title}" (/anchor undo to put back)`, 'info');
496
303
  updateAnchorStatusBar(ctx, store);
497
304
  } else if (action.startsWith('⚡')) {
498
305
  store.touch(anchor.id);
499
- ctx.ui.notify(`⌖ Woke up #${anchor.id}: "${anchor.title}"`, 'info');
306
+ ctx.ui.notify(`⌖ Woke up #${anchor.id.replace(/^anc-/, '')}: "${anchor.title}"`, 'info');
500
307
  updateAnchorStatusBar(ctx, store);
501
308
  } else if (action.startsWith('ℹ')) {
502
309
  const details = [
503
- `#${anchor.id} [${anchor.priority.toUpperCase()} · ${anchor.status.toUpperCase()}] ${anchor.title}`,
504
- anchor.description ? `Description: ${anchor.description}` : null,
505
- anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
506
- anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(', ')}` : null,
507
- anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
508
- `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || 'global'})`
310
+ `⌖ Note #${anchor.id.replace(/^anc-/, '')}: ${anchor.title}`,
311
+ anchor.description ? `Details: ${anchor.description}` : null,
312
+ anchor.files.length > 0 ? `Files: ${anchor.files.join(', ')}` : null,
313
+ `Workspace: ${anchor.project || 'global'} · Pinned ${formatRelativeAge(anchor.createdAt)}`
509
314
  ].filter(Boolean).join('\n');
510
315
  ctx.ui.notify(details, 'info');
511
316
  }