@cortexkit/aft-opencode 0.56.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/bg-notifications.d.ts +1 -0
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/config.d.ts +2 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/entry/server.js +2682 -282
  6. package/dist/index.js +448 -244
  7. package/dist/lsp-github-install.d.ts.map +1 -1
  8. package/dist/permissions/v2-service.d.ts +30 -0
  9. package/dist/permissions/v2-service.d.ts.map +1 -0
  10. package/dist/permissions/v2.d.ts +121 -9
  11. package/dist/permissions/v2.d.ts.map +1 -1
  12. package/dist/shared/rpc-server.d.ts.map +1 -1
  13. package/dist/shared/status.d.ts +8 -1
  14. package/dist/shared/status.d.ts.map +1 -1
  15. package/dist/subc-tool-schemas.d.ts +11 -0
  16. package/dist/subc-tool-schemas.d.ts.map +1 -1
  17. package/dist/tool-registration.d.ts.map +1 -1
  18. package/dist/tools/definitions/v2.d.ts +6 -0
  19. package/dist/tools/definitions/v2.d.ts.map +1 -1
  20. package/dist/tools/hoisted/v2.d.ts +12 -2
  21. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  22. package/dist/tools/hoisted.d.ts.map +1 -1
  23. package/dist/tools/semantic.d.ts.map +1 -1
  24. package/dist/tui/v2-status.d.ts +8 -0
  25. package/dist/tui/v2-status.d.ts.map +1 -1
  26. package/index.js +14 -0
  27. package/package.json +14 -10
  28. package/server.js +8 -0
  29. package/src/entry/server-runtime.mjs +11 -0
  30. package/src/shared/rpc-server.ts +5 -0
  31. package/src/shared/status.ts +16 -9
  32. package/src/tui/index.tsx +1 -0
  33. package/src/tui/sidebar-view.tsx +722 -0
  34. package/src/tui/sidebar.tsx +42 -560
  35. package/src/tui/v2-status.ts +8 -1
  36. package/src/tui/v2.tsx +173 -30
  37. package/src/tui-compiled/index.tsx +1 -1
  38. package/src/tui-compiled/sidebar-view.tsx +815 -0
  39. package/src/tui-compiled/sidebar.tsx +45 -693
  40. package/src/tui-compiled/v2-status.ts +8 -1
  41. package/src/tui-compiled/v2.tsx +163 -54
  42. package/tui.js +7 -0
package/src/tui/v2.tsx CHANGED
@@ -2,13 +2,20 @@
2
2
  // @ts-nocheck
3
3
 
4
4
  import { createEffect, createSignal, onCleanup } from "solid-js";
5
+ import { version as packageVersion } from "../../package.json";
5
6
  import {
6
7
  type AftStatusSnapshot,
7
8
  coerceAftStatus,
8
9
  formatStatusDialogMessage,
9
10
  } from "../shared/status";
10
11
  import { AftRpc } from "./aft-rpc";
11
- import { formatAftStatusSegment, summarizeAftSidebar } from "./v2-status";
12
+ import {
13
+ AftSidebarPanel,
14
+ createSidebarPreferences,
15
+ type SidebarColor,
16
+ type SidebarPalette,
17
+ } from "./sidebar-view";
18
+ import { formatAftStatusSegment } from "./v2-status";
12
19
 
