@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
@@ -0,0 +1,49 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { suppressNativeAuthCapture as suppressDbNativeAuthCapture } from "../core/native-auth-suppression";
3
+ import { isNativeConnectInProgress } from "../core/native-connect";
4
+ import { now } from "../core/time";
5
+ import type { AuthInfo } from "../core/types";
6
+
7
+ const suppressAuthCaptureUntil = new Map<string, number>();
8
+
9
+ export function suppressNativeAuthCapture(
10
+ providerID: string,
11
+ durationMs = 2_000,
12
+ ) {
13
+ suppressAuthCaptureUntil.set(providerID, now() + durationMs);
14
+ }
15
+
16
+ export function isNativeAuthCaptureSuppressed(providerID: string) {
17
+ const suppressedUntil = suppressAuthCaptureUntil.get(providerID) ?? 0;
18
+ if (suppressedUntil <= now()) {
19
+ suppressAuthCaptureUntil.delete(providerID);
20
+ return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ export async function setNativeAuth(
26
+ client: any,
27
+ providerID: string,
28
+ auth: AuthInfo,
29
+ db?: Database,
30
+ ) {
31
+ if (db && isNativeConnectInProgress(db)) return;
32
+ suppressNativeAuthCapture(providerID);
33
+ if (db) suppressDbNativeAuthCapture(db, providerID);
34
+ try {
35
+ await client.auth.set({ body: auth, path: { id: providerID } });
36
+ } catch {}
37
+ }
38
+
39
+ export async function showToast(
40
+ client: any,
41
+ message: string,
42
+ variant: "info" | "success" | "warning" | "error" = "info",
43
+ ) {
44
+ try {
45
+ await client.tui.showToast({
46
+ body: { duration: 15_000, message, variant },
47
+ });
48
+ } catch {}
49
+ }
@@ -0,0 +1,67 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { getAccount, listAccounts, normalizeAlias } from "../core/accounts";
3
+ import { now } from "../core/time";
4
+ import type { Account } from "../core/types";
5
+
6
+ export const INTERNAL_REQUEST_HEADER = "x-opencode-balancer-request";
7
+ export const BALANCER_METADATA_KEY = "opencodeBalancerCommand";
8
+
9
+ export const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504, 529]);
10
+
11
+ type PendingRequest = {
12
+ providerID: string;
13
+ account?: Account;
14
+ };
15
+
16
+ const pendingRequests = new Map<string, PendingRequest>();
17
+
18
+ export function setPendingRequest(requestID: string, request: PendingRequest) {
19
+ pendingRequests.set(requestID, request);
20
+ }
21
+
22
+ export function takePendingRequest(requestID: string) {
23
+ const request = pendingRequests.get(requestID);
24
+ pendingRequests.delete(requestID);
25
+ return request;
26
+ }
27
+
28
+ export function __testGetPendingRequest(requestID: string) {
29
+ return pendingRequests.get(requestID);
30
+ }
31
+
32
+ export function __testClearPendingRequests() {
33
+ pendingRequests.clear();
34
+ }
35
+
36
+ export function markRateLimited(
37
+ db: Database,
38
+ providerID: string,
39
+ alias: string,
40
+ retryAfterMs = 60_000,
41
+ ) {
42
+ const account = getAccount(db, providerID, alias);
43
+ if (!account) return;
44
+
45
+ const timestamp = now();
46
+ db.query<unknown, [number, number, string, string]>(
47
+ `UPDATE accounts
48
+ SET failures = failures + 1,
49
+ rate_limited_until = ?,
50
+ updated_at = ?
51
+ WHERE provider_id = ? AND alias = ?`,
52
+ ).run(timestamp + retryAfterMs, timestamp, providerID, account.alias);
53
+ }
54
+
55
+ export function chooseFailoverAccount(
56
+ db: Database,
57
+ providerID: string,
58
+ currentAlias: string,
59
+ ) {
60
+ const timestamp = now();
61
+ const normalizedCurrentAlias = normalizeAlias(currentAlias);
62
+ return listAccounts(db, providerID).find((account) => {
63
+ if (account.alias === normalizedCurrentAlias) return false;
64
+ if (account.disabled) return false;
65
+ return !account.rateLimitedUntil || account.rateLimitedUntil <= timestamp;
66
+ });
67
+ }
@@ -1,144 +1,208 @@
1
- import { getAccount, getActiveAccount, getSelectedAccount, removeAccount, renameAccount, setActiveAccount } from "../core/accounts";
1
+ import {
2
+ getAccount,
3
+ getActiveAccount,
4
+ getSelectedAccount,
5
+ removeAccount,
6
+ renameAccount,
7
+ setActiveAccount,
8
+ } from "../core/accounts";
2
9
  import { appendEvent } from "../core/events";
