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/README.md +33 -170
- package/README_zh.md +32 -176
- package/assets/hero-en.svg +23 -39
- package/assets/hero-zh.svg +23 -39
- package/bin/anchor.js +36 -69
- package/dist/chunk-LKY6WIKH.js +1100 -0
- package/dist/extension-B1-fzCox.d.cts +167 -0
- package/dist/extension-B1-fzCox.d.ts +167 -0
- package/dist/extension.cjs +369 -1315
- package/dist/extension.d.cts +2 -20
- package/dist/extension.d.ts +2 -20
- package/dist/extension.js +4 -536
- package/dist/index.cjs +317 -1143
- package/dist/index.d.cts +17 -250
- package/dist/index.d.ts +17 -250
- package/dist/index.js +15 -103
- package/package.json +2 -2
- package/src/extension.ts +124 -362
- package/src/index.ts +1 -6
- package/src/store.ts +294 -424
- package/src/tui.ts +102 -297
- package/src/types.ts +24 -65
- package/dist/chunk-Q7IILJFA.js +0 -1563
- package/dist/store-t2_TDhJn.d.cts +0 -210
- package/dist/store-t2_TDhJn.d.ts +0 -210
- package/src/context_injector.ts +0 -166
- package/src/decay.ts +0 -125
- package/src/matcher.ts +0 -202
- package/src/observer.ts +0 -141
- package/src/protocol.ts +0 -95
- package/src/settlement.ts +0 -140
package/src/tui.ts
CHANGED
|
@@ -1,115 +1,16 @@
|
|
|
1
1
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import { AnchorStore,
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
*
|
|
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
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
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 (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
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
|
|
364
|
-
*
|
|
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
|
|
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(
|
|
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', `⌖
|
|
213
|
+
truncateToWidth(fg('accent', `⌖ ${label}`), width)
|
|
388
214
|
];
|
|
389
215
|
|
|
390
|
-
for (
|
|
391
|
-
const a = topItems[i];
|
|
216
|
+
for (const a of topItems) {
|
|
392
217
|
const num = a.id.replace(/^anc-/, '').padStart(2, '0');
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
lines.push(
|
|
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', `
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
431
|
-
ctx.ui.notify('⌖ No
|
|
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
|
|
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 <
|
|
441
|
-
const titleWidth = isNarrow ? Math.max(
|
|
262
|
+
const isNarrow = termCols < 84;
|
|
263
|
+
const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
|
|
442
264
|
|
|
443
|
-
for (
|
|
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
|
|
451
|
-
|
|
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(
|
|
459
|
-
const colOrigin = padToWidth(origin,
|
|
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}${
|
|
465
|
-
: `${colNum}${
|
|
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('⌖
|
|
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
|
-
'✓
|
|
479
|
-
...(anchor.status === 'sleeping' ? ['⚡ Wake up (
|
|
480
|
-
'ℹ
|
|
481
|
-
'↩
|
|
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
|
|
492
|
-
|
|
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
|
-
|
|
504
|
-
anchor.description ? `
|
|
505
|
-
anchor.
|
|
506
|
-
|
|
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
|
}
|