opencode-navigator 0.19.3 → 0.20.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.
@@ -0,0 +1,739 @@
1
+ import {PLUGIN_ID
2
+ } from "./chunk-cmmnfes2.js";
3
+ function createCodexAppServerClient(input){const command=input.command??["codex","app-server","--stdio"]
4
+ const timeoutMs=input.timeoutMs??1e4
5
+ const pending=new Map
6
+ let child
7
+ let startup
8
+ let sequence=0
9
+ let disposed=false
10
+ function failAll(error){for(const [id,request] of pending){clearTimeout(request.timer)
11
+ request.cleanup()
12
+ request.reject(error)
13
+ pending.delete(id)
14
+ }}
15
+ function write(message){if(!child?.stdin||typeof child.stdin==="number")
16
+ throw new Error("Codex app-server is unavailable")
17
+ child.stdin.write(`${JSON.stringify(message)}
18
+ `)
19
+ }
20
+ function accept(line){let message
21
+ try{message=JSON.parse(line)
22
+ } catch{return
23
+ }
24
+ if(typeof message.id!=="number")
25
+ return
26
+ const request=pending.get(message.id)
27
+ if(!request)
28
+ return
29
+ pending.delete(message.id)
30
+ clearTimeout(request.timer)
31
+ request.cleanup()
32
+ if(message.error){request.reject(new Error(message.error.code===-32601?"Codex app-server method unavailable":"Codex app-server request failed"))
33
+ return
34
+ }
35
+ request.resolve(message.result)
36
+ }
37
+ function stop(process,error){if(child!==process)
38
+ return
39
+ child=undefined
40
+ startup=undefined
41
+ failAll(error)
42
+ process.kill()
43
+ }
44
+ async function read(process){const output=process.stdout
45
+ if(!output||typeof output==="number")
46
+ return
47
+ const decoder=new TextDecoder
48
+ const reader=output.getReader()
49
+ let buffer=""
50
+ try{while(true){const next=await reader.read()
51
+ if(next.done)
52
+ break
53
+ buffer += decoder.decode(next.value,{stream:true})
54
+ if(buffer.length>1e6)
55
+ throw new Error("Codex app-server response exceeds 1 MB")
56
+ let end
57
+ while((end=buffer.indexOf(`
58
+ `))!==-1){const line=buffer.slice(0,end)
59
+ buffer=buffer.slice(end + 1)
60
+ if(line.length>0)
61
+ accept(line)
62
+ }}} catch(error){stop(process,error instanceof Error?error:new Error("Codex app-server stream failed"))
63
+ } finally{stop(process,new Error("Codex app-server closed"))
64
+ }}
65
+ function send(method,params,signal){if(disposed)
66
+ return Promise.reject(new DOMException("Codex app-server closed","AbortError"))
67
+ if(!child)
68
+ return Promise.reject(new Error("Codex app-server is unavailable"))
69
+ return new Promise((resolve,reject)=>{const id=++sequence
70
+ const abort=()=>{const request=pending.get(id)
71
+ if(!request)
72
+ return
73
+ pending.delete(id)
74
+ clearTimeout(request.timer)
75
+ request.cleanup()
76
+ reject(new DOMException("Codex request cancelled","AbortError"))
77
+ }
78
+ const timer=setTimeout(()=>{pending.delete(id)
79
+ signal?.removeEventListener("abort",abort)
80
+ reject(new Error("Codex app-server timed out"))
81
+ },timeoutMs)
82
+ if(signal?.aborted){clearTimeout(timer)
83
+ reject(new DOMException("Codex request cancelled","AbortError"))
84
+ return
85
+ }
86
+ signal?.addEventListener("abort",abort,{once:true})
87
+ pending.set(id,{resolve,reject,timer,cleanup:()=>signal?.removeEventListener("abort",abort)})
88
+ try{write({id,method,params})
89
+ } catch(error){abort()
90
+ reject(error)
91
+ }})
92
+ }
93
+ async function initialize(){if(disposed)
94
+ throw new Error("Codex app-server is unavailable")
95
+ const process=Bun.spawn([...command],{stdin:"pipe",stdout:"pipe",stderr:"ignore"})
96
+ child=process
97
+ read(process)
98
+ process.exited.then(()=>stop(process,new Error("Codex app-server closed")))
99
+ try{await send("initialize",{clientInfo:{name:"opencode-navigator",title:"Navigator",version:input.version},capabilities:null
100
+ })
101
+ write({method:"initialized"})
102
+ } catch(error){stop(process,error instanceof Error?error:new Error("Codex initialization failed"))
103
+ throw error
104
+ }}
105
+ return{async request(method,params={},signal){if(disposed)
106
+ throw new DOMException("Codex app-server closed","AbortError")
107
+ const starting=startup ??= initialize()
108
+ try{await starting
109
+ } catch(error){if(startup===starting)
110
+ startup=undefined
111
+ throw error
112
+ }
113
+ return await send(method,params,signal)
114
+ },dispose(){disposed=true
115
+ failAll(new DOMException("Codex app-server closed","AbortError"))
116
+ child?.kill()
117
+ }}}
118
+ function normalizeProviderQuota(snapshot){if(!snapshot.providerId||!snapshot.providerName||!snapshot.model.providerID||!snapshot.model.modelID||!snapshot.model.sessionID||!Number.isFinite(snapshot.fetchedAt)){throw new Error("Invalid provider quota identity")
119
+ }
120
+ const windows=snapshot.windows.map((window)=>{if(!window.id||!window.label||window.modelID!==snapshot.model.modelID||!window.unit||!Number.isFinite(window.used)||window.total!==undefined&&(!Number.isFinite(window.total)||window.total<0)||window.durationMinutes!==undefined&&(!Number.isFinite(window.durationMinutes)||window.durationMinutes<0)||window.resetsAt!==undefined&&!Number.isFinite(window.resetsAt)){throw new Error("Quota window does not match the selected model or contains invalid values")
121
+ }
122
+ return{...window}})
123
+ const bankedResets=snapshot.bankedResets
124
+ if(bankedResets&&(!Number.isSafeInteger(bankedResets.availableCount)||bankedResets.availableCount<0)){throw new Error("Invalid banked reset count")
125
+ }
126
+ return{...snapshot,windows,...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
127
+ import {createSignal as createSignal2,untrack} from "solid-js"
128
+ import {createSignal} from "solid-js"
129
+ var idle={status:"idle"}
130
+ function isAbortError(cause){return typeof DOMException!=="undefined"&&cause instanceof DOMException&&cause.name==="AbortError"||cause instanceof Error&&cause.name==="AbortError"||typeof cause==="object"&&cause!==null&&"name" in cause&&cause.name==="AbortError"
131
+ }
132
+ function isRetryableError(cause){if(isAbortError(cause))
133
+ return false
134
+ if(typeof cause!=="object"||cause===null)
135
+ return true
136
+ const value=cause
137
+ const status=typeof value.status==="number"?value.status:value.response?.status
138
+ if(typeof status!=="number")
139
+ return true
140
+ return status===408||status===425||status===429||status>=500
141
+ }
142
+ function requestError(cause,operation,target){return{operation,target,message:cause instanceof Error?cause.message:`Failed to ${operation}`,retryable:isRetryableError(cause)
143
+ }}
144
+ function createRequestState(lifecycleSignal){const [states,setStates]=createSignal({})
145
+ const active=new Map
146
+ let sequence=0
147
+ function state(target){return states()[target]??idle
148
+ }
149
+ function abort(target){const request=active.get(target)
150
+ if(!request)
151
+ return
152
+ request.controller.abort()
153
+ active.delete(target)
154
+ const current=state(target)
155
+ setStates((values)=>({...values,[target]:current.lastSuccessfulAt?{status:"ready",lastSuccessfulAt:current.lastSuccessfulAt}:idle
156
+ }))
157
+ }
158
+ function abortAll(){for(const target of active.keys())
159
+ abort(target)
160
+ }
161
+ lifecycleSignal?.addEventListener("abort",abortAll,{once:true})
162
+ function start(target,operation,hasData,force=false){const previous=active.get(target)
163
+ if(previous&&!force)
164
+ return
165
+ previous?.controller.abort()
166
+ const id=++sequence
167
+ const controller=new AbortController
168
+ if(lifecycleSignal?.aborted)
169
+ controller.abort(lifecycleSignal.reason)
170
+ const current=state(target)
171
+ active.set(target,{id,controller})
172
+ setStates((values)=>({...values,[target]:{status:hasData?"refreshing":"loading",lastSuccessfulAt:current.lastSuccessfulAt
173
+ }}))
174
+ function isCurrent(){return active.get(target)?.id===id
175
+ }
176
+ function finish(){if(isCurrent())
177
+ active.delete(target)
178
+ }
179
+ return{signal:controller.signal,isCurrent,succeed(){if(!isCurrent())
180
+ return
181
+ setStates((values)=>({...values,[target]:{status:"ready",lastSuccessfulAt:Date.now()}}))
182
+ },fail(cause){if(!isCurrent())
183
+ return
184
+ if(controller.signal.aborted||isAbortError(cause)){setStates((values)=>({...values,[target]:current.lastSuccessfulAt?{status:"ready",lastSuccessfulAt:current.lastSuccessfulAt}:idle
185
+ }))
186
+ return
187
+ }
188
+ setStates((values)=>({...values,[target]:{status:"error",error:requestError(cause,operation,target),lastSuccessfulAt:current.lastSuccessfulAt
189
+ }}))
190
+ },finish
191
+ }}
192
+ return{state,start,abort,abortAll}}
193
+ function createProviderLimitsController(adapters,selectedModel,signal){const requests=createRequestState(signal)
194
+ const [snapshots,setSnapshots]=createSignal2({})
195
+ const targetKey=(model)=>JSON.stringify(model)
196
+ let activeKey
197
+ let targetAbort=new AbortController
198
+ function current(){const model=selectedModel()
199
+ const adapter=model&&adapters.find((candidate)=>candidate.supports(model))
200
+ const key=model?targetKey(model):undefined
201
+ if(activeKey!==key){untrack(requests.abortAll)
202
+ targetAbort.abort()
203
+ targetAbort=new AbortController
204
+ activeKey=key
205
+ }
206
+ return{model,adapter}}
207
+ async function refresh(force=false){const{model,adapter}=current()
208
+ if(!model||!adapter)
209
+ return
210
+ const key=targetKey(model)
211
+ const request=requests.start(key,"read provider limits",Boolean(snapshots()[key]),force)
212
+ if(!request)
213
+ return
214
+ try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal))
215
+ if(!request.isCurrent()||targetKey(current().model??model)!==key)
216
+ return
217
+ setSnapshots((value)=>({...value,[key]:snapshot}))
218
+ request.succeed()
219
+ } catch(error){const status=error?.status??error?.response?.status
220
+ if(status===429&&request.isCurrent()){setSnapshots((value)=>({...value,[key]:{model,providerId:adapter.id,providerName:adapter.name,fetchedAt:Date.now(),availability:"rate_limited",windows:[]
221
+ }}))
222
+ request.succeed()
223
+ } else
224
+ request.fail(error)
225
+ } finally{request.finish()
226
+ }}
227
+ return{current,targetSignal(){current()
228
+ return signal?AbortSignal.any([signal,targetAbort.signal]):targetAbort.signal
229
+ },refresh,snapshot(){const{model,adapter}=current()
230
+ return model&&adapter?snapshots()[targetKey(model)]:undefined
231
+ },state(){const{model,adapter}=current()
232
+ return model&&adapter?requests.state(targetKey(model)):{status:"idle"}},dispose(){targetAbort.abort()
233
+ requests.abortAll()
234
+ for(const adapter of adapters)
235
+ adapter.dispose?.()
236
+ }}}
237
+ function windows(limit,model){return ["primary","secondary"].flatMap((name)=>{const value=limit[name]
238
+ return value?[{id:`${limit.limitId ?? model.modelID}.${name}`,label:limit.limitName?`${limit.limitName} \xB7 ${name}`:name,limitID:limit.limitId??undefined,modelID:model.modelID,unit:"%",used:value.usedPercent,...value.windowDurationMins!==null&&{durationMinutes:value.windowDurationMins},...value.resetsAt!==null&&{resetsAt:value.resetsAt}}]:[]
239
+ })
240
+ }
241
+ function createCodexQuotaAdapter(client){const adapter={id:"codex",name:"Codex CLI",supports:(model)=>model.providerID==="openai"&&Boolean(model.accountID),async discoverAccount(signal){const account=await client.request("account/read",{refreshToken:false},signal)
242
+ if(account.account?.type!=="chatgpt")
243
+ throw new Error("Sign in to Codex CLI with ChatGPT")
244
+ const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:true},signal)
245
+ if(!usage.accountId)
246
+ throw new Error("Codex account identity is unavailable")
247
+ return usage.accountId
248
+ },async read(model,signal){const base={model,providerId:"codex",providerName:"Codex CLI",fetchedAt:Date.now(),windows:[]
249
+ }
250
+ const account=await client.request("account/read",{refreshToken:false},signal)
251
+ if(account.account?.type!=="chatgpt")
252
+ return{...base,availability:"unauthenticated"}
253
+ const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:false},signal)
254
+ if(!usage.accountId||usage.accountId!==model.accountID)
255
+ return{...base,availability:"unauthenticated"}
256
+ const candidates=Object.values(usage.rateLimitsByLimitId??{})
257
+ const matched=(candidates.length>0?candidates:[usage.rateLimits]).filter((limit)=>limit.normalModelSlug===model.modelID)
258
+ if(matched.length===0)
259
+ return{...base,accountId:usage.accountId,availability:"unsupported"}
260
+ const creditSummary=usage.rateLimitResetCredits
261
+ return normalizeProviderQuota({...base,accountId:usage.accountId,availability:"ready",ordinaryUsageAllowed:usage.ordinaryUsageAllowed??undefined,windows:matched.flatMap((limit)=>windows(limit,model)),...creditSummary&&{bankedResets:{availableCount:creditSummary.availableCount,...creditSummary.credits&&{credits:creditSummary.credits.map((credit)=>({id:credit.id,type:credit.resetType,status:credit.status,grantedAt:credit.grantedAt,...credit.expiresAt!==null&&{expiresAt:credit.expiresAt},...credit.title&&{title:credit.title},...credit.description&&{description:credit.description}}))
262
+ }}}})
263
+ },async prepareResetCreditConsumption(model,creditID,signal){const snapshot=await adapter.read(model,signal)
264
+ if(snapshot.availability==="unauthenticated"||!snapshot.accountId||snapshot.accountId!==model.accountID){throw new Error("Codex account is unavailable or changed")
265
+ }
266
+ if(snapshot.availability!=="ready"||snapshot.ordinaryUsageAllowed!==false||snapshot.windows.length===0){throw new Error("Selected model has no eligible rate-limit window")
267
+ }
268
+ const credits=snapshot.bankedResets
269
+ if(!credits?.availableCount)
270
+ throw new Error("Codex reset credit is unavailable")
271
+ if(creditID){const credit=credits.credits?.find((item)=>item.id===creditID)
272
+ if(!credit||credit.type!=="codexRateLimits"||credit.status!=="available"||credit.expiresAt!==undefined&&credit.expiresAt<=Math.floor(Date.now() / 1000)){throw new Error("Selected banked reset is no longer available")
273
+ }}
274
+ return snapshot
275
+ },async consumeResetCredit(model,attempt,signal){if(!model.accountID||model.accountID!==attempt.accountID)
276
+ throw new Error("Codex account changed")
277
+ if(attempt.uncertain){const latest=await adapter.read(model,signal)
278
+ if(latest.accountId!==attempt.accountID)
279
+ throw new Error("Codex account changed")
280
+ } else
281
+ await adapter.prepareResetCreditConsumption(model,attempt.creditID,signal)
282
+ signal.throwIfAborted()
283
+ const result=await client.request("account/rateLimitResetCredit/consume",{idempotencyKey:attempt.idempotencyKey,...attempt.creditID&&{creditId:attempt.creditID}},signal)
284
+ return result.outcome
285
+ },dispose:client.dispose
286
+ }
287
+ return adapter
288
+ }
289
+ import {createComponent as _$createComponent} from "@opentui/solid"
290
+ import {createSignal as createSignal3} from "solid-js"
291
+ import {GuardedConfirmation} from "./tui.js"
292
+ function createCodexAccountLink(input){const [busy,setBusy]=createSignal3(false)
293
+ async function link(){const selected=input.model()
294
+ if(busy()||!selected||selected.providerID!=="openai"||!input.adapter.discoverAccount)
295
+ return
296
+ const key=JSON.stringify(selected)
297
+ const current=()=>JSON.stringify(input.model())===key
298
+ setBusy(true)
299
+ try{const accountID=await input.adapter.discoverAccount(input.api.lifecycle?.signal??new AbortController().signal)
300
+ if(!current())
301
+ return
302
+ input.dialogs.open(()=>_$createComponent(GuardedConfirmation,{get api(){return input.api
303
+ },current,title:"Link Codex CLI account",get message(){return `Use Codex CLI account ${accountID} for ${selected.modelID}? Only link accounts you know match this OpenCode provider.`
304
+ },onConfirm:()=>{if(!current())
305
+ return
306
+ input.preferences.setCodexAccountBinding(selected.providerID,selected.modelID,accountID)
307
+ input.dialogs.back()
308
+ }}))
309
+ } catch{if(current())
310
+ input.api.ui.toast({variant:"warning",title:"Codex account",message:"Install a supported Codex CLI and sign in with ChatGPT, then try again."
311
+ })
312
+ } finally{setBusy(false)
313
+ }}
314
+ return{link,busy
315
+ }}
316
+ import {effect as _$effect} from "@opentui/solid"
317
+ import {createTextNode as _$createTextNode} from "@opentui/solid"
318
+ import {insertNode as _$insertNode} from "@opentui/solid"
319
+ import {insert as _$insert} from "@opentui/solid"
320
+ import {memo as _$memo} from "@opentui/solid"
321
+ import {setProp as _$setProp} from "@opentui/solid"
322
+ import {createElement as _$createElement} from "@opentui/solid"
323
+ function QuotaWindowRow(props){const reset=()=>props.window.resetsAt===undefined?"":` \xB7 resets ${new Date(props.window.resetsAt * 1000).toLocaleString()}`
324
+ return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("text"),_el$3=_$createTextNode(`: `),_el$5=_$createTextNode(` used`),_el$6=_$createElement("text")
325
+ _$insertNode(_el$,_el$2)
326
+ _$insertNode(_el$,_el$6)
327
+ _$insertNode(_el$2,_el$3)
328
+ _$insertNode(_el$2,_el$5)
329
+ _$setProp(_el$2,"wrapMode","none")
330
+ _$setProp(_el$2,"truncate",true)
331
+ _$insert(_el$2,(()=>{var _c$=_$memo(()=>props.window.durationMinutes===undefined)
332
+ return()=>_c$()?props.window.label:`${props.window.durationMinutes} min`
333
+ })(),_el$3)
334
+ _$insert(_el$2,()=>props.window.used,_el$5)
335
+ _$insert(_el$2,(()=>{var _c$2=_$memo(()=>props.window.total===undefined)
336
+ return()=>_c$2()?"":`/${props.window.total}`
337
+ })(),_el$5)
338
+ _$insert(_el$2,()=>props.window.unit,_el$5)
339
+ _$setProp(_el$6,"wrapMode","word")
340
+ _$insert(_el$6,()=>reset().replace(" \xB7 ","")||"Reset time unavailable")
341
+ _$effect((_p$)=>{var _v$=props.api.theme.current.text,_v$2=props.api.theme.current.textMuted
342
+ _v$!==_p$.e&&(_p$.e=_$setProp(_el$2,"fg",_v$,_p$.e))
343
+ _v$2!==_p$.t&&(_p$.t=_$setProp(_el$6,"fg",_v$2,_p$.t))
344
+ return _p$
345
+ },{e:undefined,t:undefined
346
+ })
347
+ return _el$
348
+ })()
349
+ }
350
+ import {createComponent as _$createComponent3} from "@opentui/solid"
351
+ import {createEffect} from "solid-js"
352
+ import {SelectionMenu,useDialogs} from "./tui.js"
353
+ import {createComponent as _$createComponent2} from "@opentui/solid"
354
+ import {randomUUID} from "crypto"
355
+ import {GuardedConfirmation as GuardedConfirmation2,useDialogState} from "./tui.js"
356
+ function createResetCreditActions(input){const [busy,setBusy]=useDialogState("credit-busy",false)
357
+ const signal=input.controller.targetSignal
358
+ const warn=(message)=>input.api.ui.toast({variant:"warning",title:"Codex reset credit",message
359
+ })
360
+ async function consume(creditID){if(busy()||!input.current()||!input.model.accountID||!input.adapter.consumeResetCredit)
361
+ return
362
+ setBusy(true)
363
+ const previous=input.preferences.pendingResetAttempt()
364
+ const attempt=previous??{providerID:input.model.providerID,modelID:input.model.modelID,accountID:input.model.accountID,...creditID&&{creditID
365
+ },idempotencyKey:randomUUID(),createdAt:Date.now()
366
+ }
367
+ try{if(attempt.accountID!==input.model.accountID||attempt.modelID!==input.model.modelID||attempt.providerID!==input.model.providerID){throw new Error("Resolve the earlier attempt on its linked account and model first")
368
+ }
369
+ if(!previous)
370
+ await input.preferences.beginResetAttempt(attempt)
371
+ if(!input.current())
372
+ return
373
+ const outcome=await input.adapter.consumeResetCredit(input.model,{...attempt,uncertain:Boolean(previous)
374
+ },signal())
375
+ await input.preferences.finishResetAttempt(attempt.idempotencyKey)
376
+ await input.controller.refresh(true)
377
+ input.api.ui.toast({variant:outcome==="reset"||outcome==="alreadyRedeemed"?"success":"info",title:"Codex reset credit",message:{reset:"Reset applied. Restart or continue your Codex session.",alreadyRedeemed:"This attempt was already redeemed; no second credit was used.",noCredit:"No reset credit is available.",nothingToReset:"There is no eligible usage window to reset."
378
+ }[outcome]
379
+ })
380
+ if(input.current())
381
+ input.dialogs.close()
382
+ } catch(error){if(input.current()){warn(`${error instanceof Error ? error.message : "Reset request failed"} \xB7 retry keeps the same attempt`)
383
+ input.dialogs.back()
384
+ }} finally{setBusy(false)
385
+ }}
386
+ async function review(creditID){if(busy()||!input.current()||!input.adapter.prepareResetCreditConsumption||!input.adapter.consumeResetCredit)
387
+ return
388
+ setBusy(true)
389
+ try{const snapshot=await input.adapter.prepareResetCreditConsumption(input.model,creditID,signal())
390
+ if(!input.current()||snapshot.accountId!==input.model.accountID)
391
+ return
392
+ input.dialogs.open(()=>_$createComponent2(GuardedConfirmation2,{get api(){return input.api
393
+ },get current(){return input.current
394
+ },title:"Use one banked reset?",get message(){return `Account ${snapshot.accountId} \xB7 model ${input.model.modelID}. This may reset other eligible Codex windows.`
395
+ },onConfirm:()=>void consume(creditID)
396
+ }))
397
+ } catch(error){if(input.current())
398
+ warn(error instanceof Error?error.message:"Credit eligibility is unavailable")
399
+ } finally{setBusy(false)
400
+ }}
401
+ return{review,consume,busy
402
+ }}
403
+ function ResetCreditDialog(props){const dialogs=useDialogs(props.api)
404
+ const key=JSON.stringify(props.model)
405
+ const current=()=>JSON.stringify(props.controller.current().model)===key
406
+ const pending=props.preferences.pendingResetAttempt
407
+ const credits=()=>props.controller.snapshot()?.bankedResets
408
+ const actions=createResetCreditActions({...props,dialogs,current
409
+ })
410
+ createEffect(()=>{if(!current())
411
+ dialogs.close()
412
+ })
413
+ const options=()=>[...credits()?.credits?.map((credit)=>({title:credit.title??`Credit ${credit.id}`,value:`credit:${credit.id}`,icon:"presets",description:[credit.status,credit.description,credit.expiresAt&&`expires ${new Date(credit.expiresAt * 1000).toLocaleString()}`].filter(Boolean).join(" \xB7 ")
414
+ }))??[],...credits()?.availableCount&&(credits()?.credits?.length??0)<credits().availableCount?[{title:"Use next available credit",value:"credit:",icon:"presets",description:"Credit details unavailable"
415
+ }]:[],...pending()?[{title:"Reconcile pending attempt",value:"retry",icon:"retry",description:"Reuse the previous idempotency key"
416
+ }]:[],{title:"Cancel",value:"cancel",icon:"close",description:"No credit is used"
417
+ }]
418
+ return _$createComponent3(SelectionMenu,{get api(){return props.api
419
+ },get title(){return `Banked resets \xB7 ${credits()?.availableCount ?? 0} available${actions.busy() ? " \xB7 pending" : ""}`
420
+ },get options(){return options()
421
+ },get height(){return Math.min(18,options().length*3)
422
+ },onSelect:(option)=>{if(option.value==="cancel")
423
+ dialogs.back()
424
+ else if(option.value==="retry")
425
+ actions.consume(pending()?.creditID)
426
+ else if(option.value.startsWith("credit:"))
427
+ actions.review(option.value.slice("credit:".length)||undefined)
428
+ }})
429
+ }
430
+ import {insert as _$insert5} from "@opentui/solid"
431
+ import {setProp as _$setProp5} from "@opentui/solid"
432
+ import {effect as _$effect5} from "@opentui/solid"
433
+ import {createTextNode as _$createTextNode3} from "@opentui/solid"
434
+ import {insertNode as _$insertNode3} from "@opentui/solid"
435
+ import {createElement as _$createElement5} from "@opentui/solid"
436
+ import {memo as _$memo3} from "@opentui/solid"
437
+ import {createComponent as _$createComponent7} from "@opentui/solid"
438
+ import {For,Show as Show3} from "solid-js"
439
+ import {useDialogs as useDialogs2,useIcons as useIcons2} from "./tui.js"
440
+ import {createComponent as _$createComponent4} from "@opentui/solid"
441
+ import {effect as _$effect2} from "@opentui/solid"
442
+ import {insert as _$insert2} from "@opentui/solid"
443
+ import {setProp as _$setProp2} from "@opentui/solid"
444
+ import {createElement as _$createElement2} from "@opentui/solid"
445
+ import {SelectionBox} from "./tui.js"
446
+ import {createSignal as createSignal4,onCleanup} from "solid-js"
447
+ function sidebarInteractiveColors(theme,state,normalColor=theme.text,mode="row"){const highlighted=!state.disabled&&(state.focused||mode==="control"&&state.hovered)
448
+ let backgroundColor=theme.backgroundPanel
449
+ let foregroundColor=normalColor
450
+ if(highlighted)
451
+ backgroundColor=theme.primary
452
+ else if(mode==="control")
453
+ backgroundColor="transparent"
454
+ else if(state.focused||state.hovered)
455
+ backgroundColor=theme.backgroundElement
456
+ if(state.disabled)
457
+ foregroundColor=theme.textMuted
458
+ else if(highlighted)
459
+ foregroundColor=theme.selectedListItemText
460
+ return{backgroundColor,foregroundColor}}
461
+ function useSidebarItem(api,interaction,descriptor,normalColor=()=>api.theme.current.text,mode="row"){let unregister
462
+ const [hovered,setHovered]=createSignal4(false)
463
+ const disabled=()=>descriptor.disabled?.()===true
464
+ const focused=()=>Boolean(interaction?.ownsFocus()&&interaction.isSelected(descriptor.id))
465
+ const colors=()=>sidebarInteractiveColors(api.theme.current,{focused:focused(),hovered:hovered(),disabled:disabled()},normalColor(),mode)
466
+ const ref=(renderable)=>{unregister?.()
467
+ unregister=interaction?.register({...descriptor,renderable})
468
+ }
469
+ onCleanup(()=>unregister?.())
470
+ return{ref,disabled,focused,backgroundColor:()=>colors().backgroundColor,foregroundColor:()=>colors().foregroundColor,onMouseOver:()=>setHovered(true),onMouseOut:()=>setHovered(false),select(event){interaction?.mouseSelect(descriptor.id,event)
471
+ },activate(event){if(interaction)
472
+ return interaction.mouseActivate(descriptor.id,event)
473
+ if(disabled())
474
+ return false
475
+ descriptor.activate()
476
+ return true
477
+ }}}
478
+ function LimitsAction(props){const item=useSidebarItem(props.api,props.interaction,{id:props.id,position:()=>props.position,disabled:()=>props.disabled===true,activate:props.onActivate
479
+ })
480
+ return _$createComponent4(SelectionBox,{ref:(node)=>item.ref(node),get id(){return props.id
481
+ },height:1,get backgroundColor(){return item.backgroundColor()
482
+ },get onMouseOver(){return item.onMouseOver
483
+ },get onMouseOut(){return item.onMouseOut
484
+ },onMouseUp:(event)=>{event.stopPropagation()
485
+ item.activate(event)
486
+ },get children(){var _el$=_$createElement2("text")
487
+ _$setProp2(_el$,"wrapMode","none")
488
+ _$setProp2(_el$,"truncate",true)
489
+ _$insert2(_el$,()=>props.label)
490
+ _$effect2((_$p)=>_$setProp2(_el$,"fg",item.foregroundColor(),_$p))
491
+ return _el$
492
+ }})
493
+ }
494
+ import {createComponent as _$createComponent5} from "@opentui/solid"
495
+ import {effect as _$effect3} from "@opentui/solid"
496
+ import {insert as _$insert3} from "@opentui/solid"
497
+ import {setProp as _$setProp3} from "@opentui/solid"
498
+ import {createElement as _$createElement3} from "@opentui/solid"
499
+ import {Show} from "solid-js"
500
+ function LimitsStatus(props){const message=()=>{const snapshot=props.controller.snapshot()
501
+ const state=props.controller.state()
502
+ if(state.status==="error")
503
+ return snapshot?"Stale \xB7 refresh failed; retry below":"Quota read failed \xB7 retry below"
504
+ if(!snapshot)
505
+ return state.status==="loading"?"Reading provider limits\u2026":"Refresh to read provider limits"
506
+ const availability=snapshot.availability
507
+ if(availability==="unauthenticated")
508
+ return "Codex sign-in or linked account changed \xB7 relink below"
509
+ if(availability==="unsupported")
510
+ return "No documented quota bucket for this model"
511
+ if(availability==="rate_limited")
512
+ return "Provider rate-limited this read \xB7 try again later"
513
+ if(availability==="stale")
514
+ return "Stale provider data \xB7 refresh below"
515
+ return `Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()}`
516
+ }
517
+ return _$createComponent5(Show,{get when(){return props.controller.current().adapter
518
+ },get children(){var _el$=_$createElement3("text")
519
+ _$setProp3(_el$,"wrapMode","word")
520
+ _$insert3(_el$,message)
521
+ _$effect3((_$p)=>_$setProp3(_el$,"fg",props.api.theme.current.textMuted,_$p))
522
+ return _el$
523
+ }})
524
+ }
525
+ import {createComponent as _$createComponent6} from "@opentui/solid"
526
+ import {effect as _$effect4} from "@opentui/solid"
527
+ import {createTextNode as _$createTextNode2} from "@opentui/solid"
528
+ import {insertNode as _$insertNode2} from "@opentui/solid"
529
+ import {memo as _$memo2} from "@opentui/solid"
530
+ import {insert as _$insert4} from "@opentui/solid"
531
+ import {setProp as _$setProp4} from "@opentui/solid"
532
+ import {createElement as _$createElement4} from "@opentui/solid"
533
+ import {Show as Show2,createEffect as createEffect2,createSignal as createSignal5} from "solid-js"
534
+ import {SelectionBox as SelectionBox2,useIcons} from "./tui.js"
535
+ var graphemes=new Intl.Segmenter(undefined,{granularity:"grapheme"
536
+ })
537
+ function truncateEnd(value,maxWidth){if(Bun.stringWidth(value)<=maxWidth)
538
+ return value
539
+ if(maxWidth<=0)
540
+ return ""
541
+ const ellipsis=maxWidth>=3?"...":".".repeat(maxWidth)
542
+ const contentWidth=maxWidth-Bun.stringWidth(ellipsis)
543
+ let result=""
544
+ for(const{segment
545
+ } of graphemes.segment(value)){if(Bun.stringWidth(result + segment)>contentWidth)
546
+ break
547
+ result += segment
548
+ }
549
+ return result + ellipsis
550
+ }
551
+ function Section(props){const icons=useIcons()
552
+ const [actionWidth,setActionWidth]=createSignal5()
553
+ const icon=()=>props.section?icons.section(props.section):props.icon
554
+ const theme=()=>props.api.theme.current
555
+ const item=useSidebarItem(props.api,props.interaction,{id:props.sectionId??`section.${props.title}`,position:()=>({section:props.navigationSection??0,row:0,column:0
556
+ }),activate:props.onToggle
557
+ })
558
+ const action=props.headerAction?useSidebarItem(props.api,props.interaction,{id:props.headerAction.id,position:()=>({section:props.navigationSection??0,row:0,column:1
559
+ }),disabled:props.headerAction.disabled,activate:props.headerAction.onActivate
560
+ },()=>theme().accent,"control"):undefined
561
+ createEffect2(()=>{props.headerAction?.label()
562
+ setActionWidth(undefined)
563
+ })
564
+ const actionLabel=()=>{const label=props.headerAction?.label()??""
565
+ const width=actionWidth()
566
+ return width===undefined?label:truncateEnd(label,width-2)
567
+ }
568
+ return(()=>{var _el$=_$createElement4("box")
569
+ _$setProp4(_el$,"gap",1)
570
+ _$insert4(_el$,_$createComponent6(SelectionBox2,{ref:(node)=>item.ref(node),get id(){return props.sectionId
571
+ },flexDirection:"row",height:1,justifyContent:"space-between",gap:1,paddingLeft:1,paddingRight:1,get backgroundColor(){return item.backgroundColor()
572
+ },get onMouseOver(){return item.onMouseOver
573
+ },get onMouseOut(){return item.onMouseOut
574
+ },onMouseDown:(event)=>item.activate(event),onSizeChange:()=>setActionWidth(undefined),get children(){return [(()=>{var _el$2=_$createElement4("text"),_el$3=_$createElement4("span"),_el$4=_$createTextNode2(` `),_el$5=_$createElement4("span"),_el$6=_$createElement4("b")
575
+ _$insertNode2(_el$2,_el$3)
576
+ _$insertNode2(_el$2,_el$4)
577
+ _$insertNode2(_el$2,_el$5)
578
+ _$insertNode2(_el$2,_el$6)
579
+ _$setProp4(_el$2,"flexShrink",0)
580
+ _$setProp4(_el$2,"wrapMode","none")
581
+ _$insert4(_el$3,()=>icons.icon(props.open?"expanded":"collapsed"))
582
+ _$insert4(_el$5,(()=>{var _c$=_$memo2(()=>!!icon())
583
+ return()=>_c$()?`${icon()} `:""
584
+ })())
585
+ _$insert4(_el$6,()=>props.title)
586
+ _$effect4((_p$)=>{var _v$=item.foregroundColor(),_v$2={fg:item.focused()?item.foregroundColor():theme().accent
587
+ },_v$3={fg:item.focused()?item.foregroundColor():theme().accent
588
+ }
589
+ _v$!==_p$.e&&(_p$.e=_$setProp4(_el$2,"fg",_v$,_p$.e))
590
+ _v$2!==_p$.t&&(_p$.t=_$setProp4(_el$3,"style",_v$2,_p$.t))
591
+ _v$3!==_p$.a&&(_p$.a=_$setProp4(_el$5,"style",_v$3,_p$.a))
592
+ return _p$
593
+ },{e:undefined,t:undefined,a:undefined
594
+ })
595
+ return _el$2
596
+ })(),(()=>{var _el$7=_$createElement4("box"),_el$8=_$createElement4("text")
597
+ _$insertNode2(_el$7,_el$8)
598
+ _$setProp4(_el$7,"flexDirection","row")
599
+ _$setProp4(_el$7,"gap",1)
600
+ _$setProp4(_el$7,"flexGrow",1)
601
+ _$setProp4(_el$7,"minWidth",0)
602
+ _$setProp4(_el$7,"justifyContent","flex-end")
603
+ _$insert4(_el$7,_$createComponent6(Show2,{when:action,children:(control)=>_$createComponent6(SelectionBox2,{ref:(node)=>control().ref(node),get id(){return props.headerAction?.id
604
+ },flexShrink:1,minWidth:3,overflow:"hidden",get backgroundColor(){return control().backgroundColor()
605
+ },onSizeChange:function(){setActionWidth(this.width)
606
+ },get onMouseOver(){return control().onMouseOver
607
+ },get onMouseOut(){return control().onMouseOut
608
+ },onMouseDown:(event)=>event.stopPropagation(),onMouseUp:(event)=>{event.stopPropagation()
609
+ control().activate(event)
610
+ },get children(){var _el$9=_$createElement4("text")
611
+ _$setProp4(_el$9,"wrapMode","none")
612
+ _$setProp4(_el$9,"height",1)
613
+ _$insert4(_el$9,actionLabel)
614
+ _$effect4((_$p)=>_$setProp4(_el$9,"fg",control().foregroundColor(),_$p))
615
+ return _el$9
616
+ }})
617
+ }),_el$8)
618
+ _$setProp4(_el$8,"wrapMode","none")
619
+ _$setProp4(_el$8,"truncate",true)
620
+ _$setProp4(_el$8,"height",1)
621
+ _$insert4(_el$8,()=>props.summary)
622
+ _$effect4((_p$)=>{var _v$4=item.focused()?item.foregroundColor():theme().textMuted,_v$5=props.headerAction?0:1
623
+ _v$4!==_p$.e&&(_p$.e=_$setProp4(_el$8,"fg",_v$4,_p$.e))
624
+ _v$5!==_p$.t&&(_p$.t=_$setProp4(_el$8,"flexShrink",_v$5,_p$.t))
625
+ return _p$
626
+ },{e:undefined,t:undefined
627
+ })
628
+ return _el$7
629
+ })()]
630
+ }}),null)
631
+ _$insert4(_el$,_$createComponent6(Show2,{get when(){return props.open
632
+ },get children(){return props.children
633
+ }}),null)
634
+ return _el$
635
+ })()
636
+ }
637
+ function ProviderLimitsSection(props){const icons=useIcons2()
638
+ const dialogs=useDialogs2(props.api)
639
+ const model=()=>props.controller.current().model
640
+ const snapshot=props.controller.snapshot
641
+ const accountLink=createCodexAccountLink({api:props.api,preferences:props.preferences,adapter:props.codex,model,dialogs
642
+ })
643
+ const action=(id,row,label,onActivate,disabled=false)=>_$createComponent7(LimitsAction,{get api(){return props.api
644
+ },get interaction(){return props.interaction
645
+ },id:`${PLUGIN_ID}.limits.${id}`,get position(){return{section:props.navigationSection,row,column:0
646
+ }},label,onActivate,disabled
647
+ })
648
+ function openCredits(){const selected=model()
649
+ if(!selected)
650
+ return
651
+ dialogs.open(()=>_$createComponent7(ResetCreditDialog,{get api(){return props.api
652
+ },get preferences(){return props.preferences
653
+ },get controller(){return props.controller
654
+ },get adapter(){return props.codex
655
+ },model:selected
656
+ }))
657
+ }
658
+ return _$createComponent7(Section,{get api(){return props.api
659
+ },get interaction(){return props.interaction
660
+ },sectionId:`${PLUGIN_ID}.section.limits`,get navigationSection(){return props.navigationSection
661
+ },title:"LIMITS",section:"limits",get summary(){return _$memo3(()=>snapshot()?.availability==="ready")()?String(snapshot()?.windows.length):""
662
+ },get open(){return props.preferences.expanded().limits
663
+ },onToggle:()=>props.preferences.toggleSectionExpanded("limits"),get children(){return _$createComponent7(Show3,{get when(){return model()
664
+ },get fallback(){return(()=>{var _el$=_$createElement5("text")
665
+ _$insertNode3(_el$,_$createTextNode3(`Open a session to view model limits`))
666
+ _$effect5((_$p)=>_$setProp5(_el$,"fg",props.api.theme.current.textMuted,_$p))
667
+ return _el$
668
+ })()
669
+ },children:(selected)=>(()=>{var _el$3=_$createElement5("box"),_el$4=_$createElement5("text"),_el$5=_$createTextNode3(`/`)
670
+ _$insertNode3(_el$3,_el$4)
671
+ _$setProp5(_el$3,"gap",1)
672
+ _$insertNode3(_el$4,_el$5)
673
+ _$setProp5(_el$4,"wrapMode","none")
674
+ _$setProp5(_el$4,"truncate",true)
675
+ _$insert5(_el$4,()=>selected().providerID,_el$5)
676
+ _$insert5(_el$4,()=>selected().modelID,null)
677
+ _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>!!!selected().accountID)()&&selected().providerID==="openai"
678
+ },get children(){return [(()=>{var _el$6=_$createElement5("text")
679
+ _$insertNode3(_el$6,_$createTextNode3(`Link the matching Codex account to read quotas`))
680
+ _$effect5((_$p)=>_$setProp5(_el$6,"fg",props.api.theme.current.textMuted,_$p))
681
+ return _el$6
682
+ })(),_$memo3(()=>action("link",1,`${icons.icon("scope")} Link Codex CLI account`,()=>void accountLink.link(),accountLink.busy()))]
683
+ }}),null)
684
+ _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>!!selected().accountID)()&&snapshot()?.availability==="ready"
685
+ },get children(){return [(()=>{var _el$8=_$createElement5("text"),_el$9=_$createTextNode3(`Codex CLI \xB7 `)
686
+ _$insertNode3(_el$8,_el$9)
687
+ _$setProp5(_el$8,"wrapMode","none")
688
+ _$setProp5(_el$8,"truncate",true)
689
+ _$insert5(_el$8,()=>snapshot()?.accountId,null)
690
+ _$effect5((_$p)=>_$setProp5(_el$8,"fg",props.api.theme.current.textMuted,_$p))
691
+ return _el$8
692
+ })(),_$createComponent7(For,{get each(){return snapshot()?.windows
693
+ },children:(window)=>_$createComponent7(QuotaWindowRow,{get api(){return props.api
694
+ },window
695
+ })
696
+ }),_$createComponent7(Show3,{get when(){return snapshot()?.bankedResets
697
+ },children:(credits)=>[(()=>{var _el$10=_$createElement5("text"),_el$11=_$createTextNode3(`Banked resets: `)
698
+ _$insertNode3(_el$10,_el$11)
699
+ _$insert5(_el$10,()=>credits().availableCount,null)
700
+ _$effect5((_$p)=>_$setProp5(_el$10,"fg",props.api.theme.current.textMuted,_$p))
701
+ return _el$10
702
+ })(),_$createComponent7(Show3,{get when(){return credits().availableCount>0||props.preferences.pendingResetAttempt()
703
+ },get children(){return action("credits",2,`${icons.icon("info")} Review reset credits`,openCredits)
704
+ }})]
705
+ })]
706
+ }}),null)
707
+ _$insert5(_el$3,_$createComponent7(LimitsStatus,{get api(){return props.api
708
+ },get controller(){return props.controller
709
+ }}),null)
710
+ _$insert5(_el$3,_$createComponent7(Show3,{get when(){return selected().providerID!=="openai"
711
+ },get children(){var _el$0=_$createElement5("text")
712
+ _$insertNode3(_el$0,_$createTextNode3(`No documented quota API for this model`))
713
+ _$effect5((_$p)=>_$setProp5(_el$0,"fg",props.api.theme.current.textMuted,_$p))
714
+ return _el$0
715
+ }}),null)
716
+ _$insert5(_el$3,_$createComponent7(Show3,{get when(){return props.controller.current().adapter
717
+ },get children(){return action("refresh",3,`${icons.icon("retry")} ${props.controller.state().status === "error" ? "Retry" : "Refresh"}`,()=>void props.controller.refresh(true),["loading","refreshing"].includes(props.controller.state().status))
718
+ }}),null)
719
+ _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>selected().providerID==="openai")()&&selected().accountID
720
+ },get children(){return [_$memo3(()=>action("unlink",4,`${icons.icon("close")} Unlink Codex account`,()=>props.preferences.setCodexAccountBinding(selected().providerID,selected().modelID))),_$createComponent7(Show3,{get when(){return snapshot()?.availability==="unauthenticated"
721
+ },get children(){return action("relink",5,`${icons.icon("scope")} Relink Codex account`,()=>void accountLink.link(),accountLink.busy())
722
+ }})]
723
+ }}),null)
724
+ _$effect5((_$p)=>_$setProp5(_el$4,"fg",props.api.theme.current.textMuted,_$p))
725
+ return _el$3
726
+ })()
727
+ })
728
+ }})
729
+ }
730
+ function selectedV1Model(api){const route=api.route.current
731
+ const sessionID="params" in route?route.params?.sessionID:undefined
732
+ if(route.name!=="session"||typeof sessionID!=="string")
733
+ return
734
+ const model=api.state.session.get(sessionID)?.model
735
+ if(!model)
736
+ return
737
+ return{sessionID,providerID:model.providerID,modelID:model.id,...model.variant&&{variant:model.variant}}}
738
+ export{ProviderLimitsSection,createCodexAppServerClient,createCodexQuotaAdapter,createProviderLimitsController,selectedV1Model
739
+ }