opencode-navigator 0.25.0 → 0.27.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.
@@ -1,5 +1,5 @@
1
1
  import {PLUGIN_ID
2
- } from "./chunk-bv0nm1tj.js";
2
+ } from "./chunk-dm1y8x49.js";
3
3
  function createCodexAppServerClient(input){const command=input.command??["codex","app-server","--stdio"]
4
4
  const timeoutMs=input.timeoutMs??1e4
5
5
  const pending=new Map
@@ -14,8 +14,9 @@ pending.delete(id)
14
14
  }}
15
15
  function write(message){if(!child?.stdin||typeof child.stdin==="number")
16
16
  throw new Error("Codex app-server is unavailable")
17
- child.stdin.write(`${JSON.stringify(message)}
18
- `)
17
+ Promise.resolve(child.stdin.write(`${JSON.stringify(message)}
18
+ `)).catch((error)=>{failAll(error instanceof Error?error:new Error("Codex app-server write failed"))
19
+ })
19
20
  }
20
21
  function accept(line){let message
21
22
  try{message=JSON.parse(line)
@@ -133,6 +134,29 @@ return{...balance}})
133
134
  if(bankedResets&&(!Number.isSafeInteger(bankedResets.availableCount)||bankedResets.availableCount<0)){throw new Error("Invalid banked reset count")
134
135
  }
135
136
  return{...snapshot,windows,...balances&&{balances},...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
137
+ var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"])
138
+ function providerAccountIdentity(provider,credentials){if(!provider.integrationID||provider.activation==="disabled")
139
+ return{status:"unsupported"}
140
+ const active=credentials.filter((entry)=>entry.integrationID===provider.integrationID&&entry.active)
141
+ if(active.length!==1)
142
+ return{status:"unavailable"}
143
+ const credential=active[0]
144
+ const id=`credential:${credential.id}`
145
+ if(credential.value.type==="external")
146
+ return{status:"unsupported",id}
147
+ const providerID=provider.canonical??provider.id
148
+ if(credential.value.type==="key")
149
+ return{status:"ready",id,method:"api",providerID}
150
+ if(provider.integrationID!=="openai")
151
+ return{status:"ready",id,method:"oauth",providerID}
152
+ if(!credential.value.methodID||!CHATGPT_METHODS.has(credential.value.methodID))
153
+ return{status:"unsupported",id}
154
+ const account=credential.value.metadata?.accountID
155
+ const accountID=typeof account==="string"&&account.trim()?account:undefined
156
+ const header=Object.entries(provider.headers??{}).find(([name])=>name.toLowerCase()==="chatgpt-account-id")?.[1]
157
+ if(accountID&&header&&header!==accountID)
158
+ return{status:"loading"}
159
+ return{status:"ready",id,method:"oauth",providerID,...accountID&&{accountID}}}
136
160
  import {createSignal as createSignal2,untrack} from "solid-js"
137
161
  import {createSignal} from "solid-js"
138
162
  var idle={status:"idle"}
@@ -200,6 +224,7 @@ setStates((values)=>({...values,[target]:{status:"error",error:requestError(caus
200
224
  }}
201
225
  return{state,start,abort,abortAll}}
202
226
  function createProviderLimitsController(adapters,selectedModel,signal,refreshContext){const requests=createRequestState(signal)
227
+ const pending=new Map
203
228
  const [snapshots,setSnapshots]=createSignal2({})
204
229
  const targetKey=(model)=>JSON.stringify(model)
205
230
  let activeKey
@@ -208,6 +233,7 @@ function current(){const model=selectedModel()
208
233
  const adapter=model&&adapters.find((candidate)=>candidate.supports(model))
209
234
  const key=model?targetKey(model):undefined
210
235
  if(activeKey!==key){untrack(requests.abortAll)
236
+ pending.clear()
211
237
  targetAbort.abort()
212
238
  targetAbort=new AbortController
213
239
  activeKey=key
@@ -219,10 +245,13 @@ const{model,adapter}=current()
219
245
  if(!model||!adapter)
220
246
  return
221
247
  const key=targetKey(model)
248
+ const existing=pending.get(key)
249
+ if(existing&&!force)
250
+ return existing
222
251
  const request=requests.start(key,"read provider limits",Boolean(snapshots()[key]),force)
223
252
  if(!request)
224
253
  return
225
- try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal,force))
254
+ const promise=(async ()=>{try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal,force))
226
255
  if(!request.isCurrent()||targetKey(current().model??model)!==key)
227
256
  return
228
257
  setSnapshots((value)=>({...value,[key]:snapshot}))
@@ -234,6 +263,11 @@ request.succeed()
234
263
  } else
235
264
  request.fail(error)
236
265
  } finally{request.finish()
266
+ }})()
267
+ pending.set(key,promise)
268
+ try{await promise
269
+ } finally{if(pending.get(key)===promise)
270
+ pending.delete(key)
237
271
  }}
