@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
@@ -5,25 +5,41 @@ import type { ProviderUsageSnapshot } from "../../core/usage/types";
5
5
  import { UsageBar } from "./usage-bar";
6
6
 
7
7
  export function snapshotPercent(snapshot: ProviderUsageSnapshot | undefined) {
8
- if (!snapshot || snapshot.confidence === "unavailable") return undefined;
9
- if (snapshot.usedPercent !== undefined) return snapshot.usedPercent;
10
- if (snapshot.usedTokens !== undefined && snapshot.remainingTokens !== undefined) {
11
- const total = snapshot.usedTokens + snapshot.remainingTokens;
12
- if (total > 0) return (snapshot.usedTokens / total) * 100;
13
- }
14
- return undefined;
8
+ if (!snapshot || snapshot.confidence === "unavailable") return undefined;
9
+ if (snapshot.usedPercent !== undefined) return snapshot.usedPercent;
10
+ if (
11
+ snapshot.usedTokens !== undefined &&
12
+ snapshot.remainingTokens !== undefined
13
+ ) {
14
+ const total = snapshot.usedTokens + snapshot.remainingTokens;
15
+ if (total > 0) return (snapshot.usedTokens / total) * 100;
16
+ }
17
+ return undefined;
15
18
  }
16
19
 
17
20
  export function snapshotLabel(snapshot: ProviderUsageSnapshot | undefined) {
18
- if (!snapshot) return "usage unavailable";
19
- if (snapshot.confidence === "unavailable") return snapshot.message;
20
- const parts: string[] = [];
21
- if (snapshot.usedTokens !== undefined) parts.push(`used ${snapshot.usedTokens}`);
22
- if (snapshot.remainingTokens !== undefined) parts.push(`remaining ${snapshot.remainingTokens}`);
23
- if (snapshot.planName) parts.push(snapshot.planName);
24
- return parts.length > 0 ? parts.join(" · ") : snapshot.message;
21
+ if (!snapshot) return "usage unavailable";
22
+ if (snapshot.confidence === "unavailable") return snapshot.message;
23
+ const parts: string[] = [];
24
+ if (snapshot.usedTokens !== undefined)
25
+ parts.push(`used ${snapshot.usedTokens}`);
26
+ if (snapshot.remainingTokens !== undefined)
27
+ parts.push(`remaining ${snapshot.remainingTokens}`);
28
+ if (snapshot.planName) parts.push(snapshot.planName);
29
+ return parts.length > 0 ? parts.join(" · ") : snapshot.message;
25
30
  }
26
31
 
