@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
@@ -2,70 +2,87 @@
2
2
 
3
3
  import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
4
  import { setSelectedModel } from "../../core/accounts";
5
- import { createNativeModelApplier, type NativeModelApplier } from "../native-model-apply";
6
- import { providerModelOptions, type ProviderModelOption } from "../provider-models";
5
+ import {
6
+ createNativeModelApplier,
7
+ type NativeModelApplier,
8
+ } from "../native-model-apply";
9
+ import {
10
+ type ProviderModelOption,
11
+ providerModelOptions,
12
+ } from "../provider-models";
7
13
  import type { BalancerTuiState } from "../state";
8
14
 
9
15
  type OpenProviderModelDialogOptions = {
10
- // Test seam: replays the chosen model into opencode's native dialog so the
11
- // native model bar updates. Defaults to driving opencode via simulated keys.
12
- applyNativeSelection?: NativeModelApplier;
13
- onSelected?: (model: { providerID: string; modelID: string }) => void;
16
+ // Test seam: replays the chosen model into opencode's native dialog so the
17
+ // native model bar updates. Defaults to driving opencode via simulated keys.
18
+ applyNativeSelection?: NativeModelApplier | false;
19
+ onComplete?: () => void;
20
+ onSelected?: (model: { providerID: string; modelID: string }) => void;
14
21
  };
15
22
 
16
23
  export function openProviderModelDialog(
17
- api: TuiPluginApi,
18
- state: Pick<BalancerTuiState, "db" | "refresh">,
19
- providerID: string,
20
- options: OpenProviderModelDialogOptions = {},
24
+ api: TuiPluginApi,
25
+ state: Pick<BalancerTuiState, "db" | "refresh">,
26
+ providerID: string,
27
+ options: OpenProviderModelDialogOptions = {},
21
28
  ) {
22
- const modelOptions = providerModelOptions(api.state.provider, providerID);
23
- const providerName = modelOptions[0]?.providerName ?? providerID;
24
- const applyNativeSelection = options.applyNativeSelection ?? createNativeModelApplier(api);
29
+ const modelOptions = providerModelOptions(api.state.provider, providerID);
30
+ const providerName = modelOptions[0]?.providerName ?? providerID;
31
+ const applyNativeSelection =
32
+ options.applyNativeSelection ?? createNativeModelApplier(api);
25
33
 
26
- api.ui.dialog.setSize("medium");
27
- api.ui.dialog.replace(() => (
28
- <api.ui.DialogSelect<ProviderModelOption>
29
- title={`Select ${providerName} model`}
30
- placeholder={`Search ${providerName} models`}
31
- flat
32
- skipFilter={false}
33
- options={modelOptions.map((option) => ({
34
- title: option.title,
35
- description: option.providerName,
36
- value: option,
37
- footer: `${option.providerID}/${option.modelID}`,
38
- }))}
39
- onSelect={(option) => {
40
- const model = {
41
- providerID: option.value.providerID,
42
- modelID: option.value.modelID,
43
- };
34
+ api.ui.dialog.setSize("medium");
35
+ api.ui.dialog.replace(() => (
36
+ <api.ui.DialogSelect<ProviderModelOption>
37
+ flat
38
+ onSelect={(option) => {
39
+ const model = {
40
+ modelID: option.value.modelID,
41
+ providerID: option.value.providerID,
42
+ };
44
43
 
45
- // Persist first: in manual mode this stores the selected model;
46
- // priority mode injects its own persistence callback.
47
- if (options.onSelected) options.onSelected(model);
48
- else setSelectedModel(state.db, model.providerID, model.modelID);
49
- state.refresh();
44
+ // Persist first: in manual mode this stores the selected model;
45
+ // priority mode injects its own persistence callback.
46
+ if (options.onSelected) options.onSelected(model);
47
+ else setSelectedModel(state.db, model.providerID, model.modelID);
48
+ state.refresh();
50
49
 
51
- // Close our picker, then replay the choice into opencode's native
52
- // model dialog so its bottom model bar reflects the selection.
53
- api.ui.dialog.clear();
50
+ // Close our picker, then replay the choice into opencode's native
51
+ // model dialog so its bottom model bar reflects the selection.
52
+ api.ui.dialog.clear();
54
53
 
55
- void Promise.resolve(applyNativeSelection(model, option.value.title)).then((applied) => {
56
- if (applied) {
57
- api.ui.toast({
58
- variant: "success",
59
- message: `Switched to ${option.value.providerName}/${option.value.title}.`,
60
- });
61
- return;
62
- }
63
- api.ui.toast({
64
- variant: "warning",
65
- message: `Selected ${option.value.providerName}/${option.value.title}; prompts will use it, but opencode's model bar may not have refreshed.`,
66
- });
67
- });
68
- }}
69
- />
70
- ));
54
+ if (applyNativeSelection === false) {
55
+ options.onComplete?.();
56
+ return;
57
+ }
58
+
59
+ void Promise.resolve(applyNativeSelection(model, option.value.title))
60
+ .then((applied) => {
61
+ if (applied) {
62
+ api.ui.toast({
63
+ message: `Switched to ${option.value.providerName}/${option.value.title}.`,
64
+ variant: "success",
65
+ });
66
+ return;
67
+ }
68
+ api.ui.toast({
69
+ message: `Selected ${option.value.providerName}/${option.value.title}; prompts will use it, but opencode's model bar may not have refreshed.`,
70
+ variant: "warning",
71
+ });
72
+ })
73
+ .finally(() => {
74
+ options.onComplete?.();
75
+ });
76
+ }}
77
+ options={modelOptions.map((option) => ({
78
+ description: option.providerName,
79
+ footer: `${option.providerID}/${option.modelID}`,
80
+ title: option.title,
81
+ value: option,
82
+ }))}
83
+ placeholder={`Search ${providerName} models`}
84
+ skipFilter={false}
85
+ title={`Select ${providerName} model`}
86
+ />
87
+ ));
71
88
  }
