@cortexkit/aft-opencode 0.33.0 → 0.35.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.
@@ -142,6 +142,43 @@ const SectionHeader = (props: { theme: TuiThemeCurrent; title: string; marginTop
142
142
  </box>
143
143
  );
144
144
 
145
+ // Map a status tone to a theme color — used for the collapsed-view status dots.
146
+ function toneColor(theme: TuiThemeCurrent, tone: "ok" | "warn" | "err" | "muted"): string {
147
+ switch (tone) {
148
+ case "ok":
149
+ return theme.success ?? theme.accent;
150
+ case "warn":
151
+ return theme.warning;
152
+ case "err":
153
+ return theme.error;
154
+ default:
155
+ return theme.textMuted;
156
+ }
157
+ }
158
+
159
+ // Collapsed-view row: label on the left, a status dot (or compact value) on the
160
+ // right. Mirrors the expanded StatRow layout so the columns line up.
161
+ const CollapsedRow = (props: { theme: TuiThemeCurrent; label: string; children: JSX.Element }) => (
162
+ <box width="100%" flexDirection="row" justifyContent="space-between">
163
+ <text fg={props.theme.textMuted}>{props.label}</text>
164
+ {props.children}
165
+ </box>
166
+ );
167
+
168
+ // Compact "saved / ratio" string for the collapsed Compression row — e.g.
169
+ // "7.6M / 64%". Uses the local `formatCount` (not the aft-bridge token
170
+ // formatter) so the TUI bundle doesn't pull the bridge barrel, which exports
171
+ // URL-fetch helpers unsuitable for Bun's TUI runtime. Returns null when no
172
+ // compression has been recorded yet.
173
+ export function collapsedCompressionValue(
174
+ compression: StatusCompression | undefined,
175
+ ): string | null {
176
+ if (!compression || compression.project.events <= 0) return null;
177
+ const { savings_tokens, original_tokens } = compression.project;
178
+ const pct = original_tokens > 0 ? Math.round((savings_tokens / original_tokens) * 100) : 0;
179
+ return `${formatCount(savings_tokens)} / ${pct}%`;
180
+ }
181
+
145
182
  // v0.27 moved AFT storage to the CortexKit root. TUI code must use a
146
183
  // lightweight local path helper rather than the shared bridge barrel, which
147
184
  // also exports URL-fetch helpers unsuitable for Bun's TUI runtime.
@@ -177,6 +214,22 @@ export function scopedSidebarSnapshot(
177
214
  return scoped.snapshot;
178
215
  }
179
216
 
217
+ /**
218
+ * Stale-while-revalidate guard. A transient `not_initialized` snapshot (bridge
219
+ * mid-respawn after a binary swap, or a momentary session-dir key miss) arrives
220
+ * over RPC as `success: true`, so a naive `setStatus` would overwrite a good
221
+ * snapshot and collapse the panel to the lazy-bridge placeholder — the blank
222
+ * flicker that recovers on the next poll. Suppress the downgrade only when we
223
+ * already hold initialized data for the same context; never blocks the first
224
+ * real snapshot, and a genuine context switch clears separately.
225
+ */
226
+ export function shouldSuppressUninitializedDowngrade(
227
+ incomingCacheRole: string | undefined,
228
+ haveInitializedForContext: boolean,
229
+ ): boolean {
230
+ return incomingCacheRole === "not_initialized" && haveInitializedForContext;
231
+ }
232
+
180
233
  const SidebarContent = (props: {
181
234
  api: TuiPluginApi;
182
235
  sessionID: () => string;
@@ -184,6 +237,9 @@ const SidebarContent = (props: {
184
237
  pluginVersion: string;
185
238
  }) => {
186
239
  const [status, setStatus] = createSignal<ScopedSidebarStatus | null>(null);
240
+ // Collapsed/expanded toggle — local UI state (not persisted), mirroring
241
+ // OpenCode's native MCP sidebar section. Default expanded.
242
+ const [collapsed, setCollapsed] = createSignal(false);
187
243
  let inflight: {
188
244
  controller: AbortController;
189
245
  generation: number;
@@ -251,6 +307,16 @@ const SidebarContent = (props: {
251
307
  if (currentDirectory() !== directory || props.sessionID() !== sid) return;
252
308
  if (response && (response as Record<string, unknown>).success !== false) {
253
309
  const snapshot = coerceAftStatus(response as Record<string, unknown>);
310
+ // Stale-while-revalidate: keep the last-good snapshot instead of
311
+ // flickering to the lazy-bridge placeholder on a transient
312
+ // not_initialized. See shouldSuppressUninitializedDowngrade.
313
+ const current = status();
314
+ const haveGoodForContext =
315
+ current !== null &&
316
+ current.directory === directory &&
317
+ current.sessionID === sid &&
318
+ current.snapshot.cache_role !== "not_initialized";
319
+ if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext)) return;
254
320
  setStatus({ directory, sessionID: sid, snapshot });
255
321
  requestRender();
256
322
  }
@@ -394,12 +460,27 @@ const SidebarContent = (props: {
394
460
  paddingLeft={1}
395
461
  paddingRight={1}
396
462
  >
397
- {/* Header: AFT badge + binary version + degraded badge (when active) */}
398
- <box flexDirection="row" justifyContent="space-between" alignItems="center">
463
+ {/* Header: triangle toggle + AFT badge + binary version + degraded badge.
464
+ Clicking the header row collapses/expands the panel (mirrors OpenCode's
465
+ native MCP sidebar section). Only interactive once initialized — the
466
+ lazy-bridge placeholder has nothing to collapse. */}
467
+ <box
468
+ flexDirection="row"
469
+ justifyContent="space-between"
470
+ alignItems="center"
471
+ onMouseDown={() => {
472
+ if (!notInitialized()) setCollapsed((x) => !x);
473
+ }}
474
+ >
399
475
  <box flexDirection="row" alignItems="center">
476
+ {/* Triangle lives inside the accent badge so the toggle reads as one
477
+ unit: "▶ AFT" / "▼ AFT". Hidden pre-init (nothing to collapse). */}
400
478
  <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
401
479
  <text fg={props.theme.background}>
402
- <b>AFT</b>
480
+ <b>
481
+ {notInitialized() ? "" : collapsed() ? "▶ " : "▼ "}
482
+ AFT
483
+ </b>
403
484
  </text>
404
485
  </box>
405
486
  {s()?.degraded && (
@@ -445,8 +526,29 @@ const SidebarContent = (props: {
445
526
  </box>
446
527
  )}
447
528
 
529
+ {/* Collapsed view — condensed status dots + compact compression. Shown
530
+ only when initialized AND collapsed. Three rows mirroring the section
531
+ order of the expanded grid. */}
532
+ {!notInitialized() && collapsed() && (
533
+ <box width="100%" flexDirection="column">
534
+ <CollapsedRow theme={props.theme} label="Search Index">
535
+ <text fg={toneColor(props.theme, searchStatus().tone)}>●</text>
536
+ </CollapsedRow>
537
+ <CollapsedRow theme={props.theme} label="Semantic Index">
538
+ <text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
539
+ </CollapsedRow>
540
+ {collapsedCompressionValue(s()?.compression) && (
541
+ <CollapsedRow theme={props.theme} label="Compression">
542
+ <text fg={props.theme.textMuted}>
543
+ <b>{collapsedCompressionValue(s()?.compression)}</b>
544
+ </text>
545
+ </CollapsedRow>
546
+ )}
547
+ </box>
548
+ )}
549
+
448
550
  {/* Search index */}
449
- {!notInitialized() && (
551
+ {!notInitialized() && !collapsed() && (
450
552
  <>
451
553
  <SectionHeader theme={props.theme} title="Search Index" />
452
554
  <StatRow