claude-accounts-usage 0.1.2 → 0.1.3
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/README.md +3 -3
- package/package.json +1 -1
- package/src/dialogs.tsx +127 -15
- package/tui.tsx +3 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| 命令 | 作用 |
|
|
10
10
|
|------|------|
|
|
11
11
|
| `/usage` | 弹框显示所有账号的用量(5h / 7d / 7d-Sonnet 三个窗口,带进度条与重置倒计时) |
|
|
12
|
-
| `/switch` |
|
|
12
|
+
| `/switch` | 弹框选择账号并切换(立即生效);列表内联显示每个账号的 5h / 7d / 7d-Sonnet 用量,`↑↓` 选择、`enter` 切换、`esc` 关闭 |
|
|
13
13
|
|
|
14
14
|
账号会在**插件加载时**以及每次 `/usage`、`/switch` 时**自动收录**当前 ex-machina 登录的账号,无需手动添加。
|
|
15
15
|
|
|
@@ -41,13 +41,13 @@ TUI 插件只在 `~/.config/opencode/tui.json` 配置,**不要**放进 `opencode
|
|
|
41
41
|
```json
|
|
42
42
|
{
|
|
43
43
|
"$schema": "https://opencode.ai/tui.json",
|
|
44
|
-
"plugin": ["claude-accounts-usage@0.1.
|
|
44
|
+
"plugin": ["claude-accounts-usage@0.1.3"]
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
OpenCode 会自动解析并安装该包,无需手动 `npm install`。
|
|
49
49
|
|
|
50
|
-
> **建议带上版本号**(如 `@0.1.
|
|
50
|
+
> **建议带上版本号**(如 `@0.1.3`)。OpenCode 按"含版本号的包名"建独立缓存目录:写死版本号后,以后升级只需把后缀改成新版本号;若不带版本号,会被首次安装的版本锁住,发布新版也不会自动更新。
|
|
51
51
|
|
|
52
52
|
### 方式二:本地 clone(开发/离线)
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-accounts-usage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenCode TUI plugin to view multi-account Claude usage and switch the active account. Coexists with @ex-machina/opencode-anthropic-auth.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/dialogs.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For, Show } from "solid-js"
|
|
2
|
+
import { createMemo, createSignal, For, Show } from "solid-js"
|
|
3
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
4
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
4
5
|
import type { StoredAccount } from "./accounts.ts"
|
|
5
6
|
import type { AccountUsage, UsageWindow } from "./usage.ts"
|
|
@@ -115,24 +116,135 @@ export function openUsageDialog(api: TuiPluginApi, state: () => UsageState): voi
|
|
|
115
116
|
api.ui.dialog.replace(() => <UsageDialog api={api} state={state} />)
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
function SwitchAccountRow(props: {
|
|
120
|
+
api: TuiPluginApi
|
|
121
|
+
account: StoredAccount
|
|
122
|
+
activeId?: string
|
|
123
|
+
usage?: AccountUsage
|
|
124
|
+
selected: boolean
|
|
125
|
+
loading: boolean
|
|
126
|
+
}) {
|
|
127
|
+
const theme = () => props.api.theme.current
|
|
128
|
+
const isActive = () => props.account.id === props.activeId
|
|
129
|
+
return (
|
|
130
|
+
<box flexDirection="column">
|
|
131
|
+
<box flexDirection="row" gap={1}>
|
|
132
|
+
<text fg={props.selected ? theme().primary : theme().textMuted}>{props.selected ? "▶" : " "}</text>
|
|
133
|
+
<text fg={props.selected ? theme().primary : theme().text}>
|
|
134
|
+
{isActive() ? "●" : "○"} {props.account.label}
|
|
135
|
+
{isActive() ? " (当前)" : ""}
|
|
136
|
+
</text>
|
|
137
|
+
</box>
|
|
138
|
+
<box flexDirection="column" paddingLeft={4}>
|
|
139
|
+
<Show when={props.usage?.error}>
|
|
140
|
+
<text fg={theme().error}>{props.usage!.error}</text>
|
|
141
|
+
</Show>
|
|
142
|
+
<Show when={props.usage?.usage}>
|
|
143
|
+
{(usage) => (
|
|
144
|
+
<box flexDirection="column">
|
|
145
|
+
<WindowRow api={props.api} name="5h" win={usage().five_hour} />
|
|
146
|
+
<WindowRow api={props.api} name="7d" win={usage().seven_day} />
|
|
147
|
+
<WindowRow api={props.api} name="Sonnet" win={usage().seven_day_sonnet} />
|
|
148
|
+
</box>
|
|
149
|
+
)}
|
|
150
|
+
</Show>
|
|
151
|
+
<Show when={props.loading && !props.usage?.usage && !props.usage?.error}>
|
|
152
|
+
<text fg={theme().textMuted}>加载中…</text>
|
|
153
|
+
</Show>
|
|
154
|
+
</box>
|
|
155
|
+
</box>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function SwitchDialog(props: {
|
|
160
|
+
api: TuiPluginApi
|
|
161
|
+
accounts: StoredAccount[]
|
|
162
|
+
activeId?: string
|
|
163
|
+
state: () => UsageState
|
|
164
|
+
onSwitch: (id: string) => void
|
|
165
|
+
}) {
|
|
166
|
+
const api = props.api
|
|
167
|
+
const theme = () => api.theme.current
|
|
168
|
+
const accounts = props.accounts
|
|
169
|
+
const start = accounts.findIndex((account) => account.id === props.activeId)
|
|
170
|
+
const [index, setIndex] = createSignal(start < 0 ? 0 : start)
|
|
171
|
+
|
|
172
|
+
const usageById = createMemo(() => {
|
|
173
|
+
const map = new Map<string, AccountUsage>()
|
|
174
|
+
for (const result of props.state().results) map.set(result.id, result)
|
|
175
|
+
return map
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
function move(delta: number): void {
|
|
179
|
+
setIndex((i) => Math.max(0, Math.min(accounts.length - 1, i + delta)))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function confirm(): void {
|
|
183
|
+
const account = accounts[index()]
|
|
184
|
+
if (!account) return
|
|
185
|
+
api.ui.dialog.clear()
|
|
186
|
+
props.onSwitch(account.id)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
useKeyboard((evt) => {
|
|
190
|
+
if (evt.name === "return") {
|
|
191
|
+
evt.preventDefault()
|
|
192
|
+
evt.stopPropagation()
|
|
193
|
+
confirm()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
197
|
+
evt.preventDefault()
|
|
198
|
+
evt.stopPropagation()
|
|
199
|
+
move(-1)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
203
|
+
evt.preventDefault()
|
|
204
|
+
evt.stopPropagation()
|
|
205
|
+
move(1)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
|
211
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
212
|
+
<text fg={theme().text}>
|
|
213
|
+
<b>切换 Claude 账号</b>
|
|
214
|
+
</text>
|
|
215
|
+
<text fg={theme().textMuted}>↑↓ 选择 · enter 切换 · esc 关闭</text>
|
|
216
|
+
</box>
|
|
217
|
+
<For each={accounts}>
|
|
218
|
+
{(account, i) => (
|
|
219
|
+
<SwitchAccountRow
|
|
220
|
+
api={api}
|
|
221
|
+
account={account}
|
|
222
|
+
activeId={props.activeId}
|
|
223
|
+
usage={usageById().get(account.id)}
|
|
224
|
+
selected={i() === index()}
|
|
225
|
+
loading={props.state().loading}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</For>
|
|
229
|
+
<Show when={props.state().error}>
|
|
230
|
+
<text fg={theme().error}>{props.state().error}</text>
|
|
231
|
+
</Show>
|
|
232
|
+
<Show when={props.state().updatedAt}>
|
|
233
|
+
<text fg={theme().textMuted}>更新于 {clockTime(props.state().updatedAt!)}</text>
|
|
234
|
+
</Show>
|
|
235
|
+
</box>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
118
239
|
export function openSwitchDialog(
|
|
119
240
|
api: TuiPluginApi,
|
|
120
241
|
accounts: StoredAccount[],
|
|
121
242
|
activeId: string | undefined,
|
|
243
|
+
state: () => UsageState,
|
|
122
244
|
onSwitch: (id: string) => void | Promise<void>,
|
|
123
245
|
): void {
|
|
124
|
-
api.ui.dialog.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
options: accounts.map((account) => ({
|
|
129
|
-
title: account.id === activeId ? `${account.label} (当前)` : account.label,
|
|
130
|
-
value: account.id,
|
|
131
|
-
})),
|
|
132
|
-
onSelect: (option) => {
|
|
133
|
-
api.ui.dialog.clear()
|
|
134
|
-
void onSwitch(option.value)
|
|
135
|
-
},
|
|
136
|
-
}),
|
|
137
|
-
)
|
|
246
|
+
api.ui.dialog.setSize("medium")
|
|
247
|
+
api.ui.dialog.replace(() => (
|
|
248
|
+
<SwitchDialog api={api} accounts={accounts} activeId={activeId} state={state} onSwitch={(id) => void onSwitch(id)} />
|
|
249
|
+
))
|
|
138
250
|
}
|
package/tui.tsx
CHANGED
|
@@ -60,7 +60,8 @@ const tui: TuiPlugin = async (api) => {
|
|
|
60
60
|
api.ui.toast({ variant: "warning", message: "没有账号。请先用 ex-machina 登录 Claude" })
|
|
61
61
|
return
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
setState((prev) => ({ ...prev, loading: true, error: undefined }))
|
|
64
|
+
openSwitchDialog(api, file.accounts, file.activeId, state, async (id) => {
|
|
64
65
|
try {
|
|
65
66
|
const account = await switchToAccount(id)
|
|
66
67
|
api.ui.toast({ variant: "success", message: `已切换到 ${account.label},下次对话生效` })
|
|
@@ -68,6 +69,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
68
69
|
api.ui.toast({ variant: "error", message: `切换失败: ${message(error)}` })
|
|
69
70
|
}
|
|
70
71
|
})
|
|
72
|
+
void refreshUsage()
|
|
71
73
|
},
|
|
72
74
|
},
|
|
73
75
|
])
|