@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.
- package/INSTALL.txt +53 -25
- package/README.md +95 -51
- package/dist/core/accounts.ts +404 -0
- package/dist/core/database.ts +67 -0
- package/dist/core/events.ts +75 -0
- package/dist/core/native-auth-suppression.ts +36 -0
- package/dist/core/native-connect.ts +31 -0
- package/dist/core/path.ts +34 -0
- package/dist/core/pending.ts +351 -0
- package/dist/core/priority.ts +193 -0
- package/dist/core/schema.ts +439 -0
- package/dist/core/time.ts +3 -0
- package/dist/core/types.ts +72 -0
- package/dist/core/usage/index.ts +23 -0
- package/dist/core/usage/providers/copilot.ts +243 -0
- package/dist/core/usage/providers/openai.ts +179 -0
- package/dist/core/usage/redact.ts +80 -0
- package/dist/core/usage/store.ts +66 -0
- package/dist/core/usage/types.ts +24 -0
- package/dist/index.js +173 -4
- package/dist/index.js.map +1 -1
- package/dist/server/auth-watcher.ts +318 -0
- package/dist/server/commands.ts +58 -0
- package/dist/server/fetch-patch.ts +162 -0
- package/dist/server/index.ts +134 -0
- package/dist/server/native.ts +49 -0
- package/dist/server/request-balancer.ts +67 -0
- package/dist/tui/actions.ts +176 -112
- package/dist/tui/balancer-bar-sync.ts +55 -45
- package/dist/tui/components/alias-dialog.tsx +71 -56
- package/dist/tui/components/dashboard.tsx +530 -358
- package/dist/tui/components/priority-screen.tsx +389 -267
- package/dist/tui/components/provider-model-dialog.tsx +71 -64
- package/dist/tui/components/rename-dialog.tsx +35 -28
- package/dist/tui/components/sidebar.tsx +103 -79
- package/dist/tui/components/status-indicator.tsx +78 -59
- package/dist/tui/components/usage-bar.tsx +18 -7
- package/dist/tui/components/usage-display.tsx +32 -16
- package/dist/tui/connect.ts +104 -73
- package/dist/tui/dashboard-keys.ts +53 -41
- package/dist/tui/native-model-apply.ts +45 -36
- package/dist/tui/priority-keys.ts +44 -36
- package/dist/tui/provider-models.ts +32 -25
- package/dist/tui/responsive.ts +10 -7
- package/dist/tui/selected-account-bar-sync.ts +23 -23
- package/dist/tui/selection-colors.ts +38 -30
- package/dist/tui/state.ts +61 -44
- package/dist/tui/status-format.ts +24 -20
- package/dist/tui/tui.js +165 -153
- package/dist/tui/tui.js.map +1 -1
- package/dist/tui/tui.tsx +194 -144
- package/dist/tui/usage-auto-refresh.ts +52 -45
- package/dist/tui/usage-format.ts +9 -9
- package/package.json +61 -52
- package/dist/core/accounts.d.ts +0 -14
- package/dist/core/accounts.js +0 -260
- package/dist/core/accounts.js.map +0 -1
- package/dist/core/database.d.ts +0 -4
- package/dist/core/database.js +0 -69
- package/dist/core/database.js.map +0 -1
- package/dist/core/events.d.ts +0 -18
- package/dist/core/events.js +0 -39
- package/dist/core/events.js.map +0 -1
- package/dist/core/native-auth-suppression.d.ts +0 -3
- package/dist/core/native-auth-suppression.js +0 -19
- package/dist/core/native-auth-suppression.js.map +0 -1
- package/dist/core/native-connect.d.ts +0 -4
- package/dist/core/native-connect.js +0 -19
- package/dist/core/native-connect.js.map +0 -1
- package/dist/core/path.d.ts +0 -4
- package/dist/core/path.js +0 -26
- package/dist/core/path.js.map +0 -1
- package/dist/core/pending.d.ts +0 -9
- package/dist/core/pending.js +0 -237
- package/dist/core/pending.js.map +0 -1
- package/dist/core/priority.d.ts +0 -20
- package/dist/core/priority.js +0 -120
- package/dist/core/priority.js.map +0 -1
- package/dist/core/schema.d.ts +0 -2
- package/dist/core/schema.js +0 -265
- package/dist/core/schema.js.map +0 -1
- package/dist/core/time.d.ts +0 -1
- package/dist/core/time.js +0 -4
- package/dist/core/time.js.map +0 -1
- package/dist/core/types.d.ts +0 -59
- package/dist/core/types.js +0 -2
- package/dist/core/types.js.map +0 -1
- package/dist/core/usage/index.d.ts +0 -4
- package/dist/core/usage/index.js +0 -16
- package/dist/core/usage/index.js.map +0 -1
- package/dist/core/usage/providers/copilot.d.ts +0 -2
- package/dist/core/usage/providers/copilot.js +0 -169
- package/dist/core/usage/providers/copilot.js.map +0 -1
- package/dist/core/usage/providers/openai.d.ts +0 -2
- package/dist/core/usage/providers/openai.js +0 -133
- package/dist/core/usage/providers/openai.js.map +0 -1
- package/dist/core/usage/redact.d.ts +0 -3
- package/dist/core/usage/redact.js +0 -67
- package/dist/core/usage/redact.js.map +0 -1
- package/dist/core/usage/store.d.ts +0 -4
- package/dist/core/usage/store.js +0 -31
- package/dist/core/usage/store.js.map +0 -1
- package/dist/core/usage/types.d.ts +0 -21
- package/dist/core/usage/types.js +0 -2
- package/dist/core/usage/types.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/server/auth-watcher.d.ts +0 -32
- package/dist/server/auth-watcher.js +0 -227
- package/dist/server/auth-watcher.js.map +0 -1
- package/dist/server/commands.d.ts +0 -2
- package/dist/server/commands.js +0 -46
- package/dist/server/commands.js.map +0 -1
- package/dist/server/fetch-patch.d.ts +0 -3
- package/dist/server/fetch-patch.js +0 -118
- package/dist/server/fetch-patch.js.map +0 -1
- package/dist/server/index.d.ts +0 -8
- package/dist/server/index.js +0 -94
- package/dist/server/index.js.map +0 -1
- package/dist/server/native.d.ts +0 -6
- package/dist/server/native.js +0 -35
- package/dist/server/native.js.map +0 -1
- package/dist/server/request-balancer.d.ts +0 -16
- package/dist/server/request-balancer.js +0 -43
- package/dist/server/request-balancer.js.map +0 -1
- package/dist/tui/actions.d.ts +0 -41
- package/dist/tui/actions.js +0 -92
- package/dist/tui/actions.js.map +0 -1
- package/dist/tui/balancer-bar-sync.d.ts +0 -19
- package/dist/tui/balancer-bar-sync.js +0 -45
- package/dist/tui/balancer-bar-sync.js.map +0 -1
- package/dist/tui/components/alias-dialog.d.ts +0 -4
- package/dist/tui/components/dashboard.d.ts +0 -12
- package/dist/tui/components/priority-screen.d.ts +0 -9
- package/dist/tui/components/provider-model-dialog.d.ts +0 -14
- package/dist/tui/components/rename-dialog.d.ts +0 -4
- package/dist/tui/components/sidebar.d.ts +0 -10
- package/dist/tui/components/status-indicator.d.ts +0 -9
- package/dist/tui/components/usage-bar.d.ts +0 -8
- package/dist/tui/components/usage-display.d.ts +0 -10
- package/dist/tui/connect.d.ts +0 -30
- package/dist/tui/connect.js +0 -75
- package/dist/tui/connect.js.map +0 -1
- package/dist/tui/dashboard-keys.d.ts +0 -45
- package/dist/tui/dashboard-keys.js +0 -44
- package/dist/tui/dashboard-keys.js.map +0 -1
- package/dist/tui/native-model-apply.d.ts +0 -21
- package/dist/tui/native-model-apply.js +0 -53
- package/dist/tui/native-model-apply.js.map +0 -1
- package/dist/tui/priority-keys.d.ts +0 -40
- package/dist/tui/priority-keys.js +0 -38
- package/dist/tui/priority-keys.js.map +0 -1
- package/dist/tui/provider-models.d.ts +0 -19
- package/dist/tui/provider-models.js +0 -17
- package/dist/tui/provider-models.js.map +0 -1
- package/dist/tui/responsive.d.ts +0 -9
- package/dist/tui/responsive.js +0 -13
- package/dist/tui/responsive.js.map +0 -1
- package/dist/tui/selected-account-bar-sync.d.ts +0 -10
- package/dist/tui/selected-account-bar-sync.js +0 -26
- package/dist/tui/selected-account-bar-sync.js.map +0 -1
- package/dist/tui/selection-colors.d.ts +0 -10
- package/dist/tui/selection-colors.js +0 -38
- package/dist/tui/selection-colors.js.map +0 -1
- package/dist/tui/state.d.ts +0 -14
- package/dist/tui/state.js +0 -46
- package/dist/tui/state.js.map +0 -1
- package/dist/tui/status-format.d.ts +0 -15
- package/dist/tui/status-format.js +0 -17
- package/dist/tui/status-format.js.map +0 -1
- package/dist/tui/tui.d.ts +0 -7
- package/dist/tui/usage-auto-refresh.d.ts +0 -16
- package/dist/tui/usage-auto-refresh.js +0 -46
- package/dist/tui/usage-auto-refresh.js.map +0 -1
- package/dist/tui/usage-format.d.ts +0 -2
- package/dist/tui/usage-format.js +0 -17
- 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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: {
|
|
28
|
-
|
|
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
|
}
|
package/dist/tui/connect.ts
CHANGED
|
@@ -1,102 +1,133 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import { listAccounts, saveAccount } from "../core/accounts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
11
|
+
| { ok: true; auth: Record<string, AuthInfo> }
|
|
12
|
+
| { ok: false };
|
|
10
13
|
|
|
11
14
|
type ConnectApi = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
43
|
+
return JSON.stringify(auth ?? null);
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
function changedProvider(
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
readAuth: () => NativeAuthReadResult,
|
|
70
|
+
before: NativeAuthReadResult,
|
|
71
|
+
options: {
|
|
72
|
+
wait: (ms: number) => Promise<void>;
|
|
73
|
+
maxWaitMs: number;
|
|
74
|
+
pollIntervalMs: number;
|
|
75
|
+
},
|
|
58
76
|
) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
2
|
+
name: string;
|
|
3
3
|
};
|
|
4
4
|
|
|
5
5
|
export type DashboardIntent =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
22
|
+
state: { area: DashboardFocusArea; cursor: number; rowCount: number },
|
|
23
|
+
delta: -1 | 1,
|
|
24
24
|
): { area: DashboardFocusArea; cursor: number } {
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
+
if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
focusedArea: DashboardFocusArea;
|
|
47
|
+
itemArea: DashboardFocusArea;
|
|
48
|
+
selected: boolean;
|
|
41
49
|
}) {
|
|
42
|
-
|
|
50
|
+
return input.selected && input.focusedArea === input.itemArea ? "▶" : " ";
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export function reduceDashboardKey(key: DashboardKey): DashboardIntent {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 = (
|
|
27
|
+
export type NativeModelApplier = (
|
|
28
|
+
model: { providerID: string; modelID: string },
|
|
29
|
+
title: string,
|
|
30
|
+
) => Promise<boolean>;
|
|
27
31
|
|
|
28
32
|
export type NativeModelApplyDeps = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
44
|
-
|
|
47
|
+
export async function applyNativeModelSelection(
|
|
48
|
+
deps: NativeModelApplyDeps,
|
|
49
|
+
title: string,
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
const settle = deps.settleMs ?? 90;
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
deps.dispatchCommand("model.list");
|
|
54
|
+
await deps.wait(settle);
|
|
55
|
+
if (!deps.isDialogOpen()) return false;
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
if (!deps.feed(title)) return false;
|
|
58
|
+
await deps.wait(settle);
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
if (!deps.feed(ENTER)) return false;
|
|
61
|
+
await deps.wait(settle);
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
3
|
-
|
|
2
|
+
name: string;
|
|
3
|
+
shift?: boolean;
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
export type PriorityIntent =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
18
|
+
state: { area: PriorityFocusArea; cursor: number; rowCount: number },
|
|
19
|
+
delta: -1 | 1,
|
|
20
20
|
): { area: PriorityFocusArea; cursor: number } {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
+
if (delta < 0 && state.cursor <= 0) return { area: "header", cursor: 0 };
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
focusedArea: PriorityFocusArea;
|
|
43
|
+
itemArea: PriorityFocusArea;
|
|
44
|
+
selected: boolean;
|
|
37
45
|
}) {
|
|
38
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|