@cortexkit/opencode-openai-auth 0.5.2 → 0.6.0
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 +2 -1
- package/dist/cli.js +1 -1
- package/dist/commands.d.ts +16 -0
- package/dist/core/accounts.d.ts +22 -0
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/refresh-all-quota.d.ts +3 -0
- package/dist/core/reset-credits.d.ts +114 -0
- package/dist/index-87145jkw.js +2 -0
- package/dist/{index-rfgyxxxq.js → index-dz9a4xwy.js} +1 -1
- package/dist/{index-zkzb0twr.js → index-e08r33g5.js} +2 -2
- package/dist/{index-s5fbqqwx.js → index-hxpv25e8.js} +1 -1
- package/dist/index-meg0ng0z.js +2 -0
- package/dist/{index-vrdc90yd.js → index-n00ybbbq.js} +1 -1
- package/dist/{index-6rnw2cgs.js → index-nwy7ec7p.js} +1 -1
- package/dist/{index-wpd7mg3p.js → index-s2vhsb2v.js} +1 -1
- package/dist/index-zddpw2sa.js +89 -0
- package/dist/index.d.ts +30 -1
- package/dist/index.js +120 -70
- package/dist/quota-normalize-vf2pj6n7.js +1 -0
- package/dist/quota-normalize.d.ts +1 -0
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/rpc-client.d.ts +2 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/rpc/rpc-server.d.ts +1 -0
- package/dist/sidebar-state.d.ts +2 -0
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-5hdnazxp.js → token-esc14v6e.js} +1 -1
- package/dist/token-util-qkpkvy5t.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
- package/src/rpc/protocol.ts +1 -0
- package/src/rpc/rpc-client.ts +7 -3
- package/src/rpc/rpc-server.ts +24 -4
- package/src/sidebar-state.ts +62 -3
- package/src/tui/command-dialogs.tsx +245 -0
- package/src/tui-compiled/rpc/protocol.ts +1 -0
- package/src/tui-compiled/rpc/rpc-client.ts +7 -3
- package/src/tui-compiled/sidebar-state.ts +62 -3
- package/src/tui-compiled/tui/command-dialogs.tsx +162 -1
- package/src/tui-compiled/tui.tsx +1 -1
- package/src/tui-compiled/util/error.ts +9 -0
- package/src/tui.tsx +4 -1
- package/dist/index-2ng03cby.js +0 -2
- package/dist/index-qhvsskdz.js +0 -89
- package/dist/index-wnxfzjfr.js +0 -2
- package/dist/quota-normalize-n02dfdx0.js +0 -1
- package/dist/token-util-3d2nj45d.js +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
|
|
3
|
+
import { createLogger } from '../logger'
|
|
3
4
|
import type { OpenDialogPayload } from '../rpc/protocol.js'
|
|
4
5
|
import {
|
|
5
6
|
type AccountQuota,
|
|
@@ -8,8 +9,11 @@ import {
|
|
|
8
9
|
resolveSessionSidebarRouting,
|
|
9
10
|
type SidebarState,
|
|
10
11
|
} from '../sidebar-state.js'
|
|
12
|
+
import { errorMessage } from '../util/error'
|
|
11
13
|
import { openUrl } from '../util/open-url'
|
|
12
14
|
|
|
15
|
+
const log = createLogger('rpc-tui')
|
|
16
|
+
|
|
13
17
|
type ApplyFn = (
|
|
14
18
|
command: OpenDialogPayload['command'],
|
|
15
19
|
args: string,
|
|
@@ -104,6 +108,242 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
|
|
|
104
108
|
]
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
type ResetAccountKnob = {
|
|
112
|
+
accountKey: string
|
|
113
|
+
label: string
|
|
114
|
+
usedPercent?: number
|
|
115
|
+
availableCount?: number
|
|
116
|
+
applicableAvailableCount?: number
|
|
117
|
+
eligible: boolean
|
|
118
|
+
reason?: string
|
|
119
|
+
selectedCreditId?: string
|
|
120
|
+
selectedCreditExpiresAt?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type ResetPreviewKnob = ResetAccountKnob & {
|
|
124
|
+
chatgptAccountId?: string
|
|
125
|
+
resetTime?: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type ResetDialogOption = {
|
|
129
|
+
title: string
|
|
130
|
+
value: string
|
|
131
|
+
description: string
|
|
132
|
+
disabled?: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resetAccountOptions(payload: OpenDialogPayload): ResetDialogOption[] {
|
|
136
|
+
const accounts = Array.isArray(payload.knobs.accounts)
|
|
137
|
+
? (payload.knobs.accounts as ResetAccountKnob[])
|
|
138
|
+
: []
|
|
139
|
+
return [
|
|
140
|
+
...accounts.map((account) => {
|
|
141
|
+
const used =
|
|
142
|
+
account.usedPercent === undefined
|
|
143
|
+
? 'quota unavailable'
|
|
144
|
+
: `${account.usedPercent}%`
|
|
145
|
+
const counts = `${account.applicableAvailableCount ?? 0}/${account.availableCount ?? 0}`
|
|
146
|
+
const status = account.eligible
|
|
147
|
+
? 'eligible'
|
|
148
|
+
: (account.reason ?? 'unavailable')
|
|
149
|
+
const details = `${used} · ${counts}`
|
|
150
|
+
const shortDate =
|
|
151
|
+
account.selectedCreditExpiresAt?.slice(0, 10) ?? 'unknown'
|
|
152
|
+
return {
|
|
153
|
+
title: `${account.label} — ${status}`,
|
|
154
|
+
value: `account:${account.accountKey}`,
|
|
155
|
+
description: account.eligible
|
|
156
|
+
? `${details} · exp ${shortDate}`
|
|
157
|
+
: details,
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
{
|
|
161
|
+
title: 'Refresh',
|
|
162
|
+
value: 'refresh',
|
|
163
|
+
description: 'Re-check account quota and reset credits',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
title: 'Close',
|
|
167
|
+
value: 'close',
|
|
168
|
+
description: 'Close this dialog',
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resetResultOptions(payload: OpenDialogPayload): ResetDialogOption[] {
|
|
174
|
+
const accountKey =
|
|
175
|
+
typeof payload.knobs.accountKey === 'string'
|
|
176
|
+
? payload.knobs.accountKey
|
|
177
|
+
: undefined
|
|
178
|
+
const chatgptAccountId =
|
|
179
|
+
typeof payload.knobs.chatgptAccountId === 'string'
|
|
180
|
+
? payload.knobs.chatgptAccountId
|
|
181
|
+
: undefined
|
|
182
|
+
const code = typeof payload.knobs.code === 'string' ? payload.knobs.code : ''
|
|
183
|
+
const retrySafe =
|
|
184
|
+
(code === 'ambiguous' ||
|
|
185
|
+
code === 'http_error' ||
|
|
186
|
+
code === 'expired_unreconciled' ||
|
|
187
|
+
payload.knobs.stateWriteFailed === true) &&
|
|
188
|
+
typeof payload.knobs.retryGuidance === 'string' &&
|
|
189
|
+
accountKey !== undefined &&
|
|
190
|
+
chatgptAccountId !== undefined
|
|
191
|
+
return [
|
|
192
|
+
{
|
|
193
|
+
title: payload.text,
|
|
194
|
+
value: 'result',
|
|
195
|
+
description: 'Command result',
|
|
196
|
+
},
|
|
197
|
+
...(retrySafe
|
|
198
|
+
? [
|
|
199
|
+
{
|
|
200
|
+
title: 'Retry',
|
|
201
|
+
value: 'retry',
|
|
202
|
+
description:
|
|
203
|
+
'Reuse the bound request and credit identifiers for this uncertain outcome',
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
{
|
|
208
|
+
title: 'Refresh',
|
|
209
|
+
value: 'refresh',
|
|
210
|
+
description: 'Re-check account quota and reset credits',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
title: 'Close',
|
|
214
|
+
value: 'close',
|
|
215
|
+
description: 'Close this dialog',
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function openResetDialog(
|
|
221
|
+
api: TuiPluginApi,
|
|
222
|
+
payload: OpenDialogPayload,
|
|
223
|
+
apply: ApplyFn,
|
|
224
|
+
) {
|
|
225
|
+
let applyGeneration = 0
|
|
226
|
+
const render = (state: OpenDialogPayload) => {
|
|
227
|
+
const applyAndRender = (args: string) => {
|
|
228
|
+
const generation = ++applyGeneration
|
|
229
|
+
log.debug('reset dialog apply', { args, generation })
|
|
230
|
+
void apply('openai-reset', args)
|
|
231
|
+
.then((result) => {
|
|
232
|
+
log.debug('reset dialog apply result', {
|
|
233
|
+
generation,
|
|
234
|
+
currentGeneration: applyGeneration,
|
|
235
|
+
stage: result.knobs?.stage,
|
|
236
|
+
accountRows: Array.isArray(result.knobs?.accounts)
|
|
237
|
+
? (result.knobs.accounts as unknown[]).length
|
|
238
|
+
: undefined,
|
|
239
|
+
})
|
|
240
|
+
if (generation !== applyGeneration) return
|
|
241
|
+
if (typeof result.knobs?.stage !== 'string') {
|
|
242
|
+
render({
|
|
243
|
+
command: 'openai-reset',
|
|
244
|
+
text: 'The reset request is still completing or returned no result. Choose Refresh to check the current state — do NOT re-confirm.',
|
|
245
|
+
knobs: { stage: 'result', code: 'unknown' },
|
|
246
|
+
})
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
render({
|
|
250
|
+
command: 'openai-reset',
|
|
251
|
+
text: result.text,
|
|
252
|
+
knobs: result.knobs,
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
.catch((error) => {
|
|
256
|
+
if (generation !== applyGeneration) return
|
|
257
|
+
log.warn('reset dialog apply rejected', {
|
|
258
|
+
error: errorMessage(error),
|
|
259
|
+
})
|
|
260
|
+
render({
|
|
261
|
+
command: 'openai-reset',
|
|
262
|
+
text: 'Command failed — the plugin could not complete the request; the plugin log has details.',
|
|
263
|
+
knobs: { stage: 'result', code: 'error' },
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
const stage = state.knobs.stage
|
|
268
|
+
log.debug('reset dialog render', {
|
|
269
|
+
stage,
|
|
270
|
+
accountRows: Array.isArray(state.knobs.accounts)
|
|
271
|
+
? (state.knobs.accounts as unknown[]).length
|
|
272
|
+
: undefined,
|
|
273
|
+
knobKeys: Object.keys(state.knobs),
|
|
274
|
+
})
|
|
275
|
+
api.ui.dialog.setSize('xlarge')
|
|
276
|
+
|
|
277
|
+
if (stage === 'confirm') {
|
|
278
|
+
const preview = state.knobs.preview as ResetPreviewKnob | undefined
|
|
279
|
+
const accountKey = preview?.accountKey
|
|
280
|
+
const chatgptAccountId = preview?.chatgptAccountId
|
|
281
|
+
const applicableCount = preview?.applicableAvailableCount ?? 0
|
|
282
|
+
const DialogConfirm = api.ui.DialogConfirm
|
|
283
|
+
api.ui.dialog.replace(() => (
|
|
284
|
+
<DialogConfirm
|
|
285
|
+
title='Reset quota window'
|
|
286
|
+
message={`${state.text}\n\nThis SPENDS 1 of ${applicableCount} reset credits — irreversible.\n\nChoose Reset to continue or Cancel to return.\n\nEnter = Cancel (host default). Press Tab then Enter to Reset.`}
|
|
287
|
+
onConfirm={() => {
|
|
288
|
+
if (!accountKey || !chatgptAccountId) return
|
|
289
|
+
applyAndRender(
|
|
290
|
+
`confirm ${encodeURIComponent(accountKey)} ${encodeURIComponent(chatgptAccountId)}`,
|
|
291
|
+
)
|
|
292
|
+
}}
|
|
293
|
+
onCancel={() => applyAndRender('refresh')}
|
|
294
|
+
/>
|
|
295
|
+
))
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
300
|
+
const options =
|
|
301
|
+
stage === 'result'
|
|
302
|
+
? resetResultOptions(state)
|
|
303
|
+
: resetAccountOptions(state)
|
|
304
|
+
api.ui.dialog.replace(() => (
|
|
305
|
+
<DialogSelect
|
|
306
|
+
title={
|
|
307
|
+
stage === 'result' ? 'OpenAI reset result' : 'OpenAI reset credits'
|
|
308
|
+
}
|
|
309
|
+
options={options}
|
|
310
|
+
onSelect={(option) => {
|
|
311
|
+
if (option.disabled) return
|
|
312
|
+
if (option.value === 'result') return
|
|
313
|
+
if (option.value === 'close') {
|
|
314
|
+
applyGeneration += 1
|
|
315
|
+
api.ui.dialog.clear()
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
if (option.value === 'refresh') {
|
|
319
|
+
applyAndRender('refresh')
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
if (option.value === 'retry') {
|
|
323
|
+
const accountKey = state.knobs.accountKey
|
|
324
|
+
const chatgptAccountId = state.knobs.chatgptAccountId
|
|
325
|
+
if (
|
|
326
|
+
typeof accountKey !== 'string' ||
|
|
327
|
+
typeof chatgptAccountId !== 'string'
|
|
328
|
+
) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
applyAndRender(
|
|
332
|
+
`retry ${encodeURIComponent(accountKey)} ${encodeURIComponent(chatgptAccountId)}`,
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
if (!option.value.startsWith('account:')) return
|
|
337
|
+
const accountKey = option.value.slice('account:'.length)
|
|
338
|
+
applyAndRender(`select ${encodeURIComponent(accountKey)}`)
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
render(payload)
|
|
345
|
+
}
|
|
346
|
+
|
|
107
347
|
function showText(api: TuiPluginApi, text: string) {
|
|
108
348
|
api.ui.dialog.setSize('xlarge')
|
|
109
349
|
api.ui.dialog.replace(() => (
|
|
@@ -157,6 +397,11 @@ export function openCommandDialog(
|
|
|
157
397
|
apply: ApplyFn,
|
|
158
398
|
sessionId?: string,
|
|
159
399
|
) {
|
|
400
|
+
if (payload.command === 'openai-reset') {
|
|
401
|
+
openResetDialog(api, payload, apply)
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
160
405
|
if (payload.command === 'openai-routing') {
|
|
161
406
|
const current = (payload.knobs.mode as string) ?? 'main-first'
|
|
162
407
|
const DialogSelect = api.ui.DialogSelect<string>
|
|
@@ -6,21 +6,24 @@ export interface RpcClient {
|
|
|
6
6
|
lastReceivedId: number,
|
|
7
7
|
sessionId?: string,
|
|
8
8
|
) => Promise<RpcNotification[]>
|
|
9
|
-
apply: (request: ApplyRequest) => Promise<ApplyResult>
|
|
9
|
+
apply: (request: ApplyRequest, timeoutMs?: number) => Promise<ApplyResult>
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export const DEFAULT_RPC_TIMEOUT_MS = 2_000
|
|
13
|
+
|
|
12
14
|
async function call<T>(
|
|
13
15
|
dir: string,
|
|
14
16
|
expectedPid: number | undefined,
|
|
15
17
|
onSelected: ((entry: PortFileEntry | null) => void) | undefined,
|
|
16
18
|
method: string,
|
|
17
19
|
params: Record<string, unknown>,
|
|
20
|
+
timeoutMs = DEFAULT_RPC_TIMEOUT_MS,
|
|
18
21
|
): Promise<T | null> {
|
|
19
22
|
const entry = await discoverPortFile(dir, expectedPid)
|
|
20
23
|
onSelected?.(entry)
|
|
21
24
|
if (!entry) return null
|
|
22
25
|
const controller = new AbortController()
|
|
23
|
-
const timer = setTimeout(() => controller.abort(),
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
24
27
|
try {
|
|
25
28
|
const res = await fetch(`http://127.0.0.1:${entry.port}/rpc/${method}`, {
|
|
26
29
|
method: 'POST',
|
|
@@ -62,7 +65,7 @@ export function createRpcClient(
|
|
|
62
65
|
)
|
|
63
66
|
return out?.messages ?? []
|
|
64
67
|
},
|
|
65
|
-
async apply(request) {
|
|
68
|
+
async apply(request, timeoutMs) {
|
|
66
69
|
const out = await call<ApplyResult>(
|
|
67
70
|
dir,
|
|
68
71
|
expectedPid,
|
|
@@ -71,6 +74,7 @@ export function createRpcClient(
|
|
|
71
74
|
{
|
|
72
75
|
...request,
|
|
73
76
|
},
|
|
77
|
+
timeoutMs,
|
|
74
78
|
)
|
|
75
79
|
return out ?? { text: 'apply failed', knobs: {} }
|
|
76
80
|
},
|
|
@@ -100,6 +100,7 @@ export type ActiveRoutingMap = Record<string, ActiveRoutingEntry>
|
|
|
100
100
|
|
|
101
101
|
export interface StickyAssignment {
|
|
102
102
|
accountId: string
|
|
103
|
+
wireAccountId?: string
|
|
103
104
|
assignedAt: number
|
|
104
105
|
lastSeenAt: number
|
|
105
106
|
inputBytes: number
|
|
@@ -120,6 +121,7 @@ export interface ResolveStickyAssignmentInput {
|
|
|
120
121
|
validPinnedAccountIds: readonly string[]
|
|
121
122
|
excludeAccountIds?: readonly string[]
|
|
122
123
|
quotaCheckedAtByAccount: Readonly<Record<string, number | undefined>>
|
|
124
|
+
wireAccountIdByAccount?: Readonly<Record<string, string | undefined>>
|
|
123
125
|
choose: (
|
|
124
126
|
pendingBytes: ReadonlyMap<string, number>,
|
|
125
127
|
) => StickyAssignmentChoice | undefined
|
|
@@ -256,12 +258,14 @@ function normalizeStickyAssignments(
|
|
|
256
258
|
) {
|
|
257
259
|
continue
|
|
258
260
|
}
|
|
261
|
+
const wireAccountId = assignment.wireAccountId
|
|
259
262
|
normalized[sessionHash] = {
|
|
260
263
|
accountId: assignment.accountId,
|
|
261
264
|
assignedAt: assignment.assignedAt,
|
|
262
265
|
lastSeenAt: assignment.lastSeenAt,
|
|
263
266
|
inputBytes: assignment.inputBytes,
|
|
264
267
|
...(quotaCheckedAt === undefined ? {} : { quotaCheckedAt }),
|
|
268
|
+
...(typeof wireAccountId === 'string' ? { wireAccountId } : {}),
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
|
|
@@ -652,10 +656,32 @@ function stickyAssignmentNeedsMetadataUpdate(
|
|
|
652
656
|
assignment: StickyAssignment,
|
|
653
657
|
requestBytes: number,
|
|
654
658
|
now: number,
|
|
659
|
+
wireAccountId: string | undefined,
|
|
655
660
|
): boolean {
|
|
656
661
|
return (
|
|
657
662
|
requestBytes > assignment.inputBytes ||
|
|
658
|
-
now - assignment.lastSeenAt >= STICKY_ASSIGNMENT_LAST_SEEN_TOUCH_MS
|
|
663
|
+
now - assignment.lastSeenAt >= STICKY_ASSIGNMENT_LAST_SEEN_TOUCH_MS ||
|
|
664
|
+
(assignment.wireAccountId === undefined && wireAccountId !== undefined)
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function hasStickyIdentityMismatch(
|
|
669
|
+
assignment: StickyAssignment,
|
|
670
|
+
wireAccountId: string | undefined,
|
|
671
|
+
): boolean {
|
|
672
|
+
// True only when both sides know their ChatGPT account and those accounts
|
|
673
|
+
// differ, which means the slot this session was placed on now holds a
|
|
674
|
+
// different account than it did at placement.
|
|
675
|
+
//
|
|
676
|
+
// An unknown on either side deliberately reports no mismatch. A session is
|
|
677
|
+
// pinned to keep its prompt cache warm, and treating missing data as a
|
|
678
|
+
// change would place sessions again for no reason and throw those caches
|
|
679
|
+
// away — worse than leaving a rare wrong pin in place until real evidence
|
|
680
|
+
// arrives.
|
|
681
|
+
return (
|
|
682
|
+
typeof assignment.wireAccountId === 'string' &&
|
|
683
|
+
typeof wireAccountId === 'string' &&
|
|
684
|
+
assignment.wireAccountId !== wireAccountId
|
|
659
685
|
)
|
|
660
686
|
}
|
|
661
687
|
|
|
@@ -692,9 +718,11 @@ function readonlyPendingBytes(
|
|
|
692
718
|
function pendingBytesForAssignments(
|
|
693
719
|
assignments: StickyAssignmentMap | undefined,
|
|
694
720
|
quotaCheckedAtByAccount: Readonly<Record<string, number | undefined>>,
|
|
721
|
+
excludedSessionHash?: string,
|
|
695
722
|
): ReadonlyMap<string, number> {
|
|
696
723
|
const pendingBytes = new Map<string, number>()
|
|
697
|
-
for (const assignment of Object.
|
|
724
|
+
for (const [sessionHash, assignment] of Object.entries(assignments ?? {})) {
|
|
725
|
+
if (sessionHash === excludedSessionHash) continue
|
|
698
726
|
if (
|
|
699
727
|
assignment.quotaCheckedAt !==
|
|
700
728
|
quotaCheckedAtByAccount[assignment.accountId]
|
|
@@ -1178,10 +1206,15 @@ export async function resolveSidebarStickyAssignment(
|
|
|
1178
1206
|
excludedAccountIds,
|
|
1179
1207
|
input.now,
|
|
1180
1208
|
) &&
|
|
1209
|
+
!hasStickyIdentityMismatch(
|
|
1210
|
+
existing,
|
|
1211
|
+
input.wireAccountIdByAccount?.[existing.accountId],
|
|
1212
|
+
) &&
|
|
1181
1213
|
!stickyAssignmentNeedsMetadataUpdate(
|
|
1182
1214
|
existing,
|
|
1183
1215
|
input.requestBytes,
|
|
1184
1216
|
input.now,
|
|
1217
|
+
input.wireAccountIdByAccount?.[existing.accountId],
|
|
1185
1218
|
)
|
|
1186
1219
|
) {
|
|
1187
1220
|
return existing
|
|
@@ -1202,23 +1235,38 @@ export async function resolveSidebarStickyAssignment(
|
|
|
1202
1235
|
stickyAssignments,
|
|
1203
1236
|
)
|
|
1204
1237
|
const current = stickyAssignments?.[sessionHash]
|
|
1238
|
+
const currentIdentityMismatch =
|
|
1239
|
+
current !== undefined &&
|
|
1240
|
+
hasStickyIdentityMismatch(
|
|
1241
|
+
current,
|
|
1242
|
+
input.wireAccountIdByAccount?.[current.accountId],
|
|
1243
|
+
)
|
|
1205
1244
|
if (
|
|
1206
1245
|
isValidStickyAssignment(
|
|
1207
1246
|
current,
|
|
1208
1247
|
validPinnedAccountIds,
|
|
1209
1248
|
excludedAccountIds,
|
|
1210
1249
|
input.now,
|
|
1211
|
-
)
|
|
1250
|
+
) &&
|
|
1251
|
+
!currentIdentityMismatch
|
|
1212
1252
|
) {
|
|
1213
1253
|
const metadataNeedsUpdate = stickyAssignmentNeedsMetadataUpdate(
|
|
1214
1254
|
current,
|
|
1215
1255
|
input.requestBytes,
|
|
1216
1256
|
input.now,
|
|
1257
|
+
input.wireAccountIdByAccount?.[current.accountId],
|
|
1217
1258
|
)
|
|
1218
1259
|
const assignment = metadataNeedsUpdate
|
|
1219
1260
|
? {
|
|
1220
1261
|
...current,
|
|
1221
1262
|
inputBytes: Math.max(current.inputBytes, input.requestBytes),
|
|
1263
|
+
...(current.wireAccountId === undefined &&
|
|
1264
|
+
input.wireAccountIdByAccount?.[current.accountId] !== undefined
|
|
1265
|
+
? {
|
|
1266
|
+
wireAccountId:
|
|
1267
|
+
input.wireAccountIdByAccount?.[current.accountId],
|
|
1268
|
+
}
|
|
1269
|
+
: {}),
|
|
1222
1270
|
...(input.now - current.lastSeenAt >=
|
|
1223
1271
|
STICKY_ASSIGNMENT_LAST_SEEN_TOUCH_MS
|
|
1224
1272
|
? { lastSeenAt: input.now }
|
|
@@ -1241,6 +1289,12 @@ export async function resolveSidebarStickyAssignment(
|
|
|
1241
1289
|
pendingBytesForAssignments(
|
|
1242
1290
|
stickyAssignments,
|
|
1243
1291
|
input.quotaCheckedAtByAccount,
|
|
1292
|
+
// Pending bytes weigh how much traffic each account is already
|
|
1293
|
+
// committed to, so the session being placed must not weigh its own
|
|
1294
|
+
// stale entry: that entry belongs to the account it is moving off,
|
|
1295
|
+
// and counting it would bias placement away from a perfectly good
|
|
1296
|
+
// destination. Other sessions' entries still count.
|
|
1297
|
+
currentIdentityMismatch ? sessionHash : undefined,
|
|
1244
1298
|
),
|
|
1245
1299
|
)
|
|
1246
1300
|
if (!choice) {
|
|
@@ -1261,6 +1315,11 @@ export async function resolveSidebarStickyAssignment(
|
|
|
1261
1315
|
...(choice.quotaCheckedAt === undefined
|
|
1262
1316
|
? {}
|
|
1263
1317
|
: { quotaCheckedAt: choice.quotaCheckedAt }),
|
|
1318
|
+
...(input.wireAccountIdByAccount?.[choice.accountId] === undefined
|
|
1319
|
+
? {}
|
|
1320
|
+
: {
|
|
1321
|
+
wireAccountId: input.wireAccountIdByAccount?.[choice.accountId],
|
|
1322
|
+
}),
|
|
1264
1323
|
}
|
|
1265
1324
|
return {
|
|
1266
1325
|
...latest,
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
2
2
|
import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
3
|
-
import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
4
3
|
import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
5
4
|
import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
6
5
|
import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
7
6
|
import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
7
|
+
import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
8
8
|
/** @jsxImportSource @opentui/solid */
|
|
9
9
|
|
|
10
|
+
import { createLogger } from "../logger";
|
|
10
11
|
import { getCollapsedQuotaSummary, getSidebarState, resolveSessionSidebarRouting } from "../sidebar-state.js";
|
|
12
|
+
import { errorMessage } from "../util/error";
|
|
11
13
|
import { openUrl } from "../util/open-url";
|
|
14
|
+
const log = createLogger('rpc-tui');
|
|
12
15
|
export function buildCachekeepDialogOptions(payload) {
|
|
13
16
|
const enabled = payload.knobs.enabled === true;
|
|
14
17
|
const subagents = payload.knobs.subagents === true;
|
|
@@ -59,6 +62,160 @@ export function buildCachekeepDialogOptions(payload) {
|
|
|
59
62
|
description: 'Close this dialog'
|
|
60
63
|
}];
|
|
61
64
|
}
|
|
65
|
+
function resetAccountOptions(payload) {
|
|
66
|
+
const accounts = Array.isArray(payload.knobs.accounts) ? payload.knobs.accounts : [];
|
|
67
|
+
return [...accounts.map(account => {
|
|
68
|
+
const used = account.usedPercent === undefined ? 'quota unavailable' : `${account.usedPercent}%`;
|
|
69
|
+
const counts = `${account.applicableAvailableCount ?? 0}/${account.availableCount ?? 0}`;
|
|
70
|
+
const status = account.eligible ? 'eligible' : account.reason ?? 'unavailable';
|
|
71
|
+
const details = `${used} · ${counts}`;
|
|
72
|
+
const shortDate = account.selectedCreditExpiresAt?.slice(0, 10) ?? 'unknown';
|
|
73
|
+
return {
|
|
74
|
+
title: `${account.label} — ${status}`,
|
|
75
|
+
value: `account:${account.accountKey}`,
|
|
76
|
+
description: account.eligible ? `${details} · exp ${shortDate}` : details
|
|
77
|
+
};
|
|
78
|
+
}), {
|
|
79
|
+
title: 'Refresh',
|
|
80
|
+
value: 'refresh',
|
|
81
|
+
description: 'Re-check account quota and reset credits'
|
|
82
|
+
}, {
|
|
83
|
+
title: 'Close',
|
|
84
|
+
value: 'close',
|
|
85
|
+
description: 'Close this dialog'
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
function resetResultOptions(payload) {
|
|
89
|
+
const accountKey = typeof payload.knobs.accountKey === 'string' ? payload.knobs.accountKey : undefined;
|
|
90
|
+
const chatgptAccountId = typeof payload.knobs.chatgptAccountId === 'string' ? payload.knobs.chatgptAccountId : undefined;
|
|
91
|
+
const code = typeof payload.knobs.code === 'string' ? payload.knobs.code : '';
|
|
92
|
+
const retrySafe = (code === 'ambiguous' || code === 'http_error' || code === 'expired_unreconciled' || payload.knobs.stateWriteFailed === true) && typeof payload.knobs.retryGuidance === 'string' && accountKey !== undefined && chatgptAccountId !== undefined;
|
|
93
|
+
return [{
|
|
94
|
+
title: payload.text,
|
|
95
|
+
value: 'result',
|
|
96
|
+
description: 'Command result'
|
|
97
|
+
}, ...(retrySafe ? [{
|
|
98
|
+
title: 'Retry',
|
|
99
|
+
value: 'retry',
|
|
100
|
+
description: 'Reuse the bound request and credit identifiers for this uncertain outcome'
|
|
101
|
+
}] : []), {
|
|
102
|
+
title: 'Refresh',
|
|
103
|
+
value: 'refresh',
|
|
104
|
+
description: 'Re-check account quota and reset credits'
|
|
105
|
+
}, {
|
|
106
|
+
title: 'Close',
|
|
107
|
+
value: 'close',
|
|
108
|
+
description: 'Close this dialog'
|
|
109
|
+
}];
|
|
110
|
+
}
|
|
111
|
+
function openResetDialog(api, payload, apply) {
|
|
112
|
+
let applyGeneration = 0;
|
|
113
|
+
const render = state => {
|
|
114
|
+
const applyAndRender = args => {
|
|
115
|
+
const generation = ++applyGeneration;
|
|
116
|
+
log.debug('reset dialog apply', {
|
|
117
|
+
args,
|
|
118
|
+
generation
|
|
119
|
+
});
|
|
120
|
+
void apply('openai-reset', args).then(result => {
|
|
121
|
+
log.debug('reset dialog apply result', {
|
|
122
|
+
generation,
|
|
123
|
+
currentGeneration: applyGeneration,
|
|
124
|
+
stage: result.knobs?.stage,
|
|
125
|
+
accountRows: Array.isArray(result.knobs?.accounts) ? result.knobs.accounts.length : undefined
|
|
126
|
+
});
|
|
127
|
+
if (generation !== applyGeneration) return;
|
|
128
|
+
if (typeof result.knobs?.stage !== 'string') {
|
|
129
|
+
render({
|
|
130
|
+
command: 'openai-reset',
|
|
131
|
+
text: 'The reset request is still completing or returned no result. Choose Refresh to check the current state — do NOT re-confirm.',
|
|
132
|
+
knobs: {
|
|
133
|
+
stage: 'result',
|
|
134
|
+
code: 'unknown'
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
render({
|
|
140
|
+
command: 'openai-reset',
|
|
141
|
+
text: result.text,
|
|
142
|
+
knobs: result.knobs
|
|
143
|
+
});
|
|
144
|
+
}).catch(error => {
|
|
145
|
+
if (generation !== applyGeneration) return;
|
|
146
|
+
log.warn('reset dialog apply rejected', {
|
|
147
|
+
error: errorMessage(error)
|
|
148
|
+
});
|
|
149
|
+
render({
|
|
150
|
+
command: 'openai-reset',
|
|
151
|
+
text: 'Command failed — the plugin could not complete the request; the plugin log has details.',
|
|
152
|
+
knobs: {
|
|
153
|
+
stage: 'result',
|
|
154
|
+
code: 'error'
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const stage = state.knobs.stage;
|
|
160
|
+
log.debug('reset dialog render', {
|
|
161
|
+
stage,
|
|
162
|
+
accountRows: Array.isArray(state.knobs.accounts) ? state.knobs.accounts.length : undefined,
|
|
163
|
+
knobKeys: Object.keys(state.knobs)
|
|
164
|
+
});
|
|
165
|
+
api.ui.dialog.setSize('xlarge');
|
|
166
|
+
if (stage === 'confirm') {
|
|
167
|
+
const preview = state.knobs.preview;
|
|
168
|
+
const accountKey = preview?.accountKey;
|
|
169
|
+
const chatgptAccountId = preview?.chatgptAccountId;
|
|
170
|
+
const applicableCount = preview?.applicableAvailableCount ?? 0;
|
|
171
|
+
const DialogConfirm = api.ui.DialogConfirm;
|
|
172
|
+
api.ui.dialog.replace(() => _$createComponent(DialogConfirm, {
|
|
173
|
+
title: "Reset quota window",
|
|
174
|
+
get message() {
|
|
175
|
+
return `${state.text}\n\nThis SPENDS 1 of ${applicableCount} reset credits — irreversible.\n\nChoose Reset to continue or Cancel to return.\n\nEnter = Cancel (host default). Press Tab then Enter to Reset.`;
|
|
176
|
+
},
|
|
177
|
+
onConfirm: () => {
|
|
178
|
+
if (!accountKey || !chatgptAccountId) return;
|
|
179
|
+
applyAndRender(`confirm ${encodeURIComponent(accountKey)} ${encodeURIComponent(chatgptAccountId)}`);
|
|
180
|
+
},
|
|
181
|
+
onCancel: () => applyAndRender('refresh')
|
|
182
|
+
}));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const DialogSelect = api.ui.DialogSelect;
|
|
186
|
+
const options = stage === 'result' ? resetResultOptions(state) : resetAccountOptions(state);
|
|
187
|
+
api.ui.dialog.replace(() => _$createComponent(DialogSelect, {
|
|
188
|
+
title: stage === 'result' ? 'OpenAI reset result' : 'OpenAI reset credits',
|
|
189
|
+
options: options,
|
|
190
|
+
onSelect: option => {
|
|
191
|
+
if (option.disabled) return;
|
|
192
|
+
if (option.value === 'result') return;
|
|
193
|
+
if (option.value === 'close') {
|
|
194
|
+
applyGeneration += 1;
|
|
195
|
+
api.ui.dialog.clear();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (option.value === 'refresh') {
|
|
199
|
+
applyAndRender('refresh');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (option.value === 'retry') {
|
|
203
|
+
const accountKey = state.knobs.accountKey;
|
|
204
|
+
const chatgptAccountId = state.knobs.chatgptAccountId;
|
|
205
|
+
if (typeof accountKey !== 'string' || typeof chatgptAccountId !== 'string') {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
applyAndRender(`retry ${encodeURIComponent(accountKey)} ${encodeURIComponent(chatgptAccountId)}`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!option.value.startsWith('account:')) return;
|
|
212
|
+
const accountKey = option.value.slice('account:'.length);
|
|
213
|
+
applyAndRender(`select ${encodeURIComponent(accountKey)}`);
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
};
|
|
217
|
+
render(payload);
|
|
218
|
+
}
|
|
62
219
|
function showText(api, text) {
|
|
63
220
|
api.ui.dialog.setSize('xlarge');
|
|
64
221
|
api.ui.dialog.replace(() => (() => {
|
|
@@ -107,6 +264,10 @@ function showSetCachekeepWindowPrompt(api, apply, state, render) {
|
|
|
107
264
|
}));
|
|
108
265
|
}
|
|
109
266
|
export function openCommandDialog(api, payload, apply, sessionId) {
|
|
267
|
+
if (payload.command === 'openai-reset') {
|
|
268
|
+
openResetDialog(api, payload, apply);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
110
271
|
if (payload.command === 'openai-routing') {
|
|
111
272
|
const current = payload.knobs.mode ?? 'main-first';
|
|
112
273
|
const DialogSelect = api.ui.DialogSelect;
|
package/src/tui-compiled/tui.tsx
CHANGED
|
@@ -1118,7 +1118,7 @@ const tui = async api => {
|
|
|
1118
1118
|
}));
|
|
1119
1119
|
continue;
|
|
1120
1120
|
}
|
|
1121
|
-
openCommandDialog(api, message.payload, (command, args) => rpcClient.apply(buildApplyRequest(command, args, sessionId)), sessionId);
|
|
1121
|
+
openCommandDialog(api, message.payload, (command, args) => rpcClient.apply(buildApplyRequest(command, args, sessionId), command === 'openai-reset' ? 90_000 : undefined), sessionId);
|
|
1122
1122
|
}
|
|
1123
1123
|
}).catch(() => {}).finally(() => {
|
|
1124
1124
|
rpcInFlight = false;
|