3
- import { completePendingConnection, removePendingConnection } from "../core/pending";
10
+ import { suppressNativeAuthCapture } from "../core/native-auth-suppression";
11
+ import {
12
+ completePendingConnection,
13
+ removePendingConnection,
14
+ } from "../core/pending";
4
15
  import { refreshAccountUsage } from "../core/usage";
5
16
  import { saveUsageSnapshot } from "../core/usage/store";
6
- import { suppressNativeAuthCapture } from "../core/native-auth-suppression";
7
17
  import type { BalancerTuiState } from "./state";
8
18
 
9
19
  type AuthSetApi = {
10
- client: {
11
- auth: {
12
- set: (input: any) => unknown;
13
- };
14
- };
15
- keymap?: {
16
- dispatchCommand?: (command: string) => unknown;
17
- };
18
- ui?: {
19
- toast: (input: { variant: "success" | "warning" | "error" | "info"; message: string }) => unknown;
20
- };
20
+ client: {
21
+ auth: {
22
+ set: (input: any) => unknown;
23
+ };
24
+ };
25
+ keymap?: {
26
+ dispatchCommand?: (command: string) => unknown;
27
+ };
28
+ ui?: {
29
+ toast: (input: {
30
+ variant: "success" | "warning" | "error" | "info";
31
+ message: string;
32
+ }) => unknown;
33
+ };
21
34
  };
22
35
 
23
36
  type ActivateAccountOptions = {
24
- sessionProviderID?: string;
25
- applyNativeProviderModel?: (providerID: string) => Promise<boolean>;
37
+ sessionProviderID?: string;
38
+ applyNativeProviderModel?: (providerID: string) => Promise<boolean>;
26
39
  };
27
40
 
28
41
  type ToastApi = {
29
- ui: {
30
- toast: (input: { variant: "success" | "warning" | "error" | "info"; message: string }) => unknown;
31
- };
42
+ ui: {
43
+ toast: (input: {
44
+ variant: "success" | "warning" | "error" | "info";
45
+ message: string;
46
+ }) => unknown;
47
+ };
32
48
  };
33
49
 
34
50
  type RefreshUsageOptions = {
35
- refreshUsage?: typeof refreshAccountUsage;
36
- silent?: boolean;
51
+ refreshUsage?: typeof refreshAccountUsage;
52
+ silent?: boolean;
37
53
  };
38
54
 
