@thelioo/opencode-balancer 0.1.8 → 0.2.1

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 (176) hide show
  1. package/INSTALL.txt +53 -25
  2. package/README.md +95 -51
  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 -112
  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 -267
  33. package/dist/tui/components/provider-model-dialog.tsx +71 -64
  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 +104 -73
  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 +23 -23
  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 -153
  50. package/dist/tui/tui.js.map +1 -1
  51. package/dist/tui/tui.tsx +194 -144
  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/core/accounts.d.ts +0 -14
  56. package/dist/core/accounts.js +0 -260
  57. package/dist/core/accounts.js.map +0 -1
  58. package/dist/core/database.d.ts +0 -4
  59. package/dist/core/database.js +0 -69
  60. package/dist/core/database.js.map +0 -1
  61. package/dist/core/events.d.ts +0 -18
  62. package/dist/core/events.js +0 -39
  63. package/dist/core/events.js.map +0 -1
  64. package/dist/core/native-auth-suppression.d.ts +0 -3
  65. package/dist/core/native-auth-suppression.js +0 -19
  66. package/dist/core/native-auth-suppression.js.map +0 -1
  67. package/dist/core/native-connect.d.ts +0 -4
  68. package/dist/core/native-connect.js +0 -19
  69. package/dist/core/native-connect.js.map +0 -1
  70. package/dist/core/path.d.ts +0 -4
  71. package/dist/core/path.js +0 -26
  72. package/dist/core/path.js.map +0 -1
  73. package/dist/core/pending.d.ts +0 -9
  74. package/dist/core/pending.js +0 -237
  75. package/dist/core/pending.js.map +0 -1
  76. package/dist/core/priority.d.ts +0 -20
  77. package/dist/core/priority.js +0 -120
  78. package/dist/core/priority.js.map +0 -1
  79. package/dist/core/schema.d.ts +0 -2
  80. package/dist/core/schema.js +0 -265
  81. package/dist/core/schema.js.map +0 -1
  82. package/dist/core/time.d.ts +0 -1
  83. package/dist/core/time.js +0 -4
  84. package/dist/core/time.js.map +0 -1
  85. package/dist/core/types.d.ts +0 -59
  86. package/dist/core/types.js +0 -2
  87. package/dist/core/types.js.map +0 -1
  88. package/dist/core/usage/index.d.ts +0 -4
  89. package/dist/core/usage/index.js +0 -16
  90. package/dist/core/usage/index.js.map +0 -1
  91. package/dist/core/usage/providers/copilot.d.ts +0 -2
  92. package/dist/core/usage/providers/copilot.js +0 -169
  93. package/dist/core/usage/providers/copilot.js.map +0 -1
  94. package/dist/core/usage/providers/openai.d.ts +0 -2
  95. package/dist/core/usage/providers/openai.js +0 -133
  96. package/dist/core/usage/providers/openai.js.map +0 -1
  97. package/dist/core/usage/redact.d.ts +0 -3
  98. package/dist/core/usage/redact.js +0 -67
  99. package/dist/core/usage/redact.js.map +0 -1
  100. package/dist/core/usage/store.d.ts +0 -4
  101. package/dist/core/usage/store.js +0 -31
  102. package/dist/core/usage/store.js.map +0 -1
  103. package/dist/core/usage/types.d.ts +0 -21
  104. package/dist/core/usage/types.js +0 -2
  105. package/dist/core/usage/types.js.map +0 -1
  106. package/dist/index.d.ts +0 -5
  107. package/dist/server/auth-watcher.d.ts +0 -32
  108. package/dist/server/auth-watcher.js +0 -227
  109. package/dist/server/auth-watcher.js.map +0 -1
  110. package/dist/server/commands.d.ts +0 -2
  111. package/dist/server/commands.js +0 -46
  112. package/dist/server/commands.js.map +0 -1
  113. package/dist/server/fetch-patch.d.ts +0 -3
  114. package/dist/server/fetch-patch.js +0 -118
  115. package/dist/server/fetch-patch.js.map +0 -1
  116. package/dist/server/index.d.ts +0 -8
  117. package/dist/server/index.js +0 -94
  118. package/dist/server/index.js.map +0 -1
  119. package/dist/server/native.d.ts +0 -6
  120. package/dist/server/native.js +0 -35
  121. package/dist/server/native.js.map +0 -1
  122. package/dist/server/request-balancer.d.ts +0 -16
  123. package/dist/server/request-balancer.js +0 -43
  124. package/dist/server/request-balancer.js.map +0 -1
  125. package/dist/tui/actions.d.ts +0 -41
  126. package/dist/tui/actions.js +0 -92
  127. package/dist/tui/actions.js.map +0 -1
  128. package/dist/tui/balancer-bar-sync.d.ts +0 -19
  129. package/dist/tui/balancer-bar-sync.js +0 -45
  130. package/dist/tui/balancer-bar-sync.js.map +0 -1
  131. package/dist/tui/components/alias-dialog.d.ts +0 -4
  132. package/dist/tui/components/dashboard.d.ts +0 -12
  133. package/dist/tui/components/priority-screen.d.ts +0 -9
  134. package/dist/tui/components/provider-model-dialog.d.ts +0 -14
  135. package/dist/tui/components/rename-dialog.d.ts +0 -4
  136. package/dist/tui/components/sidebar.d.ts +0 -10
  137. package/dist/tui/components/status-indicator.d.ts +0 -9
  138. package/dist/tui/components/usage-bar.d.ts +0 -8
  139. package/dist/tui/components/usage-display.d.ts +0 -10
  140. package/dist/tui/connect.d.ts +0 -30
  141. package/dist/tui/connect.js +0 -75
  142. package/dist/tui/connect.js.map +0 -1
  143. package/dist/tui/dashboard-keys.d.ts +0 -45
  144. package/dist/tui/dashboard-keys.js +0 -44
  145. package/dist/tui/dashboard-keys.js.map +0 -1
  146. package/dist/tui/native-model-apply.d.ts +0 -21
  147. package/dist/tui/native-model-apply.js +0 -53
  148. package/dist/tui/native-model-apply.js.map +0 -1
  149. package/dist/tui/priority-keys.d.ts +0 -40
  150. package/dist/tui/priority-keys.js +0 -38
  151. package/dist/tui/priority-keys.js.map +0 -1
  152. package/dist/tui/provider-models.d.ts +0 -19
  153. package/dist/tui/provider-models.js +0 -17
  154. package/dist/tui/provider-models.js.map +0 -1
  155. package/dist/tui/responsive.d.ts +0 -9
  156. package/dist/tui/responsive.js +0 -13
  157. package/dist/tui/responsive.js.map +0 -1
  158. package/dist/tui/selected-account-bar-sync.d.ts +0 -10
  159. package/dist/tui/selected-account-bar-sync.js +0 -26
  160. package/dist/tui/selected-account-bar-sync.js.map +0 -1
  161. package/dist/tui/selection-colors.d.ts +0 -10
  162. package/dist/tui/selection-colors.js +0 -38
  163. package/dist/tui/selection-colors.js.map +0 -1
  164. package/dist/tui/state.d.ts +0 -14
  165. package/dist/tui/state.js +0 -46
  166. package/dist/tui/state.js.map +0 -1
  167. package/dist/tui/status-format.d.ts +0 -15
  168. package/dist/tui/status-format.js +0 -17
  169. package/dist/tui/status-format.js.map +0 -1
  170. package/dist/tui/tui.d.ts +0 -7
  171. package/dist/tui/usage-auto-refresh.d.ts +0 -16
  172. package/dist/tui/usage-auto-refresh.js +0 -46
  173. package/dist/tui/usage-auto-refresh.js.map +0 -1
  174. package/dist/tui/usage-format.d.ts +0 -2
  175. package/dist/tui/usage-format.js +0 -17
  176. 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,280 +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, onComplete?: () => void) => 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, restoreFocus);
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
- const restoreFocus = () => queueMicrotask(() => container?.focus?.());
133
- onMount(() => container?.focus?.());
158
+ let container: { focus?: () => void } | undefined;
159
+ const restoreFocus = () => queueMicrotask(() => container?.focus?.());
160
+ onMount(() => container?.focus?.());
134
161
 