27
- export function UsageSnapshotBar(props: { theme: TuiThemeCurrent; snapshot?: ProviderUsageSnapshot; muted?: boolean }) {
28
- return <UsageBar theme={props.theme} percent={snapshotPercent(props.snapshot)} label={snapshotLabel(props.snapshot)} muted={props.muted} />;
32
+ export function UsageSnapshotBar(props: {
33
+ theme: TuiThemeCurrent;
34
+ snapshot?: ProviderUsageSnapshot;
35
+ muted?: boolean;
36
+ }) {
37
+ return (
38
+ <UsageBar
39
+ label={snapshotLabel(props.snapshot)}
40
+ muted={props.muted}
41
+ percent={snapshotPercent(props.snapshot)}
42
+ theme={props.theme}
43
+ />
44
+ );
29
45
  }
@@ -1,102 +1,133 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { listAccounts, saveAccount } from "../core/accounts";
3
- import { clearNativeConnectInProgress, markNativeConnectInProgress } from "../core/native-connect";
4
- import { readNativeAuth, sameSavedAuth } from "../server/auth-watcher";
3
+ import {
4
+ clearNativeConnectInProgress,
5
+ markNativeConnectInProgress,
6
+ } from "../core/native-connect";
5
7
  import type { AuthInfo } from "../core/types";
8
+ import { readNativeAuth, sameSavedAuth } from "../server/auth-watcher";
6
9
 
7
10
  type NativeAuthReadResult =
8
- | { ok: true; auth: Record<string, AuthInfo> }
9
- | { ok: false };
11
+ | { ok: true; auth: Record<string, AuthInfo> }
12
+ | { ok: false };
10
13
 
11
14
  type ConnectApi = {
12
- db?: Database;
13
- readAuth?: () => NativeAuthReadResult;
14
- generateAlias?: () => string;
15
- wait?: (ms: number) => Promise<void>;
16
- maxWaitMs?: number;
17
- pollIntervalMs?: number;
18
- keymap?: {
19
- dispatchCommand?: (command: string) => unknown;
20
- };
21
- ui?: {
22
- dialog?: { open?: boolean };
23
- toast?: (input: { variant: "success" | "error"; message: string }) => unknown;
24
- };
15
+ db?: Database;
16
+ readAuth?: () => NativeAuthReadResult;
17
+ generateAlias?: () => string;
18
+ wait?: (ms: number) => Promise<void>;
19
+ maxWaitMs?: number;
20
+ pollIntervalMs?: number;
21
+ keymap?: {
22
+ dispatchCommand?: (command: string) => unknown;
23
+ };
24
+ ui?: {
25
+ dialog?: { open?: boolean };
26
+ toast?: (input: {
27
+ variant: "success" | "error";
28
+ message: string;
29
+ }) => unknown;
30
+ };
25
31
  };
26
32
 
27
33
  const aliasAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
28
34
 
29
35
  function generatedAlias() {
30
- let alias = "";
31
- for (let index = 0; index < 5; index++) alias += aliasAlphabet[Math.floor(Math.random() * aliasAlphabet.length)];
32
- return alias;
36
+ let alias = "";
37
+ for (let index = 0; index < 5; index++)
38
+ alias += aliasAlphabet[Math.floor(Math.random() * aliasAlphabet.length)];
39
+ return alias;
33
40
  }
34
41
 
35
42
  function authKey(auth: AuthInfo | undefined) {
36
- return JSON.stringify(auth ?? null);
43
+ return JSON.stringify(auth ?? null);
37
44
  }
38
45
 
39
- function changedProvider(before: Record<string, AuthInfo>, after: Record<string, AuthInfo>) {
40
- return Object.entries(after).find(([providerID, auth]) => authKey(before[providerID]) !== authKey(auth));
46
+ function changedProvider(
47
+ before: Record<string, AuthInfo>,
48
+ after: Record<string, AuthInfo>,
49
+ ) {
50
+ return Object.entries(after).find(
51
+ ([providerID, auth]) => authKey(before[providerID]) !== authKey(auth),
52
+ );
41
53
  }
42
54
 
43
55
  function uniqueAlias(db: Database, providerID: string, generate: () => string) {
44
- for (let attempt = 0; attempt < 100; attempt++) {
45
- const alias = generate();
46
- const existing = db
47
- .query<{ alias: string }, [string, string]>("SELECT alias FROM accounts WHERE provider_id = ? AND alias = ?")
48
- .get(providerID, alias);
49
- if (!existing) return alias;
50
- }
51
- throw new Error("Could not generate a unique alias");
56
+ for (let attempt = 0; attempt < 100; attempt++) {
57
+ const alias = generate();
58
+ const existing = db
59
+ .query<{ alias: string }, [string, string]>(
60
+ "SELECT alias FROM accounts WHERE provider_id = ? AND alias = ?",
61
+ )
62
+ .get(providerID, alias);
63
+ if (!existing) return alias;
64
+ }
65
+ throw new Error("Could not generate a unique alias");
52
66
  }
53
67
 
54
68
  async function waitForChangedProvider(
55
- readAuth: () => NativeAuthReadResult,
56
- before: NativeAuthReadResult,
57
- options: { wait: (ms: number) => Promise<void>; maxWaitMs: number; pollIntervalMs: number },
69
+ readAuth: () => NativeAuthReadResult,
70
+ before: NativeAuthReadResult,
71
+ options: {
72
+ wait: (ms: number) => Promise<void>;
73
+ maxWaitMs: number;
74
+ pollIntervalMs: number;
75
+ },
58
76
  ) {
59
- const started = Date.now();
60
- while (true) {
61
- const after = readAuth();
62
- if (before.ok && after.ok) {
63
- const changed = changedProvider(before.auth, after.auth);
64
- if (changed) return changed;
65
- }
66
- if (Date.now() - started >= options.maxWaitMs) return;
67
- await options.wait(options.pollIntervalMs);
68
- }
77
+ const started = Date.now();
78
+ while (true) {
79
+ const after = readAuth();
80
+ if (before.ok && after.ok) {
81
+ const changed = changedProvider(before.auth, after.auth);
82
+ if (changed) return changed;
83
+ }
84
+ if (Date.now() - started >= options.maxWaitMs) return;
85
+ await options.wait(options.pollIntervalMs);
86
+ }
69
87
  }
70
88
 
71
89
  export async function openNativeConnect(api: ConnectApi) {
72
- if (api.keymap?.dispatchCommand) {
73
- const readAuth = api.readAuth ?? readNativeAuth;
74
- const before = readAuth();
75
- if (api.db) markNativeConnectInProgress(api.db);
76
- await api.keymap.dispatchCommand("provider.connect");
77
- if (api.db) {
78
- try {
79
- const changed = await waitForChangedProvider(readAuth, before, {
80
- wait: api.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
81
- maxWaitMs: api.maxWaitMs ?? (api.ui?.dialog ? 10 * 60 * 1000 : 0),
82
- pollIntervalMs: api.pollIntervalMs ?? 500,
83
- });
84
- if (changed) {
85
- const [providerID, auth] = changed;
86
- const existing = listAccounts(api.db, providerID).find((candidate) => sameSavedAuth(candidate.auth, auth));
87
- const alias = existing?.alias ?? uniqueAlias(api.db, providerID, api.generateAlias ?? generatedAlias);
88
- const account = saveAccount(api.db, providerID, alias, auth);
89
- api.ui?.toast?.({ variant: "success", message: `Saved ${account.providerID}/${account.alias}.` });
90
- }
91
- } finally {
92
- clearNativeConnectInProgress(api.db);
93
- }
94
- }
95
- return;
96
- }
90
+ if (api.keymap?.dispatchCommand) {
91
+ const readAuth = api.readAuth ?? readNativeAuth;
92
+ const before = readAuth();
93
+ if (api.db) markNativeConnectInProgress(api.db);
94
+ await api.keymap.dispatchCommand("provider.connect");
95
+ if (api.db) {
96
+ try {
97
+ const changed = await waitForChangedProvider(readAuth, before, {
98
+ maxWaitMs: api.maxWaitMs ?? (api.ui?.dialog ? 10 * 60 * 1000 : 0),
99
+ pollIntervalMs: api.pollIntervalMs ?? 500,
100
+ wait:
101
+ api.wait ??
102
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
103
+ });
104
+ if (changed) {
105
+ const [providerID, auth] = changed;
106
+ const existing = listAccounts(api.db, providerID).find((candidate) =>
107
+ sameSavedAuth(candidate.auth, auth),
108
+ );
109
+ const alias =
110
+ existing?.alias ??
111
+ uniqueAlias(
112
+ api.db,
113
+ providerID,
114
+ api.generateAlias ?? generatedAlias,
115
+ );
116
+ const account = saveAccount(api.db, providerID, alias, auth);
117
+ api.ui?.toast?.({
118
+ message: `Saved ${account.providerID}/${account.alias}.`,
119
+ variant: "success",
120
+ });
121
+ }
122
+ } finally {
123
+ clearNativeConnectInProgress(api.db);
124
+ }
125
+ }
126
+ return;
127
+ }
97
128
 
98
- api.ui?.toast?.({
99
- variant: "error",
100
- message: "Native provider connect is unavailable in this opencode build.",
101
- });
129
+ api.ui?.toast?.({
130
+ message: "Native provider connect is unavailable in this opencode build.",
131
+ variant: "error",
132
+ });
102
133
  }
@@ -1,60 +1,72 @@
1
1
  export type DashboardKey = {
2
- name: string;
2
+ name: string;
3
3
  };
4
4
 
5
5
  export type DashboardIntent =
6
- | { type: "move-cursor"; delta: -1 | 1 }
7
- | { type: "move-header"; delta: -1 | 1 }
8
- | { type: "primary" }
9
- | { type: "priority" }
10
- | { type: "connect" }
11
- | { type: "alias" }
12
- | { type: "rename" }
13
- | { type: "remove" }
14
- | { type: "confirm" }
15
- | { type: "cancel" }
16
- | { type: "back" }
17
- | { type: "none" };
6
+ | { type: "move-cursor"; delta: -1 | 1 }
7
+ | { type: "move-header"; delta: -1 | 1 }
8
+ | { type: "primary" }
9
+ | { type: "priority" }
10
+ | { type: "connect" }
11
+ | { type: "alias" }
12
+ | { type: "rename" }
13
+ | { type: "remove" }
14
+ | { type: "confirm" }
15
+ | { type: "cancel" }
16
+ | { type: "back" }
17
+ | { type: "none" };
18
18
 
19
19
  export type DashboardFocusArea = "header" | "content";
20
20
 
21
21
  export function moveDashboardFocus(
22
- state: { area: DashboardFocusArea; cursor: number; rowCount: number },
23
- delta: -1 | 1,
22
+ state: { area: DashboardFocusArea; cursor: number; rowCount: number },
23
+ delta: -1 | 1,
24
24
  ): { area: DashboardFocusArea; cursor: number } {
25
- if (state.area === "header") {
26
- return delta > 0 ? { area: "content", cursor: Math.max(0, Math.min(state.cursor, state.rowCount - 1)) } : { area: "header", cursor: state.cursor };
27
- }
25
+ if (state.area === "header") {
26
+ return delta > 0
27
+ ? {
28
+ area: "content",
29
+ cursor: Math.max(0, Math.min(state.cursor, state.rowCount - 1)),
30
+ }
31
+ : { area: "header", cursor: state.cursor };
32
+ }
28
33
 
29
- if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
34
+ if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
30
35
 
31
- return {
32
- area: "content",
33
- cursor: Math.max(0, Math.min(state.cursor + delta, Math.max(0, state.rowCount - 1))),
34
- };
36
+ return {
37
+ area: "content",
38
+ cursor: Math.max(
39
+ 0,
40
+ Math.min(state.cursor + delta, Math.max(0, state.rowCount - 1)),
41
+ ),
42
+ };
35
43
  }
36
44
 
37
45
  export function dashboardSelectionMarker(input: {
38
- focusedArea: DashboardFocusArea;
39
- itemArea: DashboardFocusArea;
40
- selected: boolean;
46
+ focusedArea: DashboardFocusArea;
47
+ itemArea: DashboardFocusArea;
48
+ selected: boolean;
41
49
  }) {
42
- return input.selected && input.focusedArea === input.itemArea ? "▶" : " ";
50
+ return input.selected && input.focusedArea === input.itemArea ? "▶" : " ";
43
51
  }
44
52
 
45
53
  export function reduceDashboardKey(key: DashboardKey): DashboardIntent {
46
- if (key.name === "up" || key.name === "k") return { type: "move-cursor", delta: -1 };
47
- if (key.name === "down" || key.name === "j") return { type: "move-cursor", delta: 1 };
48
- if (key.name === "left" || key.name === "h") return { type: "move-header", delta: -1 };
49
- if (key.name === "right" || key.name === "l") return { type: "move-header", delta: 1 };
50
- if (key.name === "return" || key.name === "space") return { type: "primary" };
51
- if (key.name === "p") return { type: "priority" };
52
- if (key.name === "c") return { type: "connect" };
53
- if (key.name === "a") return { type: "alias" };
54
- if (key.name === "r") return { type: "rename" };
55
- if (key.name === "d") return { type: "remove" };
56
- if (key.name === "y") return { type: "confirm" };
57
- if (key.name === "n") return { type: "cancel" };
58
- if (key.name === "escape") return { type: "back" };
59
- return { type: "none" };
54
+ if (key.name === "up" || key.name === "k")
55
+ return { delta: -1, type: "move-cursor" };
56
+ if (key.name === "down" || key.name === "j")
57
+ return { delta: 1, type: "move-cursor" };
58
+ if (key.name === "left" || key.name === "h")
59
+ return { delta: -1, type: "move-header" };
60
+ if (key.name === "right" || key.name === "l")
61
+ return { delta: 1, type: "move-header" };
62
+ if (key.name === "return" || key.name === "space") return { type: "primary" };
63
+ if (key.name === "p") return { type: "priority" };
64
+ if (key.name === "c") return { type: "connect" };
65
+ if (key.name === "a") return { type: "alias" };
66
+ if (key.name === "r") return { type: "rename" };
67
+ if (key.name === "d") return { type: "remove" };
68
+ if (key.name === "y") return { type: "confirm" };
69
+ if (key.name === "n") return { type: "cancel" };
70
+ if (key.name === "escape") return { type: "back" };
71
+ return { type: "none" };
60
72
  }
@@ -6,8 +6,9 @@ const ESCAPE = "\x1B";
6
6
  type StdinLike = { emit: (event: string, data: unknown) => unknown };
7
7
 
8
8
  function rendererStdin(api: TuiPluginApi): StdinLike | undefined {
9
- const stdin = (api.renderer as unknown as { stdin?: StdinLike } | undefined)?.stdin;
10
- return stdin && typeof stdin.emit === "function" ? stdin : undefined;
9
+ const stdin = (api.renderer as unknown as { stdin?: StdinLike } | undefined)
10
+ ?.stdin;
11
+ return stdin && typeof stdin.emit === "function" ? stdin : undefined;
11
12
  }
12
13
 
13
14
  /**
@@ -17,20 +18,23 @@ function rendererStdin(api: TuiPluginApi): StdinLike | undefined {
17
18
  * Solid instance and cannot touch opencode's reactive state directly.
18
19
  */
19
20
  function feed(api: TuiPluginApi, sequence: string): boolean {
20
- const stdin = rendererStdin(api);
21
- if (!stdin) return false;
22
- stdin.emit("data", Buffer.from(sequence));
23
- return true;
21
+ const stdin = rendererStdin(api);
22
+ if (!stdin) return false;
23
+ stdin.emit("data", Buffer.from(sequence));
24
+ return true;
24
25
  }
25
26
 
26
- export type NativeModelApplier = (model: { providerID: string; modelID: string }, title: string) => Promise<boolean>;
27
+ export type NativeModelApplier = (
28
+ model: { providerID: string; modelID: string },
29
+ title: string,
30
+ ) => Promise<boolean>;
27
31
 
28
32
  export type NativeModelApplyDeps = {
29
- dispatchCommand: (command: string) => void;
30
- isDialogOpen: () => boolean;
31
- feed: (sequence: string) => boolean;
32
- wait: (ms: number) => Promise<void>;
33
- settleMs?: number;
33
+ dispatchCommand: (command: string) => void;
34
+ isDialogOpen: () => boolean;
35
+ feed: (sequence: string) => boolean;
36
+ wait: (ms: number) => Promise<void>;
37
+ settleMs?: number;
34
38
  };
35
39
 
36
40
  /**
@@ -40,34 +44,39 @@ export type NativeModelApplyDeps = {
40
44
  * 3. press Enter to select -> opencode runs `local.model.set` and the bar updates
41
45
  * 4. dismiss a follow-up variant dialog if one appears
42
46
  */
43
- export async function applyNativeModelSelection(deps: NativeModelApplyDeps, title: string): Promise<boolean> {
44
- const settle = deps.settleMs ?? 90;
47
+ export async function applyNativeModelSelection(
48
+ deps: NativeModelApplyDeps,
49
+ title: string,
50
+ ): Promise<boolean> {
51
+ const settle = deps.settleMs ?? 90;
45
52
 
46
- deps.dispatchCommand("model.list");
47
- await deps.wait(settle);
48
- if (!deps.isDialogOpen()) return false;
53
+ deps.dispatchCommand("model.list");
54
+ await deps.wait(settle);
55
+ if (!deps.isDialogOpen()) return false;
49
56
 
50
- if (!deps.feed(title)) return false;
51
- await deps.wait(settle);
57
+ if (!deps.feed(title)) return false;
58
+ await deps.wait(settle);
52
59
 
53
- if (!deps.feed(ENTER)) return false;
54
- await deps.wait(settle);
60
+ if (!deps.feed(ENTER)) return false;
61
+ await deps.wait(settle);
55
62
 
56
- // A model with variants opens a follow-up dialog; close it so the bar keeps
57
- // the model we just set (with its default variant).
58
- if (deps.isDialogOpen()) deps.feed(ESCAPE);
59
- return true;
63
+ // A model with variants opens a follow-up dialog; close it so the bar keeps
64
+ // the model we just set (with its default variant).
65
+ if (deps.isDialogOpen()) deps.feed(ESCAPE);
66
+ return true;
60
67
  }
61
68
 
62
- export function createNativeModelApplier(api: TuiPluginApi): NativeModelApplier {
63
- return (_model, title) =>
64
- applyNativeModelSelection(
65
- {
66
- dispatchCommand: (command) => api.keymap.dispatchCommand(command),
67
- isDialogOpen: () => api.ui.dialog.open,
68
- feed: (sequence) => feed(api, sequence),
69
- wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
70
- },
71
- title,
72
- );
69
+ export function createNativeModelApplier(
70
+ api: TuiPluginApi,
71
+ ): NativeModelApplier {
72
+ return (_model, title) =>
73
+ applyNativeModelSelection(
74
+ {
75
+ dispatchCommand: (command) => api.keymap.dispatchCommand(command),
76
+ feed: (sequence) => feed(api, sequence),
77
+ isDialogOpen: () => api.ui.dialog.open,
78
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
79
+ },
80
+ title,
81
+ );
73
82
  }
@@ -1,41 +1,49 @@
1
1
  export type PriorityKey = {
2
- name: string;
3
- shift?: boolean;
2
+ name: string;
3
+ shift?: boolean;
4
4
  };
5
5
 
6
6
  export type PriorityIntent =
7
- | { type: "move-cursor"; delta: -1 | 1 }
8
- | { type: "reorder"; direction: -1 | 1 }
9
- | { type: "toggle-enabled" }
10
- | { type: "open-model" }
11
- | { type: "toggle-balancing" }
12
- | { type: "back" }
13
- | { type: "none" };
7
+ | { type: "move-cursor"; delta: -1 | 1 }
8
+ | { type: "reorder"; direction: -1 | 1 }
9
+ | { type: "toggle-enabled" }
10
+ | { type: "open-model" }
11
+ | { type: "toggle-balancing" }
12
+ | { type: "back" }
13
+ | { type: "none" };
14
14
 
15
15
  export type PriorityFocusArea = "header" | "content";
16
16
 
17
17
  export function movePriorityFocus(
18
- state: { area: PriorityFocusArea; cursor: number; rowCount: number },
19
- delta: -1 | 1,
18
+ state: { area: PriorityFocusArea; cursor: number; rowCount: number },
19
+ delta: -1 | 1,
20
20
  ): { area: PriorityFocusArea; cursor: number } {
21
- if (state.area === "header") {
22
- return delta > 0 ? { area: "content", cursor: Math.max(0, Math.min(state.cursor, state.rowCount - 1)) } : { area: "header", cursor: state.cursor };
23
- }
21
+ if (state.area === "header") {
22
+ return delta > 0
23
+ ? {
24
+ area: "content",
25
+ cursor: Math.max(0, Math.min(state.cursor, state.rowCount - 1)),
26
+ }
27
+ : { area: "header", cursor: state.cursor };
28
+ }
24
29
 
25
- if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
30
+ if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
26
31
 
27
- return {
28
- area: "content",
29
- cursor: Math.max(0, Math.min(state.cursor + delta, Math.max(0, state.rowCount - 1))),
30
- };
32
+ return {
33
+ area: "content",
34
+ cursor: Math.max(
35
+ 0,
36
+ Math.min(state.cursor + delta, Math.max(0, state.rowCount - 1)),
37
+ ),
38
+ };
31
39
  }
32
40
 
33
41
  export function prioritySelectionMarker(input: {
34
- focusedArea: PriorityFocusArea;
35
- itemArea: PriorityFocusArea;
36
- selected: boolean;
42
+ focusedArea: PriorityFocusArea;
43
+ itemArea: PriorityFocusArea;
44
+ selected: boolean;
37
45
  }) {
38
- return input.selected && input.focusedArea === input.itemArea ? "▶" : " ";
46
+ return input.selected && input.focusedArea === input.itemArea ? "▶" : " ";
39
47
  }
40
48
 
41
49
  /**
@@ -43,17 +51,17 @@ export function prioritySelectionMarker(input: {
43
51
  * rendering so the whole keyboard contract is testable without a terminal.
44
52
  */
45
53
  export function reducePriorityKey(key: PriorityKey): PriorityIntent {
46
- const name = key.name;
47
-
48
- if ((name === "up" || name === "down") && key.shift) {
49
- return { type: "reorder", direction: name === "up" ? -1 : 1 };
50
- }
51
- if (name === "up" || name === "k") return { type: "move-cursor", delta: -1 };
52
- if (name === "down" || name === "j") return { type: "move-cursor", delta: 1 };
53
- if (name === "space") return { type: "toggle-enabled" };
54
- if (name === "return") return { type: "open-model" };
55
- if (name === "b") return { type: "toggle-balancing" };
56
- if (name === "escape") return { type: "back" };
57
-
58
- return { type: "none" };
54
+ const name = key.name;
55
+
56
+ if ((name === "up" || name === "down") && key.shift) {
57
+ return { direction: name === "up" ? -1 : 1, type: "reorder" };
58
+ }
59
+ if (name === "up" || name === "k") return { delta: -1, type: "move-cursor" };
60
+ if (name === "down" || name === "j") return { delta: 1, type: "move-cursor" };
61
+ if (name === "space") return { type: "toggle-enabled" };
62
+ if (name === "return") return { type: "open-model" };
63
+ if (name === "b") return { type: "toggle-balancing" };
64
+ if (name === "escape") return { type: "back" };
65
+
66
+ return { type: "none" };
59
67
  }