238
272
  return{current,targetSignal(){current()
239
273
  return signal?AbortSignal.any([signal,targetAbort.signal]):targetAbort.signal
@@ -242,6 +276,7 @@ return model&&adapter?snapshots()[targetKey(model)]:undefined
242
276
  },state(){const{model,adapter}=current()
243
277
  return model&&adapter?requests.state(targetKey(model)):{status:"idle"}},dispose(){targetAbort.abort()
244
278
  requests.abortAll()
279
+ pending.clear()
245
280
  for(const adapter of adapters)
246
281
  adapter.dispose?.()
247
282
  }}}
@@ -282,7 +317,9 @@ return [{id:`${id}.${name}`,label:account?`Codex account \xB7 ${name}`:`${limit.
282
317
  })
283
318
  })
284
319
  }
285
- function createCodexQuotaAdapter(client,validateHost){const adapter={id:"codex",name:"Codex CLI",supports:(model)=>(model.providerID==="openai"||model.hostConnection?.providerID==="openai")&&model.hostConnection?.method!=="api"&&Boolean(model.accountID)&&(!model.hostConnection||model.hostConnection.status==="ready"),...validateHost&&{validateHost},async discoverAccount(signal){const account=await client.request("account/read",{refreshToken:false},signal)
320
+ function supportsCodexQuota(model){return(model.providerID==="openai"||model.hostConnection?.providerID==="openai")&&model.hostConnection?.method!=="api"&&Boolean(model.accountID)&&(!model.hostConnection||model.hostConnection.status==="ready")
321
+ }
322
+ function createCodexQuotaAdapter(client,validateHost){const adapter={id:"codex",name:"Codex CLI",supports:supportsCodexQuota,...validateHost&&{validateHost},async discoverAccount(signal){const account=await client.request("account/read",{refreshToken:false},signal)
286
323
  if(account.account?.type!=="chatgpt")
287
324
  throw new Error("Sign in to Codex CLI with ChatGPT")
288
325
  const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:true},signal)
@@ -337,35 +374,53 @@ return adapter
337
374
  import {createComponent as _$createComponent} from "@opentui/solid"
338
375
  import {createSignal as createSignal3} from "solid-js"
339
376
  import {GuardedConfirmation} from "./tui.js"
377
+ var alwaysCurrent=()=>true
378
+ class CodexAccountMismatchError extends Error{constructor(){super("Sign in to Codex CLI with the account selected in OpenCode.")
379
+ this.name="CodexAccountMismatchError"
380
+ }}
381
+ function canLinkCodexAccount(model){return(model.providerID==="openai"||model.hostConnection?.providerID==="openai")&&model.hostConnection?.method!=="api"&&(!model.hostConnection||model.hostConnection.status==="ready")
382
+ }
383
+ function createCodexAccountLinkModel(input){const model=input.selected()
384
+ const key=JSON.stringify(model)
385
+ const current=()=>!input.signal.aborted&&(input.current??alwaysCurrent)()&&JSON.stringify(input.selected())===key
386
+ const validHost=async ()=>Boolean(model&&(!input.adapter.validateHost||await input.adapter.validateHost(model,input.signal)))
387
+ async function discover(){if(!model||!current()||!input.adapter.discoverAccount||!canLinkCodexAccount(model))
388
+ return
389
+ if(!await validHost()||!current())
390
+ return
391
+ const accountID=await input.adapter.discoverAccount(input.signal)
392
+ if(!current())
393
+ return
394
+ if(model.hostConnection?.accountID&&model.hostConnection.accountID!==accountID)
395
+ throw new CodexAccountMismatchError
396
+ return{accountID,model}}
397
+ return{current,discover,async confirm(preview){const latest=await discover()
398
+ if(!latest||!current()||latest.accountID!==preview.accountID)
399
+ return false
400
+ await input.bind(latest.model.providerID,latest.model.hostConnection?.id,latest.accountID)
401
+ return true
402
+ }}}
340
403
  function createCodexAccountLink(input){const [busy,setBusy]=createSignal3(false)
341
404
  async function link(){const selected=input.model()
342
405
  if(busy()||!selected||selected.providerID!=="openai"&&selected.hostConnection?.providerID!=="openai"||!input.adapter.discoverAccount)
343
406
  return
344
407
  if(selected.hostConnection&&selected.hostConnection.status!=="ready")
345
408
  return
346
- const key=JSON.stringify(selected)
347
- const current=()=>JSON.stringify(input.model())===key
348
409
  const signal=input.api.lifecycle?.signal??new AbortController().signal
349
- const validHost=async ()=>!input.adapter.validateHost||await input.adapter.validateHost(selected,signal)
350
- setBusy(true)
351
- try{if(!await validHost()||!current())
352
- return
353
- const accountID=await input.adapter.discoverAccount(signal)
354
- if(!current())
355
- return
356
- if(selected.hostConnection?.accountID&&selected.hostConnection.accountID!==accountID){input.api.ui.toast({variant:"warning",title:"Codex account",message:"Sign in to Codex CLI with the account selected in OpenCode."
410
+ const model=createCodexAccountLinkModel({adapter:input.adapter,selected:input.model,signal,bind:input.preferences.setCodexConnectionBinding
357
411
  })
412
+ const current=model.current
413
+ setBusy(true)
414
+ try{const preview=await model.discover()
415
+ if(!preview)
358
416
  return
359
- }
417
+ const{accountID
418
+ }=preview
360
419
  async function confirm(){if(busy()||!current())
361
420
  return
362
421
  setBusy(true)
363
- try{if(!await validHost()||!current())
364
- return
365
- const latest=await input.adapter.discoverAccount(signal)
366
- if(!current()||latest!==accountID)
367
- return
368
- input.preferences.setCodexConnectionBinding(selected.providerID,selected.hostConnection?.id,accountID)
422
+ try{if(await model.confirm({accountID
423
+ }))
369
424
  input.dialogs.back()
370
425
  } catch{if(current())
371
426
  input.api.ui.toast({variant:"warning",title:"Codex account",message:"Account verification failed; try again."
@@ -376,8 +431,8 @@ input.dialogs.open(()=>_$createComponent(GuardedConfirmation,{get api(){return i
376
431
  },current,title:"Link Codex CLI account",get message(){return `Use Codex CLI account ${accountID} for ${selected.providerID}${selected.hostConnection?.id ? " connection" : ""} across models? Only confirm matching accounts.`
377
432
  },onConfirm:()=>void confirm()
378
433
  }))
379
- } catch{if(current())
380
- input.api.ui.toast({variant:"warning",title:"Codex account",message:"Install a supported Codex CLI and sign in with ChatGPT, then try again."
434
+ } catch(error){if(current())
435
+ input.api.ui.toast({variant:"warning",title:"Codex account",message:error instanceof CodexAccountMismatchError?error.message:"Install a supported Codex CLI and sign in with ChatGPT, then try again."
381
436
  })
382
437
  } finally{setBusy(false)
383
438
  }}
@@ -479,41 +534,55 @@ import {SelectionMenu,useDialogs} from "./tui.js"
479
534
  import {createComponent as _$createComponent2} from "@opentui/solid"
480
535
  import {randomUUID} from "crypto"
481
536
  import {GuardedConfirmation as GuardedConfirmation2,useDialogState} from "./tui.js"
482
- function createResetCreditActions(input){const [busy,setBusy]=useDialogState("credit-busy",false)
483
- const signal=input.controller.targetSignal
484
- const warn=(message)=>input.api.ui.toast({variant:"warning",title:"Codex reset credit",message
485
- })
486
- async function consume(creditID){if(busy()||!input.current()||!input.model.accountID||!input.adapter.consumeResetCredit)
537
+ var resetCreditOutcomeMessage={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."
538
+ };
539
+ function resetAttemptMatches(model,attempt){return attempt.accountID===model.accountID&&attempt.modelID===model.modelID&&attempt.providerID===model.providerID&&(attempt.connectionID===undefined||attempt.connectionID===model.hostConnection?.id)
540
+ }
541
+ function createResetCreditModel(input){async function consume(creditID){if(input.busy()||!input.current()||!input.model.accountID||!input.adapter.consumeResetCredit)
487
542
  return
488
- setBusy(true)
543
+ input.setBusy(true)
489
544
  const previous=input.preferences.pendingResetAttempt()
490
- const attempt=previous??{providerID:input.model.providerID,modelID:input.model.modelID,accountID:input.model.accountID,...creditID&&{creditID
491
- },idempotencyKey:randomUUID(),createdAt:Date.now(),...input.model.hostConnection?.id&&{connectionID:input.model.hostConnection.id
492
- }}
493
- try{if(attempt.accountID!==input.model.accountID||attempt.modelID!==input.model.modelID||attempt.providerID!==input.model.providerID||attempt.connectionID!==undefined&&attempt.connectionID!==input.model.hostConnection?.id){throw new Error("Resolve the earlier attempt on its linked account and model first")
494
- }
545
+ const attempt=previous??{providerID:input.model.providerID,modelID:input.model.modelID,accountID:input.model.accountID,...creditID&&{creditID},idempotencyKey:input.id(),createdAt:input.now(),...input.model.hostConnection?.id&&{connectionID:input.model.hostConnection.id}}
546
+ try{if(!resetAttemptMatches(input.model,attempt))
547
+ throw new Error("Resolve the earlier attempt on its linked account and model first")
495
548
  if(!previous)
496
549
  await input.preferences.beginResetAttempt(attempt)
497
550
  if(!input.current())
498
551
  return
499
- const outcome=await input.adapter.consumeResetCredit(input.model,{...attempt,uncertain:Boolean(previous)
500
- },signal())
552
+ const outcome=await input.adapter.consumeResetCredit(input.model,{...attempt,uncertain:Boolean(previous)},input.signal())
501
553
  await input.preferences.finishResetAttempt(attempt.idempotencyKey)
502
- await input.controller.refresh(true)
503
- 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."
504
- }[outcome]
554
+ await input.refresh()
555
+ return outcome
556
+ } finally{input.setBusy(false)
557
+ }}
558
+ async function review(creditID){if(input.busy()||!input.current()||!input.adapter.prepareResetCreditConsumption||!input.adapter.consumeResetCredit)
559
+ return
560
+ input.setBusy(true)
561
+ try{const snapshot=await input.adapter.prepareResetCreditConsumption(input.model,creditID,input.signal())
562
+ if(!input.current()||snapshot.accountId!==input.model.accountID)
563
+ return
564
+ return snapshot
565
+ } finally{input.setBusy(false)
566
+ }}
567
+ return{review,consume,busy:input.busy}}
568
+ function createResetCreditActions(input){const [busy,setBusy]=useDialogState("credit-busy",false)
569
+ const signal=input.controller.targetSignal
570
+ const warn=(message)=>input.api.ui.toast({variant:"warning",title:"Codex reset credit",message
571
+ })
572
+ const model=createResetCreditModel({preferences:input.preferences,adapter:input.adapter,model:input.model,current:input.current,signal,busy,setBusy,id:randomUUID,now:Date.now,refresh:()=>input.controller.refresh(true)
573
+ })
574
+ async function consume(creditID){try{const outcome=await model.consume(creditID)
575
+ if(!outcome)
576
+ return
577
+ input.api.ui.toast({variant:outcome==="reset"||outcome==="alreadyRedeemed"?"success":"info",title:"Codex reset credit",message:resetCreditOutcomeMessage[outcome]
505
578
  })
506
579
  if(input.current())
507
580
  input.dialogs.close()
508
581
  } catch(error){if(input.current()){warn(`${error instanceof Error ? error.message : "Reset request failed"} \xB7 retry keeps the same attempt`)
509
582
  input.dialogs.back()
510
- }} finally{setBusy(false)
511
- }}
512
- async function review(creditID){if(busy()||!input.current()||!input.adapter.prepareResetCreditConsumption||!input.adapter.consumeResetCredit)
513
- return
514
- setBusy(true)
515
- try{const snapshot=await input.adapter.prepareResetCreditConsumption(input.model,creditID,signal())
516
- if(!input.current()||snapshot.accountId!==input.model.accountID)
583
+ }}}
584
+ async function review(creditID){try{const snapshot=await model.review(creditID)
585
+ if(!snapshot)
517
586
  return
518
587
  input.dialogs.open(()=>_$createComponent2(GuardedConfirmation2,{get api(){return input.api
519
588
  },get current(){return input.current
@@ -522,7 +591,6 @@ input.dialogs.open(()=>_$createComponent2(GuardedConfirmation2,{get api(){return
522
591
  }))
523
592
  } catch(error){if(input.current())
524
593
  warn(error instanceof Error?error.message:"Credit eligibility is unavailable")
525
- } finally{setBusy(false)
526
594
  }}
527
595
  return{review,consume,busy
528
596
  }}
@@ -677,13 +745,13 @@ setRevision((value)=>value + 1)
677
745
  }),context.data.on("server.connected",()=>{generation++
678
746
  setRevision((value)=>value + 1)
679
747
  })]
680
- const adapter={id:"opencode-usage",name:"OpenCode workspace limits",supports:(model)=>typeof context.client.usage?.limits==="function"&&provider(model)?.integrationID==="opencode",async read(model,signal){signal.throwIfAborted()
748
+ const adapter={id:"opencode-usage",name:"OpenCode workspace limits",supports:(model)=>typeof context.client.experimental?.usage?.limits==="function"&&provider(model)?.integrationID==="opencode",async read(model,signal){signal.throwIfAborted()
681
749
  const epoch=generation
682
750
  const target=key()
683
751
  const activeProvider=provider(model)
684
752
  if(!activeProvider||activeProvider.integrationID!=="opencode")
685
753
  throw new Error("Native usage provider changed")
686
- try{const result=await context.client.usage.limits({location:{directory:location().directory}},{signal})
754
+ try{const result=await context.client.experimental.usage.limits({location:{directory:location().directory}},{signal})
687
755
  if(epoch!==generation||target!==key()||signal.aborted||result.location.directory!==location().directory)
688
756
  throw new DOMException("Workspace usage target changed","AbortError")
689
757
  if(!result.data)
@@ -920,35 +988,103 @@ import {createElement as _$createElement8} from "@opentui/solid"
920
988
  import {createComponent as _$createComponent11} from "@opentui/solid"
921
989
  import {memo as _$memo5} from "@opentui/solid"
922
990
  import {Show as Show5} from "solid-js"
923
- function LimitsStatus(props){const message=()=>{const model=props.controller.current().model
924
- const connection=model?.hostConnection
925
- const snapshot=props.controller.snapshot()
926
- const state=props.controller.state()
991
+ import {createSignal as createSignal8} from "solid-js"
992
+ function createProviderAccountSource(ports){const [connections,setConnections]=createSignal8({})
993
+ let generation=0
994
+ let disposed=false
995
+ let lastModel
996
+ let pending
997
+ function save(target,connection){if(JSON.stringify(connections()[target])!==JSON.stringify(connection))
998
+ setConnections((values)=>({...values,[target]:connection}))
999
+ }
1000
+ async function read(model,signal=ports.lifecycle){if(disposed||ports.lifecycle.aborted||signal.aborted)
1001
+ return{status:"unavailable"}
1002
+ const target=ports.key(model)
1003
+ const epoch=generation
1004
+ if(pending?.key===target&&pending.generation===epoch)
1005
+ return pending.promise
1006
+ const promise=(async ()=>{try{const connection=await ports.read(model,AbortSignal.any([ports.lifecycle,signal]))
1007
+ if(disposed||ports.lifecycle.aborted||signal.aborted||epoch!==generation||ports.key(model)!==target)
1008
+ return{status:"unavailable"}
1009
+ save(target,connection)
1010
+ return connection
1011
+ } catch{if(!disposed&&!ports.lifecycle.aborted&&!signal.aborted&&epoch===generation)
1012
+ save(target,{status:"unavailable"})
1013
+ return{status:"unavailable"}}})()
1014
+ pending={key:target,generation:epoch,promise}
1015
+ try{return await promise
1016
+ } finally{if(pending?.promise===promise)
1017
+ pending=undefined
1018
+ }}
1019
+ const unsubscribe=ports.subscribe((eager=true)=>{generation++
1020
+ if(eager)
1021
+ setConnections({})
1022
+ else{const active=lastModel&&ports.key(lastModel)
1023
+ setConnections((values)=>Object.fromEntries(Object.entries(values).filter(([target])=>target===active)))
1024
+ }
1025
+ if(lastModel&&!disposed)
1026
+ read(lastModel)
1027
+ })
1028
+ return{suspend(){if(!lastModel)
1029
+ return
1030
+ lastModel=undefined
1031
+ generation++
1032
+ pending=undefined
1033
+ },current(model){lastModel=model
1034
+ return{...model,hostConnection:connections()[ports.key(model)]??{status:"loading"}}},async refresh(model){await read(model)
1035
+ },async validate(model,signal){const actual=await read(model,signal)
1036
+ const expected=model.hostConnection
1037
+ return actual.status==="ready"&&expected?.status==="ready"&&actual.id===expected.id&&actual.accountID===expected.accountID&&(!actual.accountID||actual.accountID===model.accountID)
1038
+ },dispose(){disposed=true
1039
+ generation++
1040
+ unsubscribe()
1041
+ }}}
1042
+ function bindCodexAccount(model,preferences){const connection=model.hostConnection
1043
+ if(model.providerID!=="openai"&&connection?.providerID!=="openai"||connection?.method==="api")
1044
+ return{...model,accountID:undefined,accountSource:undefined}
1045
+ if(connection&&connection.status!=="ready")
1046
+ return{...model,accountID:undefined,accountSource:undefined}
1047
+ const accountID=connection?.accountID??preferences?.codexAccountForConnection(model.providerID,connection?.id,model.modelID)
1048
+ const accountSource=connection?.accountID?"host":"binding"
1049
+ return{...model,accountID,accountSource:accountID?accountSource:undefined}}
1050
+ import {createEffect as createEffect3,createMemo,on as on2,onCleanup as onCleanup2} from "solid-js"
1051
+ function quotaStatusMessage(input){const{model,snapshot,state}=input
927
1052
  if(state.status==="error")
928
1053
  return snapshot?"Stale \xB7 refresh failed":"Quota read failed \xB7 refresh to retry"
929
- if(snapshot?.availability==="ready"&&(snapshot.windows.length>0||snapshot.balances?.length))
1054
+ if(!input.showReadyMessage&&snapshot?.availability==="ready"&&(snapshot.windows.length>0||snapshot.balances?.length))
930
1055
  return
931
- if(snapshot?.message)
932
- return snapshot.availability==="stale"?`Stale \xB7 ${snapshot.message}`:snapshot.availability==="ready"?`Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()} \xB7 ${snapshot.message}`:snapshot.message
1056
+ if(snapshot?.message){if(snapshot.availability==="stale")
1057
+ return `Stale \xB7 ${snapshot.message}`
1058
+ return snapshot.availability==="ready"?`Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()} \xB7 ${snapshot.message}`:snapshot.message
1059
+ }
1060
+ const connection=model?.hostConnection
933
1061
  if(connection?.status==="loading")
934
1062
  return "Checking the OpenCode account\u2026"
935
1063
  if(connection?.status==="unavailable")
936
1064
  return "Account metadata unavailable \xB7 refresh to retry"
937
1065
  if(connection?.status==="unsupported")
938
1066
  return "Codex quotas require an active ChatGPT OAuth connection"
939
- if(!snapshot)
940
- return state.status==="loading"?"Reading provider limits\u2026":props.controller.current().adapter?"Refresh to read provider limits":providerQuotaCapability(model?.providerID??"").reason
941
- const availability=snapshot.availability
942
- if(availability==="unauthenticated")
1067
+ if(!snapshot){if(state.status==="loading")
1068
+ return "Reading provider limits\u2026"
1069
+ return input.hasAdapter?"Refresh to read provider limits":providerQuotaCapability(model?.providerID??"").reason
1070
+ }
1071
+ if(snapshot.availability==="unauthenticated")
943
1072
  return model?.accountSource==="host"?"Sign in to Codex CLI with the account selected in OpenCode":"Codex sign-in or linked account changed \xB7 relink below"
944
- if(availability==="unsupported")
1073
+ if(snapshot.availability==="unsupported")
945
1074
  return "No documented quota bucket for this model"
946
- if(availability==="rate_limited")
1075
+ if(snapshot.availability==="rate_limited")
947
1076
  return "Provider rate-limited this read \xB7 try again later"
948
- if(availability==="stale")
1077
+ if(snapshot.availability==="stale")
949
1078
  return "Stale provider data \xB7 refresh to update"
1079
+ if(snapshot.availability==="ready"&&input.showReadyMessage&&(snapshot.windows.length>0||snapshot.balances?.length))
1080
+ return
950
1081
  return `Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()}`
951
1082
  }
1083
+ function LimitsStatus(props){const message=()=>{const{model,adapter
1084
+ }=props.controller.current()
1085
+ return quotaStatusMessage({model,snapshot:props.controller.snapshot(),state:props.controller.state(),hasAdapter:Boolean(adapter)
1086
+ })
1087
+ }
952
1088
  return _$createComponent11(Show5,{get when(){return _$memo5(()=>!!props.controller.current().model)()&&message()
953
1089
  },children:(value)=>(()=>{var _el$=_$createElement8("text")
954
1090
  _$setProp8(_el$,"wrapMode","word")
@@ -966,10 +1102,10 @@ import {memo as _$memo6} from "@opentui/solid"
966
1102
  import {insert as _$insert8} from "@opentui/solid"
967
1103
  import {setProp as _$setProp9} from "@opentui/solid"
968
1104
  import {createElement as _$createElement9} from "@opentui/solid"
969
- import {Show as Show6,createSignal as createSignal8} from "solid-js"
1105
+ import {Show as Show6,createSignal as createSignal9} from "solid-js"
970
1106
  import {SelectionBox as SelectionBox3,useIcons as useIcons4} from "./tui.js"
971
1107
  function Section(props){const icons=useIcons4()
972
- const [headerWidth,setHeaderWidth]=createSignal8(0)
1108
+ const [headerWidth,setHeaderWidth]=createSignal9(0)
973
1109
  const icon=()=>props.section?icons.section(props.section):props.icon
974
1110
  const theme=()=>props.api.theme.current
975
1111
  const item=useSidebarItem(props.api,props.interaction,{id:props.sectionId??`section.${props.title}`,position:()=>({section:props.navigationSection??0,row:0,column:0
@@ -1062,7 +1198,7 @@ return _$createComponent13(Section,{get api(){return props.api
1062
1198
  },get navigationSection(){return props.navigationSection
1063
1199
  },get credits(){return(snapshot()?.bankedResets?.availableCount??0)>0||Boolean(props.preferences.pendingResetAttempt())
1064
1200
  },get refreshing(){return ["loading","refreshing"].includes(props.controller.state().status)
1065
- },get canRefresh(){return Boolean(model()&&(props.controller.current().adapter||model()?.hostConnection?.status==="unavailable"))
1201
+ },get canRefresh(){return Boolean(model()&&(props.controller.current().adapter??model()?.hostConnection?.status==="unavailable"))
1066
1202
  },onCredits:openCredits,onRefresh:()=>void props.controller.refresh(true)
1067
1203
  })
1068
1204
  },get open(){return props.preferences.expanded().limits
@@ -1103,49 +1239,11 @@ const model=api.state.session.get(sessionID)?.model
1103
1239
  if(!model)
1104
1240
  return
1105
1241
  return{sessionID,providerID:model.providerID,modelID:model.id,...model.variant&&{variant:model.variant}}}
1106
- import {createSignal as createSignal9,untrack as untrack2} from "solid-js"
1107
- var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"])
1108
- function providerAccountIdentity(provider,credentials){if(!provider.integrationID||provider.activation==="disabled")
1109
- return{status:"unsupported"}
1110
- const active=credentials.filter((entry)=>entry.integrationID===provider.integrationID&&entry.active)
1111
- if(active.length!==1)
1112
- return{status:"unavailable"}
1113
- const credential=active[0]
1114
- const id=`credential:${credential.id}`
1115
- if(credential.value.type==="external")
1116
- return{status:"unsupported",id}
1117
- const providerID=provider.canonical??provider.id
1118
- if(credential.value.type==="key")
1119
- return{status:"ready",id,method:"api",providerID}
1120
- if(provider.integrationID!=="openai")
1121
- return{status:"ready",id,method:"oauth",providerID}
1122
- if(!CHATGPT_METHODS.has(credential.value.methodID))
1123
- return{status:"unsupported",id}
1124
- const account=credential.value.metadata?.accountID
1125
- const accountID=typeof account==="string"&&account.trim()?account:undefined
1126
- const header=Object.entries(provider.headers??{}).find(([name])=>name.toLowerCase()==="chatgpt-account-id")?.[1]
1127
- if(accountID&&header&&header!==accountID)
1128
- return{status:"loading"}
1129
- return{status:"ready",id,method:"oauth",providerID,...accountID&&{accountID}}}
1130
- function createOpenCodeV2AccountSource(context,lifecycle){const [connections,setConnections]=createSignal9({})
1131
- let generation=0
1132
- let disposed=false
1133
- let lastModel
1134
- let pending
1135
- const location=()=>context.location??context.data.location.default()
1136
- const key=(model)=>JSON.stringify([model.providerID,location()])
1137
- function save(target,connection){if(JSON.stringify(connections()[target])!==JSON.stringify(connection))
1138
- setConnections((values)=>({...values,[target]:connection}))
1139
- }
1140
- async function read(model,signal=lifecycle){if(disposed||signal.aborted)
1141
- return{status:"unavailable"}
1142
- const target=key(model)
1143
- const epoch=generation
1144
- if(pending?.key===target&&pending.generation===epoch)
1145
- return pending.promise
1146
- const promise=(async ()=>{try{if(!context.client.provider?.get||!context.client.credential?.list)
1242
+ import {untrack as untrack2} from "solid-js"
1243
+ function createOpenCodeV2AccountSource(context,lifecycle){const location=()=>context.location??context.data.location.default()
1244
+ return createProviderAccountSource({lifecycle,key:(model)=>JSON.stringify([model.providerID,location()]),async read(model,signal){if(!context.client.provider?.get||!context.client.credential?.list)
1147
1245
  throw new Error("Account metadata unavailable")
1148
- const options={signal:AbortSignal.any([lifecycle,signal])}
1246
+ const options={signal}
1149
1247
  const [provider,credentials]=await Promise.all([context.client.provider.get({providerID:model.providerID,location:location()},options),context.client.credential.list(options)])
1150
1248
  let connection=provider.data.id===model.providerID?providerAccountIdentity(provider.data,credentials):{status:"unavailable"}
1151
1249
  if(connection.status==="unavailable"&&credentials.every((entry)=>!(entry.integrationID===provider.data.integrationID&&entry.active))&&provider.data.integrationID&&typeof context.client.integration?.get==="function"){const integration=await context.client.integration.get({integrationID:provider.data.integrationID,location:location()},options)
@@ -1153,56 +1251,17 @@ const active=integration.data.connections[0]
1153
1251
  if(active?.type==="env"&&!active.status)
1154
1252
  connection={status:"ready",id:`env:${active.name}`,method:"api",providerID:provider.data.canonical??provider.data.id
1155
1253
  }}
1156
- if(disposed||signal.aborted||epoch!==generation||key(model)!==target)
1157
- return{status:"unavailable"}
1158
- save(target,connection)
1159
1254
  return connection
1160
- } catch{if(!disposed&&!signal.aborted&&epoch===generation)
1161
- save(target,{status:"unavailable"})
1162
- return{status:"unavailable"}}})()
1163
- pending={key:target,generation:epoch,promise}
1164
- try{return await promise
1165
- } finally{if(pending?.promise===promise)
1166
- pending=undefined
1167
- }}
1168
- function invalidate(eager=true){generation++
1169
- if(eager)
1170
- setConnections({})
1171
- else{const active=lastModel&&key(lastModel)
1172
- setConnections((values)=>Object.fromEntries(Object.entries(values).filter(([target])=>target===active)))
1173
- }
1174
- if(lastModel&&!disposed)
1175
- read(lastModel)
1176
- }
1177
- const events=["credential.switched","credential.updated","integration.updated","provider.updated","server.connected"]
1178
- const unsubscribes=events.map((event)=>context.data.on(event,(observation)=>untrack2(()=>{const directory=observation?.location?.directory
1255
+ },subscribe(invalidate){const events=["credential.switched","credential.updated","integration.updated","provider.updated","server.connected"]
1256
+ const cleanups=events.map((event)=>context.data.on(event,(observation)=>untrack2(()=>{const directory=observation?.location?.directory
1179
1257
  if((event==="provider.updated"||event==="integration.updated")&&directory&&directory!==location().directory)
1180
1258
  return
1181
1259
  invalidate(event!=="provider.updated")
1182
1260
  })))
1183
- return{suspend(){if(!lastModel)
1184
- return
1185
- lastModel=undefined
1186
- generation++
1187
- pending=undefined
1188
- },current(model){lastModel=model
1189
- return{...model,hostConnection:connections()[key(model)]??{status:"loading"}}},async refresh(model){await read(model)
1190
- },async validate(model,signal){const actual=await read(model,signal)
1191
- const expected=model.hostConnection
1192
- return actual.status==="ready"&&expected?.status==="ready"&&actual.id===expected.id&&actual.accountID===expected.accountID&&(!actual.accountID||actual.accountID===model.accountID)
1193
- },dispose(){disposed=true
1194
- generation++
1195
- for(const unsubscribe of unsubscribes)
1196
- unsubscribe()
1197
- }}}
1198
- function bindCodexAccount(model,preferences){const connection=model.hostConnection
1199
- if(model.providerID!=="openai"&&connection?.providerID!=="openai"||connection?.method==="api")
1200
- return{...model,accountID:undefined,accountSource:undefined}
1201
- if(connection&&connection.status!=="ready")
1202
- return{...model,accountID:undefined,accountSource:undefined}
1203
- const accountID=connection?.accountID??preferences.codexAccountForConnection(model.providerID,connection?.id,model.modelID)
1204
- const accountSource=connection?.accountID?"host":"binding"
1205
- return{...model,accountID,accountSource:accountID?accountSource:undefined}}
1261
+ return()=>{for(const cleanup of cleanups)
1262
+ cleanup()
1263
+ }}})
1264
+ }
1206
1265
  var commandName=(kind)=>kind==="model"?"model.list":"variant.list"
1207
1266
  function createLimitsModelSelection(api,current,context){const sameTarget=(model)=>{const active=current()
1208
1267
  return active?.sessionID===model.sessionID&&active.providerID===model.providerID&&active.modelID===model.modelID
@@ -1,5 +1,5 @@
1
1
  import {PLUGIN_ID
2
- } from "./chunk-bv0nm1tj.js";
2
+ } from "./chunk-dm1y8x49.js";
3
3
  import {memo as _$memo} from "@opentui/solid"
4
4
  import {createComponent as _$createComponent} from "@opentui/solid"
5
5
  import {use as _$use} from "@opentui/solid"
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 1,
3
3
  "dependencies": {
4
- "@opencode/plugin": "2.2.5",
5
- "@opentui/core": "0.8.1",
6
- "@opentui/keymap": "0.8.1",
7
- "@opentui/solid": "0.8.1",
4
+ "@opencode/plugin": "2.2.6",
5
+ "@opentui/core": "0.8.3",
6
+ "@opentui/keymap": "0.8.3",
7
+ "@opentui/solid": "0.8.3",
8
8
  "solid-js": "1.9.18"
9
9
  }
10
10
  }