@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.39.0",
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.0",
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.0",
41
- "@cortexkit/aft-darwin-x64": "0.39.0",
42
- "@cortexkit/aft-linux-arm64": "0.39.0",
43
- "@cortexkit/aft-linux-x64": "0.39.0",
44
- "@cortexkit/aft-win32-arm64": "0.39.0",
45
- "@cortexkit/aft-win32-x64": "0.39.0"
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
+ }
@@ -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
- // Collapsed/expanded toggle local UI state (not persisted), mirroring
311
- // OpenCode's native MCP sidebar section. Default expanded.
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 (!notInitialized()) setCollapsed((x) => !x);
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
- AFT
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
- <CollapsedRow theme={props.theme} label="Search Index">
626
- <text fg={toneColor(props.theme, searchStatus().tone)}>●</text>
627
- </CollapsedRow>
628
- <CollapsedRow theme={props.theme} label="Semantic Index">
629
- <text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
630
- </CollapsedRow>
631
- {collapsedHealthLights(statusBar()) && (
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
- <SectionHeader theme={props.theme} title="Search Index" />
658
- <StatRow
659
- theme={props.theme}
660
- label="Status"
661
- value={searchStatus().label}
662
- tone={searchStatus().tone}
663
- />
664
- {(s()?.search_index?.files ?? null) != null && (
665
- <StatRow
666
- theme={props.theme}
667
- label="Files"
668
- value={formatCount(s()!.search_index.files)}
669
- tone="muted"
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
- {/* Semantic index */}
680
- <SectionHeader theme={props.theme} title="Semantic Index" />
681
- <StatRow
682
- theme={props.theme}
683
- label="Status"
684
- value={semanticStatus().label}
685
- tone={semanticStatus().tone}
686
- />
687
- {semanticRefreshing() && (
688
- <box width="100%">
689
- <text fg={props.theme.textMuted}>{semanticRefreshing()}</text>
690
- </box>
691
- )}
692
- {/* When loading, magic-context-style progress hint helps users see
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
- {s()?.semantic_index?.status === "loading" &&
695
- s()?.semantic_index?.entries_total != null &&
696
- s()!.semantic_index.entries_total! > 0 && (
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="Progress"
700
- value={`${formatCount(s()!.semantic_index.entries_done)} / ${formatCount(
701
- s()!.semantic_index.entries_total,
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(api: TuiPluginApi, pluginVersion: string): TuiSlotPlugin {
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
- // 150 matches magic-context's order chosen so AFT renders below
808
- // higher-priority panels but above default plugin slots. If both
809
- // plugins are loaded together, magic-context will appear first.
810
- order: 160,
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);