@cortexkit/aft-opencode 0.45.1 → 0.46.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/config.d.ts +0 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +347 -123
- package/dist/shared/ignored-message.d.ts +9 -7
- package/dist/shared/ignored-message.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +23 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +7 -0
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/notification-socket.d.ts.map +1 -1
- package/package.json +17 -14
- package/src/shared/ignored-message.ts +38 -19
- package/src/shared/rpc-client.ts +116 -4
- package/src/tui/entry.mjs +16 -0
- package/src/tui/notification-socket.ts +28 -1
- package/src/tui-compiled/badge-contrast.ts +43 -0
- package/src/tui-compiled/index.tsx +992 -0
- package/src/tui-compiled/notification-socket.ts +448 -0
- package/src/tui-compiled/preferences.ts +243 -0
- package/src/tui-compiled/sidebar.tsx +936 -0
- package/src/tui-compiled/types/opencode-plugin-tui.d.ts +239 -0
- package/dist/tui.js +0 -13462
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
2
|
+
import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
3
|
+
import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
4
|
+
import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
5
|
+
import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
6
|
+
import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
7
|
+
import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
8
|
+
import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
9
|
+
/** @jsxImportSource @opentui/solid */
|
|
10
|
+
// @ts-nocheck
|
|
11
|
+
|
|
12
|
+
// AFT sidebar slot. Header with "AFT" badge + version, then live status of search and semantic
|
|
13
|
+
// indexes plus their on-disk size. Refreshes on mount/session change and on
|
|
14
|
+
// server-pushed status invalidations with a small debounce, so the panel stays
|
|
15
|
+
// current without polling.
|
|
16
|
+
|
|
17
|
+
import { canonicalizeProjectRoot } from "@cortexkit/aft-bridge";
|
|
18
|
+
import { createEffect, createMemo, createSignal, on, onCleanup } from "opentui:runtime-module:solid-js";
|
|
19
|
+
import { AftRpcClient } from "../shared/rpc-client";
|
|
20
|
+
import { coerceAftStatus, formatSemanticIndexStatus, formatSemanticRefreshing } from "../shared/status";
|
|
21
|
+
import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
|
|
22
|
+
import { badgeTextColor } from "./badge-contrast";
|
|
23
|
+
import { createDebouncedStatusRefresh, refreshAftTuiSocketScope, subscribeStatusInvalidations } from "./notification-socket";
|
|
24
|
+
import { computeEffectiveOrder, DEFAULT_PREFS, DEFAULT_SLOT_ORDER, PLUGIN_KEY, persistCollapsedIfEnabled, readTuiPreferencesFile, resolveAftPrefs, seedCollapsedFromPrefs, watchTuiPreferences } from "./preferences";
|
|
25
|
+
const SINGLE_BORDER = {
|
|
26
|
+
type: "single"
|
|
27
|
+
};
|
|
28
|
+
const REFRESH_DEBOUNCE_MS = 200;
|
|
29
|
+
function formatBytes(n) {
|
|
30
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
31
|
+
if (n >= 1_073_741_824) return `${(n / 1_073_741_824).toFixed(1)} GB`;
|
|
32
|
+
if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`;
|
|
33
|
+
if (n >= 1_024) return `${Math.round(n / 1_024)} KB`;
|
|
34
|
+
return `${n} B`;
|
|
35
|
+
}
|
|
36
|
+
function formatCount(n) {
|
|
37
|
+
if (n == null || !Number.isFinite(n)) return "—";
|
|
38
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
39
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
|
|
40
|
+
return String(n);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Tagged rows for the Compression section. Each scope (Session / Project)
|
|
44
|
+
* emits a "scope" header followed by two "stat" rows — Tokens Saved and
|
|
45
|
+
* Compression Ratio — so the renderer can use the same StatRow layout as
|
|
46
|
+
* Search Index / Semantic Index above. Pi's monospace overlay and the
|
|
47
|
+
* OpenCode TUI dialog/sidebar all consume this same shape. */
|
|
48
|
+
|
|
49
|
+
function appendScope(rows, label, scope) {
|
|
50
|
+
const savings = scope.savings_tokens;
|
|
51
|
+
const pct = scope.original_tokens > 0 ? Math.round(savings / scope.original_tokens * 100) : 0;
|
|
52
|
+
rows.push({
|
|
53
|
+
kind: "scope",
|
|
54
|
+
label
|
|
55
|
+
});
|
|
56
|
+
rows.push({
|
|
57
|
+
kind: "stat",
|
|
58
|
+
label: "Tokens Saved",
|
|
59
|
+
value: savings.toLocaleString("en-US")
|
|
60
|
+
});
|
|
61
|
+
rows.push({
|
|
62
|
+
kind: "stat",
|
|
63
|
+
label: "Compression Ratio",
|
|
64
|
+
value: `${pct}%`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function formatCompressionSidebarRows(compression) {
|
|
68
|
+
if (!compression || compression.project.events <= 0) return [];
|
|
69
|
+
const rows = [];
|
|
70
|
+
if (compression.session.events > 0) {
|
|
71
|
+
appendScope(rows, "Session", compression.session);
|
|
72
|
+
}
|
|
73
|
+
appendScope(rows, "Project", compression.project);
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Map index status → (label, theme color name). The label is what we want
|
|
78
|
+
// the user to see; the color encodes severity so the eye lands on warnings.
|
|
79
|
+
function statusDisplay(status) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case "ready":
|
|
82
|
+
return {
|
|
83
|
+
label: "ready",
|
|
84
|
+
tone: "ok"
|
|
85
|
+
};
|
|
86
|
+
case "loading":
|
|
87
|
+
case "building":
|
|
88
|
+
return {
|
|
89
|
+
label: status,
|
|
90
|
+
tone: "warn"
|
|
91
|
+
};
|
|
92
|
+
case "failed":
|
|
93
|
+
case "error":
|
|
94
|
+
return {
|
|
95
|
+
label: status,
|
|
96
|
+
tone: "err"
|
|
97
|
+
};
|
|
98
|
+
case "disabled":
|
|
99
|
+
return {
|
|
100
|
+
label: "disabled",
|
|
101
|
+
tone: "muted"
|
|
102
|
+
};
|
|
103
|
+
default:
|
|
104
|
+
return {
|
|
105
|
+
label: status || "unknown",
|
|
106
|
+
tone: "muted"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const StatRow = props => {
|
|
111
|
+
const fg = createMemo(() => {
|
|
112
|
+
switch (props.tone) {
|
|
113
|
+
case "ok":
|
|
114
|
+
return props.theme.success ?? props.theme.accent;
|
|
115
|
+
case "warn":
|
|
116
|
+
return props.theme.warning;
|
|
117
|
+
case "err":
|
|
118
|
+
return props.theme.error;
|
|
119
|
+
case "muted":
|
|
120
|
+
return props.theme.textMuted;
|
|
121
|
+
case "accent":
|
|
122
|
+
return props.theme.accent;
|
|
123
|
+
default:
|
|
124
|
+
return props.theme.text;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return (() => {
|
|
128
|
+
var _el$ = _$createElement("box"),
|
|
129
|
+
_el$2 = _$createElement("text"),
|
|
130
|
+
_el$3 = _$createElement("text"),
|
|
131
|
+
_el$4 = _$createElement("b");
|
|
132
|
+
_$insertNode(_el$, _el$2);
|
|
133
|
+
_$insertNode(_el$, _el$3);
|
|
134
|
+
_$setProp(_el$, "width", "100%");
|
|
135
|
+
_$setProp(_el$, "flexDirection", "row");
|
|
136
|
+
_$setProp(_el$, "justifyContent", "space-between");
|
|
137
|
+
_$insert(_el$2, () => props.label);
|
|
138
|
+
_$insertNode(_el$3, _el$4);
|
|
139
|
+
_$insert(_el$4, () => props.value);
|
|
140
|
+
_$effect(_p$ => {
|
|
141
|
+
var _v$ = props.theme.textMuted,
|
|
142
|
+
_v$2 = fg();
|
|
143
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
|
144
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$2, _p$.t));
|
|
145
|
+
return _p$;
|
|
146
|
+
}, {
|
|
147
|
+
e: undefined,
|
|
148
|
+
t: undefined
|
|
149
|
+
});
|
|
150
|
+
return _el$;
|
|
151
|
+
})();
|
|
152
|
+
};
|
|
153
|
+
const SectionHeader = props => (() => {
|
|
154
|
+
var _el$5 = _$createElement("box"),
|
|
155
|
+
_el$6 = _$createElement("text"),
|
|
156
|
+
_el$7 = _$createElement("b");
|
|
157
|
+
_$insertNode(_el$5, _el$6);
|
|
158
|
+
_$setProp(_el$5, "width", "100%");
|
|
159
|
+
_$insertNode(_el$6, _el$7);
|
|
160
|
+
_$insert(_el$7, () => props.title);
|
|
161
|
+
_$effect(_p$ => {
|
|
162
|
+
var _v$3 = props.marginTop ?? 1,
|
|
163
|
+
_v$4 = props.theme.text;
|
|
164
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "marginTop", _v$3, _p$.e));
|
|
165
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
|
|
166
|
+
return _p$;
|
|
167
|
+
}, {
|
|
168
|
+
e: undefined,
|
|
169
|
+
t: undefined
|
|
170
|
+
});
|
|
171
|
+
return _el$5;
|
|
172
|
+
})();
|
|
173
|
+
|
|
174
|
+
// Map a status tone to a theme color — used for the collapsed-view status dots.
|
|
175
|
+
function toneColor(theme, tone) {
|
|
176
|
+
switch (tone) {
|
|
177
|
+
case "ok":
|
|
178
|
+
return theme.success ?? theme.accent;
|
|
179
|
+
case "warn":
|
|
180
|
+
return theme.warning;
|
|
181
|
+
case "err":
|
|
182
|
+
return theme.error;
|
|
183
|
+
default:
|
|
184
|
+
return theme.textMuted;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Collapsed-view row: label on the left, a status dot (or compact value) on the
|
|
189
|
+
// right. Mirrors the expanded StatRow layout so the columns line up.
|
|
190
|
+
const CollapsedRow = props => (() => {
|
|
191
|
+
var _el$8 = _$createElement("box"),
|
|
192
|
+
_el$9 = _$createElement("text");
|
|
193
|
+
_$insertNode(_el$8, _el$9);
|
|
194
|
+
_$setProp(_el$8, "width", "100%");
|
|
195
|
+
_$setProp(_el$8, "flexDirection", "row");
|
|
196
|
+
_$setProp(_el$8, "justifyContent", "space-between");
|
|
197
|
+
_$insert(_el$9, () => props.label);
|
|
198
|
+
_$insert(_el$8, () => props.children, null);
|
|
199
|
+
_$effect(_$p => _$setProp(_el$9, "fg", props.theme.textMuted, _$p));
|
|
200
|
+
return _el$8;
|
|
201
|
+
})();
|
|
202
|
+
|
|
203
|
+
// Compact "saved / ratio" string for the collapsed Compression row — e.g.
|
|
204
|
+
// "7.6M / 64%". Uses the local `formatCount` (not the aft-bridge token
|
|
205
|
+
// formatter) so the TUI bundle doesn't pull the bridge barrel, which exports
|
|
206
|
+
// URL-fetch helpers unsuitable for Bun's TUI runtime. Returns null when no
|
|
207
|
+
// compression has been recorded yet.
|
|
208
|
+
export function collapsedCompressionValue(compression) {
|
|
209
|
+
if (!compression || compression.project.events <= 0) return null;
|
|
210
|
+
const {
|
|
211
|
+
savings_tokens,
|
|
212
|
+
original_tokens
|
|
213
|
+
} = compression.project;
|
|
214
|
+
const pct = original_tokens > 0 ? Math.round(savings_tokens / original_tokens * 100) : 0;
|
|
215
|
+
return `${formatCount(savings_tokens)} / ${pct}%`;
|
|
216
|
+
}
|
|
217
|
+
// Degraded-mode reason → human-readable hint. Distinct strings per reason
|
|
218
|
+
// because the UX direction is different: "home_root" tells the user to open a
|
|
219
|
+
// real project subdirectory, "search_too_many_files" tells them the tree is too
|
|
220
|
+
// big for full indexing, and "watcher_unavailable" is an honest soft
|
|
221
|
+
// degradation (AFT continues without live external-change invalidation).
|
|
222
|
+
export function degradedReasonLabel(reason) {
|
|
223
|
+
if (reason === "home_root") {
|
|
224
|
+
return "project root is your home directory";
|
|
225
|
+
}
|
|
226
|
+
if (reason.startsWith("search_too_many_files:")) {
|
|
227
|
+
const threshold = reason.split(":")[1] ?? "20000";
|
|
228
|
+
return `project exceeds ${threshold} files`;
|
|
229
|
+
}
|
|
230
|
+
if (reason === "watcher_unavailable") {
|
|
231
|
+
return "file watcher unavailable; continuing without live external-change invalidation";
|
|
232
|
+
}
|
|
233
|
+
return reason; // unknown reason — surface verbatim so users can grep logs
|
|
234
|
+
}
|
|
235
|
+
// Three traffic lights for the collapsed view, replacing the cramped
|
|
236
|
+
// `E· W· | D· U· C· | T·` string. Returns null until populated.
|
|
237
|
+
export function collapsedHealthLights(statusBar) {
|
|
238
|
+
if (!statusBar) return null;
|
|
239
|
+
const diagnostics = statusBar.errors > 0 ? "err" : statusBar.warnings > 0 ? "warn" : "ok";
|
|
240
|
+
const code = statusBar.dead_code > 0 || statusBar.unused_exports > 0 || statusBar.duplicates > 0 ? "warn" : "ok";
|
|
241
|
+
const todos = statusBar.todos > 0 ? "warn" : "ok";
|
|
242
|
+
return {
|
|
243
|
+
diagnostics,
|
|
244
|
+
code,
|
|
245
|
+
todos
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// v0.27 moved AFT storage to the CortexKit root. TUI code must use a
|
|
250
|
+
// lightweight local path helper rather than the shared bridge barrel, which
|
|
251
|
+
// also exports URL-fetch helpers unsuitable for Bun's TUI runtime.
|
|
252
|
+
export function resolveTuiStorageDir() {
|
|
253
|
+
return resolveCortexKitStorageRoot();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// One RPC client per project directory — same pattern as the /aft-status
|
|
257
|
+
// dialog handler in tui/index.tsx. Sharing the map avoids opening a second
|
|
258
|
+
// connection just for the sidebar.
|
|
259
|
+
const sidebarClients = new Map();
|
|
260
|
+
function getClient(directory) {
|
|
261
|
+
let client = sidebarClients.get(directory);
|
|
262
|
+
if (client) return client;
|
|
263
|
+
client = new AftRpcClient(resolveTuiStorageDir(), directory);
|
|
264
|
+
sidebarClients.set(directory, client);
|
|
265
|
+
return client;
|
|
266
|
+
}
|
|
267
|
+
export function scopedSidebarSnapshot(scoped, directory, sessionID) {
|
|
268
|
+
if (!scoped) return null;
|
|
269
|
+
if (scoped.directory !== directory || scoped.sessionID !== sessionID) return null;
|
|
270
|
+
return scoped.snapshot;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stale-while-revalidate guard. A transient `not_initialized` snapshot (bridge
|
|
275
|
+
* mid-respawn after a binary swap, or a momentary session-dir key miss) arrives
|
|
276
|
+
* over RPC as `success: true`, so a naive `setStatus` would overwrite a good
|
|
277
|
+
* snapshot and collapse the panel to the lazy-bridge placeholder — the blank
|
|
278
|
+
* flicker that recovers on the next refresh. Suppress the downgrade only when we
|
|
279
|
+
* already hold initialized data for the same context; never blocks the first
|
|
280
|
+
* real snapshot, and a genuine context switch clears separately.
|
|
281
|
+
*/
|
|
282
|
+
export function shouldSuppressUninitializedDowngrade(incomingCacheRole, haveInitializedForContext) {
|
|
283
|
+
return incomingCacheRole === "not_initialized" && haveInitializedForContext;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Cross-project contamination belt. The RPC layer can hand back a snapshot
|
|
288
|
+
* describing a DIFFERENT project than the one this sidebar asked about — a
|
|
289
|
+
* multi-project host (Desktop / `opencode serve`) whose status handler
|
|
290
|
+
* resolved another project's warm bridge, including long-lived processes
|
|
291
|
+
* still running pre-fix plugin code. Rendering it shows another repo's
|
|
292
|
+
* indexes/health in this window.
|
|
293
|
+
*
|
|
294
|
+
* A mismatched project_root is acceptable ONLY when the serving handler says
|
|
295
|
+
* it resolved that directory DELIBERATELY: new servers attach
|
|
296
|
+
* `served_directory` (their own cwd, or the SDK-verified `opencode -s` resume
|
|
297
|
+
* directory) to every status response. That marker is handler-attached
|
|
298
|
+
* provenance — it cannot be faked by snapshot contents. We explicitly do NOT
|
|
299
|
+
* use `snapshot.session.id` here: Rust echoes the REQUESTED session id into
|
|
300
|
+
* the snapshot, so it matches even when the data came from another project's
|
|
301
|
+
* bridge (the hole that let contamination through this belt's first version).
|
|
302
|
+
*
|
|
303
|
+
* Rules:
|
|
304
|
+
* - placeholder/synthetic snapshots (no project_root) → accept (not data)
|
|
305
|
+
* - project_root (or canonical_root) matches the sidebar directory → accept
|
|
306
|
+
* - mismatched root AND served_directory matches a snapshot root → accept
|
|
307
|
+
* (deliberate, SDK-verified resume serve from a new server)
|
|
308
|
+
* - otherwise → reject (stray; includes everything old servers cross-serve)
|
|
309
|
+
*/
|
|
310
|
+
export function isSnapshotForContext(snapshot, directory, servedDirectory) {
|
|
311
|
+
// Canonicalize both sides through the SAME canonicalizer the bridge routes
|
|
312
|
+
// by, so a symlinked / `/var`-vs-`/private/var` / trailing-slash spelling of
|
|
313
|
+
// the sidebar directory still matches Rust's canonical_root. A raw stripSlash
|
|
314
|
+
// compare (the old behavior) rejected legitimate snapshots whenever the TUI
|
|
315
|
+
// directory and Rust's root were different spellings of the same location,
|
|
316
|
+
// leaving the sidebar blank on aliased roots.
|
|
317
|
+
const canon = p => canonicalizeProjectRoot(p);
|
|
318
|
+
const roots = [snapshot.project_root, snapshot.canonical_root].filter(r => typeof r === "string" && r.length > 0);
|
|
319
|
+
if (roots.length === 0) return true; // placeholder / synthetic
|
|
320
|
+
const dir = canon(directory);
|
|
321
|
+
if (roots.some(r => canon(r) === dir)) return true;
|
|
322
|
+
if (typeof servedDirectory === "string" && servedDirectory.length > 0) {
|
|
323
|
+
const served = canon(servedDirectory);
|
|
324
|
+
return roots.some(r => canon(r) === served);
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const SidebarContent = props => {
|
|
329
|
+
const [status, setStatus] = createSignal(null);
|
|
330
|
+
const [prefs, setPrefs] = createSignal(structuredClone(DEFAULT_PREFS));
|
|
331
|
+
const [collapsed, setCollapsed] = createSignal(seedCollapsedFromPrefs(DEFAULT_PREFS));
|
|
332
|
+
let inflight = null;
|
|
333
|
+
let generation = 0;
|
|
334
|
+
const currentDirectory = () => props.api.state.path.directory ?? "";
|
|
335
|
+
const requestRender = () => {
|
|
336
|
+
try {
|
|
337
|
+
props.api.renderer.requestRender();
|
|
338
|
+
} catch {
|
|
339
|
+
// renderer may not be available during teardown; safe to ignore
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const abortInflight = () => {
|
|
343
|
+
if (!inflight) return;
|
|
344
|
+
inflight.controller.abort();
|
|
345
|
+
inflight = null;
|
|
346
|
+
};
|
|
347
|
+
const clearStatusForContext = (directory, sessionID) => {
|
|
348
|
+
const current = status();
|
|
349
|
+
if (!current) return;
|
|
350
|
+
if (current.directory === directory && current.sessionID === sessionID) return;
|
|
351
|
+
setStatus(null);
|
|
352
|
+
requestRender();
|
|
353
|
+
};
|
|
354
|
+
const refresh = async () => {
|
|
355
|
+
const sid = props.sessionID();
|
|
356
|
+
const directory = currentDirectory();
|
|
357
|
+
if (!sid || !directory) {
|
|
358
|
+
generation++;
|
|
359
|
+
abortInflight();
|
|
360
|
+
if (status()) {
|
|
361
|
+
setStatus(null);
|
|
362
|
+
requestRender();
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
clearStatusForContext(directory, sid);
|
|
367
|
+
if (inflight) {
|
|
368
|
+
if (inflight.directory === directory && inflight.sessionID === sid) return;
|
|
369
|
+
generation++;
|
|
370
|
+
abortInflight();
|
|
371
|
+
}
|
|
372
|
+
const requestGeneration = ++generation;
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
inflight = {
|
|
375
|
+
controller,
|
|
376
|
+
generation: requestGeneration,
|
|
377
|
+
directory,
|
|
378
|
+
sessionID: sid
|
|
379
|
+
};
|
|
380
|
+
try {
|
|
381
|
+
const client = getClient(directory);
|
|
382
|
+
const response = await client.call("status", {
|
|
383
|
+
sessionID: sid
|
|
384
|
+
}, {
|
|
385
|
+
signal: controller.signal,
|
|
386
|
+
// With several RPC servers alive for this project hash, a stray
|
|
387
|
+
// warm response (another project's bridge) must not beat the right
|
|
388
|
+
// server or the placeholder — skip it at the port-scan level.
|
|
389
|
+
accept: result => {
|
|
390
|
+
const rec = result;
|
|
391
|
+
if (rec?.success === false) return true; // errors handled below
|
|
392
|
+
return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
if (controller.signal.aborted || requestGeneration !== generation) return;
|
|
396
|
+
if (currentDirectory() !== directory || props.sessionID() !== sid) return;
|
|
397
|
+
if (response && response.success !== false) {
|
|
398
|
+
const snapshot = coerceAftStatus(response);
|
|
399
|
+
// Belt: never render a snapshot describing another project (see
|
|
400
|
+
// isSnapshotForContext). Keep whatever we currently show instead.
|
|
401
|
+
const servedDirectory = response.served_directory;
|
|
402
|
+
if (!isSnapshotForContext(snapshot, directory, servedDirectory)) return;
|
|
403
|
+
// Stale-while-revalidate: keep the last-good snapshot instead of
|
|
404
|
+
// flickering to the lazy-bridge placeholder on a transient
|
|
405
|
+
// not_initialized. See shouldSuppressUninitializedDowngrade.
|
|
406
|
+
const current = status();
|
|
407
|
+
const haveGoodForContext = current !== null && current.directory === directory && current.sessionID === sid && current.snapshot.cache_role !== "not_initialized";
|
|
408
|
+
if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext)) return;
|
|
409
|
+
// Equality gate: a pushed invalidation can still produce the same
|
|
410
|
+
// snapshot (for example, a session-scoped status frame that does not
|
|
411
|
+
// affect this sidebar's visible fields). Minting a new status object
|
|
412
|
+
// would run SolidJS reactivity and schedule a host frame for no visible
|
|
413
|
+
// change. Skip the update when the freshly-fetched snapshot is
|
|
414
|
+
// byte-identical to what we already show for this exact context.
|
|
415
|
+
// JSON.stringify is sound here because the snapshot is a plain object
|
|
416
|
+
// coerced from the status RPC's JSON.
|
|
417
|
+
if (current !== null && current.directory === directory && current.sessionID === sid && JSON.stringify(current.snapshot) === JSON.stringify(snapshot)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
setStatus({
|
|
421
|
+
directory,
|
|
422
|
+
sessionID: sid,
|
|
423
|
+
snapshot
|
|
424
|
+
});
|
|
425
|
+
requestRender();
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
if (controller.signal.aborted || requestGeneration !== generation) return;
|
|
429
|
+
// RPC server may not be ready yet, or the bridge may be respawning
|
|
430
|
+
// after a binary swap. Keep the previous snapshot only when it belongs
|
|
431
|
+
// to the current project/session; mismatched snapshots were cleared above.
|
|
432
|
+
} finally {
|
|
433
|
+
if (inflight?.generation === requestGeneration) inflight = null;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const statusDebouncer = createDebouncedStatusRefresh(refresh, REFRESH_DEBOUNCE_MS);
|
|
437
|
+
const scheduleRefresh = () => statusDebouncer.schedule();
|
|
438
|
+
const reloadPrefs = async () => {
|
|
439
|
+
const root = await readTuiPreferencesFile();
|
|
440
|
+
const next = resolveAftPrefs(root);
|
|
441
|
+
setPrefs(next);
|
|
442
|
+
setCollapsed(seedCollapsedFromPrefs(next));
|
|
443
|
+
requestRender();
|
|
444
|
+
};
|
|
445
|
+
void reloadPrefs();
|
|
446
|
+
const unwatchPrefs = watchTuiPreferences(() => {
|
|
447
|
+
void reloadPrefs();
|
|
448
|
+
});
|
|
449
|
+
onCleanup(() => {
|
|
450
|
+
unwatchPrefs();
|
|
451
|
+
generation++;
|
|
452
|
+
abortInflight();
|
|
453
|
+
statusDebouncer.dispose();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Refresh on session id change + initial load
|
|
457
|
+
createEffect(on(props.sessionID, () => {
|
|
458
|
+
refreshAftTuiSocketScope();
|
|
459
|
+
void refresh();
|
|
460
|
+
}));
|
|
461
|
+
|
|
462
|
+
// Wire live updates: the server pushes a lightweight invalidation whenever
|
|
463
|
+
// the bridge reports a status change. The sidebar coalesces bursts into one
|
|
464
|
+
// trailing status fetch and stays completely idle when no backend state changes.
|
|
465
|
+
createEffect(on(props.sessionID, sessionID => {
|
|
466
|
+
if (!sessionID) return;
|
|
467
|
+
const unsubscribe = subscribeStatusInvalidations(event => {
|
|
468
|
+
if (event.sessionId && event.sessionId !== props.sessionID()) return;
|
|
469
|
+
scheduleRefresh();
|
|
470
|
+
});
|
|
471
|
+
onCleanup(() => {
|
|
472
|
+
unsubscribe();
|
|
473
|
+
generation++;
|
|
474
|
+
abortInflight();
|
|
475
|
+
});
|
|
476
|
+
}, {
|
|
477
|
+
defer: false
|
|
478
|
+
}));
|
|
479
|
+
const s = () => scopedSidebarSnapshot(status(), currentDirectory(), props.sessionID());
|
|
480
|
+
|
|
481
|
+
// Lazy-bridge: while AFT has no live bridge yet, the RPC server returns a
|
|
482
|
+
// synthetic snapshot with `cache_role === "not_initialized"`. In that state
|
|
483
|
+
// every metric is unknown by design — not "disabled" — so we hide the
|
|
484
|
+
// version line and the entire Search Index / Semantic Index / Compression
|
|
485
|
+
// grid until a first tool call warms the bridge. Users were reading the
|
|
486
|
+
// pre-init `vunknown` + `Status: unknown` rows as broken state instead of
|
|
487
|
+
// "AFT has not been used yet for this project".
|
|
488
|
+
const notInitialized = () => s()?.cache_role === "not_initialized";
|
|
489
|
+
|
|
490
|
+
// Pre-compute display values so the JSX stays readable. createMemo for
|
|
491
|
+
// each derived field would be overkill — these are cheap derivations.
|
|
492
|
+
const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled");
|
|
493
|
+
const semanticStatus = () => {
|
|
494
|
+
const rawStatus = s()?.semantic_index?.status ?? "disabled";
|
|
495
|
+
const display = statusDisplay(rawStatus);
|
|
496
|
+
return {
|
|
497
|
+
...display,
|
|
498
|
+
label: formatSemanticIndexStatus(rawStatus, s()?.semantic_index?.stage)
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
const semanticRefreshing = () => formatSemanticRefreshing(s()?.semantic_index?.refreshing_count ?? 0);
|
|
502
|
+
const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
|
|
503
|
+
const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
|
|
504
|
+
const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
|
|
505
|
+
const statusBar = () => s()?.status_bar;
|
|
506
|
+
const degradedSummary = () => {
|
|
507
|
+
const snap = s();
|
|
508
|
+
if (!snap?.degraded) return null;
|
|
509
|
+
const reasons = snap.degraded_reasons ?? [];
|
|
510
|
+
if (reasons.length === 0) return null;
|
|
511
|
+
return reasons.map(degradedReasonLabel).join("; ");
|
|
512
|
+
};
|
|
513
|
+
return (() => {
|
|
514
|
+
var _el$0 = _$createElement("box"),
|
|
515
|
+
_el$1 = _$createElement("box"),
|
|
516
|
+
_el$10 = _$createElement("box"),
|
|
517
|
+
_el$11 = _$createElement("box"),
|
|
518
|
+
_el$12 = _$createElement("text"),
|
|
519
|
+
_el$13 = _$createElement("b");
|
|
520
|
+
_$insertNode(_el$0, _el$1);
|
|
521
|
+
_$setProp(_el$0, "width", "100%");
|
|
522
|
+
_$setProp(_el$0, "flexDirection", "column");
|
|
523
|
+
_$setProp(_el$0, "border", SINGLE_BORDER);
|
|
524
|
+
_$setProp(_el$0, "paddingTop", 1);
|
|
525
|
+
_$setProp(_el$0, "paddingBottom", 1);
|
|
526
|
+
_$setProp(_el$0, "paddingLeft", 1);
|
|
527
|
+
_$setProp(_el$0, "paddingRight", 1);
|
|
528
|
+
_$insertNode(_el$1, _el$10);
|
|
529
|
+
_$setProp(_el$1, "flexDirection", "row");
|
|
530
|
+
_$setProp(_el$1, "justifyContent", "space-between");
|
|
531
|
+
_$setProp(_el$1, "alignItems", "center");
|
|
532
|
+
_$setProp(_el$1, "onMouseDown", () => {
|
|
533
|
+
if (notInitialized()) return;
|
|
534
|
+
setCollapsed(x => {
|
|
535
|
+
const next = !x;
|
|
536
|
+
persistCollapsedIfEnabled(prefs(), next);
|
|
537
|
+
return next;
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
_$insertNode(_el$10, _el$11);
|
|
541
|
+
_$setProp(_el$10, "flexDirection", "row");
|
|
542
|
+
_$setProp(_el$10, "alignItems", "center");
|
|
543
|
+
_$insertNode(_el$11, _el$12);
|
|
544
|
+
_$setProp(_el$11, "paddingLeft", 1);
|
|
545
|
+
_$setProp(_el$11, "paddingRight", 1);
|
|
546
|
+
_$insertNode(_el$12, _el$13);
|
|
547
|
+
_$insert(_el$13, (() => {
|
|
548
|
+
var _c$ = _$memo(() => !!notInitialized());
|
|
549
|
+
return () => _c$() ? "" : collapsed() ? "▶ " : "▼ ";
|
|
550
|
+
})(), null);
|
|
551
|
+
_$insert(_el$13, () => prefs().header.label, null);
|
|
552
|
+
_$insert(_el$10, (() => {
|
|
553
|
+
var _c$2 = _$memo(() => !!s()?.degraded);
|
|
554
|
+
return () => _c$2() && (() => {
|
|
555
|
+
var _el$14 = _$createElement("box"),
|
|
556
|
+
_el$15 = _$createElement("text"),
|
|
557
|
+
_el$16 = _$createElement("b");
|
|
558
|
+
_$insertNode(_el$14, _el$15);
|
|
559
|
+
_$setProp(_el$14, "paddingLeft", 1);
|
|
560
|
+
_$setProp(_el$14, "paddingRight", 1);
|
|
561
|
+
_$setProp(_el$14, "marginLeft", 1);
|
|
562
|
+
_$insertNode(_el$15, _el$16);
|
|
563
|
+
_$insertNode(_el$16, _$createTextNode(`DEGRADED`));
|
|
564
|
+
_$effect(_p$ => {
|
|
565
|
+
var _v$8 = props.theme.warning,
|
|
566
|
+
_v$9 = badgeTextColor(props.theme.warning, props.theme.background);
|
|
567
|
+
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$14, "backgroundColor", _v$8, _p$.e));
|
|
568
|
+
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$15, "fg", _v$9, _p$.t));
|
|
569
|
+
return _p$;
|
|
570
|
+
}, {
|
|
571
|
+
e: undefined,
|
|
572
|
+
t: undefined
|
|
573
|
+
});
|
|
574
|
+
return _el$14;
|
|
575
|
+
})();
|
|
576
|
+
})(), null);
|
|
577
|
+
_$insert(_el$1, (() => {
|
|
578
|
+
var _c$3 = _$memo(() => !!(!notInitialized() && prefs().header.showVersion));
|
|
579
|
+
return () => _c$3() && (() => {
|
|
580
|
+
var _el$18 = _$createElement("text"),
|
|
581
|
+
_el$19 = _$createTextNode(`v`);
|
|
582
|
+
_$insertNode(_el$18, _el$19);
|
|
583
|
+
_$insert(_el$18, () => s()?.version ?? props.pluginVersion, null);
|
|
584
|
+
_$effect(_$p => _$setProp(_el$18, "fg", props.theme.textMuted, _$p));
|
|
585
|
+
return _el$18;
|
|
586
|
+
})();
|
|
587
|
+
})(), null);
|
|
588
|
+
_$insert(_el$0, (() => {
|
|
589
|
+
var _c$4 = _$memo(() => !!(s()?.degraded && degradedSummary()));
|
|
590
|
+
return () => _c$4() && (() => {
|
|
591
|
+
var _el$20 = _$createElement("box"),
|
|
592
|
+
_el$21 = _$createElement("text"),
|
|
593
|
+
_el$22 = _$createTextNode(`⚠ `);
|
|
594
|
+
_$insertNode(_el$20, _el$21);
|
|
595
|
+
_$setProp(_el$20, "marginTop", 1);
|
|
596
|
+
_$setProp(_el$20, "width", "100%");
|
|
597
|
+
_$insertNode(_el$21, _el$22);
|
|
598
|
+
_$insert(_el$21, degradedSummary, null);
|
|
599
|
+
_$effect(_$p => _$setProp(_el$21, "fg", props.theme.warning, _$p));
|
|
600
|
+
return _el$20;
|
|
601
|
+
})();
|
|
602
|
+
})(), null);
|
|
603
|
+
_$insert(_el$0, (() => {
|
|
604
|
+
var _c$5 = _$memo(() => !!notInitialized());
|
|
605
|
+
return () => _c$5() && (() => {
|
|
606
|
+
var _el$23 = _$createElement("box"),
|
|
607
|
+
_el$24 = _$createElement("text");
|
|
608
|
+
_$insertNode(_el$23, _el$24);
|
|
609
|
+
_$setProp(_el$23, "marginTop", 1);
|
|
610
|
+
_$setProp(_el$23, "width", "100%");
|
|
611
|
+
_$insert(_el$24, () => s().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call.");
|
|
612
|
+
_$effect(_$p => _$setProp(_el$24, "fg", props.theme.textMuted, _$p));
|
|
613
|
+
return _el$23;
|
|
614
|
+
})();
|
|
615
|
+
})(), null);
|
|
616
|
+
_$insert(_el$0, (() => {
|
|
617
|
+
var _c$6 = _$memo(() => !!(!notInitialized() && collapsed()));
|
|
618
|
+
return () => _c$6() && (() => {
|
|
619
|
+
var _el$25 = _$createElement("box");
|
|
620
|
+
_$setProp(_el$25, "width", "100%");
|
|
621
|
+
_$setProp(_el$25, "flexDirection", "column");
|
|
622
|
+
_$insert(_el$25, (() => {
|
|
623
|
+
var _c$8 = _$memo(() => !!prefs().sections.searchIndex);
|
|
624
|
+
return () => _c$8() && _$createComponent(CollapsedRow, {
|
|
625
|
+
get theme() {
|
|
626
|
+
return props.theme;
|
|
627
|
+
},
|
|
628
|
+
label: "Search Index",
|
|
629
|
+
get children() {
|
|
630
|
+
var _el$26 = _$createElement("text");
|
|
631
|
+
_$insertNode(_el$26, _$createTextNode(`●`));
|
|
632
|
+
_$effect(_$p => _$setProp(_el$26, "fg", toneColor(props.theme, searchStatus().tone), _$p));
|
|
633
|
+
return _el$26;
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
})(), null);
|
|
637
|
+
_$insert(_el$25, (() => {
|
|
638
|
+
var _c$9 = _$memo(() => !!prefs().sections.semanticIndex);
|
|
639
|
+
return () => _c$9() && _$createComponent(CollapsedRow, {
|
|
640
|
+
get theme() {
|
|
641
|
+
return props.theme;
|
|
642
|
+
},
|
|
643
|
+
label: "Semantic Index",
|
|
644
|
+
get children() {
|
|
645
|
+
var _el$28 = _$createElement("text");
|
|
646
|
+
_$insertNode(_el$28, _$createTextNode(`●`));
|
|
647
|
+
_$effect(_$p => _$setProp(_el$28, "fg", toneColor(props.theme, semanticStatus().tone), _$p));
|
|
648
|
+
return _el$28;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
})(), null);
|
|
652
|
+
_$insert(_el$25, (() => {
|
|
653
|
+
var _c$0 = _$memo(() => !!(prefs().sections.codeHealth && collapsedHealthLights(statusBar())));
|
|
654
|
+
return () => _c$0() && _$createComponent(CollapsedRow, {
|
|
655
|
+
get theme() {
|
|
656
|
+
return props.theme;
|
|
657
|
+
},
|
|
658
|
+
label: "Code Health",
|
|
659
|
+
get children() {
|
|
660
|
+
var _el$30 = _$createElement("box"),
|
|
661
|
+
_el$31 = _$createElement("text"),
|
|
662
|
+
_el$33 = _$createElement("text"),
|
|
663
|
+
_el$35 = _$createElement("text");
|
|
664
|
+
_$insertNode(_el$30, _el$31);
|
|
665
|
+
_$insertNode(_el$30, _el$33);
|
|
666
|
+
_$insertNode(_el$30, _el$35);
|
|
667
|
+
_$setProp(_el$30, "flexDirection", "row");
|
|
668
|
+
_$setProp(_el$30, "gap", 1);
|
|
669
|
+
_$insertNode(_el$31, _$createTextNode(`●`));
|
|
670
|
+
_$insertNode(_el$33, _$createTextNode(`●`));
|
|
671
|
+
_$insertNode(_el$35, _$createTextNode(`●`));
|
|
672
|
+
_$effect(_p$ => {
|
|
673
|
+
var _v$0 = toneColor(props.theme, collapsedHealthLights(statusBar()).diagnostics),
|
|
674
|
+
_v$1 = toneColor(props.theme, collapsedHealthLights(statusBar()).code),
|
|
675
|
+
_v$10 = toneColor(props.theme, collapsedHealthLights(statusBar()).todos);
|
|
676
|
+
_v$0 !== _p$.e && (_p$.e = _$setProp(_el$31, "fg", _v$0, _p$.e));
|
|
677
|
+
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$33, "fg", _v$1, _p$.t));
|
|
678
|
+
_v$10 !== _p$.a && (_p$.a = _$setProp(_el$35, "fg", _v$10, _p$.a));
|
|
679
|
+
return _p$;
|
|
680
|
+
}, {
|
|
681
|
+
e: undefined,
|
|
682
|
+
t: undefined,
|
|
683
|
+
a: undefined
|
|
684
|
+
});
|
|
685
|
+
return _el$30;
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
})(), null);
|
|
689
|
+
_$insert(_el$25, (() => {
|
|
690
|
+
var _c$1 = _$memo(() => !!(prefs().sections.compression && collapsedCompressionValue(s()?.compression)));
|
|
691
|
+
return () => _c$1() && _$createComponent(CollapsedRow, {
|
|
692
|
+
get theme() {
|
|
693
|
+
return props.theme;
|
|
694
|
+
},
|
|
695
|
+
label: "Compression",
|
|
696
|
+
get children() {
|
|
697
|
+
var _el$37 = _$createElement("text"),
|
|
698
|
+
_el$38 = _$createElement("b");
|
|
699
|
+
_$insertNode(_el$37, _el$38);
|
|
700
|
+
_$insert(_el$38, () => collapsedCompressionValue(s()?.compression));
|
|
701
|
+
_$effect(_$p => _$setProp(_el$37, "fg", props.theme.textMuted, _$p));
|
|
702
|
+
return _el$37;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
})(), null);
|
|
706
|
+
return _el$25;
|
|
707
|
+
})();
|
|
708
|
+
})(), null);
|
|
709
|
+
_$insert(_el$0, (() => {
|
|
710
|
+
var _c$7 = _$memo(() => !!(!notInitialized() && !collapsed()));
|
|
711
|
+
return () => _c$7() && [_$memo(() => _$memo(() => !!prefs().sections.searchIndex)() && [_$createComponent(SectionHeader, {
|
|
712
|
+
get theme() {
|
|
713
|
+
return props.theme;
|
|
714
|
+
},
|
|
715
|
+
title: "Search Index"
|
|
716
|
+
}), _$createComponent(StatRow, {
|
|
717
|
+
get theme() {
|
|
718
|
+
return props.theme;
|
|
719
|
+
},
|
|
720
|
+
label: "Status",
|
|
721
|
+
get value() {
|
|
722
|
+
return searchStatus().label;
|
|
723
|
+
},
|
|
724
|
+
get tone() {
|
|
725
|
+
return searchStatus().tone;
|
|
726
|
+
}
|
|
727
|
+
}), _$memo(() => _$memo(() => (s()?.search_index?.files ?? null) != null)() && _$createComponent(StatRow, {
|
|
728
|
+
get theme() {
|
|
729
|
+
return props.theme;
|
|
730
|
+
},
|
|
731
|
+
label: "Files",
|
|
732
|
+
get value() {
|
|
733
|
+
return formatCount(s().search_index.files);
|
|
734
|
+
},
|
|
735
|
+
tone: "muted"
|
|
736
|
+
})), _$createComponent(StatRow, {
|
|
737
|
+
get theme() {
|
|
738
|
+
return props.theme;
|
|
739
|
+
},
|
|
740
|
+
label: "Disk",
|
|
741
|
+
get value() {
|
|
742
|
+
return formatBytes(trigramBytes());
|
|
743
|
+
},
|
|
744
|
+
tone: "muted"
|
|
745
|
+
})]), _$memo(() => _$memo(() => !!prefs().sections.semanticIndex)() && [_$createComponent(SectionHeader, {
|
|
746
|
+
get theme() {
|
|
747
|
+
return props.theme;
|
|
748
|
+
},
|
|
749
|
+
title: "Semantic Index"
|
|
750
|
+
}), _$createComponent(StatRow, {
|
|
751
|
+
get theme() {
|
|
752
|
+
return props.theme;
|
|
753
|
+
},
|
|
754
|
+
label: "Status",
|
|
755
|
+
get value() {
|
|
756
|
+
return semanticStatus().label;
|
|
757
|
+
},
|
|
758
|
+
get tone() {
|
|
759
|
+
return semanticStatus().tone;
|
|
760
|
+
}
|
|
761
|
+
}), _$memo(() => _$memo(() => !!semanticRefreshing())() && (() => {
|
|
762
|
+
var _el$39 = _$createElement("box"),
|
|
763
|
+
_el$40 = _$createElement("text");
|
|
764
|
+
_$insertNode(_el$39, _el$40);
|
|
765
|
+
_$setProp(_el$39, "width", "100%");
|
|
766
|
+
_$insert(_el$40, semanticRefreshing);
|
|
767
|
+
_$effect(_$p => _$setProp(_el$40, "fg", props.theme.textMuted, _$p));
|
|
768
|
+
return _el$39;
|
|
769
|
+
})()), _$memo(() => _$memo(() => !!(s()?.semantic_index?.status === "loading" && s()?.semantic_index?.entries_total != null && s().semantic_index.entries_total > 0))() && _$createComponent(StatRow, {
|
|
770
|
+
get theme() {
|
|
771
|
+
return props.theme;
|
|
772
|
+
},
|
|
773
|
+
label: "Progress",
|
|
774
|
+
get value() {
|
|
775
|
+
return `${formatCount(s().semantic_index.entries_done)} / ${formatCount(s().semantic_index.entries_total)}`;
|
|
776
|
+
},
|
|
777
|
+
tone: "warn"
|
|
778
|
+
})), _$memo(() => _$memo(() => (s()?.semantic_index?.entries ?? null) != null)() && _$createComponent(StatRow, {
|
|
779
|
+
get theme() {
|
|
780
|
+
return props.theme;
|
|
781
|
+
},
|
|
782
|
+
label: "Entries",
|
|
783
|
+
get value() {
|
|
784
|
+
return formatCount(s().semantic_index.entries);
|
|
785
|
+
},
|
|
786
|
+
tone: "muted"
|
|
787
|
+
})), _$createComponent(StatRow, {
|
|
788
|
+
get theme() {
|
|
789
|
+
return props.theme;
|
|
790
|
+
},
|
|
791
|
+
label: "Disk",
|
|
792
|
+
get value() {
|
|
793
|
+
return formatBytes(semanticBytes());
|
|
794
|
+
},
|
|
795
|
+
tone: "muted"
|
|
796
|
+
})]), _$memo(() => _$memo(() => !!(prefs().sections.codeHealth && statusBar()))() && [_$createComponent(SectionHeader, {
|
|
797
|
+
get theme() {
|
|
798
|
+
return props.theme;
|
|
799
|
+
},
|
|
800
|
+
get title() {
|
|
801
|
+
return statusBar().tier2_stale ? "Code Health ~" : "Code Health";
|
|
802
|
+
}
|
|
803
|
+
}), _$createComponent(StatRow, {
|
|
804
|
+
get theme() {
|
|
805
|
+
return props.theme;
|
|
806
|
+
},
|
|
807
|
+
label: "Errors",
|
|
808
|
+
get value() {
|
|
809
|
+
return formatCount(statusBar().errors);
|
|
810
|
+
},
|
|
811
|
+
get tone() {
|
|
812
|
+
return statusBar().errors > 0 ? "err" : "muted";
|
|
813
|
+
}
|
|
814
|
+
}), _$createComponent(StatRow, {
|
|
815
|
+
get theme() {
|
|
816
|
+
return props.theme;
|
|
817
|
+
},
|
|
818
|
+
label: "Warnings",
|
|
819
|
+
get value() {
|
|
820
|
+
return formatCount(statusBar().warnings);
|
|
821
|
+
},
|
|
822
|
+
get tone() {
|
|
823
|
+
return statusBar().warnings > 0 ? "warn" : "muted";
|
|
824
|
+
}
|
|
825
|
+
}), _$createComponent(StatRow, {
|
|
826
|
+
get theme() {
|
|
827
|
+
return props.theme;
|
|
828
|
+
},
|
|
829
|
+
label: "Dead Code",
|
|
830
|
+
get value() {
|
|
831
|
+
return formatCount(statusBar().dead_code);
|
|
832
|
+
},
|
|
833
|
+
tone: "muted"
|
|
834
|
+
}), _$createComponent(StatRow, {
|
|
835
|
+
get theme() {
|
|
836
|
+
return props.theme;
|
|
837
|
+
},
|
|
838
|
+
label: "Unused Exports",
|
|
839
|
+
get value() {
|
|
840
|
+
return formatCount(statusBar().unused_exports);
|
|
841
|
+
},
|
|
842
|
+
tone: "muted"
|
|
843
|
+
}), _$createComponent(StatRow, {
|
|
844
|
+
get theme() {
|
|
845
|
+
return props.theme;
|
|
846
|
+
},
|
|
847
|
+
label: "Duplicates",
|
|
848
|
+
get value() {
|
|
849
|
+
return formatCount(statusBar().duplicates);
|
|
850
|
+
},
|
|
851
|
+
tone: "muted"
|
|
852
|
+
}), _$createComponent(StatRow, {
|
|
853
|
+
get theme() {
|
|
854
|
+
return props.theme;
|
|
855
|
+
},
|
|
856
|
+
label: "TODOs",
|
|
857
|
+
get value() {
|
|
858
|
+
return formatCount(statusBar().todos);
|
|
859
|
+
},
|
|
860
|
+
tone: "muted"
|
|
861
|
+
})]), _$memo(() => _$memo(() => !!(prefs().sections.compression && compressionRows().length > 0))() && [_$createComponent(SectionHeader, {
|
|
862
|
+
get theme() {
|
|
863
|
+
return props.theme;
|
|
864
|
+
},
|
|
865
|
+
title: "Compression"
|
|
866
|
+
}), _$memo(() => compressionRows().map(row => row.kind === "scope" ? (() => {
|
|
867
|
+
var _el$41 = _$createElement("box"),
|
|
868
|
+
_el$42 = _$createElement("text");
|
|
869
|
+
_$insertNode(_el$41, _el$42);
|
|
870
|
+
_$setProp(_el$41, "width", "100%");
|
|
871
|
+
_$insert(_el$42, () => row.label);
|
|
872
|
+
_$effect(_$p => _$setProp(_el$42, "fg", props.theme.text, _$p));
|
|
873
|
+
return _el$41;
|
|
874
|
+
})() : _$createComponent(StatRow, {
|
|
875
|
+
get theme() {
|
|
876
|
+
return props.theme;
|
|
877
|
+
},
|
|
878
|
+
get label() {
|
|
879
|
+
return row.label;
|
|
880
|
+
},
|
|
881
|
+
get value() {
|
|
882
|
+
return row.value;
|
|
883
|
+
},
|
|
884
|
+
tone: "muted"
|
|
885
|
+
})))]), _$memo(() => _$memo(() => !!(s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error))() && (() => {
|
|
886
|
+
var _el$43 = _$createElement("box"),
|
|
887
|
+
_el$44 = _$createElement("text"),
|
|
888
|
+
_el$45 = _$createTextNode(`⚠ `);
|
|
889
|
+
_$insertNode(_el$43, _el$44);
|
|
890
|
+
_$setProp(_el$43, "marginTop", 1);
|
|
891
|
+
_$setProp(_el$43, "width", "100%");
|
|
892
|
+
_$insertNode(_el$44, _el$45);
|
|
893
|
+
_$insert(_el$44, () => s().semantic_index.error, null);
|
|
894
|
+
_$effect(_$p => _$setProp(_el$44, "fg", props.theme.error, _$p));
|
|
895
|
+
return _el$43;
|
|
896
|
+
})())];
|
|
897
|
+
})(), null);
|
|
898
|
+
_$effect(_p$ => {
|
|
899
|
+
var _v$5 = props.theme.borderActive,
|
|
900
|
+
_v$6 = props.theme.accent,
|
|
901
|
+
_v$7 = badgeTextColor(props.theme.accent, props.theme.background);
|
|
902
|
+
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$0, "borderColor", _v$5, _p$.e));
|
|
903
|
+
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "backgroundColor", _v$6, _p$.t));
|
|
904
|
+
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$12, "fg", _v$7, _p$.a));
|
|
905
|
+
return _p$;
|
|
906
|
+
}, {
|
|
907
|
+
e: undefined,
|
|
908
|
+
t: undefined,
|
|
909
|
+
a: undefined
|
|
910
|
+
});
|
|
911
|
+
return _el$0;
|
|
912
|
+
})();
|
|
913
|
+
};
|
|
914
|
+
export async function createAftSidebarSlot(api, pluginVersion) {
|
|
915
|
+
const root = await readTuiPreferencesFile();
|
|
916
|
+
const order = computeEffectiveOrder(root, PLUGIN_KEY, DEFAULT_SLOT_ORDER);
|
|
917
|
+
return {
|
|
918
|
+
// DEFAULT_SLOT_ORDER (180) is AFT's coordinated default in the shared
|
|
919
|
+
// tui-preferences ladder (anthropic-auth 160, AFT 180, magic-context 200).
|
|
920
|
+
// Override via `order` or `forceToTop` in tui-preferences.jsonc.
|
|
921
|
+
order,
|
|
922
|
+
slots: {
|
|
923
|
+
sidebar_content: (ctx, value) => {
|
|
924
|
+
const theme = createMemo(() => ctx.theme.current);
|
|
925
|
+
return _$createComponent(SidebarContent, {
|
|
926
|
+
api: api,
|
|
927
|
+
sessionID: () => value.session_id,
|
|
928
|
+
get theme() {
|
|
929
|
+
return theme();
|
|
930
|
+
},
|
|
931
|
+
pluginVersion: pluginVersion
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|