@cortexkit/aft-opencode 0.56.2 → 0.57.1

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.
Files changed (39) hide show
  1. package/dist/bg-notifications.d.ts +1 -0
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/entry/server.js +2650 -267
  4. package/dist/index.js +420 -228
  5. package/dist/lsp-github-install.d.ts.map +1 -1
  6. package/dist/permissions/v2-service.d.ts +30 -0
  7. package/dist/permissions/v2-service.d.ts.map +1 -0
  8. package/dist/permissions/v2.d.ts +121 -9
  9. package/dist/permissions/v2.d.ts.map +1 -1
  10. package/dist/shared/rpc-server.d.ts.map +1 -1
  11. package/dist/shared/status.d.ts +8 -1
  12. package/dist/shared/status.d.ts.map +1 -1
  13. package/dist/subc-tool-schemas.d.ts +11 -0
  14. package/dist/subc-tool-schemas.d.ts.map +1 -1
  15. package/dist/tool-registration.d.ts.map +1 -1
  16. package/dist/tools/definitions/v2.d.ts +6 -0
  17. package/dist/tools/definitions/v2.d.ts.map +1 -1
  18. package/dist/tools/hoisted/v2.d.ts +12 -2
  19. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  20. package/dist/tools/hoisted.d.ts.map +1 -1
  21. package/dist/tools/semantic.d.ts.map +1 -1
  22. package/dist/tui/v2-status.d.ts +8 -0
  23. package/dist/tui/v2-status.d.ts.map +1 -1
  24. package/index.js +14 -0
  25. package/package.json +14 -10
  26. package/server.js +8 -0
  27. package/src/shared/rpc-server.ts +5 -0
  28. package/src/shared/status.ts +16 -9
  29. package/src/tui/index.tsx +1 -0
  30. package/src/tui/sidebar-view.tsx +722 -0
  31. package/src/tui/sidebar.tsx +42 -560
  32. package/src/tui/v2-status.ts +8 -1
  33. package/src/tui/v2.tsx +173 -30
  34. package/src/tui-compiled/index.tsx +1 -1
  35. package/src/tui-compiled/sidebar-view.tsx +815 -0
  36. package/src/tui-compiled/sidebar.tsx +45 -693
  37. package/src/tui-compiled/v2-status.ts +8 -1
  38. package/src/tui-compiled/v2.tsx +163 -54
  39. package/tui.js +7 -0
@@ -10,238 +10,54 @@ import { canonicalizeProjectRoot } from "@cortexkit/aft-bridge";
10
10
  import type { TuiPluginApi, TuiSlotPlugin, TuiThemeCurrent } from "@opencode/plugin/tui";
11
11
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
12
12
  import { AftRpcClient } from "../shared/rpc-client";
13
- import {
14
- type AftStatusSnapshot,
15
- coerceAftStatus,
16
- formatSemanticIndexStatus,
17
- formatSemanticRefreshing,
18
- type StatusBar,
19
- type StatusCompression,
20
- worktreeCacheRoleNote,
21
- } from "../shared/status";
13
+ import { type AftStatusSnapshot, coerceAftStatus } from "../shared/status";
22
14
  import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
23
- import { badgeTextColor } from "./badge-contrast";
24
15
  import {
25
16
  createDebouncedStatusRefresh,
26
17
  refreshAftTuiSocketScope,
27
18
  subscribeStatusInvalidations,
28
19
  } from "./notification-socket";
29
20
  import {
30
- type AftTuiPrefs,
31
21
  computeEffectiveOrder,
32
- DEFAULT_PREFS,
33
22
  DEFAULT_SLOT_ORDER,
34
23
  PLUGIN_KEY,
35
- persistCollapsedIfEnabled,
36
24
  readTuiPreferencesFile,
37
- resolveAftPrefs,
38
- seedCollapsedFromPrefs,
39
- watchTuiPreferences,
40
25
  } from "./preferences";
