@thelioo/opencode-balancer 0.1.6 → 0.2.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 (203) hide show
  1. package/INSTALL.txt +49 -21
  2. package/README.md +92 -50
  3. package/dist/core/accounts.ts +404 -0
  4. package/dist/core/database.ts +67 -0
  5. package/dist/core/events.ts +75 -0
  6. package/dist/core/native-auth-suppression.ts +36 -0
  7. package/dist/core/native-connect.ts +31 -0
  8. package/dist/core/path.ts +34 -0
  9. package/dist/core/pending.ts +351 -0
  10. package/dist/core/priority.ts +193 -0
  11. package/dist/core/schema.ts +439 -0
  12. package/dist/core/time.ts +3 -0
  13. package/dist/core/types.ts +72 -0
  14. package/dist/core/usage/index.ts +23 -0
  15. package/dist/core/usage/providers/copilot.ts +243 -0
  16. package/dist/core/usage/providers/openai.ts +179 -0
  17. package/dist/core/usage/redact.ts +80 -0
  18. package/dist/core/usage/store.ts +66 -0
  19. package/dist/core/usage/types.ts +24 -0
  20. package/dist/index.js +173 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/auth-watcher.ts +318 -0
  23. package/dist/server/commands.ts +58 -0
  24. package/dist/server/fetch-patch.ts +162 -0
  25. package/dist/server/index.ts +134 -0
  26. package/dist/server/native.ts +49 -0
  27. package/dist/server/request-balancer.ts +67 -0
  28. package/dist/tui/actions.ts +176 -108
  29. package/dist/tui/balancer-bar-sync.ts +55 -45
  30. package/dist/tui/components/alias-dialog.tsx +71 -56
  31. package/dist/tui/components/dashboard.tsx +530 -358
  32. package/dist/tui/components/priority-screen.tsx +389 -266
  33. package/dist/tui/components/provider-model-dialog.tsx +72 -55
  34. package/dist/tui/components/rename-dialog.tsx +35 -28
  35. package/dist/tui/components/sidebar.tsx +103 -79
  36. package/dist/tui/components/status-indicator.tsx +78 -59
  37. package/dist/tui/components/usage-bar.tsx +18 -7
  38. package/dist/tui/components/usage-display.tsx +32 -16
  39. package/dist/tui/connect.ts +105 -72
  40. package/dist/tui/dashboard-keys.ts +53 -41
  41. package/dist/tui/native-model-apply.ts +45 -36
  42. package/dist/tui/priority-keys.ts +44 -36
  43. package/dist/tui/provider-models.ts +32 -25
  44. package/dist/tui/responsive.ts +10 -7
  45. package/dist/tui/selected-account-bar-sync.ts +32 -0
  46. package/dist/tui/selection-colors.ts +38 -30
  47. package/dist/tui/state.ts +61 -44
  48. package/dist/tui/status-format.ts +24 -20
  49. package/dist/tui/tui.js +165 -120
  50. package/dist/tui/tui.js.map +1 -1
  51. package/dist/tui/tui.tsx +199 -117
  52. package/dist/tui/usage-auto-refresh.ts +52 -45
  53. package/dist/tui/usage-format.ts +9 -9
  54. package/package.json +61 -52
  55. package/dist/balancer/accounts.d.ts +0 -9
  56. package/dist/balancer/accounts.js +0 -102
  57. package/dist/balancer/accounts.js.map +0 -1
  58. package/dist/balancer/auth-watcher.d.ts +0 -1
  59. package/dist/balancer/auth-watcher.js +0 -30
  60. package/dist/balancer/auth-watcher.js.map +0 -1
  61. package/dist/balancer/commands.d.ts +0 -1
  62. package/dist/balancer/commands.js +0 -94
  63. package/dist/balancer/commands.js.map +0 -1
  64. package/dist/balancer/core.d.ts +0 -6
  65. package/dist/balancer/core.js +0 -7
  66. package/dist/balancer/core.js.map +0 -1
  67. package/dist/balancer/fetch-patch.d.ts +0 -1
  68. package/dist/balancer/fetch-patch.js +0 -110
  69. package/dist/balancer/fetch-patch.js.map +0 -1
  70. package/dist/balancer/http.d.ts +0 -40
  71. package/dist/balancer/http.js +0 -199
  72. package/dist/balancer/http.js.map +0 -1
  73. package/dist/balancer/native.d.ts +0 -3
  74. package/dist/balancer/native.js +0 -16
  75. package/dist/balancer/native.js.map +0 -1
  76. package/dist/balancer/state.d.ts +0 -14
  77. package/dist/balancer/state.js +0 -16
  78. package/dist/balancer/state.js.map +0 -1
  79. package/dist/balancer/storage.d.ts +0 -8
  80. package/dist/balancer/storage.js +0 -92
  81. package/dist/balancer/storage.js.map +0 -1
  82. package/dist/balancer/types.d.ts +0 -44
  83. package/dist/balancer/types.js +0 -2
  84. package/dist/balancer/types.js.map +0 -1
  85. package/dist/core/accounts.d.ts +0 -14
  86. package/dist/core/accounts.js +0 -260
  87. package/dist/core/accounts.js.map +0 -1
  88. package/dist/core/database.d.ts +0 -4
  89. package/dist/core/database.js +0 -69
  90. package/dist/core/database.js.map +0 -1
  91. package/dist/core/events.d.ts +0 -18
  92. package/dist/core/events.js +0 -39
  93. package/dist/core/events.js.map +0 -1
  94. package/dist/core/native-auth-suppression.d.ts +0 -3
  95. package/dist/core/native-auth-suppression.js +0 -19
  96. package/dist/core/native-auth-suppression.js.map +0 -1
  97. package/dist/core/native-connect.d.ts +0 -4
  98. package/dist/core/native-connect.js +0 -19
  99. package/dist/core/native-connect.js.map +0 -1
  100. package/dist/core/path.d.ts +0 -4
  101. package/dist/core/path.js +0 -26
  102. package/dist/core/path.js.map +0 -1
  103. package/dist/core/pending.d.ts +0 -9
  104. package/dist/core/pending.js +0 -237
  105. package/dist/core/pending.js.map +0 -1
  106. package/dist/core/priority.d.ts +0 -20
  107. package/dist/core/priority.js +0 -120
  108. package/dist/core/priority.js.map +0 -1
  109. package/dist/core/schema.d.ts +0 -2
  110. package/dist/core/schema.js +0 -265
  111. package/dist/core/schema.js.map +0 -1
  112. package/dist/core/time.d.ts +0 -1
  113. package/dist/core/time.js +0 -4
  114. package/dist/core/time.js.map +0 -1
  115. package/dist/core/types.d.ts +0 -59
  116. package/dist/core/types.js +0 -2
  117. package/dist/core/types.js.map +0 -1
  118. package/dist/core/usage/index.d.ts +0 -4
  119. package/dist/core/usage/index.js +0 -16
  120. package/dist/core/usage/index.js.map +0 -1
  121. package/dist/core/usage/providers/copilot.d.ts +0 -2
  122. package/dist/core/usage/providers/copilot.js +0 -169
  123. package/dist/core/usage/providers/copilot.js.map +0 -1
  124. package/dist/core/usage/providers/openai.d.ts +0 -2
  125. package/dist/core/usage/providers/openai.js +0 -133
  126. package/dist/core/usage/providers/openai.js.map +0 -1
  127. package/dist/core/usage/redact.d.ts +0 -3
  128. package/dist/core/usage/redact.js +0 -67
  129. package/dist/core/usage/redact.js.map +0 -1
  130. package/dist/core/usage/store.d.ts +0 -4
  131. package/dist/core/usage/store.js +0 -31
  132. package/dist/core/usage/store.js.map +0 -1
  133. package/dist/core/usage/types.d.ts +0 -21
  134. package/dist/core/usage/types.js +0 -2
  135. package/dist/core/usage/types.js.map +0 -1
  136. package/dist/index.d.ts +0 -5
  137. package/dist/server/auth-watcher.d.ts +0 -31
  138. package/dist/server/auth-watcher.js +0 -227
  139. package/dist/server/auth-watcher.js.map +0 -1
  140. package/dist/server/commands.d.ts +0 -2
  141. package/dist/server/commands.js +0 -46
  142. package/dist/server/commands.js.map +0 -1
  143. package/dist/server/fetch-patch.d.ts +0 -3
  144. package/dist/server/fetch-patch.js +0 -118
  145. package/dist/server/fetch-patch.js.map +0 -1
  146. package/dist/server/index.d.ts +0 -8
  147. package/dist/server/index.js +0 -94
  148. package/dist/server/index.js.map +0 -1
  149. package/dist/server/native.d.ts +0 -6
  150. package/dist/server/native.js +0 -35
  151. package/dist/server/native.js.map +0 -1
  152. package/dist/server/request-balancer.d.ts +0 -16
  153. package/dist/server/request-balancer.js +0 -43
  154. package/dist/server/request-balancer.js.map +0 -1
  155. package/dist/tui/actions.d.ts +0 -41
  156. package/dist/tui/actions.js +0 -88
  157. package/dist/tui/actions.js.map +0 -1
  158. package/dist/tui/balancer-bar-sync.d.ts +0 -19
  159. package/dist/tui/balancer-bar-sync.js +0 -45
  160. package/dist/tui/balancer-bar-sync.js.map +0 -1
  161. package/dist/tui/components/alias-dialog.d.ts +0 -4
  162. package/dist/tui/components/dashboard.d.ts +0 -12
  163. package/dist/tui/components/priority-screen.d.ts +0 -9
  164. package/dist/tui/components/provider-model-dialog.d.ts +0 -13
  165. package/dist/tui/components/rename-dialog.d.ts +0 -4
  166. package/dist/tui/components/sidebar.d.ts +0 -10
  167. package/dist/tui/components/status-indicator.d.ts +0 -9
  168. package/dist/tui/components/usage-bar.d.ts +0 -8
  169. package/dist/tui/components/usage-display.d.ts +0 -10
  170. package/dist/tui/connect.d.ts +0 -30
  171. package/dist/tui/connect.js +0 -73
  172. package/dist/tui/connect.js.map +0 -1
  173. package/dist/tui/dashboard-keys.d.ts +0 -45
  174. package/dist/tui/dashboard-keys.js +0 -44
  175. package/dist/tui/dashboard-keys.js.map +0 -1
  176. package/dist/tui/native-model-apply.d.ts +0 -21
  177. package/dist/tui/native-model-apply.js +0 -53
  178. package/dist/tui/native-model-apply.js.map +0 -1
  179. package/dist/tui/priority-keys.d.ts +0 -40
  180. package/dist/tui/priority-keys.js +0 -38
  181. package/dist/tui/priority-keys.js.map +0 -1
  182. package/dist/tui/provider-models.d.ts +0 -19
  183. package/dist/tui/provider-models.js +0 -17
  184. package/dist/tui/provider-models.js.map +0 -1
  185. package/dist/tui/responsive.d.ts +0 -9
  186. package/dist/tui/responsive.js +0 -13
  187. package/dist/tui/responsive.js.map +0 -1
  188. package/dist/tui/selection-colors.d.ts +0 -10
  189. package/dist/tui/selection-colors.js +0 -38
  190. package/dist/tui/selection-colors.js.map +0 -1
  191. package/dist/tui/state.d.ts +0 -14
  192. package/dist/tui/state.js +0 -46
  193. package/dist/tui/state.js.map +0 -1
  194. package/dist/tui/status-format.d.ts +0 -15
  195. package/dist/tui/status-format.js +0 -17
  196. package/dist/tui/status-format.js.map +0 -1
  197. package/dist/tui/tui.d.ts +0 -7
  198. package/dist/tui/usage-auto-refresh.d.ts +0 -16
  199. package/dist/tui/usage-auto-refresh.js +0 -46
  200. package/dist/tui/usage-auto-refresh.js.map +0 -1
  201. package/dist/tui/usage-format.d.ts +0 -2
  202. package/dist/tui/usage-format.js +0 -17
  203. package/dist/tui/usage-format.js.map +0 -1