@@ -6,35 +6,42 @@ import { renameAccountFromTui } from "../actions";
6
6
  import type { BalancerTuiState } from "../state";
7
7
 
8
8
  function errorMessage(error: unknown) {
9
- return error instanceof Error && error.message ? error.message : "Failed to rename account";
9
+ return error instanceof Error && error.message
10
+ ? error.message
11
+ : "Failed to rename account";
10
12
  }
11
13
 
12
- export function openRenameDialog(api: TuiPluginApi, state: BalancerTuiState, providerID: string, alias: string) {
13
- api.ui.dialog.setSize("medium");
14
- api.ui.dialog.replace(() => (
15
- <api.ui.DialogPrompt
16
- title="Rename account"
17
- placeholder="account alias"
18
- description={() => (
19
- <text fg={api.theme.current.textMuted} wrapMode="none">
20
- Choose a new alias for {providerID}/{alias}.
21
- </text>
22
- )}
23
- onCancel={() => api.ui.dialog.clear()}
24
- onConfirm={(value) => {
25
- const nextAlias = normalizeAlias(value);
26
- if (!nextAlias) {
27
- api.ui.toast({ variant: "error", message: "Alias is required" });
28
- return;
29
- }
14
+ export function openRenameDialog(
15
+ api: TuiPluginApi,
16
+ state: BalancerTuiState,
17
+ providerID: string,
18
+ alias: string,
19
+ ) {
20
+ api.ui.dialog.setSize("medium");
21
+ api.ui.dialog.replace(() => (
22
+ <api.ui.DialogPrompt
23
+ description={() => (
24
+ <text fg={api.theme.current.textMuted} wrapMode="none">
25
+ Choose a new alias for {providerID}/{alias}.
26
+ </text>
27
+ )}
28
+ onCancel={() => api.ui.dialog.clear()}
29
+ onConfirm={(value) => {
30
+ const nextAlias = normalizeAlias(value);
31
+ if (!nextAlias) {
32
+ api.ui.toast({ message: "Alias is required", variant: "error" });
33
+ return;
34
+ }
30
35
 
31
- try {
32
- renameAccountFromTui(api, state, providerID, alias, nextAlias);
33
- api.ui.dialog.clear();
34
- } catch (error) {
35
- api.ui.toast({ variant: "error", message: errorMessage(error) });
36
- }
37
- }}
38
- />
39
- ));
36
+ try {
37
+ renameAccountFromTui(api, state, providerID, alias, nextAlias);
38
+ api.ui.dialog.clear();
39
+ } catch (error) {
40
+ api.ui.toast({ message: errorMessage(error), variant: "error" });
41
+ }
42
+ }}
43
+ placeholder="account alias"
44
+ title="Rename account"
45
+ />
46
+ ));
40
47
  }
@@ -4,94 +4,118 @@ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
4
  import { createSignal, For, onCleanup, Show } from "solid-js";
5
5
  import { listAccounts } from "../../core/accounts";
6
6
  import { getBalancingEnabled } from "../../core/priority";
7
- import { getUsageSnapshot } from "../../core/usage/store";
8
7
  import type { Account } from "../../core/types";
8
+ import { getUsageSnapshot } from "../../core/usage/store";
9
9
  import type { ProviderUsageSnapshot } from "../../core/usage/types";
10
10
  import type { BalancerTuiState } from "../state";
11
11
  import { UsageSnapshotBar } from "./usage-display";
12
12
 
13
13
  export type BalancerSidebarProps = {
14
- api: TuiPluginApi;
15
- state: BalancerTuiState;
16
- openDashboard: () => void;
17
- activateAccount: (providerID: string, alias: string) => void;
14
+ api: TuiPluginApi;
15
+ state: BalancerTuiState;
16
+ openDashboard: () => void;
17
+ activateAccount: (providerID: string, alias: string) => void;
18
18
  };
19
19
 
20
20
  export function BalancerSidebar(props: BalancerSidebarProps) {
21
- const theme = () => props.api.theme.current;
22
- const [accounts, setAccounts] = createSignal<Account[]>(listAccounts(props.state.db));
23
- const [usage, setUsage] = createSignal<Record<string, ProviderUsageSnapshot | undefined>>({});
24
- const [balancingEnabled, setBalancingEnabled] = createSignal(getBalancingEnabled(props.state.db));
25
- const refreshSidebar = () => {
26
- const nextAccounts = listAccounts(props.state.db);
27
- setAccounts(nextAccounts);
28
- setBalancingEnabled(getBalancingEnabled(props.state.db));
29
- setUsage(
30
- Object.fromEntries(
31
- nextAccounts.map((account) => [
32
- `${account.providerID}/${account.alias}`,
33
- getUsageSnapshot(props.state.db, account.providerID, account.alias),
34
- ]),
35
- ),
36
- );
37
- };
38
- refreshSidebar();
39
- const timer = setInterval(refreshSidebar, 500);
40
- onCleanup(() => clearInterval(timer));
41
- const Button = (buttonProps: { label: string; onClick: () => void }) => (
42
- <box onMouseUp={buttonProps.onClick} paddingLeft={0} paddingRight={0}>
43
- <text fg={theme().accent} wrapMode="none" truncate>
44
- [ {buttonProps.label} ]
45
- </text>
46
- </box>
47
- );
21
+ const theme = () => props.api.theme.current;
22
+ const [accounts, setAccounts] = createSignal<Account[]>(
23
+ listAccounts(props.state.db),
24
+ );
25
+ const [usage, setUsage] = createSignal<
26
+ Record<string, ProviderUsageSnapshot | undefined>
27
+ >({});
28
+ const [balancingEnabled, setBalancingEnabled] = createSignal(
29
+ getBalancingEnabled(props.state.db),
30
+ );
31
+ const refreshSidebar = () => {
32
+ const nextAccounts = listAccounts(props.state.db);
33
+ setAccounts(nextAccounts);
34
+ setBalancingEnabled(getBalancingEnabled(props.state.db));
35
+ setUsage(
36
+ Object.fromEntries(
37
+ nextAccounts.map((account) => [
38
+ `${account.providerID}/${account.alias}`,
39
+ getUsageSnapshot(props.state.db, account.providerID, account.alias),
40
+ ]),
41
+ ),
42
+ );
43
+ };
44
+ refreshSidebar();
45
+ const timer = setInterval(refreshSidebar, 500);
46
+ onCleanup(() => clearInterval(timer));
47
+ const Button = (buttonProps: { label: string; onClick: () => void }) => (
48
+ <box onMouseUp={buttonProps.onClick} paddingLeft={0} paddingRight={0}>
49
+ <text fg={theme().accent} truncate wrapMode="none">
50
+ [ {buttonProps.label} ]
51
+ </text>
52
+ </box>
53
+ );
48
54
 
49
- return (
50
- <box flexDirection="column" gap={1}>
51
- <box flexDirection="column" gap={0}>
52
- <text fg={theme().text} wrapMode="none">
53
- Balancer
54
- </text>
55
- <text fg={balancingEnabled() ? theme().success : theme().textMuted} wrapMode="none">
56
- {balancingEnabled() ? "ON" : "OFF"}
57
- </text>
58
- <Button label="dashboard" onClick={() => props.openDashboard()} />
59
- <text fg={theme().textMuted} wrapMode="none">
60
- ctrl+b
61
- </text>
62
- </box>
55
+ return (
56
+ <box flexDirection="column" gap={1}>
57
+ <box flexDirection="column" gap={0}>
58
+ <text fg={theme().text} wrapMode="none">
59
+ Balancer
60
+ </text>
61
+ <text
62
+ fg={balancingEnabled() ? theme().success : theme().textMuted}
63
+ wrapMode="none"
64
+ >
65
+ {balancingEnabled() ? "ON" : "OFF"}
66
+ </text>
67
+ <Button label="dashboard" onClick={() => props.openDashboard()} />
68
+ <text fg={theme().textMuted} wrapMode="none">
69
+ ctrl+b
70
+ </text>
71
+ </box>
63
72
 
64
- <box flexDirection="column" gap={0}>
65
- <text fg={theme().textMuted} wrapMode="none">
66
- Accounts
67
- </text>
68
- <Show
69
- when={accounts().length > 0}
70
- fallback={
71
- <text fg={theme().textMuted} wrapMode="none">
72
- none
73
- </text>
74
- }
75
- >
76
- <For each={accounts()}>
77
- {(account) => (
78
- <box flexDirection="column" gap={0} onMouseUp={() => props.activateAccount(account.providerID, account.alias)}>
79
- <text fg={theme().text} wrapMode="none" truncate>
80
- <span style={{ fg: theme().primary }}>{account.providerID}</span>/
81
- <span style={{ fg: account.disabled ? theme().textMuted : theme().text }}>
82
- {account.alias}
83
- </span>{" "}
84
- <span style={{ fg: theme().textMuted }}>({account.authType})</span>
85
- </text>
86
- <UsageSnapshotBar
87
- theme={theme()}
88
- snapshot={usage()[`${account.providerID}/${account.alias}`]}
89
- />
90
- </box>
91
- )}
92
- </For>
93
- </Show>
94
- </box>
95
- </box>
96
- );
73
+ <box flexDirection="column" gap={0}>
74
+ <text fg={theme().textMuted} wrapMode="none">
75
+ Accounts
76
+ </text>
77
+ <Show
78
+ fallback={
79
+ <text fg={theme().textMuted} wrapMode="none">
80
+ none
81
+ </text>
82
+ }
83
+ when={accounts().length > 0}
84
+ >
85
+ <For each={accounts()}>
86
+ {(account) => (
87
+ <box
88
+ flexDirection="column"
89
+ gap={0}
90
+ onMouseUp={() =>
91
+ props.activateAccount(account.providerID, account.alias)
92
+ }
93
+ >
94
+ <text fg={theme().text} truncate wrapMode="none">
95
+ <span style={{ fg: theme().primary }}>
96
+ {account.providerID}
97
+ </span>
98
+ /
99
+ <span
100
+ style={{
101
+ fg: account.disabled ? theme().textMuted : theme().text,
102
+ }}
103
+ >
104
+ {account.alias}
105
+ </span>{" "}
106
+ <span style={{ fg: theme().textMuted }}>
107
+ ({account.authType})
108
+ </span>
109
+ </text>
110
+ <UsageSnapshotBar
111
+ snapshot={usage()[`${account.providerID}/${account.alias}`]}
112
+ theme={theme()}
113
+ />
114
+ </box>
115
+ )}
116
+ </For>
117
+ </Show>
118
+ </box>
119
+ </box>
120
+ );
97
121
  }
@@ -3,76 +3,95 @@
3
3
  import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
4
  import { createSignal, onCleanup } from "solid-js";
5
5
  import { getActiveAccount, getSelectedAccount } from "../../core/accounts";
6
- import { getBalancingEnabled, resolveActiveSelection, type ActiveSelection } from "../../core/priority";
6
+ import {
7
+ type ActiveSelection,
8
+ getBalancingEnabled,
9
+ resolveActiveSelection,
10
+ } from "../../core/priority";
7
11
  import type { Account } from "../../core/types";
8
12
  import { getUsageSnapshot } from "../../core/usage/store";
9
- import { formatBalancerStatus } from "../status-format";
10
13
  import type { BalancerTuiState } from "../state";
14
+ import { formatBalancerStatus } from "../status-format";
11
15
  import { formatUsageBar } from "../usage-format";
12
16
  import { snapshotPercent } from "./usage-display";
13
17
 
14
18
  export type BalancerStatusIndicatorProps = {
15
- api: TuiPluginApi;
16
- state: BalancerTuiState;
17
- providerID?: string | (() => string | undefined);
19
+ api: TuiPluginApi;
20
+ state: BalancerTuiState;
21
+ providerID?: string | (() => string | undefined);
18
22
  };
19
23
 
20
24
  export function BalancerStatusIndicator(props: BalancerStatusIndicatorProps) {
21
- const [selected, setSelected] = createSignal<Account | undefined>();
22
- const [sessionActive, setSessionActive] = createSignal<Account | undefined>();
23
- const [balancing, setBalancing] = createSignal<ActiveSelection | undefined>();
24
- const [usage, setUsage] = createSignal<string | undefined>();
25
- const providerID = () => (typeof props.providerID === "function" ? props.providerID() : props.providerID);
26
- const refresh = () => {
27
- const currentProviderID = providerID();
28
- const nextSelected = getSelectedAccount(props.state.db);
29
- const nextSessionActive = currentProviderID ? getActiveAccount(props.state.db, currentProviderID) : undefined;
30
- const nextBalancing = getBalancingEnabled(props.state.db)
31
- ? resolveActiveSelection(props.state.db, undefined, currentProviderID)
32
- : undefined;
33
- const usageAccount = nextBalancing?.account ?? nextSelected ?? nextSessionActive;
34
- setSelected(nextSelected);
35
- setSessionActive(nextSessionActive);
36
- setBalancing(nextBalancing);
37
- setUsage(
38
- usageAccount
39
- ? formatUsageBar(
40
- snapshotPercent(getUsageSnapshot(props.state.db, usageAccount.providerID, usageAccount.alias)),
41
- 4,
42
- )
43
- : undefined,
44
- );
45
- };
46
- refresh();
47
- const timer = setInterval(refresh, 500);
48
- onCleanup(() => clearInterval(timer));
25
+ const [selected, setSelected] = createSignal<Account | undefined>();
26
+ const [sessionActive, setSessionActive] = createSignal<Account | undefined>();
27
+ const [balancing, setBalancing] = createSignal<ActiveSelection | undefined>();
28
+ const [usage, setUsage] = createSignal<string | undefined>();
29
+ const providerID = () =>
30
+ typeof props.providerID === "function"
31
+ ? props.providerID()
32
+ : props.providerID;
33
+ const refresh = () => {
34
+ const currentProviderID = providerID();
35
+ const nextSelected = getSelectedAccount(props.state.db);
36
+ const nextSessionActive = currentProviderID
37
+ ? getActiveAccount(props.state.db, currentProviderID)
38
+ : undefined;
39
+ const nextBalancing = getBalancingEnabled(props.state.db)
40
+ ? resolveActiveSelection(props.state.db, undefined, currentProviderID)
41
+ : undefined;
42
+ const usageAccount =
43
+ nextBalancing?.account ?? nextSelected ?? nextSessionActive;
44
+ setSelected(nextSelected);
45
+ setSessionActive(nextSessionActive);
46
+ setBalancing(nextBalancing);
47
+ setUsage(
48
+ usageAccount
49
+ ? formatUsageBar(
50
+ snapshotPercent(
51
+ getUsageSnapshot(
52
+ props.state.db,
53
+ usageAccount.providerID,
54
+ usageAccount.alias,
55
+ ),
56
+ ),
57
+ 4,
58
+ )
59
+ : undefined,
60
+ );
61
+ };
62
+ refresh();
63
+ const timer = setInterval(refresh, 500);
64
+ onCleanup(() => clearInterval(timer));
49
65
 
50
- const status = () => {
51
- return formatBalancerStatus({
52
- selected: selected(),
53
- sessionActive: sessionActive(),
54
- sessionProviderID: providerID(),
55
- balancing: balancing()
56
- ? {
57
- providerID: balancing()!.providerID,
58
- alias: balancing()!.account.alias,
59
- modelID: balancing()!.modelID,
60
- }
61
- : undefined,
62
- usage: usage(),
63
- });
64
- };
66
+ const status = () => {
67
+ return formatBalancerStatus({
68
+ balancing: balancing()
69
+ ? {
70
+ alias: balancing()!.account.alias,
71
+ modelID: balancing()!.modelID,
72
+ providerID: balancing()!.providerID,
73
+ }
74
+ : undefined,
75
+ selected: selected(),
76
+ sessionActive: sessionActive(),
77
+ sessionProviderID: providerID(),
78
+ usage: usage(),
79
+ });
80
+ };
65
81
 
66
- const color = () => {
67
- if (balancing()) return props.api.theme.current.success;
68
- if (providerID()) return sessionActive() ? props.api.theme.current.success : props.api.theme.current.textMuted;
69
- if (!selected()) return props.api.theme.current.textMuted;
70
- return props.api.theme.current.success;
71
- };
82
+ const color = () => {
83
+ if (balancing()) return props.api.theme.current.success;
84
+ if (providerID())
85
+ return sessionActive()
86
+ ? props.api.theme.current.success
87
+ : props.api.theme.current.textMuted;
88
+ if (!selected()) return props.api.theme.current.textMuted;
89
+ return props.api.theme.current.success;
90
+ };
72
91
 
73
- return (
74
- <text fg={color()} wrapMode="none" truncate>
75
- {status()}
76
- </text>
77
- );
92
+ return (
93
+ <text fg={color()} truncate wrapMode="none">
94
+ {status()}
95
+ </text>
96
+ );
78
97
  }
@@ -3,11 +3,22 @@
3
3
  import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
4
4
  import { formatUsageBar, truncateMiddle } from "../usage-format";
5
5
 
6
- export function UsageBar(props: { theme: TuiThemeCurrent; percent?: number; label: string; muted?: boolean }) {
7
- const color = () => (props.muted || props.percent === undefined ? props.theme.textMuted : props.theme.primary);
8
- return (
9
- <text fg={color()} wrapMode="none" overflow="hidden" truncate>
10
- {formatUsageBar(props.percent, 8)} <span style={{ fg: props.theme.textMuted }}>{truncateMiddle(props.label, 34)}</span>
11
- </text>
12
- );
6
+ export function UsageBar(props: {
7
+ theme: TuiThemeCurrent;
8
+ percent?: number;
9
+ label: string;
10
+ muted?: boolean;
11
+ }) {
12
+ const color = () =>
13
+ props.muted || props.percent === undefined
14
+ ? props.theme.textMuted
15
+ : props.theme.primary;
16
+ return (
17
+ <text fg={color()} overflow="hidden" truncate wrapMode="none">
18
+ {formatUsageBar(props.percent, 8)}{" "}
19
+ <span style={{ fg: props.theme.textMuted }}>
20
+ {truncateMiddle(props.label, 34)}
21
+ </span>
22
+ </text>
23
+ );
13
24
  }