135
- const Hint = (hintProps: { children: string }) => (
136
- <text fg={theme().textMuted} wrapMode="none" truncate>
137
- {hintProps.children}
138
- </text>
139
- );
162
+ const Hint = (hintProps: { children: string }) => (
163
+ <text fg={theme().textMuted} truncate wrapMode="none">
164
+ {hintProps.children}
165
+ </text>
166
+ );
140
167
 
141
- const Chip = (chipProps: { keyName: string; label: string; danger?: boolean }) => (
142
- <text fg={chipProps.danger ? theme().warning : theme().accent} wrapMode="none">
143
- [{chipProps.keyName}] <span style={{ fg: theme().textMuted }}>{chipProps.label}</span>
144
- </text>
145
- );
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
+ );
146
181
 
147
- const Row = (rowProps: { selected?: boolean; children: unknown; onMouseUp?: () => void }) => (
148
- <box
149
- flexDirection="row"
150
- width="100%"
151
- minWidth={0}
152
- height={1}
153
- flexShrink={0}
154
- backgroundColor={rowProps.selected ? selectedColors().bg : undefined}
155
- onMouseUp={rowProps.onMouseUp}
156
- >
157
- {rowProps.children}
158
- </box>
159
- );
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
+ );
160
199
 
161
- return (
162
- <box
163
- ref={(ref: unknown) => (container = ref as { focus?: () => void })}
164
- focusable
165
- onKeyDown={(event: KeyLike) => handleKey(event)}
166
- flexDirection="column"
167
- gap={0}
168
- padding={1}
169
- width="100%"
170
- height="100%"
171
- >
172
- <box flexDirection="column" gap={0} paddingBottom={1}>
173
- <text fg={theme().primary} wrapMode="none" overflow="hidden" truncate>
174
- opencode-balancer{compact() ? "" : " priority matrix"}
175
- </text>
176
- <Show when={!compact()}>
177
- <Hint>Choose one model per provider and order failover priority.</Hint>
178
- </Show>
179
- <box
180
- height={1}
181
- flexShrink={0}
182
- backgroundColor={headerSelected() ? selectedColors().bg : undefined}
183
- onMouseUp={() => {
184
- setFocusArea("header");
185
- props.onBack();
186
- }}
187
- >
188
- <text fg={headerSelected() ? selectedColors().fg : theme().accent} wrapMode="none" overflow="hidden" truncate>
189
- {headerMarker()} [ back to dashboard ]
190
- </text>
191
- </box>
192
- </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>
193
239
 
194
- <box flexDirection="column" gap={0} paddingBottom={1} onMouseUp={toggleBalancing}>
195
- <text fg={theme().primary} wrapMode="none" overflow="hidden" truncate>
196
- BALANCING {balancing() ? "ON" : "OFF"}
197
- </text>
198
- <Show when={!compact()}>
199
- <text fg={theme().textMuted} wrapMode="none" overflow="hidden" truncate>
200
- press B to toggle automatic provider failover
201
- </text>
202
- </Show>
203
- </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>
204
260
 
205
- <box flexDirection="column" gap={0} paddingBottom={1}>
206
- <Show when={!compact()}>
207
- <box flexDirection="row" gap={2}>
208
- <text fg={theme().primary} wrapMode="none">
209
- # PROVIDER
210
- </text>
211
- <text fg={theme().primary} wrapMode="none">
212
- MODEL
213
- </text>
214
- <text fg={theme().primary} wrapMode="none">
215
- ENABLED
216
- </text>
217
- </box>
218
- </Show>
219
- <Show
220
- when={entries().length > 0}
221
- fallback={
222
- <text fg={theme().textMuted} wrapMode="none">
223
- nenhum provider com conta ainda
224
- </text>
225
- }
226
- >
227
- <For each={entries()}>
228
- {(entry, index) => {
229
- const selected = () => focusArea() === "content" && index() === cursor();
230
- const active = () => entry.providerID === activeProviderID();
231
- const rowColor = () => {
232
- if (!entry.enabled) return theme().textMuted;
233
- if (active()) return theme().success;
234
- return theme().text;
235
- };
236
- return (
237
- <Row
238
- selected={selected()}
239
- onMouseUp={() => {
240
- setFocusArea("content");
241
- setCursor(index());
242
- }}
243
- >
244
- <box width={1} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
245
- <text fg={selected() ? selectedColors().fg : theme().accent} wrapMode="none">
246
- {rowMarker(index() === cursor())}
247
- </text>
248
- </box>
249
- <box width={3} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
250
- <text fg={selected() ? selectedColors().fg : rowColor()} wrapMode="none">
251
- {index() + 1}.
252
- </text>
253
- </box>
254
- <box width={18} flexShrink={0} backgroundColor={selected() ? selectedColors().bg : undefined}>
255
- <text fg={selected() ? selectedColors().fg : rowColor()} wrapMode="none" overflow="hidden" truncate>
256
- {entry.providerID}
257
- </text>
258
- </box>
259
- <box
260
- flexGrow={1}
261
- minWidth={0}
262
- backgroundColor={selected() ? selectedColors().bg : undefined}
263
- onMouseUp={() => props.openModelPicker(entry.providerID, restoreFocus)}
264
- >
265
- <text fg={selected() ? selectedColors().fg : entry.modelID ? rowColor() : theme().warning} wrapMode="none" overflow="hidden" truncate>
266
- {modelLabel(props.api, entry)}
267
- </text>
268
- </box>
269
- <box
270
- width={10}
271
- flexShrink={0}
272
- backgroundColor={selected() ? selectedColors().bg : undefined}
273
- onMouseUp={() => toggleEnabled(entry)}
274
- >
275
- <text fg={selected() ? selectedColors().fg : entry.enabled ? theme().success : theme().textMuted} wrapMode="none" overflow="hidden" truncate>
276
- {entry.enabled ? "enabled" : "disabled"}
277
- </text>
278
- </box>
279
- </Row>
280
- );
281
- }}
282
- </For>
283
- </Show>
284
- </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>
285
399
 
286
- <box flexDirection="column" gap={0}>
287
- <Show
288
- when={!compact()}
289
- fallback={<Hint>↑↓ move · Shift+↑↓ reorder · Enter model · Space enable · Esc back</Hint>}
290
- >
291
- <box flexDirection="row" gap={2}>
292
- <Chip keyName="↑↓" label="Move" />
293
- <Chip keyName="Shift+↑↓" label="Reorder" />
294
- <Chip keyName="Enter" label="Model" />
295
- <Chip keyName="Space" label="Enable" />
296
- <Chip keyName="Esc" label="Back" />
297
- </box>
298
- <Hint>{headerSelected() ? "Enter returns to the dashboard." : "selected provider controls the next automatic failover target."}</Hint>
299
- </Show>
300
- </box>
301
- </box>
302
- );
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
+ );
303
425
  }