@cortexkit/aft-opencode 0.56.2 → 0.57.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/dist/bg-notifications.d.ts +1 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/entry/server.js +2650 -267
- package/dist/index.js +420 -228
- package/dist/lsp-github-install.d.ts.map +1 -1
- package/dist/permissions/v2-service.d.ts +30 -0
- package/dist/permissions/v2-service.d.ts.map +1 -0
- package/dist/permissions/v2.d.ts +121 -9
- package/dist/permissions/v2.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/status.d.ts +8 -1
- package/dist/shared/status.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +11 -0
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tool-registration.d.ts.map +1 -1
- package/dist/tools/definitions/v2.d.ts +6 -0
- package/dist/tools/definitions/v2.d.ts.map +1 -1
- package/dist/tools/hoisted/v2.d.ts +12 -2
- package/dist/tools/hoisted/v2.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/v2-status.d.ts +8 -0
- package/dist/tui/v2-status.d.ts.map +1 -1
- package/index.js +14 -0
- package/package.json +14 -10
- package/server.js +8 -0
- package/src/shared/rpc-server.ts +5 -0
- package/src/shared/status.ts +16 -9
- package/src/tui/index.tsx +1 -0
- package/src/tui/sidebar-view.tsx +722 -0
- package/src/tui/sidebar.tsx +42 -560
- package/src/tui/v2-status.ts +8 -1
- package/src/tui/v2.tsx +173 -30
- package/src/tui-compiled/index.tsx +1 -1
- package/src/tui-compiled/sidebar-view.tsx +815 -0
- package/src/tui-compiled/sidebar.tsx +45 -693
- package/src/tui-compiled/v2-status.ts +8 -1
- package/src/tui-compiled/v2.tsx +163 -54
- package/tui.js +7 -0
package/src/tui/sidebar.tsx
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
package/src/tui/v2-status.ts
CHANGED
|
@@ -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 {
|