@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
@@ -2,80 +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 | false;
13
- onComplete?: () => void;
14
- 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;
15
21
  };
16
22
 
17
23
  export function openProviderModelDialog(
18
- api: TuiPluginApi,
19
- state: Pick<BalancerTuiState, "db" | "refresh">,
20
- providerID: string,
21
- options: OpenProviderModelDialogOptions = {},
24
+ api: TuiPluginApi,
25
+ state: Pick<BalancerTuiState, "db" | "refresh">,
26
+ providerID: string,
27
+ options: OpenProviderModelDialogOptions = {},
22
28
  ) {
23
- const modelOptions = providerModelOptions(api.state.provider, providerID);
24
- const providerName = modelOptions[0]?.providerName ?? providerID;
25
- 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);
26
33
 
27
- api.ui.dialog.setSize("medium");
28
- api.ui.dialog.replace(() => (
29
- <api.ui.DialogSelect<ProviderModelOption>
30
- title={`Select ${providerName} model`}
31
- placeholder={`Search ${providerName} models`}
32
- flat
33
- skipFilter={false}
34
- options={modelOptions.map((option) => ({
35
- title: option.title,
36
- description: option.providerName,
37
- value: option,
38
- footer: `${option.providerID}/${option.modelID}`,
39
- }))}
40
- onSelect={(option) => {
41
- const model = {
42
- providerID: option.value.providerID,
43
- modelID: option.value.modelID,
44
- };
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
+ };
45
43
 
46
- // Persist first: in manual mode this stores the selected model;
47
- // priority mode injects its own persistence callback.
48
- if (options.onSelected) options.onSelected(model);
49
- else setSelectedModel(state.db, model.providerID, model.modelID);
50
- 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();
51
49
 
52
- // Close our picker, then replay the choice into opencode's native
53
- // model dialog so its bottom model bar reflects the selection.
54
- 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();
55
53
 
56
- if (applyNativeSelection === false) {
57
- options.onComplete?.();
58
- return;
59
- }
54
+ if (applyNativeSelection === false) {
55
+ options.onComplete?.();
56
+ return;
57
+ }
60
58
 
61
- void Promise.resolve(applyNativeSelection(model, option.value.title))
62
- .then((applied) => {
63
- if (applied) {
64
- api.ui.toast({
65
- variant: "success",
66
- message: `Switched to ${option.value.providerName}/${option.value.title}.`,
67
- });
68
- return;
69
- }
70
- api.ui.toast({
71
- variant: "warning",
72
- message: `Selected ${option.value.providerName}/${option.value.title}; prompts will use it, but opencode's model bar may not have refreshed.`,
73
- });
74
- })
75
- .finally(() => {
76
- options.onComplete?.();
77
- });
78
- }}
79
- />
80
- ));
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
+ ));
81
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
  }