26
+ import { AftSidebarPanel, createSidebarPreferences, type SidebarPalette } from "./sidebar-view";
27
+
28
+ // The panel itself lives in ./sidebar-view, so the newer host's entry
29
+ // (./v2.tsx) renders exactly the same components. These re-exports keep this
30
+ // module the import site it has always been for callers that only want the
31
+ // formatting helpers.
32
+ export {
33
+ type CompressionRow,
34
+ collapsedCompressionValue,
35
+ collapsedHealthLights,
36
+ degradedReasonLabel,
37
+ formatCompressionSidebarRows,
38
+ type HealthLights,
39
+ type HealthLightTone,
40
+ } from "./sidebar-view";
41
41
 
42
- const SINGLE_BORDER = { type: "single" } as any;
43
42
  const REFRESH_DEBOUNCE_MS = 200;
44
43
 
45
- function formatBytes(n: number): string {
46
- if (!Number.isFinite(n) || n <= 0) return "—";
47
- if (n >= 1_073_741_824) return `${(n / 1_073_741_824).toFixed(1)} GB`;
48
- if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`;
49
- if (n >= 1_024) return `${Math.round(n / 1_024)} KB`;
50
- return `${n} B`;
51
- }
52
-
53
- function formatCount(n: number | null | undefined): string {
54
- if (n == null || !Number.isFinite(n)) return "—";
55
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
56
- if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
57
- return String(n);
58
- }
59
-
60
- /** Tagged rows for the Compression section. Each scope (Session / Project)
61
- * emits a "scope" header followed by two "stat" rows — Tokens Saved and
62
- * Compression Ratio — so the renderer can use the same StatRow layout as
63
- * Search Index / Semantic Index above. Pi's monospace overlay and the
64
- * OpenCode TUI dialog/sidebar all consume this same shape. */
65
- export type CompressionRow =
66
- | { kind: "scope"; label: string }
67
- | { kind: "stat"; label: string; value: string };
68
-
69
- function appendScope(
70
- rows: CompressionRow[],
71
- label: string,
72
- scope: {
73
- events: number;
74
- original_tokens: number;
75
- compressed_tokens: number;
76
- savings_tokens: number;
77
- },
78
- ): void {
79
- const savings = scope.savings_tokens;
80
- const pct = scope.original_tokens > 0 ? Math.round((savings / scope.original_tokens) * 100) : 0;
81
- rows.push({ kind: "scope", label });
82
- rows.push({ kind: "stat", label: "Tokens Saved", value: savings.toLocaleString("en-US") });
83
- rows.push({ kind: "stat", label: "Compression Ratio", value: `${pct}%` });
84
- }
85
-
86
- export function formatCompressionSidebarRows(
87
- compression: StatusCompression | undefined,
88
- ): CompressionRow[] {
89
- if (!compression || compression.project.events <= 0) return [];
90
-
91
- const rows: CompressionRow[] = [];
92
- if (compression.session.events > 0) {
93
- appendScope(rows, "Session", compression.session);
94
- }
95
- appendScope(rows, "Project", compression.project);
96
-
97
- return rows;
98
- }
99
-
100
- // Map index status → (label, theme color name). The label is what we want
101
- // the user to see; the color encodes severity so the eye lands on warnings.
102
- function statusDisplay(status: string): { label: string; tone: "ok" | "warn" | "err" | "muted" } {
103
- switch (status) {
104
- case "ready":
105
- return { label: "ready", tone: "ok" };
106
- case "loading":
107
- case "building":
108
- return { label: status, tone: "warn" };
109
- case "failed":
110
- case "error":
111
- return { label: status, tone: "err" };
112
- case "disabled":
113
- return { label: "disabled", tone: "muted" };
114
- default:
115
- return { label: status || "unknown", tone: "muted" };
116
- }
117
- }
118
-
119
- const StatRow = (props: {
120
- theme: TuiThemeCurrent;
121
- label: string;
122
- value: string;
123
- tone?: "ok" | "warn" | "err" | "muted" | "accent";
124
- }) => {
125
- const fg = createMemo(() => {
126
- switch (props.tone) {
127
- case "ok":
128
- return props.theme.success ?? props.theme.accent;
129
- case "warn":
130
- return props.theme.warning;
131
- case "err":
132
- return props.theme.error;
133
- case "muted":
134
- return props.theme.textMuted;
135
- case "accent":
136
- return props.theme.accent;
137
- default:
138
- return props.theme.text;
139
- }
140
- });
141
-
142
- return (
143
- <box width="100%" flexDirection="row" justifyContent="space-between">
144
- <text fg={props.theme.textMuted}>{props.label}</text>
145
- <text fg={fg()}>
146
- <b>{props.value}</b>
147
- </text>
148
- </box>
149
- );
150
- };
151
-
152
- const SectionHeader = (props: { theme: TuiThemeCurrent; title: string; marginTop?: number }) => (
153
- <box width="100%" marginTop={props.marginTop ?? 1}>
154
- <text fg={props.theme.text}>
155
- <b>{props.title}</b>
156
- </text>
157
- </box>
158
- );
159
-
160
- // Map a status tone to a theme color — used for the collapsed-view status dots.
161
- function toneColor(theme: TuiThemeCurrent, tone: "ok" | "warn" | "err" | "muted"): string {
162
- switch (tone) {
163
- case "ok":
164
- return theme.success ?? theme.accent;
165
- case "warn":
166
- return theme.warning;
167
- case "err":
168
- return theme.error;
169
- default:
170
- return theme.textMuted;
171
- }
172
- }
173
-
174
- // Collapsed-view row: label on the left, a status dot (or compact value) on the
175
- // right. Mirrors the expanded StatRow layout so the columns line up.
176
- const CollapsedRow = (props: { theme: TuiThemeCurrent; label: string; children: JSX.Element }) => (
177
- <box width="100%" flexDirection="row" justifyContent="space-between">
178
- <text fg={props.theme.textMuted}>{props.label}</text>
179
- {props.children}
180
- </box>
181
- );
182
-
183
- // Compact "saved / ratio" string for the collapsed Compression row — e.g.
184
- // "7.6M / 64%". Uses the local `formatCount` (not the aft-bridge token
185
- // formatter) so the TUI bundle doesn't pull the bridge barrel, which exports
186
- // URL-fetch helpers unsuitable for Bun's TUI runtime. Returns null when no
187
- // compression has been recorded yet.
188
- export function collapsedCompressionValue(
189
- compression: StatusCompression | undefined,
190
- ): string | null {
191
- if (!compression || compression.project.events <= 0) return null;
192
- const { savings_tokens, original_tokens } = compression.project;
193
- const pct = original_tokens > 0 ? Math.round((savings_tokens / original_tokens) * 100) : 0;
194
- return `${formatCount(savings_tokens)} / ${pct}%`;
195
- }
196
-
197
- export type HealthLightTone = "ok" | "warn" | "err" | "muted";
198
-
199
- // Degraded-mode reason → human-readable hint. Distinct strings per reason
200
- // because the UX direction is different: "home_root" tells the user to open a
201
- // real project subdirectory, "search_too_many_files" tells them the tree is too
202
- // big for full indexing, and "watcher_unavailable" is an honest soft
203
- // degradation (AFT continues without live external-change invalidation).
204
- export function degradedReasonLabel(reason: string): string {
205
- if (reason === "home_root") {
206
- return "project root is your home directory";
207
- }
208
- if (reason.startsWith("search_too_many_files:")) {
209
- const threshold = reason.split(":")[1] ?? "20000";
210
- return `project exceeds ${threshold} files`;
211
- }
212
- if (reason === "watcher_unavailable") {
213
- return "file watcher unavailable; continuing without live external-change invalidation";
214
- }
215
- return reason; // unknown reason — surface verbatim so users can grep logs
216
- }
217
-
218
- export interface HealthLights {
219
- diagnostics: HealthLightTone;
220
- code: HealthLightTone;
221
- todos: HealthLightTone;
222
- }
223
-
224
- // Missing categories are intentionally muted: a green light requires an
225
- // explicit zero for every category that feeds that light.
226
- export function collapsedHealthLights(statusBar: StatusBar | undefined): HealthLights | null {
227
- if (!statusBar) return null;
228
- const diagnostics: HealthLightTone =
229
- statusBar.errors !== undefined && statusBar.errors > 0
230
- ? "err"
231
- : statusBar.warnings !== undefined && statusBar.warnings > 0
232
- ? "warn"
233
- : statusBar.errors === 0 && statusBar.warnings === 0
234
- ? "ok"
235
- : "muted";
236
- const codeValues = [statusBar.dead_code, statusBar.unused_exports, statusBar.duplicates];
237
- const code: HealthLightTone = codeValues.some((value) => value !== undefined && value > 0)
238
- ? "warn"
239
- : codeValues.every((value) => value === 0)
240
- ? "ok"
241
- : "muted";
242
- const todos: HealthLightTone =
243
- statusBar.todos === undefined ? "muted" : statusBar.todos > 0 ? "warn" : "ok";
244
- return { diagnostics, code, todos };
44
+ /**
45
+ * Slot-plugin theme → the panel's palette. The host this file serves publishes
46
+ * one flat colour per role (`theme.success`, `theme.textMuted`, ...), so the
47
+ * mapping is a rename; `success` keeps its historical fall back to the accent
48
+ * for themes that never defined it.
49
+ */
50
+ export function resolveV1Palette(theme: TuiThemeCurrent): SidebarPalette {
51
+ return {
52
+ text: theme.text,
53
+ textMuted: theme.textMuted,
54
+ success: theme.success ?? theme.accent,
55
+ warning: theme.warning,
56
+ error: theme.error,
57
+ accent: theme.accent,
58
+ background: theme.background,
59
+ border: theme.borderActive,
60
+ };
245
61
  }
246
62
 
247
63
  // Keep the TUI on the bridge's shared resolver so its root matches the
@@ -350,8 +166,7 @@ const SidebarContent = (props: {
350
166
  pluginVersion: string;
351
167
  }) => {
352
168
  const [status, setStatus] = createSignal<ScopedSidebarStatus | null>(null);
353
- const [prefs, setPrefs] = createSignal<AftTuiPrefs>(structuredClone(DEFAULT_PREFS));
354
- const [collapsed, setCollapsed] = createSignal(seedCollapsedFromPrefs(DEFAULT_PREFS));
169
+ const preferences = createSidebarPreferences(() => requestRender());
355
170
  let inflight: {
356
171
  controller: AbortController;
357
172
  generation: number;
@@ -479,21 +294,7 @@ const SidebarContent = (props: {
479
294
  const statusDebouncer = createDebouncedStatusRefresh(refresh, REFRESH_DEBOUNCE_MS);
480
295
  const scheduleRefresh = () => statusDebouncer.schedule();
481
296
 
482
- const reloadPrefs = async () => {
483
- const root = await readTuiPreferencesFile();
484
- const next = resolveAftPrefs(root);
485
- setPrefs(next);
486
- setCollapsed(seedCollapsedFromPrefs(next));
487
- requestRender();
488
- };
489
-
490
- void reloadPrefs();
491
- const unwatchPrefs = watchTuiPreferences(() => {
492
- void reloadPrefs();
493
- });
494
-
495
297
  onCleanup(() => {
496
- unwatchPrefs();
497
298
  generation++;
498
299
  abortInflight();
499
300
  statusDebouncer.dispose();
@@ -531,334 +332,15 @@ const SidebarContent = (props: {
531
332
 
532
333
  const s = () => scopedSidebarSnapshot(status(), currentDirectory(), props.sessionID());
533
334
 
534
- // Lazy-bridge: while AFT has no live bridge yet, the RPC server returns a
535
- // synthetic snapshot with `cache_role === "not_initialized"`. In that state
536
- // every metric is unknown by design — not "disabled" — so we hide the
537
- // version line and the entire Search Index / Semantic Index / Compression
538
- // grid until a first tool call warms the bridge. Users were reading the
539
- // pre-init `vunknown` + `Status: unknown` rows as broken state instead of
540
- // "AFT has not been used yet for this project".
541
- const notInitialized = () => s()?.cache_role === "not_initialized";
542
-
543
- // Pre-compute display values so the JSX stays readable. createMemo for
544
- // each derived field would be overkill — these are cheap derivations.
545
- const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled");
546
- const semanticStatus = () => {
547
- const rawStatus = s()?.semantic_index?.status ?? "disabled";
548
- const display = statusDisplay(rawStatus);
549
- return {
550
- ...display,
551
- label: formatSemanticIndexStatus(rawStatus, s()?.semantic_index?.stage),
552
- };
553
- };
554
- const semanticRefreshing = () =>
555
- formatSemanticRefreshing(s()?.semantic_index?.refreshing_count ?? 0);
556
- const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
557
- const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
558
- const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
559
- const statusBar = () => s()?.status_bar;
560
-
561
- const degradedSummary = () => {
562
- const snap = s();
563
- if (!snap?.degraded) return null;
564
- const reasons = snap.degraded_reasons ?? [];
565
- if (reasons.length === 0) return null;
566
- return reasons.map(degradedReasonLabel).join("; ");
567
- };
568
-
569
- // Worktree borrow is a shared-index arrangement, not a degraded_reasons
570
- // entry. Keep this muted and separate from the DEGRADED badge above.
571
- const worktreeNote = () => worktreeCacheRoleNote(s()?.cache_role);
572
-
573
335
  return (
574
- <box
575
- width="100%"
576
- flexDirection="column"
577
- border={SINGLE_BORDER}
578
- borderColor={props.theme.borderActive}
579
- paddingTop={1}
580
- paddingBottom={1}
581
- paddingLeft={1}
582
- paddingRight={1}
583
- >
584
- {/* Header: triangle toggle + AFT badge + binary version + degraded badge.
585
- Clicking the header row collapses/expands the panel (mirrors OpenCode's
586
- native MCP sidebar section). Only interactive once initialized — the
587
- lazy-bridge placeholder has nothing to collapse. */}
588
- <box
589
- flexDirection="row"
590
- justifyContent="space-between"
591
- alignItems="center"
592
- onMouseDown={() => {
593
- if (notInitialized()) return;
594
- setCollapsed((x) => {
595
- const next = !x;
596
- persistCollapsedIfEnabled(prefs(), next);
597
- return next;
598
- });
599
- }}
600
- >
601
- <box flexDirection="row" alignItems="center">
602
- {/* Triangle lives inside the accent badge so the toggle reads as one
603
- unit: "▶ AFT" / "▼ AFT". Hidden pre-init (nothing to collapse). */}
604
- <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
605
- <text fg={badgeTextColor(props.theme.accent, props.theme.background)}>
606
- <b>
607
- {notInitialized() ? "" : collapsed() ? "▶ " : "▼ "}
608
- {prefs().header.label}
609
- </b>
610
- </text>
611
- </box>
612
- {s()?.degraded && (
613
- <box
614
- paddingLeft={1}
615
- paddingRight={1}
616
- marginLeft={1}
617
- backgroundColor={props.theme.warning}
618
- >
619
- <text fg={badgeTextColor(props.theme.warning, props.theme.background)}>
620
- <b>DEGRADED</b>
621
- </text>
622
- </box>
623
- )}
624
- </box>
625
- {!notInitialized() && prefs().header.showVersion && (
626
- <text fg={props.theme.textMuted}>v{s()?.version ?? props.pluginVersion}</text>
627
- )}
628
- </box>
629
-
630
- {/* Degraded reason — explains why heavy tools (aft_search, aft_callgraph)
631
- are disabled. Surface this prominently so users know to open a real
632
- project subdirectory if they want full features. */}
633
- {s()?.degraded && degradedSummary() && (
634
- <box marginTop={1} width="100%">
635
- <text fg={props.theme.warning}>⚠ {degradedSummary()}</text>
636
- </box>
637
- )}
638
-
639
- {!notInitialized() && worktreeNote() && (
640
- <box marginTop={1} width="100%">
641
- <text fg={props.theme.textMuted}>{worktreeNote()}</text>
642
- </box>
643
- )}
644
-
645
- {/* Lazy-bridge placeholder. AFT skips spawning the `aft` binary at
646
- plugin init to keep memory/CPU low on OpenCode Desktop sessions
647
- that have many projects pinned in the sidebar. The RPC server
648
- returns a synthetic `cache_role === "not_initialized"` snapshot
649
- until the first tool call routes through `callBridge()` and warms
650
- the bridge. Show the explanatory message instead of empty status
651
- rows so users understand why metrics are blank. */}
652
- {notInitialized() && (
653
- <box marginTop={1} width="100%">
654
- <text fg={props.theme.textMuted}>
655
- {s()!.message ||
656
- "AFT bridge is now spawned lazily, information here will be populated after first tool call."}
657
- </text>
658
- </box>
659
- )}
660
-
661
- {/* Collapsed view — condensed status dots + compact compression. Shown
662
- only when initialized AND collapsed. Three rows mirroring the section
663
- order of the expanded grid. */}
664
- {!notInitialized() && collapsed() && (
665
- <box width="100%" flexDirection="column">
666
- {prefs().sections.searchIndex && (
667
- <CollapsedRow theme={props.theme} label="Search Index">
668
- <text fg={toneColor(props.theme, searchStatus().tone)}>●</text>
669
- </CollapsedRow>
670
- )}
671
- {prefs().sections.semanticIndex && (
672
- <CollapsedRow theme={props.theme} label="Semantic Index">
673
- <text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
674
- </CollapsedRow>
675
- )}
676
- {prefs().sections.codeHealth && collapsedHealthLights(statusBar()) && (
677
- <CollapsedRow theme={props.theme} label="Code Health">
678
- <box flexDirection="row" gap={1}>
679
- <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.diagnostics)}>
680
- ●
681
- </text>
682
- <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.code)}>●</text>
683
- <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.todos)}>
684
- ●
685
- </text>
686
- </box>
687
- </CollapsedRow>
688
- )}
689
- {prefs().sections.compression && collapsedCompressionValue(s()?.compression) && (
690
- <CollapsedRow theme={props.theme} label="Compression">
691
- <text fg={props.theme.textMuted}>
692
- <b>{collapsedCompressionValue(s()?.compression)}</b>
693
- </text>
694
- </CollapsedRow>
695
- )}
696
- </box>
697
- )}
698
-
699
- {/* Search index */}
700
- {!notInitialized() && !collapsed() && (
701
- <>
702
- {prefs().sections.searchIndex && (
703
- <>
704
- <SectionHeader theme={props.theme} title="Search Index" />
705
- <StatRow
706
- theme={props.theme}
707
- label="Status"
708
- value={searchStatus().label}
709
- tone={searchStatus().tone}
710
- />
711
- {(s()?.search_index?.files ?? null) != null && (
712
- <StatRow
713
- theme={props.theme}
714
- label="Files"
715
- value={formatCount(s()!.search_index.files)}
716
- tone="muted"
717
- />
718
- )}
719
- <StatRow
720
- theme={props.theme}
721
- label="Disk"
722
- value={formatBytes(trigramBytes())}
723
- tone="muted"
724
- />
725
- </>
726
- )}
727
-
728
- {prefs().sections.semanticIndex && (
729
- <>
730
- <SectionHeader theme={props.theme} title="Semantic Index" />
731
- <StatRow
732
- theme={props.theme}
733
- label="Status"
734
- value={semanticStatus().label}
735
- tone={semanticStatus().tone}
736
- />
737
- {semanticRefreshing() && (
738
- <box width="100%">
739
- <text fg={props.theme.textMuted}>{semanticRefreshing()}</text>
740
- </box>
741
- )}
742
- {/* When loading, magic-context-style progress hint helps users see
743
- background work is making progress instead of stuck. */}
744
- {s()?.semantic_index?.status === "loading" &&
745
- s()?.semantic_index?.entries_total != null &&
746
- s()!.semantic_index.entries_total! > 0 && (
747
- <StatRow
748
- theme={props.theme}
749
- label="Progress"
750
- value={`${formatCount(s()!.semantic_index.entries_done)} / ${formatCount(
751
- s()!.semantic_index.entries_total,
752
- )}`}
753
- tone="warn"
754
- />
755
- )}
756
- {(s()?.semantic_index?.entries ?? null) != null && (
757
- <StatRow
758
- theme={props.theme}
759
- label="Entries"
760
- value={formatCount(s()!.semantic_index.entries)}
761
- tone="muted"
762
- />
763
- )}
764
- <StatRow
765
- theme={props.theme}
766
- label="Disk"
767
- value={formatBytes(semanticBytes())}
768
- tone="muted"
769
- />
770
- </>
771
- )}
772
-
773
- {/* Human health values are optional. A category stays absent until the
774
- server proves it, rather than appearing as a clean zero. */}
775
- {prefs().sections.codeHealth && statusBar() && (
776
- <>
777
- <SectionHeader
778
- theme={props.theme}
779
- title={statusBar()!.tier2_stale ? "Code Health ~" : "Code Health"}
780
- />
781
- {statusBar()!.errors !== undefined && (
782
- <StatRow
783
- theme={props.theme}
784
- label="Errors"
785
- value={formatCount(statusBar()!.errors)}
786
- tone={statusBar()!.errors! > 0 ? "err" : "muted"}
787
- />
788
- )}
789
- {statusBar()!.warnings !== undefined && (
790
- <StatRow
791
- theme={props.theme}
792
- label="Warnings"
793
- value={formatCount(statusBar()!.warnings)}
794
- tone={statusBar()!.warnings! > 0 ? "warn" : "muted"}
795
- />
796
- )}
797
- {statusBar()!.dead_code !== undefined && (
798
- <StatRow
799
- theme={props.theme}
800
- label="Dead Code"
801
- value={formatCount(statusBar()!.dead_code)}
802
- tone="muted"
803
- />
804
- )}
805
- {statusBar()!.unused_exports !== undefined && (
806
- <StatRow
807
- theme={props.theme}
808
- label="Unused Exports"
809
- value={formatCount(statusBar()!.unused_exports)}
810
- tone="muted"
811
- />
812
- )}
813
- {statusBar()!.duplicates !== undefined && (
814
- <StatRow
815
- theme={props.theme}
816
- label="Duplicates"
817
- value={formatCount(statusBar()!.duplicates)}
818
- tone="muted"
819
- />
820
- )}
821
- {statusBar()!.todos !== undefined && (
822
- <StatRow
823
- theme={props.theme}
824
- label="TODOs"
825
- value={formatCount(statusBar()!.todos)}
826
- tone="muted"
827
- />
828
- )}
829
- </>
830
- )}
831
-
832
- {/* Compression aggregates. Tabular layout matching Search/Semantic
833
- Index above: each scope ("Session", "Project") renders as a
834
- subheader followed by two StatRows (Tokens Saved, Compression
835
- Ratio). Keeps numbers right-aligned in the value column instead
836
- of jamming them after the label on the same line. */}
837
- {prefs().sections.compression && compressionRows().length > 0 && (
838
- <>
839
- <SectionHeader theme={props.theme} title="Compression" />
840
- {compressionRows().map((row) =>
841
- row.kind === "scope" ? (
842
- <box width="100%">
843
- <text fg={props.theme.text}>{row.label}</text>
844
- </box>
845
- ) : (
846
- <StatRow theme={props.theme} label={row.label} value={row.value} tone="muted" />
847
- ),
848
- )}
849
- </>
850
- )}
851
-
852
- {/* Surface failures clearly so users know to act (install ONNX,
853
- fix config, etc.) rather than silently leaving the panel "off". */}
854
- {s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error && (
855
- <box marginTop={1} width="100%">
856
- <text fg={props.theme.error}>⚠ {s()!.semantic_index.error}</text>
857
- </box>
858
- )}
859
- </>
860
- )}
861
- </box>
336
+ <AftSidebarPanel
337
+ palette={resolveV1Palette(props.theme)}
338
+ snapshot={s()}
339
+ prefs={preferences.prefs()}
340
+ collapsed={preferences.collapsed()}
341
+ onToggleCollapsed={preferences.toggleCollapsed}
342
+ pluginVersion={props.pluginVersion}
343
+ />
862
344
  );
863
345
  };
864
346
 
@@ -22,7 +22,6 @@ export type AftSidebarSummary = {
22
22
  semantic: string;
23
23
  health: string;
24
24
  };
25
-
26
25
  function healthSummary(bar: StatusBar | undefined): string {
27
26
  if (!bar) return "health pending";
28
27
  const stale = bar.tier2_stale ? "~" : "";
@@ -33,6 +32,14 @@ function healthSummary(bar: StatusBar | undefined): string {
33
32
  );
34
33
  }
35
34
 
35
+ /**
36
+ * A one-line-per-field digest of the status snapshot.
37
+ *
38
+ * This is no longer what the sidebar draws: the sidebar now renders the shared
39
+ * panel in ./sidebar-view, which labels each section and reuses the status
40
+ * wording ./sidebar.tsx established. The digest is still pinned by
41
+ * test/tui/v2-status.test.ts.
42
+ */
36
43
  export function summarizeAftSidebar(status: AftStatusSnapshot | null): AftSidebarSummary {
37
44
  if (!status || status.cache_role === "not_initialized") {
38
45
  return {