@cortexkit/aft-opencode 0.39.0 → 0.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +801 -531
- package/dist/tools/_shared.d.ts +1 -32
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts +0 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tui/preferences.d.ts +33 -0
- package/dist/tui/preferences.d.ts.map +1 -0
- package/dist/tui.js +8102 -84
- package/package.json +8 -8
- package/src/tui/index.tsx +1 -1
- package/src/tui/preferences.ts +243 -0
- package/src/tui/sidebar.tsx +142 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/aft-opencode",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,19 +30,19 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.2.0",
|
|
33
|
-
"@cortexkit/aft-bridge": "0.39.
|
|
33
|
+
"@cortexkit/aft-bridge": "0.39.2",
|
|
34
34
|
"@xterm/headless": "^5.5.0",
|
|
35
35
|
"comment-json": "^4.6.2",
|
|
36
36
|
"undici": "^7.25.0",
|
|
37
37
|
"zod": "^4.1.8"
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
|
-
"@cortexkit/aft-darwin-arm64": "0.39.
|
|
41
|
-
"@cortexkit/aft-darwin-x64": "0.39.
|
|
42
|
-
"@cortexkit/aft-linux-arm64": "0.39.
|
|
43
|
-
"@cortexkit/aft-linux-x64": "0.39.
|
|
44
|
-
"@cortexkit/aft-win32-arm64": "0.39.
|
|
45
|
-
"@cortexkit/aft-win32-x64": "0.39.
|
|
40
|
+
"@cortexkit/aft-darwin-arm64": "0.39.2",
|
|
41
|
+
"@cortexkit/aft-darwin-x64": "0.39.2",
|
|
42
|
+
"@cortexkit/aft-linux-arm64": "0.39.2",
|
|
43
|
+
"@cortexkit/aft-linux-x64": "0.39.2",
|
|
44
|
+
"@cortexkit/aft-win32-arm64": "0.39.2",
|
|
45
|
+
"@cortexkit/aft-win32-x64": "0.39.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "^1.15.11",
|
package/src/tui/index.tsx
CHANGED
|
@@ -727,7 +727,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
727
727
|
// command palette entry so the sidebar is available immediately when the
|
|
728
728
|
// user opens their first session.
|
|
729
729
|
try {
|
|
730
|
-
api.slots.register(createAftSidebarSlot(api, packageVersion));
|
|
730
|
+
api.slots.register(await createAftSidebarSlot(api, packageVersion));
|
|
731
731
|
} catch {
|
|
732
732
|
// Older OpenCode TUI hosts may not implement api.slots; fall through
|
|
733
733
|
// and keep the slash command working.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { parse, stringify } from "comment-json";
|
|
6
|
+
|
|
7
|
+
export const TUI_PREFS_FILE_ENV = "OPENCODE_TUI_PREFERENCES_FILE";
|
|
8
|
+
const FILE_NAME = "tui-preferences.jsonc";
|
|
9
|
+
|
|
10
|
+
export function getTuiPreferencesFile(): string {
|
|
11
|
+
const override = process.env[TUI_PREFS_FILE_ENV];
|
|
12
|
+
if (override) return override;
|
|
13
|
+
const configDir =
|
|
14
|
+
process.env.OPENCODE_CONFIG_DIR ||
|
|
15
|
+
join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
16
|
+
return join(configDir, FILE_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readTuiPreferencesFile(): Promise<Record<string, unknown>> {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await readFile(getTuiPreferencesFile(), "utf8");
|
|
26
|
+
const root: unknown = parse(raw);
|
|
27
|
+
return isRecord(root) ? (root as Record<string, unknown>) : {};
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const PLUGIN_KEY = "aft";
|
|
34
|
+
export const DEFAULT_SLOT_ORDER = 180;
|
|
35
|
+
|
|
36
|
+
export interface AftTuiPrefs {
|
|
37
|
+
forceToTop: boolean;
|
|
38
|
+
order: number;
|
|
39
|
+
startCollapsed: boolean;
|
|
40
|
+
rememberCollapsed: boolean;
|
|
41
|
+
collapsed: boolean | null;
|
|
42
|
+
header: {
|
|
43
|
+
label: string;
|
|
44
|
+
showVersion: boolean;
|
|
45
|
+
};
|
|
46
|
+
sections: {
|
|
47
|
+
searchIndex: boolean;
|
|
48
|
+
semanticIndex: boolean;
|
|
49
|
+
codeHealth: boolean;
|
|
50
|
+
compression: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_PREFS: AftTuiPrefs = {
|
|
55
|
+
forceToTop: false,
|
|
56
|
+
order: DEFAULT_SLOT_ORDER,
|
|
57
|
+
startCollapsed: false,
|
|
58
|
+
rememberCollapsed: true,
|
|
59
|
+
collapsed: null,
|
|
60
|
+
header: { label: "AFT", showVersion: true },
|
|
61
|
+
sections: {
|
|
62
|
+
searchIndex: true,
|
|
63
|
+
semanticIndex: true,
|
|
64
|
+
codeHealth: true,
|
|
65
|
+
compression: true,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function bool(value: unknown, fallback: boolean): boolean {
|
|
70
|
+
return typeof value === "boolean" ? value : fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function int(value: unknown, fallback: number, min: number, max: number): number {
|
|
74
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
75
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function label(value: unknown, fallback: string, maxLength: number): string {
|
|
79
|
+
if (typeof value !== "string" || value.length === 0) return fallback;
|
|
80
|
+
return value.slice(0, maxLength);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Initial collapsed state from persisted prefs or startCollapsed default. */
|
|
84
|
+
export function seedCollapsedFromPrefs(prefs: AftTuiPrefs): boolean {
|
|
85
|
+
return prefs.collapsed ?? prefs.startCollapsed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveAftPrefs(root: Record<string, unknown>): AftTuiPrefs {
|
|
89
|
+
const entry = root[PLUGIN_KEY];
|
|
90
|
+
if (!isRecord(entry)) return structuredClone(DEFAULT_PREFS);
|
|
91
|
+
|
|
92
|
+
const d = DEFAULT_PREFS;
|
|
93
|
+
const header = isRecord(entry.header) ? entry.header : {};
|
|
94
|
+
const sections = isRecord(entry.sections) ? entry.sections : {};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
forceToTop: bool(entry.forceToTop, d.forceToTop),
|
|
98
|
+
order: int(entry.order, d.order, -10000, 10000),
|
|
99
|
+
startCollapsed: bool(entry.startCollapsed, d.startCollapsed),
|
|
100
|
+
rememberCollapsed: bool(entry.rememberCollapsed, d.rememberCollapsed),
|
|
101
|
+
collapsed: typeof entry.collapsed === "boolean" ? entry.collapsed : null,
|
|
102
|
+
header: {
|
|
103
|
+
label: label(header.label, d.header.label, 20),
|
|
104
|
+
showVersion: bool(header.showVersion, d.header.showVersion),
|
|
105
|
+
},
|
|
106
|
+
sections: {
|
|
107
|
+
searchIndex: bool(sections.searchIndex, d.sections.searchIndex),
|
|
108
|
+
semanticIndex: bool(sections.semanticIndex, d.sections.semanticIndex),
|
|
109
|
+
codeHealth: bool(sections.codeHealth, d.sections.codeHealth),
|
|
110
|
+
compression: bool(sections.compression, d.sections.compression),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const FORCE_TOP_BASE = -100000;
|
|
116
|
+
|
|
117
|
+
export function computeEffectiveOrder(
|
|
118
|
+
root: Record<string, unknown>,
|
|
119
|
+
pluginKey: string,
|
|
120
|
+
defaultOrder: number,
|
|
121
|
+
): number {
|
|
122
|
+
const entry = root[pluginKey];
|
|
123
|
+
if (!isRecord(entry)) return defaultOrder;
|
|
124
|
+
if (entry.forceToTop === true) {
|
|
125
|
+
return FORCE_TOP_BASE + Object.keys(root).indexOf(pluginKey);
|
|
126
|
+
}
|
|
127
|
+
return int(entry.order, defaultOrder, -10000, 10000);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const TEMPLATE = `// Shared preferences for opencode TUI plugins.
|
|
131
|
+
// One top-level key per plugin (short name). See each plugin's README for
|
|
132
|
+
// its supported settings. This file is safe to hand-edit; plugins update
|
|
133
|
+
// individual keys in place and preserve comments.
|
|
134
|
+
{}
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
type JsonValue = string | number | boolean | null;
|
|
138
|
+
|
|
139
|
+
function setNested(target: Record<string, unknown>, path: string[], value: JsonValue): void {
|
|
140
|
+
let current: Record<string, unknown> = target;
|
|
141
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
142
|
+
const key = path[i];
|
|
143
|
+
if (key === undefined) return;
|
|
144
|
+
const next = current[key];
|
|
145
|
+
if (!isRecord(next)) {
|
|
146
|
+
const created: Record<string, unknown> = {};
|
|
147
|
+
current[key] = created;
|
|
148
|
+
current = created;
|
|
149
|
+
} else {
|
|
150
|
+
current = next;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const leaf = path[path.length - 1];
|
|
154
|
+
if (leaf === undefined) return;
|
|
155
|
+
current[leaf] = value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function writePreference(pluginKey: string, path: string[], value: JsonValue): Promise<void> {
|
|
159
|
+
const file = getTuiPreferencesFile();
|
|
160
|
+
await mkdir(dirname(file), { recursive: true });
|
|
161
|
+
let text: string;
|
|
162
|
+
try {
|
|
163
|
+
text = await readFile(file, "utf8");
|
|
164
|
+
} catch {
|
|
165
|
+
text = "";
|
|
166
|
+
}
|
|
167
|
+
if (text.trim() === "") text = TEMPLATE;
|
|
168
|
+
|
|
169
|
+
let root: Record<string, unknown>;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = parse(text);
|
|
172
|
+
root = isRecord(parsed) ? (parsed as Record<string, unknown>) : {};
|
|
173
|
+
} catch {
|
|
174
|
+
root = {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!isRecord(root[pluginKey])) {
|
|
178
|
+
root[pluginKey] = {};
|
|
179
|
+
}
|
|
180
|
+
const entry = root[pluginKey] as Record<string, unknown>;
|
|
181
|
+
setNested(entry, path, value);
|
|
182
|
+
|
|
183
|
+
const next = `${stringify(root, null, 2)}\n`;
|
|
184
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
185
|
+
await writeFile(tmp, next, "utf8");
|
|
186
|
+
await rename(tmp, file);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
190
|
+
|
|
191
|
+
export function queueTuiPreferenceUpdate(
|
|
192
|
+
pluginKey: string,
|
|
193
|
+
path: string[],
|
|
194
|
+
value: JsonValue,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
writeChain = writeChain.then(() => writePreference(pluginKey, path, value)).catch(() => {});
|
|
197
|
+
return writeChain;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const WATCH_DEBOUNCE_MS = 150;
|
|
201
|
+
|
|
202
|
+
export function watchTuiPreferences(onChange: () => void): () => void {
|
|
203
|
+
const file = getTuiPreferencesFile();
|
|
204
|
+
const name = basename(file);
|
|
205
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
206
|
+
let lastSeen: string | null = null;
|
|
207
|
+
void readFile(file, "utf8")
|
|
208
|
+
.then((text) => {
|
|
209
|
+
if (lastSeen === null) lastSeen = text;
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {});
|
|
212
|
+
try {
|
|
213
|
+
const watcher = watch(dirname(file), (_event, filename) => {
|
|
214
|
+
const isOurs =
|
|
215
|
+
filename === name || (filename?.startsWith(`${name}.`) && filename.endsWith(".tmp"));
|
|
216
|
+
if (filename != null && !isOurs) return;
|
|
217
|
+
if (timer) clearTimeout(timer);
|
|
218
|
+
timer = setTimeout(() => {
|
|
219
|
+
timer = null;
|
|
220
|
+
void readFile(file, "utf8")
|
|
221
|
+
.catch(() => null)
|
|
222
|
+
.then((text) => {
|
|
223
|
+
if (text === null) return;
|
|
224
|
+
if (text === lastSeen) return;
|
|
225
|
+
lastSeen = text;
|
|
226
|
+
onChange();
|
|
227
|
+
});
|
|
228
|
+
}, WATCH_DEBOUNCE_MS);
|
|
229
|
+
});
|
|
230
|
+
return () => {
|
|
231
|
+
if (timer) clearTimeout(timer);
|
|
232
|
+
watcher.close();
|
|
233
|
+
};
|
|
234
|
+
} catch {
|
|
235
|
+
return () => {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function persistCollapsedIfEnabled(prefs: AftTuiPrefs, collapsed: boolean): void {
|
|
240
|
+
if (prefs.rememberCollapsed) {
|
|
241
|
+
void queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], collapsed);
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/tui/sidebar.tsx
CHANGED
|
@@ -20,6 +20,18 @@ import {
|
|
|
20
20
|
type StatusCompression,
|
|
21
21
|
} from "../shared/status";
|
|
22
22
|
import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
|
|
23
|
+
import {
|
|
24
|
+
type AftTuiPrefs,
|
|
25
|
+
computeEffectiveOrder,
|
|
26
|
+
DEFAULT_PREFS,
|
|
27
|
+
DEFAULT_SLOT_ORDER,
|
|
28
|
+
PLUGIN_KEY,
|
|
29
|
+
persistCollapsedIfEnabled,
|
|
30
|
+
readTuiPreferencesFile,
|
|
31
|
+
resolveAftPrefs,
|
|
32
|
+
seedCollapsedFromPrefs,
|
|
33
|
+
watchTuiPreferences,
|
|
34
|
+
} from "./preferences";
|
|
23
35
|
|
|
24
36
|
const SINGLE_BORDER = { type: "single" } as any;
|
|
25
37
|
const REFRESH_DEBOUNCE_MS = 200;
|
|
@@ -182,6 +194,25 @@ export function collapsedCompressionValue(
|
|
|
182
194
|
|
|
183
195
|
export type HealthLightTone = "ok" | "warn" | "err";
|
|
184
196
|
|
|
197
|
+
// Degraded-mode reason → human-readable hint. Distinct strings per reason
|
|
198
|
+
// because the UX direction is different: "home_root" tells the user to open a
|
|
199
|
+
// real project subdirectory, "search_too_many_files" tells them the tree is too
|
|
200
|
+
// big for full indexing, and "watcher_unavailable" is an honest soft
|
|
201
|
+
// degradation (AFT continues without live external-change invalidation).
|
|
202
|
+
export function degradedReasonLabel(reason: string): string {
|
|
203
|
+
if (reason === "home_root") {
|
|
204
|
+
return "project root is your home directory";
|
|
205
|
+
}
|
|
206
|
+
if (reason.startsWith("search_too_many_files:")) {
|
|
207
|
+
const threshold = reason.split(":")[1] ?? "20000";
|
|
208
|
+
return `project exceeds ${threshold} files`;
|
|
209
|
+
}
|
|
210
|
+
if (reason === "watcher_unavailable") {
|
|
211
|
+
return "file watcher unavailable; continuing without live external-change invalidation";
|
|
212
|
+
}
|
|
213
|
+
return reason; // unknown reason — surface verbatim so users can grep logs
|
|
214
|
+
}
|
|
215
|
+
|
|
185
216
|
export interface HealthLights {
|
|
186
217
|
// Diagnostics: red if any errors, yellow if any warnings, else green.
|
|
187
218
|
diagnostics: HealthLightTone;
|
|
@@ -307,9 +338,8 @@ const SidebarContent = (props: {
|
|
|
307
338
|
pluginVersion: string;
|
|
308
339
|
}) => {
|
|
309
340
|
const [status, setStatus] = createSignal<ScopedSidebarStatus | null>(null);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const [collapsed, setCollapsed] = createSignal(false);
|
|
341
|
+
const [prefs, setPrefs] = createSignal<AftTuiPrefs>(structuredClone(DEFAULT_PREFS));
|
|
342
|
+
const [collapsed, setCollapsed] = createSignal(seedCollapsedFromPrefs(DEFAULT_PREFS));
|
|
313
343
|
let inflight: {
|
|
314
344
|
controller: AbortController;
|
|
315
345
|
generation: number;
|
|
@@ -428,7 +458,21 @@ const SidebarContent = (props: {
|
|
|
428
458
|
}, REFRESH_DEBOUNCE_MS);
|
|
429
459
|
};
|
|
430
460
|
|
|
461
|
+
const reloadPrefs = async () => {
|
|
462
|
+
const root = await readTuiPreferencesFile();
|
|
463
|
+
const next = resolveAftPrefs(root);
|
|
464
|
+
setPrefs(next);
|
|
465
|
+
setCollapsed(seedCollapsedFromPrefs(next));
|
|
466
|
+
requestRender();
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
void reloadPrefs();
|
|
470
|
+
const unwatchPrefs = watchTuiPreferences(() => {
|
|
471
|
+
void reloadPrefs();
|
|
472
|
+
});
|
|
473
|
+
|
|
431
474
|
onCleanup(() => {
|
|
475
|
+
unwatchPrefs();
|
|
432
476
|
generation++;
|
|
433
477
|
abortInflight();
|
|
434
478
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -518,20 +562,6 @@ const SidebarContent = (props: {
|
|
|
518
562
|
const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
|
|
519
563
|
const statusBar = () => s()?.status_bar;
|
|
520
564
|
|
|
521
|
-
// Degraded-mode reason → human-readable hint. Distinct strings per reason
|
|
522
|
-
// because the UX direction is different: "home_root" tells the user to
|
|
523
|
-
// open a real project subdirectory, "search_too_many_files" tells them the
|
|
524
|
-
// tree is too big for full indexing.
|
|
525
|
-
const degradedReasonLabel = (reason: string): string => {
|
|
526
|
-
if (reason === "home_root") {
|
|
527
|
-
return "project root is your home directory";
|
|
528
|
-
}
|
|
529
|
-
if (reason.startsWith("search_too_many_files:")) {
|
|
530
|
-
const threshold = reason.split(":")[1] ?? "20000";
|
|
531
|
-
return `project exceeds ${threshold} files`;
|
|
532
|
-
}
|
|
533
|
-
return reason; // unknown reason — surface verbatim so users can grep logs
|
|
534
|
-
};
|
|
535
565
|
const degradedSummary = () => {
|
|
536
566
|
const snap = s();
|
|
537
567
|
if (!snap?.degraded) return null;
|
|
@@ -560,7 +590,12 @@ const SidebarContent = (props: {
|
|
|
560
590
|
justifyContent="space-between"
|
|
561
591
|
alignItems="center"
|
|
562
592
|
onMouseDown={() => {
|
|
563
|
-
if (
|
|
593
|
+
if (notInitialized()) return;
|
|
594
|
+
setCollapsed((x) => {
|
|
595
|
+
const next = !x;
|
|
596
|
+
persistCollapsedIfEnabled(prefs(), next);
|
|
597
|
+
return next;
|
|
598
|
+
});
|
|
564
599
|
}}
|
|
565
600
|
>
|
|
566
601
|
<box flexDirection="row" alignItems="center">
|
|
@@ -570,7 +605,7 @@ const SidebarContent = (props: {
|
|
|
570
605
|
<text fg={props.theme.background}>
|
|
571
606
|
<b>
|
|
572
607
|
{notInitialized() ? "" : collapsed() ? "▶ " : "▼ "}
|
|
573
|
-
|
|
608
|
+
{prefs().header.label}
|
|
574
609
|
</b>
|
|
575
610
|
</text>
|
|
576
611
|
</box>
|
|
@@ -587,7 +622,7 @@ const SidebarContent = (props: {
|
|
|
587
622
|
</box>
|
|
588
623
|
)}
|
|
589
624
|
</box>
|
|
590
|
-
{!notInitialized() && (
|
|
625
|
+
{!notInitialized() && prefs().header.showVersion && (
|
|
591
626
|
<text fg={props.theme.textMuted}>v{s()?.version ?? props.pluginVersion}</text>
|
|
592
627
|
)}
|
|
593
628
|
</box>
|
|
@@ -622,13 +657,17 @@ const SidebarContent = (props: {
|
|
|
622
657
|
order of the expanded grid. */}
|
|
623
658
|
{!notInitialized() && collapsed() && (
|
|
624
659
|
<box width="100%" flexDirection="column">
|
|
625
|
-
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
660
|
+
{prefs().sections.searchIndex && (
|
|
661
|
+
<CollapsedRow theme={props.theme} label="Search Index">
|
|
662
|
+
<text fg={toneColor(props.theme, searchStatus().tone)}>●</text>
|
|
663
|
+
</CollapsedRow>
|
|
664
|
+
)}
|
|
665
|
+
{prefs().sections.semanticIndex && (
|
|
666
|
+
<CollapsedRow theme={props.theme} label="Semantic Index">
|
|
667
|
+
<text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
|
|
668
|
+
</CollapsedRow>
|
|
669
|
+
)}
|
|
670
|
+
{prefs().sections.codeHealth && collapsedHealthLights(statusBar()) && (
|
|
632
671
|
<CollapsedRow theme={props.theme} label="Code Health">
|
|
633
672
|
<box flexDirection="row" gap={1}>
|
|
634
673
|
<text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.diagnostics)}>
|
|
@@ -641,7 +680,7 @@ const SidebarContent = (props: {
|
|
|
641
680
|
</box>
|
|
642
681
|
</CollapsedRow>
|
|
643
682
|
)}
|
|
644
|
-
{collapsedCompressionValue(s()?.compression) && (
|
|
683
|
+
{prefs().sections.compression && collapsedCompressionValue(s()?.compression) && (
|
|
645
684
|
<CollapsedRow theme={props.theme} label="Compression">
|
|
646
685
|
<text fg={props.theme.textMuted}>
|
|
647
686
|
<b>{collapsedCompressionValue(s()?.compression)}</b>
|
|
@@ -654,69 +693,76 @@ const SidebarContent = (props: {
|
|
|
654
693
|
{/* Search index */}
|
|
655
694
|
{!notInitialized() && !collapsed() && (
|
|
656
695
|
<>
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
696
|
+
{prefs().sections.searchIndex && (
|
|
697
|
+
<>
|
|
698
|
+
<SectionHeader theme={props.theme} title="Search Index" />
|
|
699
|
+
<StatRow
|
|
700
|
+
theme={props.theme}
|
|
701
|
+
label="Status"
|
|
702
|
+
value={searchStatus().label}
|
|
703
|
+
tone={searchStatus().tone}
|
|
704
|
+
/>
|
|
705
|
+
{(s()?.search_index?.files ?? null) != null && (
|
|
706
|
+
<StatRow
|
|
707
|
+
theme={props.theme}
|
|
708
|
+
label="Files"
|
|
709
|
+
value={formatCount(s()!.search_index.files)}
|
|
710
|
+
tone="muted"
|
|
711
|
+
/>
|
|
712
|
+
)}
|
|
713
|
+
<StatRow
|
|
714
|
+
theme={props.theme}
|
|
715
|
+
label="Disk"
|
|
716
|
+
value={formatBytes(trigramBytes())}
|
|
717
|
+
tone="muted"
|
|
718
|
+
/>
|
|
719
|
+
</>
|
|
671
720
|
)}
|
|
672
|
-
<StatRow
|
|
673
|
-
theme={props.theme}
|
|
674
|
-
label="Disk"
|
|
675
|
-
value={formatBytes(trigramBytes())}
|
|
676
|
-
tone="muted"
|
|
677
|
-
/>
|
|
678
721
|
|
|
679
|
-
{
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
722
|
+
{prefs().sections.semanticIndex && (
|
|
723
|
+
<>
|
|
724
|
+
<SectionHeader theme={props.theme} title="Semantic Index" />
|
|
725
|
+
<StatRow
|
|
726
|
+
theme={props.theme}
|
|
727
|
+
label="Status"
|
|
728
|
+
value={semanticStatus().label}
|
|
729
|
+
tone={semanticStatus().tone}
|
|
730
|
+
/>
|
|
731
|
+
{semanticRefreshing() && (
|
|
732
|
+
<box width="100%">
|
|
733
|
+
<text fg={props.theme.textMuted}>{semanticRefreshing()}</text>
|
|
734
|
+
</box>
|
|
735
|
+
)}
|
|
736
|
+
{/* When loading, magic-context-style progress hint helps users see
|
|
693
737
|
background work is making progress instead of stuck. */}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
738
|
+
{s()?.semantic_index?.status === "loading" &&
|
|
739
|
+
s()?.semantic_index?.entries_total != null &&
|
|
740
|
+
s()!.semantic_index.entries_total! > 0 && (
|
|
741
|
+
<StatRow
|
|
742
|
+
theme={props.theme}
|
|
743
|
+
label="Progress"
|
|
744
|
+
value={`${formatCount(s()!.semantic_index.entries_done)} / ${formatCount(
|
|
745
|
+
s()!.semantic_index.entries_total,
|
|
746
|
+
)}`}
|
|
747
|
+
tone="warn"
|
|
748
|
+
/>
|
|
749
|
+
)}
|
|
750
|
+
{(s()?.semantic_index?.entries ?? null) != null && (
|
|
751
|
+
<StatRow
|
|
752
|
+
theme={props.theme}
|
|
753
|
+
label="Entries"
|
|
754
|
+
value={formatCount(s()!.semantic_index.entries)}
|
|
755
|
+
tone="muted"
|
|
756
|
+
/>
|
|
757
|
+
)}
|
|
697
758
|
<StatRow
|
|
698
759
|
theme={props.theme}
|
|
699
|
-
label="
|
|
700
|
-
value={
|
|
701
|
-
|
|
702
|
-
)}`}
|
|
703
|
-
tone="warn"
|
|
760
|
+
label="Disk"
|
|
761
|
+
value={formatBytes(semanticBytes())}
|
|
762
|
+
tone="muted"
|
|
704
763
|
/>
|
|
705
|
-
|
|
706
|
-
{(s()?.semantic_index?.entries ?? null) != null && (
|
|
707
|
-
<StatRow
|
|
708
|
-
theme={props.theme}
|
|
709
|
-
label="Entries"
|
|
710
|
-
value={formatCount(s()!.semantic_index.entries)}
|
|
711
|
-
tone="muted"
|
|
712
|
-
/>
|
|
764
|
+
</>
|
|
713
765
|
)}
|
|
714
|
-
<StatRow
|
|
715
|
-
theme={props.theme}
|
|
716
|
-
label="Disk"
|
|
717
|
-
value={formatBytes(semanticBytes())}
|
|
718
|
-
tone="muted"
|
|
719
|
-
/>
|
|
720
766
|
|
|
721
767
|
{/* Code Health — the agent status-bar glance, surfaced for users.
|
|
722
768
|
Hidden until the Tier-2 cache is populated (status_bar undefined),
|
|
@@ -724,7 +770,7 @@ const SidebarContent = (props: {
|
|
|
724
770
|
diagnostics; D/U/C/T come from the last background scan. A `~` on
|
|
725
771
|
the section header flags the Tier-2 counts as predating the latest
|
|
726
772
|
edit. */}
|
|
727
|
-
{statusBar() && (
|
|
773
|
+
{prefs().sections.codeHealth && statusBar() && (
|
|
728
774
|
<>
|
|
729
775
|
<SectionHeader
|
|
730
776
|
theme={props.theme}
|
|
@@ -774,7 +820,7 @@ const SidebarContent = (props: {
|
|
|
774
820
|
subheader followed by two StatRows (Tokens Saved, Compression
|
|
775
821
|
Ratio). Keeps numbers right-aligned in the value column instead
|
|
776
822
|
of jamming them after the label on the same line. */}
|
|
777
|
-
{compressionRows().length > 0 && (
|
|
823
|
+
{prefs().sections.compression && compressionRows().length > 0 && (
|
|
778
824
|
<>
|
|
779
825
|
<SectionHeader theme={props.theme} title="Compression" />
|
|
780
826
|
{compressionRows().map((row) =>
|
|
@@ -802,12 +848,17 @@ const SidebarContent = (props: {
|
|
|
802
848
|
);
|
|
803
849
|
};
|
|
804
850
|
|
|
805
|
-
export function createAftSidebarSlot(
|
|
851
|
+
export async function createAftSidebarSlot(
|
|
852
|
+
api: TuiPluginApi,
|
|
853
|
+
pluginVersion: string,
|
|
854
|
+
): Promise<TuiSlotPlugin> {
|
|
855
|
+
const root = await readTuiPreferencesFile();
|
|
856
|
+
const order = computeEffectiveOrder(root, PLUGIN_KEY, DEFAULT_SLOT_ORDER);
|
|
806
857
|
return {
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
order
|
|
858
|
+
// DEFAULT_SLOT_ORDER (180) is AFT's coordinated default in the shared
|
|
859
|
+
// tui-preferences ladder (anthropic-auth 160, AFT 180, magic-context 200).
|
|
860
|
+
// Override via `order` or `forceToTop` in tui-preferences.jsonc.
|
|
861
|
+
order,
|
|
811
862
|
slots: {
|
|
812
863
|
sidebar_content: (ctx, value) => {
|
|
813
864
|
const theme = createMemo(() => (ctx as any).theme.current);
|