@@ -1,21 +1,28 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
3
  import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
- import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
5
4
  import {
6
- getBalancingEnabled,
7
- listProviderPriority,
8
- moveProvider,
9
- resolveActiveSelection,
10
- setBalancingEnabled,
11
- setProviderEnabled,
12
- type PriorityEntry,
5
+ createMemo,
6
+ createSignal,
7
+ For,
8
+ onCleanup,
9
+ onMount,
10
+ Show,
11
+ } from "solid-js";
12
+ import {
13
+ getBalancingEnabled,
14
+ listProviderPriority,
15
+ moveProvider,
16
+ type PriorityEntry,
17
+ resolveActiveSelection,
18
+ setBalancingEnabled,
19
+ setProviderEnabled,
13
20
  } from "../../core/priority";
14
21
  import {
15
- movePriorityFocus,
16
- prioritySelectionMarker,
17
- reducePriorityKey,
18
- type PriorityFocusArea,
22
+ movePriorityFocus,
23
+ type PriorityFocusArea,
24
+ prioritySelectionMarker,
25
+ reducePriorityKey,
19
26
  } from "../priority-keys";
20
27
  import { dashboardLayoutMode } from "../responsive";
21
28
  import { selectedRowColors } from "../selection-colors";
@@ -24,279 +31,395 @@ import type { BalancerTuiState } from "../state";
24
31
  type KeyLike = { name?: string; shift?: boolean };
