pi-anchor 0.2.1 → 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 -195
- package/README_zh.md +115 -205
- package/dist/{chunk-B652T7QR.js → chunk-G72UHPBN.js} +218 -121
- package/dist/extension.cjs +286 -168
- package/dist/extension.js +73 -48
- package/dist/index.cjs +220 -121
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
- package/src/decay.ts +54 -52
- package/src/extension.ts +74 -55
- package/src/matcher.ts +27 -6
- package/src/settlement.ts +5 -3
- package/src/store.ts +75 -18
- package/src/tui.ts +82 -59
- package/src/types.ts +1 -0
package/src/tui.ts
CHANGED
|
@@ -122,13 +122,23 @@ export function getDisplayWidth(str: string): number {
|
|
|
122
122
|
return width;
|
|
123
123
|
}
|
|
124
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
|
+
|
|
125
134
|
/**
|
|
126
135
|
* Truncate string to target terminal visual width with ellipsis
|
|
127
136
|
*/
|
|
128
137
|
export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…'): string {
|
|
129
138
|
if (maxWidth <= 0) return '';
|
|
130
|
-
const
|
|
131
|
-
|
|
139
|
+
const singleLine = str.replace(/[\r\n\t]+/g, ' ');
|
|
140
|
+
const totalWidth = getDisplayWidth(singleLine);
|
|
141
|
+
if (totalWidth <= maxWidth) return singleLine;
|
|
132
142
|
|
|
133
143
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
134
144
|
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
@@ -138,18 +148,18 @@ export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…')
|
|
|
138
148
|
let accumWidth = 0;
|
|
139
149
|
let i = 0;
|
|
140
150
|
|
|
141
|
-
while (i <
|
|
151
|
+
while (i < singleLine.length) {
|
|
142
152
|
ANSI_REGEX.lastIndex = i;
|
|
143
|
-
const match = ANSI_REGEX.exec(
|
|
153
|
+
const match = ANSI_REGEX.exec(singleLine);
|
|
144
154
|
if (match && match.index === i) {
|
|
145
155
|
accumulated += match[0];
|
|
146
156
|
i += match[0].length;
|
|
147
157
|
continue;
|
|
148
158
|
}
|
|
149
159
|
|
|
150
|
-
const codePoint =
|
|
160
|
+
const codePoint = singleLine.codePointAt(i) || 0;
|
|
151
161
|
const charLen = codePoint > 0xffff ? 2 : 1;
|
|
152
|
-
const fullChar =
|
|
162
|
+
const fullChar = singleLine.slice(i, i + charLen);
|
|
153
163
|
const charWidth = getDisplayWidth(fullChar);
|
|
154
164
|
|
|
155
165
|
if (accumWidth + charWidth > target) {
|
|
@@ -163,7 +173,7 @@ export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…')
|
|
|
163
173
|
// Invariant 11: Guard against trailing open brackets right before ellipsis (e.g. "Fix auth (…" -> "Fix auth…")
|
|
164
174
|
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, '');
|
|
165
175
|
|
|
166
|
-
return accumulated + ellipsis + (
|
|
176
|
+
return accumulated + ellipsis + (singleLine.includes('\x1b') ? '\x1b[0m' : '');
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
/**
|
|
@@ -291,15 +301,19 @@ export function formatOrigin(anchor: Anchor | string): string {
|
|
|
291
301
|
* Update footer status bar capsule: `⌖ 1`
|
|
292
302
|
*/
|
|
293
303
|
export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void {
|
|
294
|
-
if (!ctx
|
|
304
|
+
if (!hasActiveUI(ctx)) return;
|
|
295
305
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
}
|
|
301
312
|
|
|
302
|
-
|
|
313
|
+
ctx.ui.setStatus('anchor', `⌖ ${active.length}`);
|
|
314
|
+
} catch {
|
|
315
|
+
// Ignore stale context errors during session replacement
|
|
316
|
+
}
|
|
303
317
|
}
|
|
304
318
|
|
|
305
319
|
/**
|
|
@@ -308,49 +322,53 @@ export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore)
|
|
|
308
322
|
* Automatically cleared on agent_start (when user sends first message).
|
|
309
323
|
*/
|
|
310
324
|
export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void {
|
|
311
|
-
if (!ctx
|
|
325
|
+
if (!hasActiveUI(ctx)) return;
|
|
312
326
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
333
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
}
|
|
354
372
|
}
|
|
355
373
|
|
|
356
374
|
/**
|
|
@@ -360,7 +378,7 @@ export async function openAnchorDashboard(
|
|
|
360
378
|
ctx: ExtensionContext,
|
|
361
379
|
store: AnchorStore
|
|
362
380
|
): Promise<void> {
|
|
363
|
-
if (!ctx
|
|
381
|
+
if (!hasActiveUI(ctx)) return;
|
|
364
382
|
|
|
365
383
|
const list = store.list({ status: 'active', cwd: ctx.cwd });
|
|
366
384
|
if (list.length === 0) {
|
|
@@ -378,10 +396,13 @@ export async function openAnchorDashboard(
|
|
|
378
396
|
|
|
379
397
|
const optionMap = new Map<string, Anchor>();
|
|
380
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;
|
|
381
402
|
|
|
382
403
|
for (let i = 0; i < sortedList.length; i++) {
|
|
383
404
|
const a = sortedList[i];
|
|
384
|
-
const num = (
|
|
405
|
+
const num = a.id.replace(/^anc-/, '').padStart(2, '0');
|
|
385
406
|
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
386
407
|
const origin = formatOrigin(a);
|
|
387
408
|
const target = formatTargetDate(a);
|
|
@@ -393,12 +414,14 @@ export async function openAnchorDashboard(
|
|
|
393
414
|
|
|
394
415
|
// Clean tabular column alignment: ID, Category, Title, Project, Target, Created
|
|
395
416
|
const colNum = `${num} `;
|
|
396
|
-
const colTitle = padToWidth(titleWithFiles,
|
|
417
|
+
const colTitle = padToWidth(titleWithFiles, titleWidth);
|
|
397
418
|
const colOrigin = padToWidth(origin, 10);
|
|
398
419
|
const colTarget = padToWidth(target, 12);
|
|
399
420
|
const colCreated = created;
|
|
400
421
|
|
|
401
|
-
const label =
|
|
422
|
+
const label = isNarrow
|
|
423
|
+
? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd()
|
|
424
|
+
: `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
402
425
|
|
|
403
426
|
optionMap.set(label, a);
|
|
404
427
|
displayOptions.push(label);
|