@esso0428/pi-subagents 0.15.16 → 0.16.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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/agent-history-list.d.ts.map +1 -1
- package/dist/agent-history-list.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -6
- package/dist/index.js.map +1 -1
- package/dist/output-file.js +3 -3
- package/dist/output-file.js.map +1 -1
- package/dist/ui/agent-widget.d.ts +11 -4
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +85 -19
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +2 -2
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +15 -10
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +1 -2
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +19 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js +85 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +1 -1
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +137 -5
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +14 -13
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-role.d.ts +1 -1
- package/dist/ui/conversation-role.d.ts.map +1 -1
- package/dist/ui/conversation-search.js +4 -4
- package/dist/ui/conversation-search.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +17 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +116 -43
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +67 -26
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/ui/deferred-ui-refresh.d.ts +15 -0
- package/dist/ui/deferred-ui-refresh.d.ts.map +1 -0
- package/dist/ui/deferred-ui-refresh.js +50 -0
- package/dist/ui/deferred-ui-refresh.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +55 -14
- package/dist/ui/fleet-list.d.ts.map +1 -1
- package/dist/ui/fleet-list.js +178 -44
- package/dist/ui/fleet-list.js.map +1 -1
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +6 -5
- package/src/agent-history-list.ts +1 -1
- package/src/index.ts +49 -7
- package/src/output-file.ts +3 -3
- package/src/ui/agent-widget.ts +18 -10
- package/src/ui/ccstyle/diff/ansi-utils.ts +2 -2
- package/src/ui/ccstyle/diff/diff-presentation.ts +0 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +22 -21
- package/src/ui/ccstyle/diff/line-width-safety.ts +1 -2
- package/src/ui/ccstyle/diff/shiki-highlight.ts +97 -0
- package/src/ui/ccstyle/tool-renderer.ts +3 -3
- package/src/ui/ccstyle/tool-result.ts +175 -7
- package/src/ui/conversation-role.ts +1 -1
- package/src/ui/conversation-search.ts +4 -4
- package/src/ui/conversation-timeline.ts +7 -7
- package/src/ui/conversation-viewer.ts +16 -7
- package/src/ui/deferred-ui-refresh.ts +72 -0
- package/src/ui/fleet-list.ts +185 -50
- package/src/write-execution.ts +2 -2
- package/test/agent-history-list.test.ts +0 -121
- package/test/agent-manager-history.test.ts +0 -88
- package/test/agent-menu-history-navigation.test.ts +0 -86
- package/test/output-file-history.test.ts +0 -50
- package/test/ui/agent-widget.test.ts +0 -161
- package/test/ui/conversation-blocks.test.ts +0 -103
- package/test/ui/conversation-diff.test.ts +0 -91
- package/test/ui/conversation-search.test.ts +0 -55
- package/test/ui/conversation-viewer.test.ts +0 -438
- package/test/ui/fleet-list.test.ts +0 -135
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { join } from "node:path";
|
|
|
15
15
|
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Container, Key, matchesKey, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
|
+
import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory } from "./agent-history.js";
|
|
19
|
+
import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
|
|
18
20
|
import { AgentManager } from "./agent-manager.js";
|
|
19
21
|
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
20
22
|
import { applyNicoOverrides, BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
@@ -24,8 +26,6 @@ import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabl
|
|
|
24
26
|
import { GroupJoinManager } from "./group-join.js";
|
|
25
27
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
26
28
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
27
|
-
import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory } from "./agent-history.js";
|
|
28
|
-
import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
|
|
29
29
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
30
30
|
import { SubagentScheduler } from "./schedule.js";
|
|
31
31
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type Theme,
|
|
50
50
|
type UICtx,
|
|
51
51
|
} from "./ui/agent-widget.js";
|
|
52
|
+
import { createDeferredUiRefresh } from "./ui/deferred-ui-refresh.js";
|
|
52
53
|
import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
|
|
53
54
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
54
55
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
@@ -374,6 +375,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
374
375
|
fleet.onAgentFinished(record.id);
|
|
375
376
|
scheduleNudge(record.id, () => emitIndividualNudge(record));
|
|
376
377
|
widget.update();
|
|
378
|
+
scheduleUiRefresh();
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
// ---- Group join manager ----
|
|
@@ -385,7 +387,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
387
|
scheduleNudge(groupKey, () => {
|
|
386
388
|
// Re-check at send time
|
|
387
389
|
const unconsumed = records.filter(r => !r.resultConsumed);
|
|
388
|
-
if (unconsumed.length === 0) {
|
|
390
|
+
if (unconsumed.length === 0) {
|
|
391
|
+
widget.update();
|
|
392
|
+
scheduleUiRefresh();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
389
395
|
|
|
390
396
|
const notifications = unconsumed.map(r => formatTaskNotification(r, 300)).join('\n\n');
|
|
391
397
|
const label = partial
|
|
@@ -406,6 +412,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
412
|
}, { deliverAs: "followUp", triggerTurn: true });
|
|
407
413
|
});
|
|
408
414
|
widget.update();
|
|
415
|
+
scheduleUiRefresh();
|
|
409
416
|
},
|
|
410
417
|
30_000,
|
|
411
418
|
);
|
|
@@ -463,6 +470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
463
470
|
widget.markFinished(record.id);
|
|
464
471
|
fleet.onAgentFinished(record.id);
|
|
465
472
|
widget.update();
|
|
473
|
+
scheduleUiRefresh();
|
|
466
474
|
return;
|
|
467
475
|
}
|
|
468
476
|
|
|
@@ -470,6 +478,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
478
|
// don't send an individual nudge — finalizeBatch will pick it up retroactively.
|
|
471
479
|
if (currentBatchAgents.some(a => a.id === record.id)) {
|
|
472
480
|
widget.update();
|
|
481
|
+
scheduleUiRefresh();
|
|
473
482
|
return;
|
|
474
483
|
}
|
|
475
484
|
|
|
@@ -480,6 +489,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
480
489
|
// 'held' → do nothing, group will fire later
|
|
481
490
|
// 'delivered' → group callback already fired
|
|
482
491
|
widget.update();
|
|
492
|
+
scheduleUiRefresh();
|
|
483
493
|
}, undefined, (record) => {
|
|
484
494
|
// Emit started event when agent transitions to running (including from queue)
|
|
485
495
|
pi.events.emit("subagents:started", {
|
|
@@ -487,6 +497,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
497
|
type: record.type,
|
|
488
498
|
description: record.description,
|
|
489
499
|
});
|
|
500
|
+
widget.ensureTimer();
|
|
501
|
+
widget.update();
|
|
502
|
+
fleet.ensureTimer();
|
|
503
|
+
fleet.update();
|
|
504
|
+
scheduleUiRefresh();
|
|
490
505
|
}, (record, info) => {
|
|
491
506
|
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
492
507
|
pi.events.emit("subagents:compacted", {
|
|
@@ -581,6 +596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
581
596
|
});
|
|
582
597
|
|
|
583
598
|
pi.on("session_before_switch", () => {
|
|
599
|
+
deferredUiRefresh.cancel();
|
|
584
600
|
manager.clearCompleted(true);
|
|
585
601
|
scheduler.stop();
|
|
586
602
|
});
|
|
@@ -602,6 +618,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
602
618
|
manager.abortAll();
|
|
603
619
|
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
604
620
|
pendingNudges.clear();
|
|
621
|
+
deferredUiRefresh.dispose();
|
|
605
622
|
fleet.dispose();
|
|
606
623
|
manager.dispose();
|
|
607
624
|
});
|
|
@@ -614,13 +631,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
631
|
let widgetMode: WidgetMode = "background";
|
|
615
632
|
function getWidgetMode(): WidgetMode { return widgetMode; }
|
|
616
633
|
const widget = new AgentWidget(manager, agentActivity, getWidgetMode);
|
|
617
|
-
function setWidgetMode(m: WidgetMode): void {
|
|
634
|
+
function setWidgetMode(m: WidgetMode): void {
|
|
635
|
+
const changed = widgetMode !== m;
|
|
636
|
+
widgetMode = m;
|
|
637
|
+
widget.update();
|
|
638
|
+
if (changed) scheduleUiRefresh();
|
|
639
|
+
}
|
|
618
640
|
|
|
619
641
|
// Claude Code-style FleetView: navigable list of main + subagents below the editor.
|
|
620
642
|
const fleet = new FleetList(manager, agentActivity, () => currentCtx?.cwd, pi, () => currentCtx);
|
|
643
|
+
|
|
644
|
+
// One render-only scheduler is shared by both widgets. The callback resolves
|
|
645
|
+
// the live TUI target at fire time because session switches invalidate the
|
|
646
|
+
// previous widget context; both widgets use the same TUI, so one successful
|
|
647
|
+
// request is sufficient.
|
|
648
|
+
const deferredUiRefresh = createDeferredUiRefresh(() => {
|
|
649
|
+
if (widget.requestUiRefresh(true)) return;
|
|
650
|
+
fleet.requestUiRefresh(true);
|
|
651
|
+
});
|
|
652
|
+
const scheduleUiRefresh = (): void => deferredUiRefresh.schedule();
|
|
653
|
+
|
|
621
654
|
let fleetViewEnabled = true;
|
|
622
655
|
function isFleetViewEnabled(): boolean { return fleetViewEnabled; }
|
|
623
|
-
function setFleetViewEnabled(b: boolean): void {
|
|
656
|
+
function setFleetViewEnabled(b: boolean): void {
|
|
657
|
+
const changed = fleetViewEnabled !== b;
|
|
658
|
+
fleetViewEnabled = b;
|
|
659
|
+
fleet.setEnabled(b);
|
|
660
|
+
if (changed) scheduleUiRefresh();
|
|
661
|
+
}
|
|
624
662
|
|
|
625
663
|
// Project/global default for writing the subagent .output transcript. A custom
|
|
626
664
|
// agent's `output_transcript` frontmatter overrides this per spawn; when the
|
|
@@ -720,8 +758,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
720
758
|
|
|
721
759
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
722
760
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
723
|
-
widget.setUICtx(ctx.ui as UICtx);
|
|
724
|
-
fleet.setUICtx(ctx.ui as unknown as FleetUICtx);
|
|
761
|
+
const widgetContextChanged = widget.setUICtx(ctx.ui as UICtx);
|
|
762
|
+
const fleetContextChanged = fleet.setUICtx(ctx.ui as unknown as FleetUICtx);
|
|
763
|
+
if (widgetContextChanged || fleetContextChanged) scheduleUiRefresh();
|
|
725
764
|
widget.onTurnStart();
|
|
726
765
|
});
|
|
727
766
|
|
|
@@ -1319,6 +1358,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1319
1358
|
widget.update();
|
|
1320
1359
|
fleet.ensureTimer();
|
|
1321
1360
|
fleet.update();
|
|
1361
|
+
scheduleUiRefresh();
|
|
1322
1362
|
|
|
1323
1363
|
// Emit created event
|
|
1324
1364
|
pi.events.emit("subagents:created", {
|
|
@@ -1379,8 +1419,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1379
1419
|
fgId = a.id;
|
|
1380
1420
|
agentActivity.set(a.id, fgState);
|
|
1381
1421
|
widget.ensureTimer();
|
|
1422
|
+
widget.update();
|
|
1382
1423
|
fleet.ensureTimer();
|
|
1383
1424
|
fleet.update();
|
|
1425
|
+
scheduleUiRefresh();
|
|
1384
1426
|
break;
|
|
1385
1427
|
}
|
|
1386
1428
|
}
|
package/src/output-file.ts
CHANGED
|
@@ -66,7 +66,6 @@ export function streamToOutputFile(
|
|
|
66
66
|
historyPath?: string,
|
|
67
67
|
): () => void {
|
|
68
68
|
let writtenCount = 1; // initial user prompt already written
|
|
69
|
-
const outputPaths = historyPath && historyPath !== path ? [path, historyPath] : [path];
|
|
70
69
|
|
|
71
70
|
const flush = () => {
|
|
72
71
|
const messages = session.messages;
|
|
@@ -80,9 +79,10 @@ export function streamToOutputFile(
|
|
|
80
79
|
timestamp: new Date().toISOString(),
|
|
81
80
|
cwd,
|
|
82
81
|
};
|
|
83
|
-
|
|
82
|
+
const entryJson = JSON.stringify(entry) + "\n";
|
|
83
|
+
for (const target of historyPath && historyPath !== path ? [path, historyPath] : [path]) {
|
|
84
84
|
try {
|
|
85
|
-
appendFileSync(
|
|
85
|
+
appendFileSync(target, entryJson, "utf-8");
|
|
86
86
|
} catch { /* ignore write errors */ }
|
|
87
87
|
}
|
|
88
88
|
writtenCount++;
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -275,16 +275,24 @@ export class AgentWidget {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/** Set the UI context (grabbed from first tool execution). */
|
|
278
|
-
setUICtx(ctx: UICtx) {
|
|
279
|
-
if (ctx
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
setUICtx(ctx: UICtx): boolean {
|
|
279
|
+
if (ctx === this.uiCtx) return false;
|
|
280
|
+
|
|
281
|
+
// UICtx changed — the widget registered on the old context is gone.
|
|
282
|
+
// Force re-registration on next update().
|
|
283
|
+
this.uiCtx = ctx;
|
|
284
|
+
this.widgetRegistered = false;
|
|
285
|
+
this.tui = undefined;
|
|
286
|
+
this.lastStatusText = undefined;
|
|
287
|
+
this.lastRenderKey = undefined;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Request a render on the currently registered TUI without touching input. */
|
|
292
|
+
requestUiRefresh(force = true): boolean {
|
|
293
|
+
if (!this.tui) return false;
|
|
294
|
+
this.tui.requestRender(force);
|
|
295
|
+
return true;
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
/**
|
|
@@ -90,7 +90,7 @@ export function stripBackgroundSgrParams(params: readonly number[]): number[] {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function filterSgrSequences(text: string, filter: (params: number[]) => number[]): string {
|
|
93
|
-
if (!text
|
|
93
|
+
if (!text?.includes("\x1b[")) {
|
|
94
94
|
return text;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -159,7 +159,7 @@ function sanitizeColonSgr(rawParams: string): string {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export function sanitizeAnsiForThemedOutput(text: string): string {
|
|
162
|
-
if (!text
|
|
162
|
+
if (!text?.includes("\x1b[")) return text;
|
|
163
163
|
return text.replace(ANSI_SGR_WITH_COLON_PATTERN, (sequence, rawParams: string) =>
|
|
164
164
|
rawParams.includes(":")
|
|
165
165
|
? sanitizeColonSgr(rawParams)
|
|
@@ -39,7 +39,6 @@ export function resolveDiffPresentationMode(
|
|
|
39
39
|
return canRenderSplitLayout ? "split" : "unified";
|
|
40
40
|
case "unified":
|
|
41
41
|
return "unified";
|
|
42
|
-
case "auto":
|
|
43
42
|
default:
|
|
44
43
|
return safeWidth >= (config.diffSplitMinWidth ?? 120) && canRenderSplitLayout ? "split" : "unified";
|
|
45
44
|
}
|
|
@@ -1,39 +1,37 @@
|
|
|
1
|
+
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import {
|
|
3
|
+
type Component,
|
|
2
4
|
Text,
|
|
3
5
|
truncateToWidth,
|
|
4
6
|
visibleWidth,
|
|
5
7
|
wrapTextWithAnsi,
|
|
6
|
-
type Component,
|
|
7
8
|
} from "@earendil-works/pi-tui";
|
|
8
|
-
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import {
|
|
10
10
|
ANSI_SGR_PATTERN,
|
|
11
|
-
|
|
11
|
+
expandSgrReset,
|
|
12
12
|
filterSgrSequences,
|
|
13
|
-
toSgrParams,
|
|
14
13
|
readSgrColorSequence,
|
|
15
|
-
|
|
16
|
-
expandSgrReset,
|
|
14
|
+
toSgrParams,
|
|
17
15
|
} from "./ansi-utils.js";
|
|
18
|
-
import {
|
|
19
|
-
buildCollapsedDiffHintText,
|
|
20
|
-
clampRenderedLineToWidth,
|
|
21
|
-
clampRenderedLinesToWidth,
|
|
22
|
-
} from "./line-width-safety.js";
|
|
23
16
|
import {
|
|
24
17
|
buildDiffSummaryText,
|
|
18
|
+
type DiffPresentationMode,
|
|
25
19
|
normalizeDiffRenderWidth,
|
|
26
20
|
resolveDiffPresentationMode,
|
|
27
|
-
type DiffPresentationMode,
|
|
28
21
|
} from "./diff-presentation.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
22
|
+
import {
|
|
23
|
+
buildCollapsedDiffHintText,
|
|
24
|
+
clampRenderedLinesToWidth,
|
|
25
|
+
clampRenderedLineToWidth,
|
|
26
|
+
} from "./line-width-safety.js";
|
|
27
|
+
import { sanitizeAnsiForThemedOutput, sanitizeToolResultText, showMoreHintText } from "./render-utils.js";
|
|
31
28
|
import {
|
|
32
29
|
DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
33
30
|
type DiffIndicatorMode,
|
|
34
|
-
type ToolDisplayConfig,
|
|
35
31
|
type PersistedDiff,
|
|
32
|
+
type ToolDisplayConfig,
|
|
36
33
|
} from "./types.js";
|
|
34
|
+
import { splitWriteContentLines } from "./write-display-utils.js";
|
|
37
35
|
|
|
38
36
|
const MAX_HL_CHARS = 200_000;
|
|
39
37
|
|
|
@@ -267,7 +265,7 @@ function wrapToWidth(text: string, width: number, wordWrap: boolean): string[] {
|
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
function resolveLanguageFromPath(rawPath: string | undefined): string | undefined {
|
|
270
|
-
if (!rawPath
|
|
268
|
+
if (!rawPath?.trim()) {
|
|
271
269
|
return undefined;
|
|
272
270
|
}
|
|
273
271
|
const normalizedPath = rawPath.replace(/^@/, "").trim();
|
|
@@ -281,7 +279,7 @@ function resolveLanguageFromPath(rawPath: string | undefined): string | undefine
|
|
|
281
279
|
}
|
|
282
280
|
}
|
|
283
281
|
|
|
284
|
-
function
|
|
282
|
+
function _resolveShikiTheme(theme: DiffTheme): string {
|
|
285
283
|
if (process.env.DIFF_THEME) return process.env.DIFF_THEME;
|
|
286
284
|
const background =
|
|
287
285
|
parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ??
|
|
@@ -828,7 +826,9 @@ function tokenizeInlineDiff(input: string): Array<{ value: string; start: number
|
|
|
828
826
|
const pattern = /(\s+|[A-Za-z0-9_]+|[^A-Za-z0-9_\s])/g;
|
|
829
827
|
let match: RegExpExecArray | null;
|
|
830
828
|
|
|
831
|
-
while (
|
|
829
|
+
while (true) {
|
|
830
|
+
match = pattern.exec(input);
|
|
831
|
+
if (match === null) break;
|
|
832
832
|
const value = match[0] ?? "";
|
|
833
833
|
if (!value) {
|
|
834
834
|
continue;
|
|
@@ -1139,13 +1139,15 @@ function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
|
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
1141
|
function extractThemeBackgroundAnsi(text: string): string | undefined {
|
|
1142
|
-
if (!text
|
|
1142
|
+
if (!text?.includes("\x1b[")) {
|
|
1143
1143
|
return undefined;
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
ANSI_SGR_PATTERN.lastIndex = 0;
|
|
1147
1147
|
let match: RegExpExecArray | null;
|
|
1148
|
-
while (
|
|
1148
|
+
while (true) {
|
|
1149
|
+
match = ANSI_SGR_PATTERN.exec(text);
|
|
1150
|
+
if (match === null) break;
|
|
1149
1151
|
const parsed = toSgrParams(match[1] ?? "");
|
|
1150
1152
|
for (let index = 0; index < parsed.length; index += 1) {
|
|
1151
1153
|
const param = parsed[index] ?? 0;
|
|
@@ -1372,7 +1374,6 @@ function resolveIndicatorGlyph(
|
|
|
1372
1374
|
return " ";
|
|
1373
1375
|
}
|
|
1374
1376
|
return kind === "add" ? "+" : "-";
|
|
1375
|
-
case "none":
|
|
1376
1377
|
default:
|
|
1377
1378
|
return " ";
|
|
1378
1379
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { normalizeDiffRenderWidth } from "./diff-presentation.js";
|
|
2
|
-
import { pluralize } from "./render-utils.js";
|
|
3
|
-
import { showMoreHintText } from "./render-utils.js";
|
|
2
|
+
import { pluralize, showMoreHintText } from "./render-utils.js";
|
|
4
3
|
|
|
5
4
|
export interface WidthMeasurementOps {
|
|
6
5
|
measure(text: string): number;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Maximum source size eligible for optional asynchronous Shiki enhancement. */
|
|
2
|
+
export const MAX_HL_CHARS = 32_000;
|
|
3
|
+
const MAX_CACHE_ENTRIES = 48;
|
|
4
|
+
|
|
5
|
+
type CodeToAnsi = (code: string, lang: string, theme: string) => Promise<string>;
|
|
6
|
+
export type ShikiLoader = () => Promise<CodeToAnsi>;
|
|
7
|
+
type Subscriber = { fallback: readonly string[]; invalidate: () => void };
|
|
8
|
+
type Pending = { work: Promise<void>; subscribers: Set<Subscriber> };
|
|
9
|
+
|
|
10
|
+
let defaultLoad: Promise<CodeToAnsi> | undefined;
|
|
11
|
+
const loadDefault: ShikiLoader = () => {
|
|
12
|
+
if (!defaultLoad) {
|
|
13
|
+
defaultLoad = import("@shikijs/cli")
|
|
14
|
+
.then((module) => module.codeToANSI as CodeToAnsi)
|
|
15
|
+
.catch((error) => {
|
|
16
|
+
defaultLoad = undefined;
|
|
17
|
+
throw error;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return defaultLoad;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function differs(lines: readonly string[], fallback: readonly string[]): boolean {
|
|
24
|
+
return lines.length !== fallback.length || lines.some((line, index) => line !== fallback[index]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Small, viewer-local async cache. The synchronous Pi highlighter remains the
|
|
29
|
+
* first paint; Shiki is only requested by an expanded card that has metadata.
|
|
30
|
+
*/
|
|
31
|
+
export class ShikiHighlightCache {
|
|
32
|
+
private readonly cache = new Map<string, string[]>();
|
|
33
|
+
private readonly pending = new Map<string, Pending>();
|
|
34
|
+
|
|
35
|
+
constructor(private readonly loader: ShikiLoader = loadDefault) {}
|
|
36
|
+
|
|
37
|
+
get(
|
|
38
|
+
code: string,
|
|
39
|
+
lang: string | undefined,
|
|
40
|
+
theme: string,
|
|
41
|
+
fallback: readonly string[],
|
|
42
|
+
invalidate?: () => void,
|
|
43
|
+
): string[] | undefined {
|
|
44
|
+
if (!lang || code.length > MAX_HL_CHARS) return undefined;
|
|
45
|
+
|
|
46
|
+
const key = `${theme}\0${lang}\0${code}`;
|
|
47
|
+
const cached = this.cache.get(key);
|
|
48
|
+
if (cached) {
|
|
49
|
+
// LRU: a frequently viewed card should not evict itself immediately.
|
|
50
|
+
this.cache.delete(key);
|
|
51
|
+
this.cache.set(key, cached);
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const existing = this.pending.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
if (invalidate && ![...existing.subscribers].some((item) => item.invalidate === invalidate)) {
|
|
58
|
+
existing.subscribers.add({ fallback, invalidate });
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pending: Pending = { work: undefined as never, subscribers: new Set() };
|
|
64
|
+
if (invalidate) pending.subscribers.add({ fallback, invalidate });
|
|
65
|
+
pending.work = Promise.resolve()
|
|
66
|
+
.then(() => this.loader())
|
|
67
|
+
.then((highlight) => highlight(code, lang, theme))
|
|
68
|
+
.then((ansi) => {
|
|
69
|
+
const lines = ansi.replace(/\r/g, "").replace(/\n$/, "").split("\n");
|
|
70
|
+
this.cache.set(key, lines);
|
|
71
|
+
while (this.cache.size > MAX_CACHE_ENTRIES) {
|
|
72
|
+
const oldest = this.cache.keys().next().value;
|
|
73
|
+
if (oldest !== undefined) this.cache.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
for (const subscriber of pending.subscribers) {
|
|
76
|
+
if (!differs(lines, subscriber.fallback)) continue;
|
|
77
|
+
try {
|
|
78
|
+
subscriber.invalidate();
|
|
79
|
+
} catch {
|
|
80
|
+
// A stale card must not prevent another card from being repainted.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {
|
|
85
|
+
// The synchronous Pi highlighter remains the durable fallback.
|
|
86
|
+
})
|
|
87
|
+
.finally(() => this.pending.delete(key));
|
|
88
|
+
this.pending.set(key, pending);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
clear(): void {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const shikiHighlightCache = new ShikiHighlightCache();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createViewerCcstyleResult, oneLine } from "./tool-result.js";
|
|
1
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
2
|
import { DEFAULT_TOOL_DISPLAY_CONFIG } from "./diff/types.js";
|
|
3
|
+
import { createViewerCcstyleResult, oneLine } from "./tool-result.js";
|
|
4
4
|
|
|
5
5
|
export interface ViewerToolRenderers {
|
|
6
6
|
name: string;
|
|
@@ -82,5 +82,5 @@ export function createViewerCcstyleTool(toolName: string, isHovered: () => boole
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export { renderRichToolResult } from "./tool-result.js";
|
|
86
85
|
export type { PersistedDiff, ViewerDiffConfig } from "./diff/types.js";
|
|
86
|
+
export { renderRichToolResult } from "./tool-result.js";
|