@dylanrussell/agent-router 2.0.1 → 2.0.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/tui.d.ts CHANGED
@@ -1,5 +1,41 @@
1
1
  import { Context } from '@opencode/plugin/tui/context';
2
2
 
3
+ /**
4
+ * Snapshot + polling layer for the TUI sidebar.
5
+ *
6
+ * Polling (not fs.watch): agent-router writes state.json atomically via
7
+ * temp-file + rename, and Bun's fs.watch drops the rename-to-target event
8
+ * (verified: Bun 1.3.14 reports only the temp file, Node reports both), so a
9
+ * watcher filtered on "state.json" never fires inside opencode. A 1.5s poll
10
+ * of two tiny reads is the reliable alternative.
11
+ */
12
+
13
+ interface ModelAssignment {
14
+ readonly model: string;
15
+ readonly variant?: string | null | undefined;
16
+ }
17
+
18
+ /**
19
+ * Pure view model for the sidebar — no opentui imports, fully unit-testable.
20
+ * `materialize` in render.ts turns these nodes into real opentui elements.
21
+ */
22
+
23
+ interface SidebarTheme {
24
+ readonly text?: unknown;
25
+ readonly textMuted?: unknown;
26
+ readonly warning?: unknown;
27
+ readonly success?: unknown;
28
+ }
29
+ interface SidebarContext {
30
+ /** Active stack when the TUI booted — differing means a restart is due. */
31
+ readonly bootActive: string | null;
32
+ readonly theme?: SidebarTheme | undefined;
33
+ /** Only the agent/model from this sidebar's live session may be marked current. */
34
+ readonly current?: (ModelAssignment & {
35
+ readonly agent: string;
36
+ }) | undefined;
37
+ }
38
+
3
39
  /**
4
40
  * Structural slice of opencode's TuiPluginApi.
5
41
  *
@@ -54,7 +90,7 @@ interface RouterTuiApi {
54
90
  readonly slots: {
55
91
  register(plugin: {
56
92
  order?: number;
57
- slots: Record<string, () => unknown>;
93
+ slots: Record<string, (sessionID?: string) => unknown>;
58
94
  }): unknown;
59
95
  };
60
96
  readonly renderer: {
@@ -78,6 +114,7 @@ interface RouterTuiApi {
78
114
  readonly state?: {
79
115
  readonly provider?: unknown;
80
116
  };
117
+ readonly currentModel?: (sessionID: string) => SidebarContext["current"];
81
118
  }
82
119
 
83
120
  /**
package/dist/tui.js CHANGED
@@ -15863,7 +15863,14 @@ function restartRequired(snapshot, ctx) {
15863
15863
  return snapshot.active !== ctx.bootActive;
15864
15864
  }
15865
15865
  function modelLabel(assignment) {
15866
- return `${assignment.model}${assignment.variant ? ` [${assignment.variant}]` : ""}`;
15866
+ const variant = normalizeVariant(assignment.variant);
15867
+ return `${assignment.model}${variant ? ` [${variant}]` : ""}`;
15868
+ }
15869
+ function normalizeVariant(variant) {
15870
+ return variant && variant !== "default" ? variant : void 0;
15871
+ }
15872
+ function sameModel(a, b) {
15873
+ return a.model === b.model && normalizeVariant(a.variant) === normalizeVariant(b.variant);
15867
15874
  }
15868
15875
  function buildSidebarNodes(snapshot, ctx) {
15869
15876
  const theme = ctx.theme ?? {};
@@ -15880,32 +15887,38 @@ function buildSidebarNodes(snapshot, ctx) {
15880
15887
  );
15881
15888
  }
15882
15889
  }
15883
- nodes.push(text("Current Stack", { fg: theme.text, attributes: TEXT_ATTR_BOLD, marginTop: 1 }));
15890
+ nodes.push(
15891
+ text(`Current Stack \xB7 ${snapshot.active ?? "(none)"}`, {
15892
+ fg: theme.text,
15893
+ attributes: TEXT_ATTR_BOLD,
15894
+ marginTop: 1
15895
+ })
15896
+ );
15884
15897
  if (snapshot.agents.length === 0) {
15885
15898
  nodes.push(text("\u2022 (none)", { fg: theme.textMuted }));
15886
15899
  } else {
15887
- nodes.push(text("Configured routing", { fg: theme.textMuted }));
15900
+ nodes.push(text("Precedence \u2193 \xB7 \u25CF current", { fg: theme.textMuted }));
15888
15901
  for (const assignment of snapshot.agents) {
15902
+ const chain = [assignment, ...assignment.fallbacks ?? []];
15903
+ const current = ctx.current?.agent === assignment.agent ? ctx.current : void 0;
15904
+ const selected = current ? chain.findIndex((model) => sameModel(model, current)) : -1;
15905
+ const modelRow = (model, active, external = false) => text(`${active ? "\u25CF " : " "}${external ? "Current \xB7 " : ""}${modelLabel(model)}`, {
15906
+ fg: active ? theme.success : theme.textMuted,
15907
+ attributes: active ? TEXT_ATTR_BOLD : 0,
15908
+ wrapMode: "char",
15909
+ width: "100%"
15910
+ });
15889
15911
  nodes.push({
15890
15912
  kind: "box",
15891
- props: { flexDirection: "column" },
15913
+ props: { flexDirection: "column", marginTop: 1 },
15892
15914
  children: [
15893
- {
15894
- kind: "box",
15895
- props: { flexDirection: "row", gap: 1 },
15896
- children: [
15897
- text("\u2022", { flexShrink: 0, style: { fg: theme.success } }),
15898
- text(assignment.agent, { fg: theme.text, attributes: TEXT_ATTR_BOLD })
15899
- ]
15900
- },
15915
+ text(assignment.agent, { fg: theme.text, attributes: TEXT_ATTR_BOLD }),
15901
15916
  {
15902
15917
  kind: "box",
15903
15918
  props: { flexDirection: "column", paddingLeft: 2 },
15904
15919
  children: [
15905
- text(`Primary \u2192 ${modelLabel(assignment)}`, { fg: theme.textMuted }),
15906
- ...(assignment.fallbacks ?? []).map(
15907
- (fallback, index) => text(`Fallback ${index + 1} \u2192 ${modelLabel(fallback)}`, { fg: theme.textMuted })
15908
- )
15920
+ ...chain.map((model, index) => modelRow(model, index === selected)),
15921
+ ...current && selected === -1 ? [modelRow(current, true, true)] : []
15909
15922
  ]
15910
15923
  }
15911
15924
  ]
@@ -16005,12 +16018,16 @@ var tui = async (api) => {
16005
16018
  if (!paths) return;
16006
16019
  let snapshot = await readStackSnapshot(paths);
16007
16020
  const bootActive = snapshot.active;
16008
- const viewContext = () => ({ bootActive, theme: api.theme?.current });
16021
+ const viewContext = (sessionID) => ({
16022
+ bootActive,
16023
+ theme: api.theme?.current,
16024
+ current: sessionID ? api.currentModel?.(sessionID) : void 0
16025
+ });
16009
16026
  try {
16010
16027
  api.slots.register({
16011
16028
  order: SIDEBAR_ORDER,
16012
16029
  slots: {
16013
- sidebar_content: () => materialize(buildSidebarNodes(snapshot, viewContext()), solid)
16030
+ sidebar_content: (sessionID) => materialize(buildSidebarNodes(snapshot, viewContext(sessionID)), solid)
16014
16031
  }
16015
16032
  });
16016
16033
  debugLog("slots.register ok");
@@ -16074,12 +16091,12 @@ var agentRouterTui = {
16074
16091
  cleanups.push(
16075
16092
  ctx.ui.slot({
16076
16093
  append: "sidebar.content",
16077
- render: () => {
16094
+ render: (input) => {
16078
16095
  const node = solid.createElement("box");
16079
16096
  solid.setProp(node, "flexDirection", "column");
16080
16097
  solid.insert(node, () => {
16081
16098
  void revision.value;
16082
- return render();
16099
+ return render(input.sessionID);
16083
16100
  });
16084
16101
  return node;
16085
16102
  }
@@ -16159,6 +16176,25 @@ var agentRouterTui = {
16159
16176
  )
16160
16177
  },
16161
16178
  lifecycle: { onDispose: (cleanup) => cleanups.push(cleanup) },
16179
+ theme: {
16180
+ get current() {
16181
+ return {
16182
+ text: ctx.theme.text.default,
16183
+ textMuted: ctx.theme.text.subdued,
16184
+ warning: ctx.theme.text.feedback.warning.default,
16185
+ success: ctx.theme.text.feedback.success.default
16186
+ };
16187
+ }
16188
+ },
16189
+ currentModel: (sessionID) => {
16190
+ const session = ctx.data.session.get(sessionID);
16191
+ if (!session?.agent || !session.model) return void 0;
16192
+ return {
16193
+ agent: session.agent,
16194
+ model: `${session.model.providerID}/${session.model.id}`,
16195
+ variant: session.model.variant ?? null
16196
+ };
16197
+ },
16162
16198
  state: {
16163
16199
  get provider() {
16164
16200
  const models = ctx.data.location.model.list(ctx.location) ?? [];