39
- export function savePendingAlias(state: BalancerTuiState, pendingID: string, alias: string) {
40
- const account = completePendingConnection(state.db, pendingID, alias);
41
- state.refresh();
42
- return account;
55
+ export function savePendingAlias(
56
+ state: BalancerTuiState,
57
+ pendingID: string,
58
+ alias: string,
59
+ ) {
60
+ const account = completePendingConnection(state.db, pendingID, alias);
61
+ state.refresh();
62
+ return account;
43
63
  }
44
64
 
45
65
  export async function activateAccount(
46
- api: AuthSetApi,
47
- state: BalancerTuiState,
48
- providerID: string,
49
- alias: string,
50
- options: ActivateAccountOptions = {},
66
+ api: AuthSetApi,
67
+ state: BalancerTuiState,
68
+ providerID: string,
69
+ alias: string,
70
+ options: ActivateAccountOptions = {},
51
71
  ) {
52
- const previousProviderID = getSelectedAccount(state.db)?.providerID;
53
- const account = setActiveAccount(state.db, providerID, alias) ?? getActiveAccount(state.db, providerID);
54
-
55
- if (account) {
56
- try {
57
- suppressNativeAuthCapture(state.db, providerID);
58
- await api.client.auth.set({ path: { id: providerID }, body: account.auth });
59
- } catch {}
60
- }
61
-
62
- state.refresh();
63
- const providerChanged = (options.sessionProviderID ?? previousProviderID) !== providerID;
64
- if (providerChanged) {
65
- await options.applyNativeProviderModel?.(providerID);
66
- }
67
- api.ui?.toast({ variant: "success", message: `Activated ${providerID}/${alias}.` });
72
+ const previousProviderID = getSelectedAccount(state.db)?.providerID;
73
+ const account =
74
+ setActiveAccount(state.db, providerID, alias) ??
75
+ getActiveAccount(state.db, providerID);
76
+
77
+ if (account) {
78
+ try {
79
+ suppressNativeAuthCapture(state.db, providerID);
80
+ await api.client.auth.set({
81
+ body: account.auth,
82
+ path: { id: providerID },
83
+ });
84
+ } catch {}
85
+ }
86
+
87
+ state.refresh();
88
+ const providerChanged =
89
+ (options.sessionProviderID ?? previousProviderID) !== providerID;
90
+ if (providerChanged) {
91
+ await options.applyNativeProviderModel?.(providerID);
92
+ }
93
+ api.ui?.toast({
94
+ message: `Activated ${providerID}/${alias}.`,
95
+ variant: "success",
96
+ });
68
97
  }
69
98
 
70
99
  export async function refreshUsageForAccount(
71
- api: ToastApi,
72
- state: BalancerTuiState,
73
- providerID: string,
74
- alias: string,
75
- options: RefreshUsageOptions = {},
100
+ api: ToastApi,
101
+ state: BalancerTuiState,
102
+ providerID: string,
103
+ alias: string,
104
+ options: RefreshUsageOptions = {},
76
105
  ) {
77
- const account = getAccount(state.db, providerID, alias) ?? state.accounts().find((candidate) => {
78
- return candidate.providerID === providerID && candidate.alias === alias;
79
- });
80
-
81
- if (!account) {
82
- api.ui.toast({ variant: "error", message: `Account not found: ${providerID}/${alias}` });
83
- return;
84
- }
85
-
86
- try {
87
- const snapshot = await (options.refreshUsage ?? refreshAccountUsage)(account);
88
- saveUsageSnapshot(state.db, snapshot);
89
- appendEvent(state.db, {
90
- type: snapshot.confidence === "unavailable" ? "usage_unavailable" : "usage_refreshed",
91
- providerID,
92
- alias,
93
- message: snapshot.message,
94
- });
95
- state.refresh();
96
- if (!options.silent) {
97
- api.ui.toast({
98
- variant: snapshot.confidence === "unavailable" ? "warning" : "success",
99
- message: snapshot.message,
100
- });
101
- }
102
- } catch (error) {
103
- const message = error instanceof Error ? error.message : "Usage refresh failed.";
104
- api.ui.toast({ variant: "error", message });
105
- }
106
+ const account =
107
+ getAccount(state.db, providerID, alias) ??
108
+ state.accounts().find((candidate) => {
109
+ return candidate.providerID === providerID && candidate.alias === alias;
110
+ });
111
+
112
+ if (!account) {
113
+ api.ui.toast({
114
+ message: `Account not found: ${providerID}/${alias}`,
115
+ variant: "error",
116
+ });
117
+ return;
118
+ }
119
+
120
+ try {
121
+ const snapshot = await (options.refreshUsage ?? refreshAccountUsage)(
122
+ account,
123
+ );
124
+ saveUsageSnapshot(state.db, snapshot);
125
+ appendEvent(state.db, {
126
+ alias,
127
+ message: snapshot.message,
128
+ providerID,
129
+ type:
130
+ snapshot.confidence === "unavailable"
131
+ ? "usage_unavailable"
132
+ : "usage_refreshed",
133
+ });
134
+ state.refresh();
135
+ if (!options.silent) {
136
+ api.ui.toast({
137
+ message: snapshot.message,
138
+ variant: snapshot.confidence === "unavailable" ? "warning" : "success",
139
+ });
140
+ }
141
+ } catch (error) {
142
+ const message =
143
+ error instanceof Error ? error.message : "Usage refresh failed.";
144
+ api.ui.toast({ message, variant: "error" });
145
+ }
106
146
  }
107
147
 
108
- export function removeAccountFromTui(api: ToastApi, state: BalancerTuiState, providerID: string, alias: string) {
109
- const removed = removeAccount(state.db, providerID, alias);
110
- if (!removed) {
111
- api.ui.toast({ variant: "error", message: `Account not found: ${providerID}/${alias}` });
112
- return;
113
- }
114
-
115
- const message = `Removed account ${providerID}/${alias}.`;
116
- appendEvent(state.db, {
117
- type: "account_removed",
118
- providerID,
119
- alias,
120
- message,
121
- });
122
- state.refresh();
123
- state.removeAccountView(providerID, alias);
124
- api.ui.toast({ variant: "success", message });
148
+ export function removeAccountFromTui(
149
+ api: ToastApi,
150
+ state: BalancerTuiState,
151
+ providerID: string,
152
+ alias: string,
153
+ ) {
154
+ const removed = removeAccount(state.db, providerID, alias);
155
+ if (!removed) {
156
+ api.ui.toast({
157
+ message: `Account not found: ${providerID}/${alias}`,
158
+ variant: "error",
159
+ });
160
+ return;
161
+ }
162
+
163
+ const message = `Removed account ${providerID}/${alias}.`;
164
+ appendEvent(state.db, {
165
+ alias,
166
+ message,
167
+ providerID,
168
+ type: "account_removed",
169
+ });
170
+ state.refresh();
171
+ state.removeAccountView(providerID, alias);
172
+ api.ui.toast({ message, variant: "success" });
125
173
  }
126
174
 
127
- export function renameAccountFromTui(api: ToastApi, state: BalancerTuiState, providerID: string, alias: string, nextAlias: string) {
128
- const account = renameAccount(state.db, providerID, alias, nextAlias);
129
- state.refresh();
130
- api.ui.toast({ variant: "success", message: `Renamed ${providerID}/${alias} to ${account.alias}.` });
131
- return account;
175
+ export function renameAccountFromTui(
176
+ api: ToastApi,
177
+ state: BalancerTuiState,
178
+ providerID: string,
179
+ alias: string,
180
+ nextAlias: string,
181
+ ) {
182
+ const account = renameAccount(state.db, providerID, alias, nextAlias);
183
+ state.refresh();
184
+ api.ui.toast({
185
+ message: `Renamed ${providerID}/${alias} to ${account.alias}.`,
186
+ variant: "success",
187
+ });
188
+ return account;
132
189
  }
133
190
 
134
- export function removePendingFromTui(api: ToastApi, state: BalancerTuiState, pendingID: string) {
135
- const removed = removePendingConnection(state.db, pendingID);
136
- if (!removed) {
137
- api.ui.toast({ variant: "error", message: "Pending connection not found." });
138
- return;
139
- }
140
-
141
- state.refresh();
142
- state.removePendingView(pendingID);
143
- api.ui.toast({ variant: "success", message: "Removed pending connection." });
191
+ export function removePendingFromTui(
192
+ api: ToastApi,
193
+ state: BalancerTuiState,
194
+ pendingID: string,
195
+ ) {
196
+ const removed = removePendingConnection(state.db, pendingID);
197
+ if (!removed) {
198
+ api.ui.toast({
199
+ message: "Pending connection not found.",
200
+ variant: "error",
201
+ });
202
+ return;
203
+ }
204
+
205
+ state.refresh();
206
+ state.removePendingView(pendingID);
207
+ api.ui.toast({ message: "Removed pending connection.", variant: "success" });
144
208
  }
@@ -1,56 +1,66 @@
1
1
  import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
2
2
  import { getBalancingEnabled, resolveActiveSelection } from "../core/priority";
3
+ import {
4
+ createNativeModelApplier,
5
+ type NativeModelApplier,
6
+ } from "./native-model-apply";
3
7
  import type { BalancerTuiState } from "./state";
4
- import { createNativeModelApplier, type NativeModelApplier } from "./native-model-apply";
5
8
 
6
9
  export type BalancerBarSyncDeps = {
7
- balancingEnabled: () => boolean;
8
- dialogOpen: () => boolean;
9
- activeSelection: () => { providerID: string; modelID: string } | undefined;
10
- modelTitle: (providerID: string, modelID: string) => string | undefined;
11
- apply: NativeModelApplier;
10
+ balancingEnabled: () => boolean;
11
+ dialogOpen: () => boolean;
12
+ activeSelection: () => { providerID: string; modelID: string } | undefined;
13
+ modelTitle: (providerID: string, modelID: string) => string | undefined;
14
+ apply: NativeModelApplier;
12
15
  };
13
16
 
14
17
  export function createBalancerBarSync(deps: BalancerBarSyncDeps) {
15
- let lastApplied: string | undefined;
16
- let applying = false;
17
-
18
- const maybeSync = async () => {
19
- if (!deps.balancingEnabled()) {
20
- lastApplied = undefined;
21
- return false;
22
- }
23
- if (applying || deps.dialogOpen()) return false;
24
-
25
- const selection = deps.activeSelection();
26
- if (!selection) return false;
27
-
28
- const key = `${selection.providerID}/${selection.modelID}`;
29
- if (key === lastApplied) return false;
30
-
31
- const title = deps.modelTitle(selection.providerID, selection.modelID) ?? selection.modelID;
32
- applying = true;
33
- try {
34
- const applied = await deps.apply(selection, title);
35
- if (applied) lastApplied = key;
36
- return applied;
37
- } finally {
38
- applying = false;
39
- }
40
- };
41
-
42
- return { maybeSync };
18
+ let lastApplied: string | undefined;
19
+ let applying = false;
20
+
21
+ const maybeSync = async () => {
22
+ if (!deps.balancingEnabled()) {
23
+ lastApplied = undefined;
24
+ return false;
25
+ }
26
+ if (applying || deps.dialogOpen()) return false;
27
+
28
+ const selection = deps.activeSelection();
29
+ if (!selection) return false;
30
+
31
+ const key = `${selection.providerID}/${selection.modelID}`;
32
+ if (key === lastApplied) return false;
33
+
34
+ const title =
35
+ deps.modelTitle(selection.providerID, selection.modelID) ??
36
+ selection.modelID;
37
+ applying = true;
38
+ try {
39
+ const applied = await deps.apply(selection, title);
40
+ if (applied) lastApplied = key;
41
+ return applied;
42
+ } finally {
43
+ applying = false;
44
+ }
45
+ };
46
+
47
+ return { maybeSync };
43
48
  }
44
49
 
45
- export function createTuiBalancerBarSync(api: TuiPluginApi, state: BalancerTuiState) {
46
- return createBalancerBarSync({
47
- balancingEnabled: () => getBalancingEnabled(state.db),
48
- dialogOpen: () => api.ui.dialog.open,
49
- activeSelection: () => resolveActiveSelection(state.db),
50
- modelTitle: (providerID, modelID) => {
51
- const provider = api.state.provider.find((item) => item.id === providerID);
52
- return provider?.models?.[modelID]?.name;
53
- },
54
- apply: createNativeModelApplier(api),
55
- });
50
+ export function createTuiBalancerBarSync(
51
+ api: TuiPluginApi,
52
+ state: BalancerTuiState,
53
+ ) {
54
+ return createBalancerBarSync({
55
+ activeSelection: () => resolveActiveSelection(state.db),
56
+ apply: createNativeModelApplier(api),
57
+ balancingEnabled: () => getBalancingEnabled(state.db),
58
+ dialogOpen: () => api.ui.dialog.open,
59
+ modelTitle: (providerID, modelID) => {
60
+ const provider = api.state.provider.find(
61
+ (item) => item.id === providerID,
62
+ );
63
+ return provider?.models?.[modelID]?.name;
64
+ },
65
+ });
56
66
  }