25
32
 
26
33
  function modelLabel(api: TuiPluginApi, entry: PriorityEntry): string {
27
- if (!entry.modelID) return "(sem modelo — Enter)";
28
- const provider = api.state.provider.find((item) => item.id === entry.providerID);
29
- const model = provider?.models?.[entry.modelID];
30
- return model?.name ?? entry.modelID;
34
+ if (!entry.modelID) return "(sem modelo — Enter)";
35
+ const provider = api.state.provider.find(
36
+ (item) => item.id === entry.providerID,
37
+ );
38
+ const model = provider?.models?.[entry.modelID];
39
+ return model?.name ?? entry.modelID;
31
40
  }
32
41
 
33
42
  export function PriorityScreen(props: {
34
- api: TuiPluginApi;
35
- state: BalancerTuiState;
36
- onBack: () => void;
37
- openModelPicker: (providerID: string) => void;
43
+ api: TuiPluginApi;
44
+ state: BalancerTuiState;
45
+ onBack: () => void;
46
+ openModelPicker: (providerID: string, onComplete?: () => void) => void;
38
47
  }) {
39
- const theme = () => props.api.theme.current;
40
- const selectedColors = () => selectedRowColors(theme());
41
- const db = props.state.db;
48
+ const theme = () => props.api.theme.current;
49
+ const selectedColors = () => selectedRowColors(theme());
50
+ const db = props.state.db;
42
51
 
43
- const [entries, setEntries] = createSignal<PriorityEntry[]>(listProviderPriority(db));
44
- const [balancing, setBalancing] = createSignal(getBalancingEnabled(db));
45
- const [cursor, setCursor] = createSignal(0);
46
- const [focusArea, setFocusArea] = createSignal<PriorityFocusArea>("content");
47
- const compact = () =>
48
- dashboardLayoutMode({
49
- width: (props.api.renderer as unknown as { width?: number }).width,
50
- height: (props.api.renderer as unknown as { height?: number }).height,
51
- }) === "compact";
52
+ const [entries, setEntries] = createSignal<PriorityEntry[]>(
53
+ listProviderPriority(db),
54
+ );
55
+ const [balancing, setBalancing] = createSignal(getBalancingEnabled(db));
56
+ const [cursor, setCursor] = createSignal(0);
57
+ const [focusArea, setFocusArea] = createSignal<PriorityFocusArea>("content");
58
+ const compact = () =>
59
+ dashboardLayoutMode({
60
+ height: (props.api.renderer as unknown as { height?: number }).height,
61
+ width: (props.api.renderer as unknown as { width?: number }).width,
62
+ }) === "compact";
52
63
 
53
- const refresh = () => {
54
- const next = listProviderPriority(db);
55
- setEntries(next);
56
- setBalancing(getBalancingEnabled(db));
57
- setCursor((value) => Math.max(0, Math.min(value, Math.max(0, next.length - 1))));
58
- };
59
- refresh();
60
- const timer = setInterval(refresh, 500);
61
- onCleanup(() => clearInterval(timer));
64
+ const refresh = () => {
65
+ const next = listProviderPriority(db);
66
+ setEntries(next);
67
+ setBalancing(getBalancingEnabled(db));
68
+ setCursor((value) =>
69
+ Math.max(0, Math.min(value, Math.max(0, next.length - 1))),
70
+ );
71
+ };
72
+ refresh();
73
+ const timer = setInterval(refresh, 500);
74
+ onCleanup(() => clearInterval(timer));
62
75
 
63
- const activeProviderID = createMemo(() => (balancing() ? resolveActiveSelection(db)?.providerID : undefined));
76
+ const activeProviderID = createMemo(() =>
77
+ balancing() ? resolveActiveSelection(db)?.providerID : undefined,
78
+ );
64
79
 
65
- const current = () => entries()[cursor()];
80
+ const current = () => entries()[cursor()];
66
81
 
67
- const clampCursor = (value: number) => Math.max(0, Math.min(value, Math.max(0, entries().length - 1)));
68
- const headerSelected = () => focusArea() === "header";
69
- const headerMarker = () =>
70
- prioritySelectionMarker({ focusedArea: focusArea(), itemArea: "header", selected: true });
71
- const rowMarker = (selected: boolean) =>
72
- prioritySelectionMarker({ focusedArea: focusArea(), itemArea: "content", selected });
82
+ const clampCursor = (value: number) =>
83
+ Math.max(0, Math.min(value, Math.max(0, entries().length - 1)));
84
+ const headerSelected = () => focusArea() === "header";
85
+ const headerMarker = () =>
86
+ prioritySelectionMarker({
87
+ focusedArea: focusArea(),
88
+ itemArea: "header",
89
+ selected: true,
90
+ });
91
+ const rowMarker = (selected: boolean) =>
92
+ prioritySelectionMarker({
93
+ focusedArea: focusArea(),
94
+ itemArea: "content",
95
+ selected,
96
+ });
73
97
 
74
- const toggleBalancing = () => {
75
- setBalancingEnabled(db, !balancing());
76
- refresh();
77
- };
98
+ const toggleBalancing = () => {
99
+ setBalancingEnabled(db, !balancing());
100
+ refresh();
101
+ };
78
102
 
79
- const toggleEnabled = (entry: PriorityEntry | undefined) => {
80
- if (!entry) return;
81
- setProviderEnabled(db, entry.providerID, !entry.enabled);
82
- refresh();
83
- };
103
+ const toggleEnabled = (entry: PriorityEntry | undefined) => {
104
+ if (!entry) return;
105
+ setProviderEnabled(db, entry.providerID, !entry.enabled);
106
+ refresh();
107
+ };
84
108
 
85
- const reorder = (direction: -1 | 1) => {
86
- const entry = current();
87
- if (!entry) return;
88
- moveProvider(db, entry.providerID, direction);
89
- refresh();
90
- setCursor((value) => clampCursor(value + direction));
91
- };
109
+ const reorder = (direction: -1 | 1) => {
110
+ const entry = current();
111
+ if (!entry) return;
112
+ moveProvider(db, entry.providerID, direction);
113
+ refresh();
114
+ setCursor((value) => clampCursor(value + direction));
115
+ };
92
116
 
93
- const handleKey = (event: KeyLike) => {
94
- const intent = reducePriorityKey({ name: event.name ?? "", shift: event.shift });
95
- switch (intent.type) {
96
- case "move-cursor":
97
- {
98
- const next = movePriorityFocus(
99
- { area: focusArea(), cursor: cursor(), rowCount: entries().length },
100
- intent.delta,
101
- );
102
- setFocusArea(next.area);
103
- setCursor(clampCursor(next.cursor));
104
- }
105
- return;
106
- case "reorder":
107
- setFocusArea("content");
108
- reorder(intent.direction);
109
- return;
110
- case "toggle-enabled":
111
- if (focusArea() === "header") return;
112
- toggleEnabled(current());
113
- return;
114
- case "open-model": {
115
- if (focusArea() === "header") return props.onBack();
116
- const entry = current();
117
- if (entry) props.openModelPicker(entry.providerID);
118
- return;
119
- }
120
- case "toggle-balancing":
121
- toggleBalancing();
122
- return;
123
- case "back":
124
- props.onBack();
125
- return;
126
- default:
127
- return;
128
- }
129
- };
117
+ const handleKey = (event: KeyLike) => {
118
+ const intent = reducePriorityKey({
119
+ name: event.name ?? "",
120
+ shift: event.shift,
121
+ });
122
+ switch (intent.type) {
123
+ case "move-cursor":
124
+ {
125
+ const next = movePriorityFocus(
126
+ { area: focusArea(), cursor: cursor(), rowCount: entries().length },
127
+ intent.delta,
128
+ );
129
+ setFocusArea(next.area);
130
+ setCursor(clampCursor(next.cursor));
131
+ }
132
+ return;
133
+ case "reorder":
134
+ setFocusArea("content");
135
+ reorder(intent.direction);
136
+ return;
137
+ case "toggle-enabled":
138
+ if (focusArea() === "header") return;
139
+ toggleEnabled(current());
140
+ return;
141
+ case "open-model": {
142
+ if (focusArea() === "header") return props.onBack();
143
+ const entry = current();
144
+ if (entry) props.openModelPicker(entry.providerID, restoreFocus);
145
+ return;
146
+ }
147
+ case "toggle-balancing":
148
+ toggleBalancing();
149
+ return;
150
+ case "back":
151
+ props.onBack();
152
+ return;
153
+ default:
154
+ return;
155
+ }
156
+ };
130
157
 
131
- let container: { focus?: () => void } | undefined;
132
- onMount(() => container?.focus?.());
158
+ let container: { focus?: () => void } | undefined;
159
+ const restoreFocus = () => queueMicrotask(() => container?.focus?.());
160
+ onMount(() => container?.focus?.());
133
161
 
134
- const Hint = (hintProps: { children: string }) => (
135
- <text fg={theme().textMuted} wrapMode="none" truncate>
136
- {hintProps.children}
137
- </text>
138
- );
162
+ const Hint = (hintProps: { children: string }) => (
163
+ <text fg={theme().textMuted} truncate wrapMode="none">
164
+ {hintProps.children}
165
+ </text>
166
+ );
139
167
 
140
- const Chip = (chipProps: { keyName: string; label: string; danger?: boolean }) => (
141
- <text fg={chipProps.danger ? theme().warning : theme().accent} wrapMode="none">
142
- [{chipProps.keyName}] <span style={{ fg: theme().textMuted }}>{chipProps.label}</span>
143
- </text>
144
- );
168
+ const Chip = (chipProps: {
169
+ keyName: string;
170
+ label: string;
171
+ danger?: boolean;
172
+ }) => (
173
+ <text
174
+ fg={chipProps.danger ? theme().warning : theme().accent}
175
+ wrapMode="none"
176
+ >
177
+ [{chipProps.keyName}]{" "}
178
+ <span style={{ fg: theme().textMuted }}>{chipProps.label}</span>
179
+ </text>
180
+ );
145
181
 
146
- const Row = (rowProps: { selected?: boolean; children: unknown; onMouseUp?: () => void }) => (
147
- <box
148
- flexDirection="row"
149
- width="100%"
150
- minWidth={0}
151
- height={1}
152
- flexShrink={0}
153
- backgroundColor={rowProps.selected ? selectedColors().bg : undefined}
154
- onMouseUp={rowProps.onMouseUp}
155
- >
156
- {rowProps.children}
157
- </box>
158
- );
182
+ const Row = (rowProps: {
183
+ selected?: boolean;
184
+ children: unknown;
185
+ onMouseUp?: () => void;
186
+ }) => (
187
+ <box
188
+ backgroundColor={rowProps.selected ? selectedColors().bg : undefined}
189
+ flexDirection="row"
190
+ flexShrink={0}
191
+ height={1}
192
+ minWidth={0}
193
+ onMouseUp={rowProps.onMouseUp}
194
+ width="100%"
195
+ >
196
+ {rowProps.children}
197
+ </box>
198
+ );
159
199
 
160
- return (
161
- <box
162
- ref={(ref: unknown) => (container = ref as { focus?: () => void })}
163
- focusable
164
- onKeyDown={(event: KeyLike) => handleKey(event)}
165
- flexDirection="column"
166
- gap={0}
167
- padding={1}
168
- width="100%"
169
- height="100%"
170
- >
171
- <box flexDirection="column" gap={0} paddingBottom={1}>
172
- <text fg={theme().primary} wrapMode="none" overflow="hidden" truncate>
173
- opencode-balancer{compact() ? "" : " priority matrix"}
174
- </text>
175
- <Show when={!compact()}>
176
- <Hint>Choose one model per provider and order failover priority.</Hint>
177
- </Show>
178
- <box
179
- height={1}
180
- flexShrink={0}
181
- backgroundColor={headerSelected() ? selectedColors().bg : undefined}
182
- onMouseUp={() => {
183
- setFocusArea("header");
184
- props.onBack();
185
- }}
186
- >
187
- <text fg={headerSelected() ? selectedColors().fg : theme().accent} wrapMode="none" overflow="hidden" truncate>
188
- {headerMarker()} [ back to dashboard ]
189
- </text>
190
- </box>
191
- </box>
200
+ return (
201
+ <box
202
+ flexDirection="column"
203
+ focusable
204
+ gap={0}
205
+ height="100%"
206
+ onKeyDown={(event: KeyLike) => handleKey(event)}
207
+ padding={1}
208
+ ref={(ref: unknown) => (container = ref as { focus?: () => void })}
209
+ width="100%"
210
+ >
211
+ <box flexDirection="column" gap={0} paddingBottom={1}>
212
+ <text fg={theme().primary} overflow="hidden" truncate wrapMode="none">
213
+ opencode-balancer{compact() ? "" : " priority matrix"}
214
+ </text>
215
+ <Show when={!compact()}>
216
+ <Hint>
217
+ Choose one model per provider and order failover priority.
218
+ </Hint>
219
+ </Show>
220
+ <box
221
+ backgroundColor={headerSelected() ? selectedColors().bg : undefined}
222
+ flexShrink={0}
223
+ height={1}
224
+ onMouseUp={() => {
225
+ setFocusArea("header");
226
+ props.onBack();
227
+ }}
228
+ >
229
+ <text
230
+ fg={headerSelected() ? selectedColors().fg : theme().accent}
231
+ overflow="hidden"
232
+ truncate
233
+ wrapMode="none"
234
+ >
235
+ {headerMarker()} [ back to dashboard ]
236
+ </text>
237
+ </box>
238
+ </box>
192
239
 
193
- <box flexDirection="column" gap={0} paddingBottom={1} onMouseUp={toggleBalancing}>
194
- <text fg={theme().primary} wrapMode="none" overflow="hidden" truncate>
195
- BALANCING {balancing() ? "ON" : "OFF"}
196
- </text>
197
- <Show when={!compact()}>
198
- <text fg={theme().textMuted} wrapMode="none" overflow="hidden" truncate>
199
- press B to toggle automatic provider failover
200
- </text>
201
- </Show>
202
- </box>
240
+ <box
241
+ flexDirection="column"
242
+ gap={0}
243
+ onMouseUp={toggleBalancing}
244
+ paddingBottom={1}
245
+ >
246
+ <text fg={theme().primary} overflow="hidden" truncate wrapMode="none">
247
+ BALANCING {balancing() ? "ON" : "OFF"}
248
+ </text>
249
+ <Show when={!compact()}>
250
+ <text
251
+ fg={theme().textMuted}
252
+ overflow="hidden"
253
+ truncate
254
+ wrapMode="none"
255
+ >
256
+ press B to toggle automatic provider failover
257
+ </text>
258
+ </Show>
259
+ </box>
203
260
 
204
- <box flexDirection="column" gap={0} paddingBottom={1}>
205
- <Show when={!compact()}>
206
- <box flexDirection="row" gap={2}>
207
- <text fg={theme().primary} wrapMode="none">
208
- # PROVIDER
209
- </text>
210
- <text fg={theme().primary} wrapMode="none">
211
- MODEL
212
- </text>
213
- <text fg={theme().primary} wrapMode="none">
214
- ENABLED
215
- </text>
216
- </box>
217
- </Show>
218
- <Show
219
- when={entries().length > 0}
220
- fallback={
221
- <text fg={theme().textMuted} wrapMode="none">
222
- nenhum provider com conta ainda
223
- </text>
224
- }
225
- >
226
- <For each={entries()}>
227
- {(entry, index) => {
228
- const selected = () => focusArea() === "content" && index() === cursor();
229
- const active = () => entry.providerID === activeProviderID();
230
- const rowColor = () => {
231
- if (!entry.enabled) return theme().textMuted;
232
- if (active()) return theme().success;
233
- return theme().text;
234
- };
235
- return (
236
- <Row
237
- selected={selected()}
238
- onMouseUp={() => {
239
- setFocusArea("content");
240
- setCursor(index());
241
- }}
242
- >
243
- <box width={1} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
244
- <text fg={selected() ? selectedColors().fg : theme().accent} wrapMode="none">
245
- {rowMarker(index() === cursor())}
246
- </text>
247
- </box>
248
- <box width={3} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
249
- <text fg={selected() ? selectedColors().fg : rowColor()} wrapMode="none">
250
- {index() + 1}.
251
- </text>
252
- </box>
253
- <box width={18} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
254
- <text fg={selected() ? selectedColors().fg : rowColor()} wrapMode="none" overflow="hidden" truncate>
255
- {entry.providerID}
256
- </text>
257
- </box>
258
- <box
259
- flexGrow={1}
260
- minWidth={0}
261
- backgroundColor={selected() ? selectedColors().bg : undefined}
262
- onMouseUp={() => props.openModelPicker(entry.providerID)}
263
- >
264
- <text fg={selected() ? selectedColors().fg : entry.modelID ? rowColor() : theme().warning} wrapMode="none" overflow="hidden" truncate>
265
- {modelLabel(props.api, entry)}
266
- </text>
267
- </box>
268
- <box
269
- width={10}
270
- flexShrink={0}
271
- backgroundColor={selected() ? selectedColors().bg : undefined}
272
- onMouseUp={() => toggleEnabled(entry)}
273
- >
274
- <text fg={selected() ? selectedColors().fg : entry.enabled ? theme().success : theme().textMuted} wrapMode="none" overflow="hidden" truncate>
275
- {entry.enabled ? "enabled" : "disabled"}
276
- </text>
277
- </box>
278
- </Row>
279
- );
280
- }}
281
- </For>
282
- </Show>
283
- </box>
261
+ <box flexDirection="column" gap={0} paddingBottom={1}>
262
+ <Show when={!compact()}>
263
+ <box flexDirection="row" gap={2}>
264
+ <text fg={theme().primary} wrapMode="none">
265
+ # PROVIDER
266
+ </text>
267
+ <text fg={theme().primary} wrapMode="none">
268
+ MODEL
269
+ </text>
270
+ <text fg={theme().primary} wrapMode="none">
271
+ ENABLED
272
+ </text>
273
+ </box>
274
+ </Show>
275
+ <Show
276
+ fallback={
277
+ <text fg={theme().textMuted} wrapMode="none">
278
+ nenhum provider com conta ainda
279
+ </text>
280
+ }
281
+ when={entries().length > 0}
282
+ >
283
+ <For each={entries()}>
284
+ {(entry, index) => {
285
+ const selected = () =>
286
+ focusArea() === "content" && index() === cursor();
287
+ const active = () => entry.providerID === activeProviderID();
288
+ const rowColor = () => {
289
+ if (!entry.enabled) return theme().textMuted;
290
+ if (active()) return theme().success;
291
+ return theme().text;
292
+ };
293
+ return (
294
+ <Row
295
+ onMouseUp={() => {
296
+ setFocusArea("content");
297
+ setCursor(index());
298
+ }}
299
+ selected={selected()}
300
+ >
301
+ <box
302
+ backgroundColor={
303
+ selected() ? selectedColors().bg : undefined
304
+ }
305
+ flexShrink={0}
306
+ width={1}
307
+ >
308
+ <text
309
+ fg={selected() ? selectedColors().fg : theme().accent}
310
+ wrapMode="none"
311
+ >
312
+ {rowMarker(index() === cursor())}
313
+ </text>
314
+ </box>
315
+ <box
316
+ backgroundColor={
317
+ selected() ? selectedColors().bg : undefined
318
+ }
319
+ flexShrink={0}
320
+ width={3}
321
+ >
322
+ <text
323
+ fg={selected() ? selectedColors().fg : rowColor()}
324
+ wrapMode="none"
325
+ >
326
+ {index() + 1}.
327
+ </text>
328
+ </box>
329
+ <box
330
+ backgroundColor={
331
+ selected() ? selectedColors().bg : undefined
332
+ }
333
+ flexShrink={0}
334
+ width={18}
335
+ >
336
+ <text
337
+ fg={selected() ? selectedColors().fg : rowColor()}
338
+ overflow="hidden"
339
+ truncate
340
+ wrapMode="none"
341
+ >
342
+ {entry.providerID}
343
+ </text>
344
+ </box>
345
+ <box
346
+ backgroundColor={
347
+ selected() ? selectedColors().bg : undefined
348
+ }
349
+ flexGrow={1}
350
+ minWidth={0}
351
+ onMouseUp={() =>
352
+ props.openModelPicker(entry.providerID, restoreFocus)
353
+ }
354
+ >
355
+ <text
356
+ fg={
357
+ selected()
358
+ ? selectedColors().fg
359
+ : entry.modelID
360
+ ? rowColor()
361
+ : theme().warning
362
+ }
363
+ overflow="hidden"
364
+ truncate
365
+ wrapMode="none"
366
+ >
367
+ {modelLabel(props.api, entry)}
368
+ </text>
369
+ </box>
370
+ <box
371
+ backgroundColor={
372
+ selected() ? selectedColors().bg : undefined
373
+ }
374
+ flexShrink={0}
375
+ onMouseUp={() => toggleEnabled(entry)}
376
+ width={10}
377
+ >
378
+ <text
379
+ fg={
380
+ selected()
381
+ ? selectedColors().fg
382
+ : entry.enabled
383
+ ? theme().success
384
+ : theme().textMuted
385
+ }
386
+ overflow="hidden"
387
+ truncate
388
+ wrapMode="none"
389
+ >
390
+ {entry.enabled ? "enabled" : "disabled"}
391
+ </text>
392
+ </box>
393
+ </Row>
394
+ );
395
+ }}
396
+ </For>
397
+ </Show>
398
+ </box>
284
399
 
285
- <box flexDirection="column" gap={0}>
286
- <Show
287
- when={!compact()}
288
- fallback={<Hint>↑↓ move · Shift+↑↓ reorder · Enter model · Space enable · Esc back</Hint>}
289
- >
290
- <box flexDirection="row" gap={2}>
291
- <Chip keyName="↑↓" label="Move" />
292
- <Chip keyName="Shift+↑↓" label="Reorder" />
293
- <Chip keyName="Enter" label="Model" />
294
- <Chip keyName="Space" label="Enable" />
295
- <Chip keyName="Esc" label="Back" />
296
- </box>
297
- <Hint>{headerSelected() ? "Enter returns to the dashboard." : "selected provider controls the next automatic failover target."}</Hint>
298
- </Show>
299
- </box>
300
- </box>
301
- );
400
+ <box flexDirection="column" gap={0}>
401
+ <Show
402
+ fallback={
403
+ <Hint>
404
+ ↑↓ move · Shift+↑↓ reorder · Enter model · Space enable · Esc back
405
+ </Hint>
406
+ }
407
+ when={!compact()}
408
+ >
409
+ <box flexDirection="row" gap={2}>
410
+ <Chip keyName="↑↓" label="Move" />
411
+ <Chip keyName="Shift+↑↓" label="Reorder" />
412
+ <Chip keyName="Enter" label="Model" />
413
+ <Chip keyName="Space" label="Enable" />
414
+ <Chip keyName="Esc" label="Back" />
415
+ </box>
416
+ <Hint>
417
+ {headerSelected()
418
+ ? "Enter returns to the dashboard."
419
+ : "selected provider controls the next automatic failover target."}
420
+ </Hint>
421
+ </Show>
422
+ </box>
423
+ </box>
424
+ );
302
425
  }