13
20
  type AftRpcClient = {
14
21
  getStatus(
@@ -24,9 +31,57 @@ type AftRpcClient = {
24
31
  };
25
32
  };
26
33
 
34
+ /**
35
+ * The colour tokens the sidebar needs out of the host's `ResolvedTheme` — the
36
+ * type `Context.theme` carries, exported by `@opencode/theme/tui`. That
37
+ * package is an optional peer of `@opencode/plugin` and is not installed
38
+ * alongside this plugin, so the shape is written out here instead of imported;
39
+ * the key names are the theme package's own. They are nested token groups,
40
+ * unlike the flat colour names the slot-plugin host in ./sidebar.tsx reads.
41
+ * Every level is optional so a host that ships a partial theme degrades to the
42
+ * fallback palette below instead of throwing during a render.
43
+ *
44
+ * What a host really hands over is recorded, token for token, in
45
+ * test/load-matrix/ga-host-theme.json; that record is what this type and the
46
+ * mapping below are written against, and the load matrix compares it to a live
47
+ * host on every run.
48
+ */
49
+ type GaFeedbackKind = "error" | "warning" | "success" | "info";
50
+
51
+ /**
52
+ * One step of a hue scale. The steps are ordered by how far the shade stands
53
+ * off the page rather than by lightness: 100 contrasts with the page most and
54
+ * 900 blends into it, in both theme modes. So the host's dark theme resolves
55
+ * accent 100 to its lightest purple and its light theme resolves accent 100 to
56
+ * its darkest orange, and a step chosen for one mode keeps its role in the
57
+ * other.
58
+ */
59
+ type GaHueStep = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
60
+
61
+ type GaResolvedTheme = {
62
+ hue?: { accent?: Partial<Record<GaHueStep, SidebarColor>> };
63
+ text?: {
64
+ base?: SidebarColor;
65
+ muted?: SidebarColor;
66
+ feedback?: Partial<Record<GaFeedbackKind, { base?: SidebarColor }>>;
67
+ };
68
+ background?: { base?: SidebarColor };
69
+ border?: { base?: SidebarColor };
70
+ };
71
+
72
+ /**
73
+ * The accent shade the AFT badge is filled with. A low step is what keeps the
74
+ * fill standing off the page in both theme modes (see GaHueStep), and 200 is
75
+ * the step the host itself draws its own accent-coloured UI in.
76
+ */
77
+ const BADGE_ACCENT_STEP: GaHueStep = "200";
78
+
27
79
  type V2TuiContext = {
28
80
  location?: unknown;
29
81
  client: { rpc(definition: typeof AftRpc): AftRpcClient };
82
+ /** Resolved for the active theme mode by the host; see GaResolvedTheme. */
83
+ theme?: GaResolvedTheme;
84
+ themeMode?: "dark" | "light";
30
85
  keymap: {
31
86
  layer(input: () => { commands: Array<Record<string, unknown>>; bindings: string[] }): void;
32
87
  };
@@ -34,12 +89,76 @@ type V2TuiContext = {
34
89
  dialog: { alert(input: { title: string; message: string }): Promise<void> };
35
90
  router: { current(): { type: string; sessionID?: string } };
36
91
  slot(claim: {
37
- append: "prompt.footer.status" | "sidebar.content";
92
+ append: "app" | "prompt.footer.status" | "sidebar.content";
38
93
  render(input: { sessionID?: string }): unknown;
39
94
  }): () => void;
40
95
  };
41
96
  };
42
97
 
98
+ // Last resort for a host that declares a theme mode but hands over no resolved
99
+ // theme. Inheriting the terminal default instead is what made the panel white
100
+ // on white in light mode, so there has to be a legible colour for every role
101
+ // even when the theme is missing.
102
+ const FALLBACK_PALETTES: Record<"dark" | "light", SidebarPalette> = {
103
+ dark: {
104
+ text: "#e6edf3",
105
+ textMuted: "#9198a1",
106
+ success: "#3fb950",
107
+ warning: "#d29922",
108
+ error: "#f85149",
109
+ accent: "#1f6feb",
110
+ background: "#0d1117",
111
+ border: "#3d444d",
112
+ },
113
+ light: {
114
+ text: "#1f2328",
115
+ textMuted: "#59636e",
116
+ success: "#1a7f37",
117
+ warning: "#9a6700",
118
+ error: "#cf222e",
119
+ accent: "#0969da",
120
+ background: "#ffffff",
121
+ border: "#d1d9e0",
122
+ },
123
+ };
124
+
125
+ /**
126
+ * Host theme → the panel's palette. The host resolves its theme for the active
127
+ * mode before handing it over, so light mode arrives as light colours here;
128
+ * what matters is that every text line asks for one of them instead of
129
+ * inheriting the terminal default.
130
+ *
131
+ * `success` falls back to the accent exactly as ./sidebar.tsx's mapping does.
132
+ */
133
+ export function resolveV2Palette(
134
+ theme: GaResolvedTheme | undefined,
135
+ themeMode: "dark" | "light" | undefined,
136
+ ): SidebarPalette {
137
+ const fallback = FALLBACK_PALETTES[themeMode === "light" ? "light" : "dark"];
138
+ if (!theme) return { ...fallback };
139
+
140
+ // The accent comes out of the hue scales, not out of `background.action`.
141
+ // The action groups describe how the host paints a button, and this theme
142
+ // paints one as bare text: its `background.action.primary.base` is a fully
143
+ // transparent colour. Filling the badge with that drew no badge at all, and
144
+ // nothing caught it, because a present-but-transparent colour never reaches
145
+ // the fallback below.
146
+ const accent = theme.hue?.accent?.[BADGE_ACCENT_STEP] ?? fallback.accent;
147
+ const feedback = theme.text?.feedback;
148
+ return {
149
+ text: theme.text?.base ?? fallback.text,
150
+ textMuted: theme.text?.muted ?? fallback.textMuted,
151
+ success: feedback?.success?.base ?? accent,
152
+ warning: feedback?.warning?.base ?? fallback.warning,
153
+ error: feedback?.error?.base ?? fallback.error,
154
+ accent,
155
+ background: theme.background?.base ?? fallback.background,
156
+ // This theme has one border colour; ./sidebar.tsx's host distinguishes an
157
+ // active one, and the panel only ever draws the active border.
158
+ border: theme.border?.base ?? fallback.border,
159
+ };
160
+ }
161
+
43
162
  function eventMatchesSession(
44
163
  eventSessionID: string | undefined,
45
164
  sessionID: string | undefined,
@@ -125,17 +244,16 @@ function FooterStatus(props: { context: V2TuiContext; rpc: AftRpcClient; session
125
244
 
126
245
  function SidebarStatus(props: { context: V2TuiContext; rpc: AftRpcClient; sessionID: string }) {
127
246
  const status = useAftStatus(props.context, props.rpc, () => props.sessionID);
128
- const summary = () => summarizeAftSidebar(status());
247
+ const preferences = createSidebarPreferences();
129
248
  return (
130
- <box width="100%" flexDirection="column">
131
- <text>
132
- <b>{summary().title}</b>
133
- {summary().version ? ` v${summary().version}` : ""}
134
- </text>
135
- <text>Search: {summary().search}</text>
136
- <text>Semantic: {summary().semantic}</text>
137
- <text>Health: {summary().health}</text>
138
- </box>
249
+ <AftSidebarPanel
250
+ palette={resolveV2Palette(props.context.theme, props.context.themeMode)}
251
+ snapshot={status()}
252
+ prefs={preferences.prefs()}
253
+ collapsed={preferences.collapsed()}
254
+ onToggleCollapsed={preferences.toggleCollapsed}
255
+ pluginVersion={packageVersion}
256
+ />
139
257
  );
140
258
  }
141
259
 
@@ -163,10 +281,53 @@ async function showStatusDialog(
163
281
  }
164
282
  }
165
283
 
284
+ // `keymap.layer` is a Solid primitive, not a plain registration call: it reads
285
+ // the host's keymap context and creates a layer owned by the component that
286
+ // calls it. The host hands that primitive to plugins unbound, and it runs the
287
+ // plugin's `setup` from a detached async continuation, outside both the Solid
288
+ // owner and the provider tree — so calling it from `setup` throws
289
+ // "Keymap.Provider is missing" and takes the whole TUI feature down with it.
290
+ // Rendering this component instead puts the call inside the provider tree,
291
+ // which is where the contract allows it. The component renders nothing; it
292
+ // exists only to own the layer, and is mounted through the `app` slot below.
293
+ function AftStatusCommands(props: { context: V2TuiContext; rpc: AftRpcClient }) {
294
+ props.context.keymap.layer(() => ({
295
+ // A layer without a mode is limited to the host's base input mode, and the
296
+ // command palette runs as a dialog, which switches the mode away from base.
297
+ // "global" opts out of that limit so the palette entry stays reachable
298
+ // while the palette itself is open.
299
+ mode: "global",
300
+ commands: [
301
+ {
302
+ id: "aft.status",
303
+ title: "AFT: Status",
304
+ description: "Show AFT status, index health, and cache usage",
305
+ group: "AFT",
306
+ palette: true,
307
+ // The host dispatches slash entries unless `arguments: true` is set;
308
+ // omitting it prevents this text from remaining in the prompt or being
309
+ // submitted to the model.
310
+ slash: { name: "aft-status" },
311
+ run: () => showStatusDialog(props.context, props.rpc, activeSessionID(props.context)),
312
+ },
313
+ ],
314
+ bindings: [],
315
+ }));
316
+ return null;
317
+ }
318
+
166
319
  export async function setupV2Tui(context: V2TuiContext): Promise<() => void> {
167
320
  const rpc = context.client.rpc(AftRpc);
168
321
  const controller = new AbortController();
169
322
  const slotCleanups = [
323
+ // `app` is the one slot the host keeps mounted for the whole TUI session,
324
+ // independent of the current route, so a command owned by a component
325
+ // rendered here stays reachable everywhere the palette and slash
326
+ // completion are.
327
+ context.ui.slot({
328
+ append: "app",
329
+ render: () => <AftStatusCommands context={context} rpc={rpc} />,
330
+ }),
170
331
  context.ui.slot({
171
332
  append: "prompt.footer.status",
172
333
  render: (input) => <FooterStatus context={context} rpc={rpc} sessionID={input.sessionID} />,
@@ -183,24 +344,6 @@ export async function setupV2Tui(context: V2TuiContext): Promise<() => void> {
183
344
  { signal: controller.signal },
184
345
  );
185
346
 
186
- context.keymap.layer(() => ({
187
- commands: [
188
- {
189
- id: "aft.status",
190
- title: "AFT: Status",
191
- description: "Show AFT status, index health, and cache usage",
192
- group: "AFT",
193
- palette: true,
194
- // V2's KeymapCommand contract (plugin/tui/context.ts:379-385) dispatches
195
- // slash entries unless `arguments: true`; omitting it prevents this text
196
- // from remaining in the prompt or being submitted to the model.
197
- slash: { name: "aft-status" },
198
- run: () => showStatusDialog(context, rpc, activeSessionID(context)),
199
- },
200
- ],
201
- bindings: [],
202
- }));
203
-
204
347
  return () => {
205
348
  controller.abort();
206
349
  stopDialogEvents();
@@ -467,7 +467,7 @@ const StatusDialog = props => {
467
467
  },
468
468
  label: "Status",
469
469
  get value() {
470
- return formatSemanticIndexStatus(status().semantic_index.status, status().semantic_index.stage);
470
+ return formatSemanticIndexStatus(status().semantic_index.status, status().semantic_index.stage, status().semantic_index.error);
471
471
  },
472
472
  get tone() {
473
473
  return statusTone(status().semantic_index.status);