pi-anchor 0.2.0 → 0.2.2
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 +101 -177
- package/README_zh.md +119 -187
- package/dist/{chunk-T62PHIQI.js → chunk-G72UHPBN.js} +261 -128
- package/dist/extension.cjs +332 -178
- package/dist/extension.js +77 -51
- package/dist/index.cjs +263 -128
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +5 -4
- package/src/decay.ts +54 -52
- package/src/extension.ts +77 -58
- package/src/matcher.ts +27 -6
- package/src/settlement.ts +5 -3
- package/src/store.ts +75 -18
- package/src/tui.ts +133 -68
- package/src/types.ts +1 -0
package/src/tui.ts
CHANGED
|
@@ -57,11 +57,13 @@ export function groupAnchorsByQuadrant(anchors: Anchor[], now: number = Date.now
|
|
|
57
57
|
return groups;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
61
|
+
|
|
60
62
|
/**
|
|
61
63
|
* Strip ANSI escape codes from string
|
|
62
64
|
*/
|
|
63
65
|
export function stripAnsi(str: string): string {
|
|
64
|
-
return str.replace(
|
|
66
|
+
return str.replace(ANSI_REGEX, '');
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -120,30 +122,58 @@ export function getDisplayWidth(str: string): number {
|
|
|
120
122
|
return width;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
export function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is ExtensionContext {
|
|
126
|
+
if (!ctx) return false;
|
|
127
|
+
try {
|
|
128
|
+
return Boolean(ctx.hasUI && ctx.ui);
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
/**
|
|
124
135
|
* Truncate string to target terminal visual width with ellipsis
|
|
125
136
|
*/
|
|
126
137
|
export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…'): string {
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
if (maxWidth <= 0) return '';
|
|
139
|
+
const singleLine = str.replace(/[\r\n\t]+/g, ' ');
|
|
140
|
+
const totalWidth = getDisplayWidth(singleLine);
|
|
141
|
+
if (totalWidth <= maxWidth) return singleLine;
|
|
129
142
|
|
|
130
143
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
131
|
-
const target = maxWidth - ellipsisWidth;
|
|
132
|
-
if (target
|
|
144
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
145
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
133
146
|
|
|
134
147
|
let accumulated = '';
|
|
135
148
|
let accumWidth = 0;
|
|
149
|
+
let i = 0;
|
|
150
|
+
|
|
151
|
+
while (i < singleLine.length) {
|
|
152
|
+
ANSI_REGEX.lastIndex = i;
|
|
153
|
+
const match = ANSI_REGEX.exec(singleLine);
|
|
154
|
+
if (match && match.index === i) {
|
|
155
|
+
accumulated += match[0];
|
|
156
|
+
i += match[0].length;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const codePoint = singleLine.codePointAt(i) || 0;
|
|
161
|
+
const charLen = codePoint > 0xffff ? 2 : 1;
|
|
162
|
+
const fullChar = singleLine.slice(i, i + charLen);
|
|
163
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
136
164
|
|
|
137
|
-
for (const char of str) {
|
|
138
|
-
const charWidth = getDisplayWidth(char);
|
|
139
165
|
if (accumWidth + charWidth > target) {
|
|
140
166
|
break;
|
|
141
167
|
}
|
|
142
|
-
accumulated +=
|
|
168
|
+
accumulated += fullChar;
|
|
143
169
|
accumWidth += charWidth;
|
|
170
|
+
i += charLen;
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
|
|
173
|
+
// Invariant 11: Guard against trailing open brackets right before ellipsis (e.g. "Fix auth (…" -> "Fix auth…")
|
|
174
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, '');
|
|
175
|
+
|
|
176
|
+
return accumulated + ellipsis + (singleLine.includes('\x1b') ? '\x1b[0m' : '');
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
/**
|
|
@@ -271,15 +301,19 @@ export function formatOrigin(anchor: Anchor | string): string {
|
|
|
271
301
|
* Update footer status bar capsule: `⌖ 1`
|
|
272
302
|
*/
|
|
273
303
|
export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void {
|
|
274
|
-
if (!ctx
|
|
304
|
+
if (!hasActiveUI(ctx)) return;
|
|
275
305
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
try {
|
|
307
|
+
const active = store.list({ status: 'active', cwd: ctx.cwd });
|
|
308
|
+
if (active.length === 0) {
|
|
309
|
+
ctx.ui.setStatus('anchor', undefined);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
281
312
|
|
|
282
|
-
|
|
313
|
+
ctx.ui.setStatus('anchor', `⌖ ${active.length}`);
|
|
314
|
+
} catch {
|
|
315
|
+
// Ignore stale context errors during session replacement
|
|
316
|
+
}
|
|
283
317
|
}
|
|
284
318
|
|
|
285
319
|
/**
|
|
@@ -288,49 +322,53 @@ export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore)
|
|
|
288
322
|
* Automatically cleared on agent_start (when user sends first message).
|
|
289
323
|
*/
|
|
290
324
|
export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void {
|
|
291
|
-
if (!ctx
|
|
325
|
+
if (!hasActiveUI(ctx)) return;
|
|
292
326
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
327
|
+
try {
|
|
328
|
+
const active = store.list({ status: 'active', cwd: ctx.cwd });
|
|
329
|
+
if (active.length === 0) {
|
|
330
|
+
ctx.ui.setWidget('anchor-startup', undefined);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
298
333
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
+
const groups = groupAnchorsByQuadrant(active);
|
|
335
|
+
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
336
|
+
const topItems = sorted.slice(0, 3);
|
|
337
|
+
|
|
338
|
+
const factory = (_tui?: any, theme?: any) => ({
|
|
339
|
+
render: (width: number) => {
|
|
340
|
+
const maxTitleWidth = Math.max(12, width - 36);
|
|
341
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c: string, text: string) => text;
|
|
342
|
+
|
|
343
|
+
const lines: string[] = [
|
|
344
|
+
truncateToWidth(fg('accent', `⌖ Anchor · ${active.length} active commitments:`), width)
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
348
|
+
const a = topItems[i];
|
|
349
|
+
const num = a.id.replace(/^anc-/, '').padStart(2, '0');
|
|
350
|
+
const q = classifyAnchor(a);
|
|
351
|
+
const tgt = formatTargetDate(a);
|
|
352
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
353
|
+
lines.push(truncateToWidth(` • ${fg('muted', `[${q}]`)} ${fg('dim', num)} ${titlePadded} ${fg('dim', `(${tgt})`)}`, width));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (sorted.length > 3) {
|
|
357
|
+
lines.push(truncateToWidth(fg('dim', ` (+${sorted.length - 3} more · run /anchor to inspect)`), width));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return lines;
|
|
361
|
+
},
|
|
362
|
+
invalidate: () => {}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Reactive component factory in real TUI mode, static string[] in mock tests
|
|
366
|
+
const content = (ctx as any).ui?.theme ? factory : factory().render(80);
|
|
367
|
+
|
|
368
|
+
ctx.ui.setWidget('anchor-startup', content as any, { placement: 'aboveEditor' });
|
|
369
|
+
} catch {
|
|
370
|
+
// Ignore stale context errors during session replacement
|
|
371
|
+
}
|
|
334
372
|
}
|
|
335
373
|
|
|
336
374
|
/**
|
|
@@ -340,7 +378,7 @@ export async function openAnchorDashboard(
|
|
|
340
378
|
ctx: ExtensionContext,
|
|
341
379
|
store: AnchorStore
|
|
342
380
|
): Promise<void> {
|
|
343
|
-
if (!ctx
|
|
381
|
+
if (!hasActiveUI(ctx)) return;
|
|
344
382
|
|
|
345
383
|
const list = store.list({ status: 'active', cwd: ctx.cwd });
|
|
346
384
|
if (list.length === 0) {
|
|
@@ -358,10 +396,13 @@ export async function openAnchorDashboard(
|
|
|
358
396
|
|
|
359
397
|
const optionMap = new Map<string, Anchor>();
|
|
360
398
|
const displayOptions: string[] = [];
|
|
399
|
+
const termCols = process.stdout?.columns || 96;
|
|
400
|
+
const isNarrow = termCols < 88;
|
|
401
|
+
const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
|
|
361
402
|
|
|
362
403
|
for (let i = 0; i < sortedList.length; i++) {
|
|
363
404
|
const a = sortedList[i];
|
|
364
|
-
const num = (
|
|
405
|
+
const num = a.id.replace(/^anc-/, '').padStart(2, '0');
|
|
365
406
|
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
366
407
|
const origin = formatOrigin(a);
|
|
367
408
|
const target = formatTargetDate(a);
|
|
@@ -373,27 +414,51 @@ export async function openAnchorDashboard(
|
|
|
373
414
|
|
|
374
415
|
// Clean tabular column alignment: ID, Category, Title, Project, Target, Created
|
|
375
416
|
const colNum = `${num} `;
|
|
376
|
-
const colTitle = padToWidth(titleWithFiles,
|
|
417
|
+
const colTitle = padToWidth(titleWithFiles, titleWidth);
|
|
377
418
|
const colOrigin = padToWidth(origin, 10);
|
|
378
419
|
const colTarget = padToWidth(target, 12);
|
|
379
420
|
const colCreated = created;
|
|
380
421
|
|
|
381
|
-
const label =
|
|
422
|
+
const label = isNarrow
|
|
423
|
+
? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd()
|
|
424
|
+
: `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
382
425
|
|
|
383
426
|
optionMap.set(label, a);
|
|
384
427
|
displayOptions.push(label);
|
|
385
428
|
}
|
|
386
429
|
|
|
387
|
-
const selected = await ctx.ui.select('⌖ Anchors (
|
|
430
|
+
const selected = await ctx.ui.select('⌖ Anchors (select to inspect or settle):', displayOptions);
|
|
388
431
|
if (!selected) return;
|
|
389
432
|
|
|
390
433
|
const anchor = optionMap.get(selected);
|
|
391
434
|
if (!anchor) return;
|
|
392
435
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
436
|
+
const action = await ctx.ui.select(
|
|
437
|
+
`⌖ Anchor #${anchor.id}: "${anchor.title}"`,
|
|
438
|
+
[
|
|
439
|
+
'✓ Settle (Mark completed and archive)',
|
|
440
|
+
'ℹ View details & associated files',
|
|
441
|
+
'↩ Cancel'
|
|
442
|
+
]
|
|
443
|
+
);
|
|
444
|
+
if (!action || action.startsWith('↩')) return;
|
|
445
|
+
|
|
446
|
+
if (action.startsWith('✓')) {
|
|
447
|
+
store.settle(anchor.id, { settledBy: 'manual-command' });
|
|
448
|
+
const successMsg = anchor.recurrence === 'daily'
|
|
449
|
+
? `⌖ Completed for today: "${anchor.title}" (resets tomorrow)`
|
|
450
|
+
: `⌖ Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
451
|
+
ctx.ui.notify(successMsg, 'info');
|
|
452
|
+
updateAnchorStatusBar(ctx, store);
|
|
453
|
+
} else if (action.startsWith('ℹ')) {
|
|
454
|
+
const details = [
|
|
455
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
456
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
457
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
458
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(', ')}` : null,
|
|
459
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
460
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || 'global'})`
|
|
461
|
+
].filter(Boolean).join('\n');
|
|
462
|
+
ctx.ui.notify(details, 'info');
|
|
463
|
+
}
|
|
399
464
|
}
|