opencode-navigator 0.19.3 → 0.21.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/CHANGELOG.md +16 -0
- package/PROVIDERS.md +92 -0
- package/README.md +136 -10
- package/dist/{chunk-r37n7w97.js → chunk-c1kw6ktv.js} +3 -4
- package/dist/chunk-cmmnfes2.js +7 -0
- package/dist/layout-preset-actions.js +438 -0
- package/dist/mcp-groups.js +4 -4
- package/dist/project-profile.js +280 -0
- package/dist/provider-limits.js +1027 -0
- package/dist/quick-actions-settings.js +2 -5
- package/dist/rpc.js +6 -1
- package/dist/server.js +589 -1
- package/dist/settings-import.js +40 -0
- package/dist/skill-groups.js +4 -4
- package/dist/trusted-skills.js +22 -0
- package/dist/tui.js +299 -273
- package/package.json +16 -11
- package/scripts/save-project-profile.mjs +41 -0
- package/scripts/update-local-snapshot.mjs +14 -12
|
@@ -0,0 +1,1027 @@
|
|
|
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||window.used!==undefined&&!Number.isFinite(window.used)||window.remaining!==undefined&&!Number.isFinite(window.remaining)||window.used===undefined&&window.remaining===undefined&&window.total===undefined||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
|
+
const balances=snapshot.balances?.map((balance)=>{if(!balance.id||!balance.label||balance.scope!=="account"&&balance.scope!=="key"||balance.amount===undefined&&!balance.unlimited||balance.amount!==undefined&&(typeof balance.amount!=="string"||!/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(balance.amount)||!Number.isFinite(Number(balance.amount)))||balance.unit!==undefined&&(typeof balance.unit!=="string"||!balance.unit))
|
|
125
|
+
throw new Error("Invalid provider balance")
|
|
126
|
+
return{...balance}})
|
|
127
|
+
if(bankedResets&&(!Number.isSafeInteger(bankedResets.availableCount)||bankedResets.availableCount<0)){throw new Error("Invalid banked reset count")
|
|
128
|
+
}
|
|
129
|
+
return{...snapshot,windows,...balances&&{balances},...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
|
|
130
|
+
import {createSignal as createSignal2,untrack} from "solid-js"
|
|
131
|
+
import {createSignal} from "solid-js"
|
|
132
|
+
var idle={status:"idle"}
|
|
133
|
+
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"
|
|
134
|
+
}
|
|
135
|
+
function isRetryableError(cause){if(isAbortError(cause))
|
|
136
|
+
return false
|
|
137
|
+
if(typeof cause!=="object"||cause===null)
|
|
138
|
+
return true
|
|
139
|
+
const value=cause
|
|
140
|
+
const status=typeof value.status==="number"?value.status:value.response?.status
|
|
141
|
+
if(typeof status!=="number")
|
|
142
|
+
return true
|
|
143
|
+
return status===408||status===425||status===429||status>=500
|
|
144
|
+
}
|
|
145
|
+
function requestError(cause,operation,target){return{operation,target,message:cause instanceof Error?cause.message:`Failed to ${operation}`,retryable:isRetryableError(cause)
|
|
146
|
+
}}
|
|
147
|
+
function createRequestState(lifecycleSignal){const [states,setStates]=createSignal({})
|
|
148
|
+
const active=new Map
|
|
149
|
+
let sequence=0
|
|
150
|
+
function state(target){return states()[target]??idle
|
|
151
|
+
}
|
|
152
|
+
function abort(target){const request=active.get(target)
|
|
153
|
+
if(!request)
|
|
154
|
+
return
|
|
155
|
+
request.controller.abort()
|
|
156
|
+
active.delete(target)
|
|
157
|
+
const current=state(target)
|
|
158
|
+
setStates((values)=>({...values,[target]:current.lastSuccessfulAt?{status:"ready",lastSuccessfulAt:current.lastSuccessfulAt}:idle
|
|
159
|
+
}))
|
|
160
|
+
}
|
|
161
|
+
function abortAll(){for(const target of active.keys())
|
|
162
|
+
abort(target)
|
|
163
|
+
}
|
|
164
|
+
lifecycleSignal?.addEventListener("abort",abortAll,{once:true})
|
|
165
|
+
function start(target,operation,hasData,force=false){const previous=active.get(target)
|
|
166
|
+
if(previous&&!force)
|
|
167
|
+
return
|
|
168
|
+
previous?.controller.abort()
|
|
169
|
+
const id=++sequence
|
|
170
|
+
const controller=new AbortController
|
|
171
|
+
if(lifecycleSignal?.aborted)
|
|
172
|
+
controller.abort(lifecycleSignal.reason)
|
|
173
|
+
const current=state(target)
|
|
174
|
+
active.set(target,{id,controller})
|
|
175
|
+
setStates((values)=>({...values,[target]:{status:hasData?"refreshing":"loading",lastSuccessfulAt:current.lastSuccessfulAt
|
|
176
|
+
}}))
|
|
177
|
+
function isCurrent(){return active.get(target)?.id===id
|
|
178
|
+
}
|
|
179
|
+
function finish(){if(isCurrent())
|
|
180
|
+
active.delete(target)
|
|
181
|
+
}
|
|
182
|
+
return{signal:controller.signal,isCurrent,succeed(){if(!isCurrent())
|
|
183
|
+
return
|
|
184
|
+
setStates((values)=>({...values,[target]:{status:"ready",lastSuccessfulAt:Date.now()}}))
|
|
185
|
+
},fail(cause){if(!isCurrent())
|
|
186
|
+
return
|
|
187
|
+
if(controller.signal.aborted||isAbortError(cause)){setStates((values)=>({...values,[target]:current.lastSuccessfulAt?{status:"ready",lastSuccessfulAt:current.lastSuccessfulAt}:idle
|
|
188
|
+
}))
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
setStates((values)=>({...values,[target]:{status:"error",error:requestError(cause,operation,target),lastSuccessfulAt:current.lastSuccessfulAt
|
|
192
|
+
}}))
|
|
193
|
+
},finish
|
|
194
|
+
}}
|
|
195
|
+
return{state,start,abort,abortAll}}
|
|
196
|
+
function createProviderLimitsController(adapters,selectedModel,signal,refreshContext){const requests=createRequestState(signal)
|
|
197
|
+
const [snapshots,setSnapshots]=createSignal2({})
|
|
198
|
+
const targetKey=(model)=>JSON.stringify(model)
|
|
199
|
+
let activeKey
|
|
200
|
+
let targetAbort=new AbortController
|
|
201
|
+
function current(){const model=selectedModel()
|
|
202
|
+
const adapter=model&&adapters.find((candidate)=>candidate.supports(model))
|
|
203
|
+
const key=model?targetKey(model):undefined
|
|
204
|
+
if(activeKey!==key){untrack(requests.abortAll)
|
|
205
|
+
targetAbort.abort()
|
|
206
|
+
targetAbort=new AbortController
|
|
207
|
+
activeKey=key
|
|
208
|
+
}
|
|
209
|
+
return{model,adapter}}
|
|
210
|
+
async function refresh(force=false){if(force&&refreshContext)
|
|
211
|
+
await refreshContext()
|
|
212
|
+
const{model,adapter}=current()
|
|
213
|
+
if(!model||!adapter)
|
|
214
|
+
return
|
|
215
|
+
const key=targetKey(model)
|
|
216
|
+
const request=requests.start(key,"read provider limits",Boolean(snapshots()[key]),force)
|
|
217
|
+
if(!request)
|
|
218
|
+
return
|
|
219
|
+
try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal,force))
|
|
220
|
+
if(!request.isCurrent()||targetKey(current().model??model)!==key)
|
|
221
|
+
return
|
|
222
|
+
setSnapshots((value)=>({...value,[key]:snapshot}))
|
|
223
|
+
request.succeed()
|
|
224
|
+
} catch(error){const status=error?.status??error?.response?.status
|
|
225
|
+
if(status===429&&request.isCurrent()){setSnapshots((value)=>({...value,[key]:{model,providerId:adapter.id,providerName:adapter.name,fetchedAt:Date.now(),availability:"rate_limited",windows:[]
|
|
226
|
+
}}))
|
|
227
|
+
request.succeed()
|
|
228
|
+
} else
|
|
229
|
+
request.fail(error)
|
|
230
|
+
} finally{request.finish()
|
|
231
|
+
}}
|
|
232
|
+
return{current,targetSignal(){current()
|
|
233
|
+
return signal?AbortSignal.any([signal,targetAbort.signal]):targetAbort.signal
|
|
234
|
+
},refresh,snapshot(){const{model,adapter}=current()
|
|
235
|
+
return model&&adapter?snapshots()[targetKey(model)]:undefined
|
|
236
|
+
},state(){const{model,adapter}=current()
|
|
237
|
+
return model&&adapter?requests.state(targetKey(model)):{status:"idle"}},dispose(){targetAbort.abort()
|
|
238
|
+
requests.abortAll()
|
|
239
|
+
for(const adapter of adapters)
|
|
240
|
+
adapter.dispose?.()
|
|
241
|
+
}}}
|
|
242
|
+
function windows(limit,model){return ["primary","secondary"].flatMap((name)=>{const value=limit[name]
|
|
243
|
+
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}}]:[]
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
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)
|
|
247
|
+
if(account.account?.type!=="chatgpt")
|
|
248
|
+
throw new Error("Sign in to Codex CLI with ChatGPT")
|
|
249
|
+
const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:true},signal)
|
|
250
|
+
if(!usage.accountId)
|
|
251
|
+
throw new Error("Codex account identity is unavailable")
|
|
252
|
+
return usage.accountId
|
|
253
|
+
},async read(model,signal){const base={model,providerId:"codex",providerName:"Codex CLI",fetchedAt:Date.now(),windows:[]
|
|
254
|
+
}
|
|
255
|
+
if(validateHost&&!await validateHost(model,signal))
|
|
256
|
+
return{...base,availability:"unauthenticated"}
|
|
257
|
+
const account=await client.request("account/read",{refreshToken:false},signal)
|
|
258
|
+
if(account.account?.type!=="chatgpt")
|
|
259
|
+
return{...base,availability:"unauthenticated"}
|
|
260
|
+
const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:false},signal)
|
|
261
|
+
if(!usage.accountId||usage.accountId!==model.accountID)
|
|
262
|
+
return{...base,availability:"unauthenticated"}
|
|
263
|
+
const candidates=Object.values(usage.rateLimitsByLimitId??{})
|
|
264
|
+
const matched=(candidates.length>0?candidates:[usage.rateLimits]).filter((limit)=>limit.normalModelSlug===model.modelID)
|
|
265
|
+
if(matched.length===0)
|
|
266
|
+
return{...base,accountId:usage.accountId,availability:"unsupported"}
|
|
267
|
+
const creditSummary=usage.rateLimitResetCredits
|
|
268
|
+
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}}))
|
|
269
|
+
}}}})
|
|
270
|
+
},async prepareResetCreditConsumption(model,creditID,signal){const snapshot=await adapter.read(model,signal)
|
|
271
|
+
if(snapshot.availability==="unauthenticated"||!snapshot.accountId||snapshot.accountId!==model.accountID){throw new Error("Codex account is unavailable or changed")
|
|
272
|
+
}
|
|
273
|
+
if(snapshot.availability!=="ready"||snapshot.ordinaryUsageAllowed!==false||snapshot.windows.length===0){throw new Error("Selected model has no eligible rate-limit window")
|
|
274
|
+
}
|
|
275
|
+
const credits=snapshot.bankedResets
|
|
276
|
+
if(!credits?.availableCount)
|
|
277
|
+
throw new Error("Codex reset credit is unavailable")
|
|
278
|
+
if(creditID){const credit=credits.credits?.find((item)=>item.id===creditID)
|
|
279
|
+
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")
|
|
280
|
+
}}
|
|
281
|
+
return snapshot
|
|
282
|
+
},async consumeResetCredit(model,attempt,signal){if(!model.accountID||model.accountID!==attempt.accountID)
|
|
283
|
+
throw new Error("Codex account changed")
|
|
284
|
+
if(attempt.uncertain){const latest=await adapter.read(model,signal)
|
|
285
|
+
if(latest.accountId!==attempt.accountID)
|
|
286
|
+
throw new Error("Codex account changed")
|
|
287
|
+
} else
|
|
288
|
+
await adapter.prepareResetCreditConsumption(model,attempt.creditID,signal)
|
|
289
|
+
signal.throwIfAborted()
|
|
290
|
+
if(validateHost&&!await validateHost(model,signal))
|
|
291
|
+
throw new Error("OpenCode account or connection changed")
|
|
292
|
+
signal.throwIfAborted()
|
|
293
|
+
const result=await client.request("account/rateLimitResetCredit/consume",{idempotencyKey:attempt.idempotencyKey,...attempt.creditID&&{creditId:attempt.creditID}},signal)
|
|
294
|
+
return result.outcome
|
|
295
|
+
},dispose:client.dispose
|
|
296
|
+
}
|
|
297
|
+
return adapter
|
|
298
|
+
}
|
|
299
|
+
import {createComponent as _$createComponent} from "@opentui/solid"
|
|
300
|
+
import {createSignal as createSignal3} from "solid-js"
|
|
301
|
+
import {GuardedConfirmation} from "./tui.js"
|
|
302
|
+
function createCodexAccountLink(input){const [busy,setBusy]=createSignal3(false)
|
|
303
|
+
async function link(){const selected=input.model()
|
|
304
|
+
if(busy()||!selected||selected.providerID!=="openai"&&selected.hostConnection?.providerID!=="openai"||!input.adapter.discoverAccount)
|
|
305
|
+
return
|
|
306
|
+
if(selected.hostConnection&&selected.hostConnection.status!=="ready")
|
|
307
|
+
return
|
|
308
|
+
const key=JSON.stringify(selected)
|
|
309
|
+
const current=()=>JSON.stringify(input.model())===key
|
|
310
|
+
const signal=input.api.lifecycle?.signal??new AbortController().signal
|
|
311
|
+
const validHost=async ()=>!input.adapter.validateHost||await input.adapter.validateHost(selected,signal)
|
|
312
|
+
setBusy(true)
|
|
313
|
+
try{if(!await validHost()||!current())
|
|
314
|
+
return
|
|
315
|
+
const accountID=await input.adapter.discoverAccount(signal)
|
|
316
|
+
if(!current())
|
|
317
|
+
return
|
|
318
|
+
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."
|
|
319
|
+
})
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
async function confirm(){if(busy()||!current())
|
|
323
|
+
return
|
|
324
|
+
setBusy(true)
|
|
325
|
+
try{if(!await validHost()||!current())
|
|
326
|
+
return
|
|
327
|
+
const latest=await input.adapter.discoverAccount(signal)
|
|
328
|
+
if(!current()||latest!==accountID)
|
|
329
|
+
return
|
|
330
|
+
input.preferences.setCodexConnectionBinding(selected.providerID,selected.hostConnection?.id,accountID)
|
|
331
|
+
input.dialogs.back()
|
|
332
|
+
} catch{if(current())
|
|
333
|
+
input.api.ui.toast({variant:"warning",title:"Codex account",message:"Account verification failed; try again."
|
|
334
|
+
})
|
|
335
|
+
} finally{setBusy(false)
|
|
336
|
+
}}
|
|
337
|
+
input.dialogs.open(()=>_$createComponent(GuardedConfirmation,{get api(){return input.api
|
|
338
|
+
},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.`
|
|
339
|
+
},onConfirm:()=>void confirm()
|
|
340
|
+
}))
|
|
341
|
+
} catch{if(current())
|
|
342
|
+
input.api.ui.toast({variant:"warning",title:"Codex account",message:"Install a supported Codex CLI and sign in with ChatGPT, then try again."
|
|
343
|
+
})
|
|
344
|
+
} finally{setBusy(false)
|
|
345
|
+
}}
|
|
346
|
+
return{link,busy
|
|
347
|
+
}}
|
|
348
|
+
import {effect as _$effect} from "@opentui/solid"
|
|
349
|
+
import {insertNode as _$insertNode} from "@opentui/solid"
|
|
350
|
+
import {insert as _$insert} from "@opentui/solid"
|
|
351
|
+
import {memo as _$memo} from "@opentui/solid"
|
|
352
|
+
import {setProp as _$setProp} from "@opentui/solid"
|
|
353
|
+
import {createElement as _$createElement} from "@opentui/solid"
|
|
354
|
+
function QuotaWindowRow(props){const kind=()=>props.window.kind==="rate_limit"?"API \xB7 ":props.window.kind==="configured_limit"?"Limit \xB7 ":""
|
|
355
|
+
const measurement=()=>{if(props.window.used!==undefined)
|
|
356
|
+
return `${props.window.used}${props.window.total === undefined ? "" : `/${props.window.total}`}${props.window.unit === "%" ? "" : " "}${props.window.unit} used`
|
|
357
|
+
if(props.window.remaining!==undefined)
|
|
358
|
+
return `${props.window.remaining} ${props.window.unit} remaining`
|
|
359
|
+
return `${props.window.total} ${props.window.unit} capacity \xB7 usage unknown`
|
|
360
|
+
}
|
|
361
|
+
return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("text"),_el$3=_$createElement("text"),_el$4=_$createElement("text")
|
|
362
|
+
_$insertNode(_el$,_el$2)
|
|
363
|
+
_$insertNode(_el$,_el$3)
|
|
364
|
+
_$insertNode(_el$,_el$4)
|
|
365
|
+
_$setProp(_el$2,"wrapMode","none")
|
|
366
|
+
_$setProp(_el$2,"truncate",true)
|
|
367
|
+
_$insert(_el$2,kind,null)
|
|
368
|
+
_$insert(_el$2,()=>props.window.label,null)
|
|
369
|
+
_$insert(_el$2,(()=>{var _c$=_$memo(()=>props.window.durationMinutes===undefined)
|
|
370
|
+
return()=>_c$()?"":` \xB7 ${props.window.durationMinutes} min`
|
|
371
|
+
})(),null)
|
|
372
|
+
_$setProp(_el$3,"wrapMode","word")
|
|
373
|
+
_$insert(_el$3,measurement)
|
|
374
|
+
_$setProp(_el$4,"wrapMode","word")
|
|
375
|
+
_$insert(_el$4,(()=>{var _c$2=_$memo(()=>props.window.resetsAt===undefined)
|
|
376
|
+
return()=>_c$2()?"Reset time unavailable":`resets ${new Date(props.window.resetsAt * 1000).toLocaleString()}`
|
|
377
|
+
})())
|
|
378
|
+
_$effect((_p$)=>{var _v$=props.api.theme.current.text,_v$2=props.api.theme.current.text,_v$3=props.api.theme.current.textMuted
|
|
379
|
+
_v$!==_p$.e&&(_p$.e=_$setProp(_el$2,"fg",_v$,_p$.e))
|
|
380
|
+
_v$2!==_p$.t&&(_p$.t=_$setProp(_el$3,"fg",_v$2,_p$.t))
|
|
381
|
+
_v$3!==_p$.a&&(_p$.a=_$setProp(_el$4,"fg",_v$3,_p$.a))
|
|
382
|
+
return _p$
|
|
383
|
+
},{e:undefined,t:undefined,a:undefined
|
|
384
|
+
})
|
|
385
|
+
return _el$
|
|
386
|
+
})()
|
|
387
|
+
}
|
|
388
|
+
import {createComponent as _$createComponent3} from "@opentui/solid"
|
|
389
|
+
import {createEffect} from "solid-js"
|
|
390
|
+
import {SelectionMenu,useDialogs} from "./tui.js"
|
|
391
|
+
import {createComponent as _$createComponent2} from "@opentui/solid"
|
|
392
|
+
import {randomUUID} from "crypto"
|
|
393
|
+
import {GuardedConfirmation as GuardedConfirmation2,useDialogState} from "./tui.js"
|
|
394
|
+
function createResetCreditActions(input){const [busy,setBusy]=useDialogState("credit-busy",false)
|
|
395
|
+
const signal=input.controller.targetSignal
|
|
396
|
+
const warn=(message)=>input.api.ui.toast({variant:"warning",title:"Codex reset credit",message
|
|
397
|
+
})
|
|
398
|
+
async function consume(creditID){if(busy()||!input.current()||!input.model.accountID||!input.adapter.consumeResetCredit)
|
|
399
|
+
return
|
|
400
|
+
setBusy(true)
|
|
401
|
+
const previous=input.preferences.pendingResetAttempt()
|
|
402
|
+
const attempt=previous??{providerID:input.model.providerID,modelID:input.model.modelID,accountID:input.model.accountID,...creditID&&{creditID
|
|
403
|
+
},idempotencyKey:randomUUID(),createdAt:Date.now(),...input.model.hostConnection?.id&&{connectionID:input.model.hostConnection.id
|
|
404
|
+
}}
|
|
405
|
+
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")
|
|
406
|
+
}
|
|
407
|
+
if(!previous)
|
|
408
|
+
await input.preferences.beginResetAttempt(attempt)
|
|
409
|
+
if(!input.current())
|
|
410
|
+
return
|
|
411
|
+
const outcome=await input.adapter.consumeResetCredit(input.model,{...attempt,uncertain:Boolean(previous)
|
|
412
|
+
},signal())
|
|
413
|
+
await input.preferences.finishResetAttempt(attempt.idempotencyKey)
|
|
414
|
+
await input.controller.refresh(true)
|
|
415
|
+
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."
|
|
416
|
+
}[outcome]
|
|
417
|
+
})
|
|
418
|
+
if(input.current())
|
|
419
|
+
input.dialogs.close()
|
|
420
|
+
} catch(error){if(input.current()){warn(`${error instanceof Error ? error.message : "Reset request failed"} \xB7 retry keeps the same attempt`)
|
|
421
|
+
input.dialogs.back()
|
|
422
|
+
}} finally{setBusy(false)
|
|
423
|
+
}}
|
|
424
|
+
async function review(creditID){if(busy()||!input.current()||!input.adapter.prepareResetCreditConsumption||!input.adapter.consumeResetCredit)
|
|
425
|
+
return
|
|
426
|
+
setBusy(true)
|
|
427
|
+
try{const snapshot=await input.adapter.prepareResetCreditConsumption(input.model,creditID,signal())
|
|
428
|
+
if(!input.current()||snapshot.accountId!==input.model.accountID)
|
|
429
|
+
return
|
|
430
|
+
input.dialogs.open(()=>_$createComponent2(GuardedConfirmation2,{get api(){return input.api
|
|
431
|
+
},get current(){return input.current
|
|
432
|
+
},title:"Use one banked reset?",get message(){return `Account ${snapshot.accountId} \xB7 model ${input.model.modelID}. This may reset other eligible Codex windows.`
|
|
433
|
+
},onConfirm:()=>void consume(creditID)
|
|
434
|
+
}))
|
|
435
|
+
} catch(error){if(input.current())
|
|
436
|
+
warn(error instanceof Error?error.message:"Credit eligibility is unavailable")
|
|
437
|
+
} finally{setBusy(false)
|
|
438
|
+
}}
|
|
439
|
+
return{review,consume,busy
|
|
440
|
+
}}
|
|
441
|
+
function ResetCreditDialog(props){const dialogs=useDialogs(props.api)
|
|
442
|
+
const key=JSON.stringify(props.model)
|
|
443
|
+
const current=()=>JSON.stringify(props.controller.current().model)===key
|
|
444
|
+
const pending=props.preferences.pendingResetAttempt
|
|
445
|
+
const credits=()=>props.controller.snapshot()?.bankedResets
|
|
446
|
+
const actions=createResetCreditActions({...props,dialogs,current
|
|
447
|
+
})
|
|
448
|
+
createEffect(()=>{if(!current())
|
|
449
|
+
dialogs.close()
|
|
450
|
+
})
|
|
451
|
+
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 ")
|
|
452
|
+
}))??[],...credits()?.availableCount&&(credits()?.credits?.length??0)<credits().availableCount?[{title:"Use next available credit",value:"credit:",icon:"presets",description:"Credit details unavailable"
|
|
453
|
+
}]:[],...pending()?[{title:"Reconcile pending attempt",value:"retry",icon:"retry",description:"Reuse the previous idempotency key"
|
|
454
|
+
}]:[],{title:"Cancel",value:"cancel",icon:"close",description:"No credit is used"
|
|
455
|
+
}]
|
|
456
|
+
return _$createComponent3(SelectionMenu,{get api(){return props.api
|
|
457
|
+
},get title(){return `Banked resets \xB7 ${credits()?.availableCount ?? 0} available${actions.busy() ? " \xB7 pending" : ""}`
|
|
458
|
+
},get options(){return options()
|
|
459
|
+
},get height(){return Math.min(18,options().length*3)
|
|
460
|
+
},onSelect:(option)=>{if(option.value==="cancel")
|
|
461
|
+
dialogs.back()
|
|
462
|
+
else if(option.value==="retry")
|
|
463
|
+
actions.consume(pending()?.creditID)
|
|
464
|
+
else if(option.value.startsWith("credit:"))
|
|
465
|
+
actions.review(option.value.slice("credit:".length)||undefined)
|
|
466
|
+
}})
|
|
467
|
+
}
|
|
468
|
+
import {createComponent as _$createComponent4} from "@opentui/solid"
|
|
469
|
+
import {effect as _$effect3} from "@opentui/solid"
|
|
470
|
+
import {insert as _$insert3} from "@opentui/solid"
|
|
471
|
+
import {memo as _$memo3} from "@opentui/solid"
|
|
472
|
+
import {setProp as _$setProp3} from "@opentui/solid"
|
|
473
|
+
import {createElement as _$createElement3} from "@opentui/solid"
|
|
474
|
+
import {For} from "solid-js"
|
|
475
|
+
import {effect as _$effect2} from "@opentui/solid"
|
|
476
|
+
import {memo as _$memo2} from "@opentui/solid"
|
|
477
|
+
import {createTextNode as _$createTextNode} from "@opentui/solid"
|
|
478
|
+
import {insertNode as _$insertNode2} from "@opentui/solid"
|
|
479
|
+
import {insert as _$insert2} from "@opentui/solid"
|
|
480
|
+
import {setProp as _$setProp2} from "@opentui/solid"
|
|
481
|
+
import {createElement as _$createElement2} from "@opentui/solid"
|
|
482
|
+
function QuotaBalanceRow(props){return(()=>{var _el$=_$createElement2("box"),_el$2=_$createElement2("text"),_el$3=_$createTextNode(` \xB7 `),_el$4=_$createElement2("text")
|
|
483
|
+
_$insertNode2(_el$,_el$2)
|
|
484
|
+
_$insertNode2(_el$,_el$4)
|
|
485
|
+
_$insertNode2(_el$2,_el$3)
|
|
486
|
+
_$setProp2(_el$2,"wrapMode","word")
|
|
487
|
+
_$insert2(_el$2,()=>props.balance.label,_el$3)
|
|
488
|
+
_$insert2(_el$2,()=>props.balance.scope,null)
|
|
489
|
+
_$setProp2(_el$4,"wrapMode","word")
|
|
490
|
+
_$insert2(_el$4,(()=>{var _c$=_$memo2(()=>!!props.balance.unlimited)
|
|
491
|
+
return()=>_c$()?"No configured cap":`${props.balance.amount} ${props.balance.unit ?? "(provider unit not specified)"}`
|
|
492
|
+
})())
|
|
493
|
+
_$effect2((_p$)=>{var _v$=props.api.theme.current.textMuted,_v$2=props.api.theme.current.text
|
|
494
|
+
_v$!==_p$.e&&(_p$.e=_$setProp2(_el$2,"fg",_v$,_p$.e))
|
|
495
|
+
_v$2!==_p$.t&&(_p$.t=_$setProp2(_el$4,"fg",_v$2,_p$.t))
|
|
496
|
+
return _p$
|
|
497
|
+
},{e:undefined,t:undefined
|
|
498
|
+
})
|
|
499
|
+
return _el$
|
|
500
|
+
})()
|
|
501
|
+
}
|
|
502
|
+
function QuotaSnapshotRows(props){return [(()=>{var _el$=_$createElement3("text")
|
|
503
|
+
_$setProp3(_el$,"wrapMode","none")
|
|
504
|
+
_$setProp3(_el$,"truncate",true)
|
|
505
|
+
_$insert3(_el$,()=>props.snapshot.providerName,null)
|
|
506
|
+
_$insert3(_el$,(()=>{var _c$=_$memo3(()=>!!props.snapshot.accountId)
|
|
507
|
+
return()=>_c$()?` \xB7 ${props.snapshot.accountId}`:""
|
|
508
|
+
})(),null)
|
|
509
|
+
_$effect3((_$p)=>_$setProp3(_el$,"fg",props.api.theme.current.textMuted,_$p))
|
|
510
|
+
return _el$
|
|
511
|
+
})(),_$createComponent4(For,{get each(){return props.snapshot.windows
|
|
512
|
+
},children:(window)=>_$createComponent4(QuotaWindowRow,{get api(){return props.api
|
|
513
|
+
},window
|
|
514
|
+
})
|
|
515
|
+
}),_$createComponent4(For,{get each(){return props.snapshot.balances
|
|
516
|
+
},children:(balance)=>_$createComponent4(QuotaBalanceRow,{get api(){return props.api
|
|
517
|
+
},balance
|
|
518
|
+
})
|
|
519
|
+
})]
|
|
520
|
+
}
|
|
521
|
+
import {createComponent as _$createComponent5} from "@opentui/solid"
|
|
522
|
+
import {createSignal as createSignal4,onMount} from "solid-js"
|
|
523
|
+
import {SelectionMenu as SelectionMenu2,useDialogs as useDialogs2} from "./tui.js"
|
|
524
|
+
var rows=[["openai","OpenAI / Codex",["subscription","api-rate-limits"],"Codex account RPC; HTTP response request/token limits for API connections.","https://developers.openai.com/api/docs/guides/rate-limits"],["anthropic","Anthropic / Claude",["api-rate-limits","configured-limits"],"Model HTTP headers; eligible organization/admin credentials can read configured model limits. Claude subscription windows are separate.","https://platform.claude.com/docs/en/manage-claude/rate-limits-api"],["openrouter","OpenRouter",["key-budget","account-balance"],"Inference-key budget; account credits require a management key. Free-model request limits apply only to free variants.","https://openrouter.ai/docs/api/api-reference/api-keys/get-current-api-key"],["deepseek","DeepSeek",["account-balance"],"Provider-reported USD/CNY available, granted and topped-up balances.","https://api-docs.deepseek.com/api/get-user-balance"],["moonshotai","Moonshot / Kimi API",["account-balance"],"International USD balances; coding subscriptions are a different product.","https://platform.moonshot.ai/docs/api/balance"],["moonshotai-cn","Moonshot / Kimi API China",["account-balance"],"China CNY balances; never reuse an international key against the China endpoint.","https://platform.moonshot.cn/docs/api/balance"],["minimax","MiniMax",["subscription","account-balance"],"Token Plan model rows/explicit percentages; official SDK balance response for pay-as-you-go keys.","https://platform.minimax.io/docs/token-plan/faq"],["minimax-cn","MiniMax China",["subscription","account-balance"],"Region-specific native Token Plan and SDK balance endpoints.","https://github.com/MiniMax-AI/cli/tree/main/src/sdk/quota"],["github-copilot","GitHub Copilot",["subscription"],"Official SDK token-scoped quota/model RPC; no direct copilot_internal calls or ambient CLI login.","https://github.com/github/copilot-sdk/blob/main/docs/features/usage-and-billing.md"],["groq","Groq",["api-rate-limits"],"Native model response headers: requests per day and tokens per minute.","https://console.groq.com/docs/rate-limits"],["fireworks-ai","Fireworks",["configured-limits","api-rate-limits"],"Native model token/second capacities and account monthly spend quota; GPU quotas are excluded.","https://docs.fireworks.ai/serverless/rate-limits"],["siliconflow","SiliconFlow",["account-balance"],"Native current-user balances; retain unknown currency when the API does not report it.","https://docs.siliconflow.com/en/api-reference/userinfo/get-user-info"],["siliconflow-cn","SiliconFlow China",["account-balance"],"Region-specific current-user balances; currency is unknown when the API omits it.","https://docs.siliconflow.cn/"],["novita-ai","Novita",["account-balance"],"Native current-user balances in 1/10000 USD.","https://novita.ai/docs/api-reference/basic-get-user-balance"],["poe","Poe",["account-balance"],"API account point balance; includes plan and add-on points, not session token usage.","https://creator.poe.com/docs/resources/usage-api"],["nano-gpt","NanoGPT",["account-balance","subscription"],"Native USD/Nano balance and documented subscription token windows; null/degraded counters remain unknown.","https://docs.nano-gpt.com/api-reference/endpoint/subscription-usage"],["aihubmix","AIHubMix",["account-balance"],"Current-account balance API needs a Manage Key; inference keys do not grant management access.","https://docs.aihubmix.com/en/api/CliEndpoints/get-self"],["azure","Azure OpenAI",["api-rate-limits"],"Remaining request/token headers for the actual deployment; no ChatGPT subscription or inferred cloud balance.","https://learn.microsoft.com/en-us/azure/ai-foundry/openai/quotas-limits"],["cerebras","Cerebras",["api-rate-limits"],"Only self-describing native counters actually returned for the selected model; console tier examples are not live quota.","https://inference-docs.cerebras.ai/support/rate-limits"],["mistral","Mistral",[],"Documented remaining header lacks a verified unit/window association; no inferred subscription balance.","https://docs.mistral.ai/resources/known-limitations"],["togetherai","Together AI",[],"Dynamic model limits expose retry intervals, not remaining quota; billing UI does not establish a remaining-credit API.","https://docs.together.ai/docs/rate-limits"],["xai","xAI / Grok",[],"Management billing needs separate team credentials; no verified portable remaining API for this inference connection.","https://docs.x.ai/"],["perplexity","Perplexity",[],"No verified remaining-credit API for an inference key. API credits, consumer subscriptions and Computer credits are distinct.","https://www.perplexity.ai/help-center/en/articles/10354847-api-payment-and-billing"],["google","Google Gemini",["configured-limits"],"Native model-matched QuotaFailure responses when provided; cloud quota permissions differ from an inference key.","https://ai.google.dev/gemini-api/docs/rate-limits"],["google-vertex","Google Vertex AI",["configured-limits"],"Native model-matched quota error details; cloud project/region management and dynamic shared quotas are separate.","https://cloud.google.com/vertex-ai/generative-ai/docs/quotas"],["amazon-bedrock","Amazon Bedrock",[],"Service Quotas/CloudWatch require cloud IAM/region/model mapping not exposed by a portable inference connection.","https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html"],["cloudflare-workers-ai","Cloudflare Workers AI",[],"Account analytics do not prove remaining free allowance; separate account management capability is required.","https://developers.cloudflare.com/workers-ai/platform/limits/"],["huggingface","Hugging Face Inference Providers",[],"Hub request quota is not an inference-model quota or inference-credit balance.","https://huggingface.co/docs/inference-providers/pricing"],["zai","Z.ai / GLM Coding Plan",[],"No verified external quota contract for monitor/dashboard APIs; private endpoints are excluded.","https://docs.z.ai/devpack/overview"],["kimi-for-coding","Kimi Code subscription",[],"Keep separate from Moonshot API balance; an external subscription usage contract/model mapping must be verified.","https://www.kimi.com/code/docs/en/"],["alibaba","Alibaba Cloud / Qwen / DashScope",[],"Cloud account/project billing permissions and model mapping are required; browser OAuth state is not a quota source.","https://www.alibabacloud.com/help/en/model-studio/"],["opencode","OpenCode Zen / Go",[],"Session token/cost statistics are not provider quota; no undocumented billing endpoints.","https://opencode.ai/docs/zen/"],["cursor","Cursor",[],"Consumer usage needs a supported external API; no cookie or credential-file integrations.","https://docs.cursor.com/"],["windsurf","Windsurf / Codeium",[],"Consumer plan windows need a supported external API; no private account endpoints.","https://docs.windsurf.com/"],["amp","Amp",[],"Product credits need a supported external account API; no cookie or local credential extraction.","https://ampcode.com/manual"],["ollama","Ollama",[],"Local inference has no provider subscription window; cloud products require their own account API.","https://docs.ollama.com/api"],["lmstudio","LM Studio",[],"Local inference has no provider subscription quota to read.","https://lmstudio.ai/docs/developer"],["nvidia","NVIDIA NIM / NGC",[],"Inference access does not identify cloud/trial NGC credit balance or self-hosted resource quota.","https://docs.nvidia.com/nim/"],["cohere","Cohere",[],"No verified native remaining-counter contract for this integration; static organization tier examples are not live quotas.","https://docs.cohere.com/docs/rate-limits"],["ai21","AI21",[],"No verified native remaining-counter contract or subscription balance endpoint for this integration.","https://docs.ai21.com/"],["replicate","Replicate",[],"Prediction cost is not remaining account credit; no inferred balance from execution cost.","https://replicate.com/docs/topics/billing"],["chutes","Chutes",[],"Current-user quota APIs exist, but their public schemas do not establish native units/model allowance semantics yet.","https://chutes.ai/docs/api-reference/users"],["requesty","Requesty",[],"Enterprise management key and key ID are required; dated usage reports alone are not remaining budget.","https://docs.requesty.ai/features/key-management-api"],["baseten","Baseten",[],"No independently verified portable remaining quota API for the configured inference connection.","https://docs.baseten.co/"],["modelscope","ModelScope",[],"No independently verified portable remaining quota API/model mapping for the selected connection.","https://opencode.ai/docs/providers/"],["nebius","Nebius",[],"Cloud billing/management access is separate from a plain inference credential.","https://docs.nebius.com/"],["vultr","Vultr",[],"Cloud management quota/billing permissions are separate from inference access.","https://docs.vultr.com/"],["v0","Vercel v0",[],"Consumer/product credits need their own documented external account API.","https://v0.dev/docs"]]
|
|
525
|
+
var PROVIDER_QUOTA_CAPABILITIES= rows.map(([id,name,sources,reason,url])=>({id,name,sources,reason,documentation:[url]}))
|
|
526
|
+
var aliases={fireworks:"fireworks-ai",novita:"novita-ai",moonshot:"moonshotai","minimax-coding-plan":"minimax","minimax-coding-plan-cn":"minimax-cn",together:"togetherai","silicon-flow":"siliconflow","github-copilot-enterprise":"github-copilot","google-vertex-anthropic":"google-vertex","z-ai":"zai",zhipuai:"zai",dashscope:"alibaba","alibaba-cn":"alibaba",qwen:"alibaba","opencode-go":"opencode","opencode-zen":"opencode",codeium:"windsurf","azure-cognitive-services":"azure"
|
|
527
|
+
}
|
|
528
|
+
function providerQuotaCapability(id,canonical){const key=aliases[canonical??id]??canonical??id
|
|
529
|
+
const found=PROVIDER_QUOTA_CAPABILITIES.find((entry)=>entry.id===key)
|
|
530
|
+
return found??{id,name:id,sources:[],reason:"No independent quota/balance API or native counter semantics are verified for this provider.",documentation:["https://opencode.ai/docs/providers/"]
|
|
531
|
+
}}
|
|
532
|
+
function providerQuotaCatalog(providers=[]){const entries=new Map(PROVIDER_QUOTA_CAPABILITIES.map((entry)=>[entry.id,entry]))
|
|
533
|
+
for(const provider of providers)
|
|
534
|
+
entries.set(provider.id,{...providerQuotaCapability(provider.id,provider.canonical),id:provider.id,name:provider.name
|
|
535
|
+
})
|
|
536
|
+
return [...entries.values()].sort((a,b)=>a.name.localeCompare(b.name))
|
|
537
|
+
}
|
|
538
|
+
function ProviderSupportDialog(props){const dialogs=useDialogs2(props.api)
|
|
539
|
+
const [entries,setEntries]=createSignal4(providerQuotaCatalog())
|
|
540
|
+
onMount(()=>{if(props.load)
|
|
541
|
+
props.load().then(setEntries).catch(()=>{})
|
|
542
|
+
})
|
|
543
|
+
return _$createComponent5(SelectionMenu2,{get api(){return props.api
|
|
544
|
+
},get title(){return `Provider sources \xB7 ${entries().length}`
|
|
545
|
+
},height:18,get options(){return [...entries().map((entry)=>({title:entry.name,value:entry.id,icon:entry.sources.length>0?"info":"help",description:`${entry.sources.length > 0 ? entry.sources.join(", ") : "API-dependent"} \xB7 ${entry.reason}`
|
|
546
|
+
})),{title:"Close",value:"close",description:"Return to the sidebar",icon:"close"
|
|
547
|
+
}]
|
|
548
|
+
},onSelect:(option)=>{if(option.value==="close")
|
|
549
|
+
dialogs.back()
|
|
550
|
+
}})
|
|
551
|
+
}
|
|
552
|
+
var text={type:"string",minLength:1,maxLength:512}
|
|
553
|
+
var model2={type:"object",properties:{providerID:text,modelID:text,sessionID:text,connectionID:text,variant:text,force:{type:"boolean"}},required:["providerID","modelID","sessionID"],additionalProperties:false
|
|
554
|
+
}
|
|
555
|
+
var NavigatorQuotaRpc={id:"opencode-navigator.quotas",methods:{read:{input:model2,output:{type:"object",additionalProperties:true}},capabilities:{input:{type:"object",additionalProperties:false},output:{type:"array",items:{type:"object",additionalProperties:true}}}},events:{updated:{schema:{type:"object",properties:{providerID:text,modelID:text},required:["providerID","modelID"],additionalProperties:false
|
|
556
|
+
}}}};
|
|
557
|
+
function missing(error){return typeof error==="object"&&error!==null&&"type" in error&&error.type==="rpc.unavailable"
|
|
558
|
+
}
|
|
559
|
+
function createNativeQuotaAdapter(context){const rpc=context.client.rpc(NavigatorQuotaRpc)
|
|
560
|
+
const listeners=new Set
|
|
561
|
+
const unsubscribe=rpc.events.on("updated",(event)=>{const location=context.location??context.data.location.default()
|
|
562
|
+
if(event.location.directory!==location.directory)
|
|
563
|
+
return
|
|
564
|
+
if(typeof event.data.providerID!=="string"||typeof event.data.modelID!=="string")
|
|
565
|
+
return
|
|
566
|
+
for(const listener of listeners)
|
|
567
|
+
listener(event.data.providerID,event.data.modelID)
|
|
568
|
+
})
|
|
569
|
+
const adapter={id:"provider-api",name:"Provider-native quotas",supports:(model)=>model.hostConnection?.status!=="loading",async read(model,signal,force){const base={model,providerId:model.providerID,providerName:model.providerID,availability:"setup_required",fetchedAt:Date.now(),windows:[]
|
|
570
|
+
}
|
|
571
|
+
try{if(typeof rpc.read!=="function")
|
|
572
|
+
return{...base,message:"Enable the Navigator OpenCode 2 server plugin for native provider quotas and balances."
|
|
573
|
+
}
|
|
574
|
+
const value=await rpc.read({providerID:model.providerID,modelID:model.modelID,sessionID:model.sessionID,...model.variant&&{variant:model.variant},...model.hostConnection?.id&&{connectionID:model.hostConnection.id},force
|
|
575
|
+
},{location:context.location??context.data.location.default(),signal})
|
|
576
|
+
if(model.hostConnection?.id&&value.connectionID&&value.connectionID!==model.hostConnection.id)
|
|
577
|
+
return{...base,availability:"unauthenticated",message:"The provider connection changed during this read."
|
|
578
|
+
}
|
|
579
|
+
return normalizeProviderQuota({model,providerId:value.providerId,providerName:value.providerName,availability:value.availability,fetchedAt:value.fetchedAt,windows:value.windows,...value.balances&&{balances:value.balances},...value.accountId&&{accountId:value.accountId},...value.connectionID&&{connectionID:value.connectionID},...value.message&&{message:value.message}})
|
|
580
|
+
} catch(error){if(!missing(error))
|
|
581
|
+
throw error
|
|
582
|
+
return{...base,message:"Enable the Navigator OpenCode 2 server plugin for native provider quotas and balances."
|
|
583
|
+
}}},dispose(){unsubscribe()
|
|
584
|
+
listeners.clear()
|
|
585
|
+
}}
|
|
586
|
+
return{adapter,onUpdated(listener){listeners.add(listener)
|
|
587
|
+
return()=>listeners.delete(listener)
|
|
588
|
+
},async capabilities(){try{return await rpc.capabilities({})
|
|
589
|
+
} catch{return providerQuotaCatalog(context.data.location.provider?.list?.(context.location)??[])
|
|
590
|
+
}}}}
|
|
591
|
+
import {mergeProps as _$mergeProps} from "@opentui/solid"
|
|
592
|
+
import {insert as _$insert7} from "@opentui/solid"
|
|
593
|
+
import {setProp as _$setProp8} from "@opentui/solid"
|
|
594
|
+
import {effect as _$effect8} from "@opentui/solid"
|
|
595
|
+
import {createTextNode as _$createTextNode4} from "@opentui/solid"
|
|
596
|
+
import {insertNode as _$insertNode5} from "@opentui/solid"
|
|
597
|
+
import {createElement as _$createElement8} from "@opentui/solid"
|
|
598
|
+
import {memo as _$memo6} from "@opentui/solid"
|
|
599
|
+
import {createComponent as _$createComponent10} from "@opentui/solid"
|
|
600
|
+
import {Show as Show4} from "solid-js"
|
|
601
|
+
import {useDialogs as useDialogs4,useIcons as useIcons3} from "./tui.js"
|
|
602
|
+
import {setProp as _$setProp5} from "@opentui/solid"
|
|
603
|
+
import {effect as _$effect5} from "@opentui/solid"
|
|
604
|
+
import {createTextNode as _$createTextNode2} from "@opentui/solid"
|
|
605
|
+
import {insertNode as _$insertNode3} from "@opentui/solid"
|
|
606
|
+
import {createElement as _$createElement5} from "@opentui/solid"
|
|
607
|
+
import {memo as _$memo4} from "@opentui/solid"
|
|
608
|
+
import {createComponent as _$createComponent7} from "@opentui/solid"
|
|
609
|
+
import {Show} from "solid-js"
|
|
610
|
+
import {useDialogs as useDialogs3,useIcons} from "./tui.js"
|
|
611
|
+
import {createComponent as _$createComponent6} from "@opentui/solid"
|
|
612
|
+
import {effect as _$effect4} from "@opentui/solid"
|
|
613
|
+
import {insert as _$insert4} from "@opentui/solid"
|
|
614
|
+
import {setProp as _$setProp4} from "@opentui/solid"
|
|
615
|
+
import {createElement as _$createElement4} from "@opentui/solid"
|
|
616
|
+
import {SelectionBox} from "./tui.js"
|
|
617
|
+
import {createSignal as createSignal5,onCleanup} from "solid-js"
|
|
618
|
+
function sidebarInteractiveColors(theme,state,normalColor=theme.text,mode="row"){const highlighted=!state.disabled&&(state.focused||mode==="control"&&state.hovered)
|
|
619
|
+
let backgroundColor=theme.backgroundPanel
|
|
620
|
+
let foregroundColor=normalColor
|
|
621
|
+
if(highlighted)
|
|
622
|
+
backgroundColor=theme.primary
|
|
623
|
+
else if(mode==="control")
|
|
624
|
+
backgroundColor="transparent"
|
|
625
|
+
else if(state.focused||state.hovered)
|
|
626
|
+
backgroundColor=theme.backgroundElement
|
|
627
|
+
if(state.disabled)
|
|
628
|
+
foregroundColor=theme.textMuted
|
|
629
|
+
else if(highlighted)
|
|
630
|
+
foregroundColor=theme.selectedListItemText
|
|
631
|
+
return{backgroundColor,foregroundColor}}
|
|
632
|
+
function useSidebarItem(api,interaction,descriptor,normalColor=()=>api.theme.current.text,mode="row"){let unregister
|
|
633
|
+
const [hovered,setHovered]=createSignal5(false)
|
|
634
|
+
const disabled=()=>descriptor.disabled?.()===true
|
|
635
|
+
const focused=()=>Boolean(interaction?.ownsFocus()&&interaction.isSelected(descriptor.id))
|
|
636
|
+
const colors=()=>sidebarInteractiveColors(api.theme.current,{focused:focused(),hovered:hovered(),disabled:disabled()},normalColor(),mode)
|
|
637
|
+
const ref=(renderable)=>{unregister?.()
|
|
638
|
+
unregister=interaction?.register({...descriptor,renderable})
|
|
639
|
+
}
|
|
640
|
+
onCleanup(()=>unregister?.())
|
|
641
|
+
return{ref,disabled,focused,backgroundColor:()=>colors().backgroundColor,foregroundColor:()=>colors().foregroundColor,onMouseOver:()=>setHovered(true),onMouseOut:()=>setHovered(false),select(event){interaction?.mouseSelect(descriptor.id,event)
|
|
642
|
+
},activate(event){if(interaction)
|
|
643
|
+
return interaction.mouseActivate(descriptor.id,event)
|
|
644
|
+
if(disabled())
|
|
645
|
+
return false
|
|
646
|
+
descriptor.activate()
|
|
647
|
+
return true
|
|
648
|
+
}}}
|
|
649
|
+
function LimitsAction(props){const item=useSidebarItem(props.api,props.interaction,{id:props.id,position:()=>props.position,disabled:()=>props.disabled===true,activate:props.onActivate
|
|
650
|
+
})
|
|
651
|
+
return _$createComponent6(SelectionBox,{ref:(node)=>item.ref(node),get id(){return props.id
|
|
652
|
+
},height:1,get backgroundColor(){return item.backgroundColor()
|
|
653
|
+
},get onMouseOver(){return item.onMouseOver
|
|
654
|
+
},get onMouseOut(){return item.onMouseOut
|
|
655
|
+
},onMouseUp:(event)=>{event.stopPropagation()
|
|
656
|
+
item.activate(event)
|
|
657
|
+
},get children(){var _el$=_$createElement4("text")
|
|
658
|
+
_$setProp4(_el$,"wrapMode","none")
|
|
659
|
+
_$setProp4(_el$,"truncate",true)
|
|
660
|
+
_$insert4(_el$,()=>props.label)
|
|
661
|
+
_$effect4((_$p)=>_$setProp4(_el$,"fg",item.foregroundColor(),_$p))
|
|
662
|
+
return _el$
|
|
663
|
+
}})
|
|
664
|
+
}
|
|
665
|
+
function CodexAccountControls(props){const icons=useIcons()
|
|
666
|
+
const model=()=>props.controller.current().model
|
|
667
|
+
const accountLink=createCodexAccountLink({api:props.api,preferences:props.preferences,adapter:props.codex,model,dialogs:useDialogs3(props.api)
|
|
668
|
+
})
|
|
669
|
+
const eligible=()=>(props.model.providerID==="openai"||props.model.hostConnection?.providerID==="openai")&&props.model.hostConnection?.method!=="api"
|
|
670
|
+
const action=(id,row,label,onActivate)=>_$createComponent7(LimitsAction,{get api(){return props.api
|
|
671
|
+
},get interaction(){return props.interaction
|
|
672
|
+
},id:`${PLUGIN_ID}.limits.${id}`,get position(){return{section:props.navigationSection,row,column:0
|
|
673
|
+
}},label,onActivate,get disabled(){return accountLink.busy()
|
|
674
|
+
}})
|
|
675
|
+
return _$createComponent7(Show,{get when(){return eligible()
|
|
676
|
+
},get children(){return [_$createComponent7(Show,{get when(){return _$memo4(()=>!!!props.model.accountID)()&&(!props.model.hostConnection||props.model.hostConnection.status==="ready")
|
|
677
|
+
},get children(){return [(()=>{var _el$=_$createElement5("text")
|
|
678
|
+
_$insertNode3(_el$,_$createTextNode2(`Link the matching Codex account once for this connection`))
|
|
679
|
+
_$effect5((_$p)=>_$setProp5(_el$,"fg",props.api.theme.current.textMuted,_$p))
|
|
680
|
+
return _el$
|
|
681
|
+
})(),_$memo4(()=>action("link",1,`${icons.icon("scope")} Link Codex CLI account`,()=>void accountLink.link()))]
|
|
682
|
+
}}),_$createComponent7(Show,{get when(){return _$memo4(()=>!!props.model.accountID)()&&props.model.accountSource!=="host"
|
|
683
|
+
},get children(){return [_$memo4(()=>action("unlink",4,`${icons.icon("close")} Unlink Codex account`,()=>props.preferences.setCodexConnectionBinding(props.model.providerID,props.model.hostConnection?.id))),_$createComponent7(Show,{get when(){return props.controller.snapshot()?.availability==="unauthenticated"
|
|
684
|
+
},get children(){return action("relink",5,`${icons.icon("scope")} Relink Codex account`,()=>void accountLink.link())
|
|
685
|
+
}})]
|
|
686
|
+
}})]
|
|
687
|
+
}})
|
|
688
|
+
}
|
|
689
|
+
import {createComponent as _$createComponent8} from "@opentui/solid"
|
|
690
|
+
import {effect as _$effect6} from "@opentui/solid"
|
|
691
|
+
import {insert as _$insert5} from "@opentui/solid"
|
|
692
|
+
import {setProp as _$setProp6} from "@opentui/solid"
|
|
693
|
+
import {createElement as _$createElement6} from "@opentui/solid"
|
|
694
|
+
import {Show as Show2} from "solid-js"
|
|
695
|
+
function LimitsStatus(props){const message=()=>{const model=props.controller.current().model
|
|
696
|
+
const connection=model?.hostConnection
|
|
697
|
+
const snapshot=props.controller.snapshot()
|
|
698
|
+
const state=props.controller.state()
|
|
699
|
+
if(state.status==="error")
|
|
700
|
+
return snapshot?"Stale \xB7 refresh failed; retry below":"Quota read failed \xB7 retry below"
|
|
701
|
+
if(snapshot?.message)
|
|
702
|
+
return snapshot.availability==="stale"?`Stale \xB7 ${snapshot.message}`:snapshot.availability==="ready"?`Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()} \xB7 ${snapshot.message}`:snapshot.message
|
|
703
|
+
if(connection?.status==="loading")
|
|
704
|
+
return "Checking the OpenCode account\u2026"
|
|
705
|
+
if(connection?.status==="unavailable")
|
|
706
|
+
return "OpenCode account metadata unavailable \xB7 refresh below"
|
|
707
|
+
if(connection?.status==="unsupported")
|
|
708
|
+
return "Codex quotas require an active ChatGPT OAuth connection"
|
|
709
|
+
if(!snapshot)
|
|
710
|
+
return state.status==="loading"?"Reading provider limits\u2026":props.controller.current().adapter?"Refresh to read provider limits":providerQuotaCapability(model?.providerID??"").reason
|
|
711
|
+
const availability=snapshot.availability
|
|
712
|
+
if(availability==="unauthenticated")
|
|
713
|
+
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"
|
|
714
|
+
if(availability==="unsupported")
|
|
715
|
+
return "No documented quota bucket for this model"
|
|
716
|
+
if(availability==="rate_limited")
|
|
717
|
+
return "Provider rate-limited this read \xB7 try again later"
|
|
718
|
+
if(availability==="stale")
|
|
719
|
+
return "Stale provider data \xB7 refresh below"
|
|
720
|
+
return `Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()}`
|
|
721
|
+
}
|
|
722
|
+
return _$createComponent8(Show2,{get when(){return props.controller.current().model
|
|
723
|
+
},get children(){var _el$=_$createElement6("text")
|
|
724
|
+
_$setProp6(_el$,"wrapMode","word")
|
|
725
|
+
_$insert5(_el$,message)
|
|
726
|
+
_$effect6((_$p)=>_$setProp6(_el$,"fg",props.api.theme.current.textMuted,_$p))
|
|
727
|
+
return _el$
|
|
728
|
+
}})
|
|
729
|
+
}
|
|
730
|
+
import {createComponent as _$createComponent9} from "@opentui/solid"
|
|
731
|
+
import {effect as _$effect7} from "@opentui/solid"
|
|
732
|
+
import {createTextNode as _$createTextNode3} from "@opentui/solid"
|
|
733
|
+
import {insertNode as _$insertNode4} from "@opentui/solid"
|
|
734
|
+
import {memo as _$memo5} from "@opentui/solid"
|
|
735
|
+
import {insert as _$insert6} from "@opentui/solid"
|
|
736
|
+
import {setProp as _$setProp7} from "@opentui/solid"
|
|
737
|
+
import {createElement as _$createElement7} from "@opentui/solid"
|
|
738
|
+
import {Show as Show3,createEffect as createEffect2,createSignal as createSignal6} from "solid-js"
|
|
739
|
+
import {SelectionBox as SelectionBox2,useIcons as useIcons2} from "./tui.js"
|
|
740
|
+
var graphemes=new Intl.Segmenter(undefined,{granularity:"grapheme"
|
|
741
|
+
})
|
|
742
|
+
function truncateEnd(value,maxWidth){if(Bun.stringWidth(value)<=maxWidth)
|
|
743
|
+
return value
|
|
744
|
+
if(maxWidth<=0)
|
|
745
|
+
return ""
|
|
746
|
+
const ellipsis=maxWidth>=3?"...":".".repeat(maxWidth)
|
|
747
|
+
const contentWidth=maxWidth-Bun.stringWidth(ellipsis)
|
|
748
|
+
let result=""
|
|
749
|
+
for(const{segment
|
|
750
|
+
} of graphemes.segment(value)){if(Bun.stringWidth(result + segment)>contentWidth)
|
|
751
|
+
break
|
|
752
|
+
result += segment
|
|
753
|
+
}
|
|
754
|
+
return result + ellipsis
|
|
755
|
+
}
|
|
756
|
+
function Section(props){const icons=useIcons2()
|
|
757
|
+
const [actionWidth,setActionWidth]=createSignal6()
|
|
758
|
+
const icon=()=>props.section?icons.section(props.section):props.icon
|
|
759
|
+
const theme=()=>props.api.theme.current
|
|
760
|
+
const item=useSidebarItem(props.api,props.interaction,{id:props.sectionId??`section.${props.title}`,position:()=>({section:props.navigationSection??0,row:0,column:0
|
|
761
|
+
}),activate:props.onToggle
|
|
762
|
+
})
|
|
763
|
+
const action=props.headerAction?useSidebarItem(props.api,props.interaction,{id:props.headerAction.id,position:()=>({section:props.navigationSection??0,row:0,column:1
|
|
764
|
+
}),disabled:props.headerAction.disabled,activate:props.headerAction.onActivate
|
|
765
|
+
},()=>theme().accent,"control"):undefined
|
|
766
|
+
createEffect2(()=>{props.headerAction?.label()
|
|
767
|
+
setActionWidth(undefined)
|
|
768
|
+
})
|
|
769
|
+
const actionLabel=()=>{const label=props.headerAction?.label()??""
|
|
770
|
+
const width=actionWidth()
|
|
771
|
+
return width===undefined?label:truncateEnd(label,width-2)
|
|
772
|
+
}
|
|
773
|
+
return(()=>{var _el$=_$createElement7("box")
|
|
774
|
+
_$setProp7(_el$,"gap",1)
|
|
775
|
+
_$insert6(_el$,_$createComponent9(SelectionBox2,{ref:(node)=>item.ref(node),get id(){return props.sectionId
|
|
776
|
+
},flexDirection:"row",height:1,justifyContent:"space-between",gap:1,paddingLeft:1,paddingRight:1,get backgroundColor(){return item.backgroundColor()
|
|
777
|
+
},get onMouseOver(){return item.onMouseOver
|
|
778
|
+
},get onMouseOut(){return item.onMouseOut
|
|
779
|
+
},onMouseDown:(event)=>item.activate(event),onSizeChange:()=>setActionWidth(undefined),get children(){return [(()=>{var _el$2=_$createElement7("text"),_el$3=_$createElement7("span"),_el$4=_$createTextNode3(` `),_el$5=_$createElement7("span"),_el$6=_$createElement7("b")
|
|
780
|
+
_$insertNode4(_el$2,_el$3)
|
|
781
|
+
_$insertNode4(_el$2,_el$4)
|
|
782
|
+
_$insertNode4(_el$2,_el$5)
|
|
783
|
+
_$insertNode4(_el$2,_el$6)
|
|
784
|
+
_$setProp7(_el$2,"flexShrink",0)
|
|
785
|
+
_$setProp7(_el$2,"wrapMode","none")
|
|
786
|
+
_$insert6(_el$3,()=>icons.icon(props.open?"expanded":"collapsed"))
|
|
787
|
+
_$insert6(_el$5,(()=>{var _c$=_$memo5(()=>!!icon())
|
|
788
|
+
return()=>_c$()?`${icon()} `:""
|
|
789
|
+
})())
|
|
790
|
+
_$insert6(_el$6,()=>props.title)
|
|
791
|
+
_$effect7((_p$)=>{var _v$=item.foregroundColor(),_v$2={fg:item.focused()?item.foregroundColor():theme().accent
|
|
792
|
+
},_v$3={fg:item.focused()?item.foregroundColor():theme().accent
|
|
793
|
+
}
|
|
794
|
+
_v$!==_p$.e&&(_p$.e=_$setProp7(_el$2,"fg",_v$,_p$.e))
|
|
795
|
+
_v$2!==_p$.t&&(_p$.t=_$setProp7(_el$3,"style",_v$2,_p$.t))
|
|
796
|
+
_v$3!==_p$.a&&(_p$.a=_$setProp7(_el$5,"style",_v$3,_p$.a))
|
|
797
|
+
return _p$
|
|
798
|
+
},{e:undefined,t:undefined,a:undefined
|
|
799
|
+
})
|
|
800
|
+
return _el$2
|
|
801
|
+
})(),(()=>{var _el$7=_$createElement7("box"),_el$8=_$createElement7("text")
|
|
802
|
+
_$insertNode4(_el$7,_el$8)
|
|
803
|
+
_$setProp7(_el$7,"flexDirection","row")
|
|
804
|
+
_$setProp7(_el$7,"gap",1)
|
|
805
|
+
_$setProp7(_el$7,"flexGrow",1)
|
|
806
|
+
_$setProp7(_el$7,"minWidth",0)
|
|
807
|
+
_$setProp7(_el$7,"justifyContent","flex-end")
|
|
808
|
+
_$insert6(_el$7,_$createComponent9(Show3,{when:action,children:(control)=>_$createComponent9(SelectionBox2,{ref:(node)=>control().ref(node),get id(){return props.headerAction?.id
|
|
809
|
+
},flexShrink:1,minWidth:3,overflow:"hidden",get backgroundColor(){return control().backgroundColor()
|
|
810
|
+
},onSizeChange:function(){setActionWidth(this.width)
|
|
811
|
+
},get onMouseOver(){return control().onMouseOver
|
|
812
|
+
},get onMouseOut(){return control().onMouseOut
|
|
813
|
+
},onMouseDown:(event)=>event.stopPropagation(),onMouseUp:(event)=>{event.stopPropagation()
|
|
814
|
+
control().activate(event)
|
|
815
|
+
},get children(){var _el$9=_$createElement7("text")
|
|
816
|
+
_$setProp7(_el$9,"wrapMode","none")
|
|
817
|
+
_$setProp7(_el$9,"height",1)
|
|
818
|
+
_$insert6(_el$9,actionLabel)
|
|
819
|
+
_$effect7((_$p)=>_$setProp7(_el$9,"fg",control().foregroundColor(),_$p))
|
|
820
|
+
return _el$9
|
|
821
|
+
}})
|
|
822
|
+
}),_el$8)
|
|
823
|
+
_$setProp7(_el$8,"wrapMode","none")
|
|
824
|
+
_$setProp7(_el$8,"truncate",true)
|
|
825
|
+
_$setProp7(_el$8,"height",1)
|
|
826
|
+
_$insert6(_el$8,()=>props.summary)
|
|
827
|
+
_$effect7((_p$)=>{var _v$4=item.focused()?item.foregroundColor():theme().textMuted,_v$5=props.headerAction?0:1
|
|
828
|
+
_v$4!==_p$.e&&(_p$.e=_$setProp7(_el$8,"fg",_v$4,_p$.e))
|
|
829
|
+
_v$5!==_p$.t&&(_p$.t=_$setProp7(_el$8,"flexShrink",_v$5,_p$.t))
|
|
830
|
+
return _p$
|
|
831
|
+
},{e:undefined,t:undefined
|
|
832
|
+
})
|
|
833
|
+
return _el$7
|
|
834
|
+
})()]
|
|
835
|
+
}}),null)
|
|
836
|
+
_$insert6(_el$,_$createComponent9(Show3,{get when(){return props.open
|
|
837
|
+
},get children(){return props.children
|
|
838
|
+
}}),null)
|
|
839
|
+
return _el$
|
|
840
|
+
})()
|
|
841
|
+
}
|
|
842
|
+
function ProviderLimitsSection(props){const icons=useIcons3()
|
|
843
|
+
const dialogs=useDialogs4(props.api)
|
|
844
|
+
const model=()=>props.controller.current().model
|
|
845
|
+
const snapshot=props.controller.snapshot
|
|
846
|
+
const count=()=>(snapshot()?.windows.length??0) + (snapshot()?.balances?.length??0)
|
|
847
|
+
const action=(id,row,label,onActivate,disabled=false)=>_$createComponent10(LimitsAction,{get api(){return props.api
|
|
848
|
+
},get interaction(){return props.interaction
|
|
849
|
+
},id:`${PLUGIN_ID}.limits.${id}`,get position(){return{section:props.navigationSection,row,column:0
|
|
850
|
+
}},label,onActivate,disabled
|
|
851
|
+
})
|
|
852
|
+
function openCredits(){const selected=model()
|
|
853
|
+
if(selected)
|
|
854
|
+
dialogs.open(()=>_$createComponent10(ResetCreditDialog,{get api(){return props.api
|
|
855
|
+
},get preferences(){return props.preferences
|
|
856
|
+
},get controller(){return props.controller
|
|
857
|
+
},get adapter(){return props.codex
|
|
858
|
+
},model:selected
|
|
859
|
+
}))
|
|
860
|
+
}
|
|
861
|
+
return _$createComponent10(Section,{get api(){return props.api
|
|
862
|
+
},get interaction(){return props.interaction
|
|
863
|
+
},sectionId:`${PLUGIN_ID}.section.limits`,get navigationSection(){return props.navigationSection
|
|
864
|
+
},title:"LIMITS",section:"limits",get summary(){return _$memo6(()=>!!count())()?String(count()):""
|
|
865
|
+
},get open(){return props.preferences.expanded().limits
|
|
866
|
+
},onToggle:()=>props.preferences.toggleSectionExpanded("limits"),get children(){return _$createComponent10(Show4,{get when(){return model()
|
|
867
|
+
},get fallback(){return(()=>{var _el$=_$createElement8("text")
|
|
868
|
+
_$insertNode5(_el$,_$createTextNode4(`Open a session to view model limits`))
|
|
869
|
+
_$effect8((_$p)=>_$setProp8(_el$,"fg",props.api.theme.current.textMuted,_$p))
|
|
870
|
+
return _el$
|
|
871
|
+
})()
|
|
872
|
+
},children:(selected)=>(()=>{var _el$3=_$createElement8("box"),_el$4=_$createElement8("text"),_el$5=_$createTextNode4(`/`)
|
|
873
|
+
_$insertNode5(_el$3,_el$4)
|
|
874
|
+
_$setProp8(_el$3,"gap",1)
|
|
875
|
+
_$insertNode5(_el$4,_el$5)
|
|
876
|
+
_$setProp8(_el$4,"wrapMode","none")
|
|
877
|
+
_$setProp8(_el$4,"truncate",true)
|
|
878
|
+
_$insert7(_el$4,()=>selected().providerID,_el$5)
|
|
879
|
+
_$insert7(_el$4,()=>selected().modelID,null)
|
|
880
|
+
_$insert7(_el$3,_$createComponent10(CodexAccountControls,_$mergeProps(props,{get model(){return selected()
|
|
881
|
+
}})),null)
|
|
882
|
+
_$insert7(_el$3,_$createComponent10(Show4,{get when(){return _$memo6(()=>!!(snapshot()&&["ready","stale"].includes(snapshot().availability)))()?snapshot():undefined
|
|
883
|
+
},children:(quota)=>_$createComponent10(QuotaSnapshotRows,{get api(){return props.api
|
|
884
|
+
},get snapshot(){return quota()
|
|
885
|
+
}})
|
|
886
|
+
}),null)
|
|
887
|
+
_$insert7(_el$3,_$createComponent10(Show4,{get when(){return snapshot()?.bankedResets
|
|
888
|
+
},children:(credits)=>[(()=>{var _el$6=_$createElement8("text"),_el$7=_$createTextNode4(`Banked resets: `)
|
|
889
|
+
_$insertNode5(_el$6,_el$7)
|
|
890
|
+
_$insert7(_el$6,()=>credits().availableCount,null)
|
|
891
|
+
_$effect8((_$p)=>_$setProp8(_el$6,"fg",props.api.theme.current.textMuted,_$p))
|
|
892
|
+
return _el$6
|
|
893
|
+
})(),_$createComponent10(Show4,{get when(){return credits().availableCount>0||props.preferences.pendingResetAttempt()
|
|
894
|
+
},get children(){return action("credits",2,`${icons.icon("info")} Review reset credits`,openCredits)
|
|
895
|
+
}})]
|
|
896
|
+
}),null)
|
|
897
|
+
_$insert7(_el$3,_$createComponent10(LimitsStatus,{get api(){return props.api
|
|
898
|
+
},get controller(){return props.controller
|
|
899
|
+
}}),null)
|
|
900
|
+
_$insert7(_el$3,_$createComponent10(Show4,{get when(){return props.controller.current().adapter||selected().hostConnection?.status==="unavailable"
|
|
901
|
+
},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))
|
|
902
|
+
}}),null)
|
|
903
|
+
_$insert7(_el$3,()=>action("sources",6,`${icons.icon("info")} Provider sources`,()=>dialogs.open(()=>_$createComponent10(ProviderSupportDialog,{get api(){return props.api
|
|
904
|
+
},get load(){return props.capabilities
|
|
905
|
+
}}))),null)
|
|
906
|
+
_$effect8((_$p)=>_$setProp8(_el$4,"fg",props.api.theme.current.textMuted,_$p))
|
|
907
|
+
return _el$3
|
|
908
|
+
})()
|
|
909
|
+
})
|
|
910
|
+
}})
|
|
911
|
+
}
|
|
912
|
+
function selectedV1Model(api){const route=api.route.current
|
|
913
|
+
const sessionID="params" in route?route.params?.sessionID:undefined
|
|
914
|
+
if(route.name!=="session"||typeof sessionID!=="string")
|
|
915
|
+
return
|
|
916
|
+
const model=api.state.session.get(sessionID)?.model
|
|
917
|
+
if(!model)
|
|
918
|
+
return
|
|
919
|
+
return{sessionID,providerID:model.providerID,modelID:model.id,...model.variant&&{variant:model.variant}}}
|
|
920
|
+
import {createSignal as createSignal7,untrack as untrack2} from "solid-js"
|
|
921
|
+
var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"])
|
|
922
|
+
function providerAccountIdentity(provider,credentials){if(!provider.integrationID||provider.activation==="disabled")
|
|
923
|
+
return{status:"unsupported"}
|
|
924
|
+
const active=credentials.filter((entry)=>entry.integrationID===provider.integrationID&&entry.active)
|
|
925
|
+
if(active.length!==1)
|
|
926
|
+
return{status:"unavailable"}
|
|
927
|
+
const credential=active[0]
|
|
928
|
+
const id=`credential:${credential.id}`
|
|
929
|
+
const providerID=provider.canonical??provider.id
|
|
930
|
+
if(credential.value.type==="key")
|
|
931
|
+
return{status:"ready",id,method:"api",providerID}
|
|
932
|
+
if(provider.integrationID!=="openai")
|
|
933
|
+
return{status:"ready",id,method:"oauth",providerID}
|
|
934
|
+
if(!CHATGPT_METHODS.has(credential.value.methodID))
|
|
935
|
+
return{status:"unsupported",id}
|
|
936
|
+
const account=credential.value.metadata?.accountID
|
|
937
|
+
const accountID=typeof account==="string"&&account.trim()?account:undefined
|
|
938
|
+
const header=Object.entries(provider.headers??{}).find(([name])=>name.toLowerCase()==="chatgpt-account-id")?.[1]
|
|
939
|
+
if(accountID&&header&&header!==accountID)
|
|
940
|
+
return{status:"loading"}
|
|
941
|
+
return{status:"ready",id,method:"oauth",providerID,...accountID&&{accountID}}}
|
|
942
|
+
function createOpenCodeV2AccountSource(context,lifecycle){const [connections,setConnections]=createSignal7({})
|
|
943
|
+
let generation=0
|
|
944
|
+
let disposed=false
|
|
945
|
+
let lastModel
|
|
946
|
+
let pending
|
|
947
|
+
const location=()=>context.location??context.data.location.default()
|
|
948
|
+
const key=(model)=>JSON.stringify([model.providerID,location()])
|
|
949
|
+
function save(target,connection){if(JSON.stringify(connections()[target])!==JSON.stringify(connection))
|
|
950
|
+
setConnections((values)=>({...values,[target]:connection}))
|
|
951
|
+
}
|
|
952
|
+
async function read(model,signal=lifecycle){if(disposed||signal.aborted)
|
|
953
|
+
return{status:"unavailable"}
|
|
954
|
+
const target=key(model)
|
|
955
|
+
const epoch=generation
|
|
956
|
+
if(pending?.key===target&&pending.generation===epoch)
|
|
957
|
+
return pending.promise
|
|
958
|
+
const promise=(async ()=>{try{if(!context.client.provider?.get||!context.client.credential?.list)
|
|
959
|
+
throw new Error("Account metadata unavailable")
|
|
960
|
+
const options={signal:AbortSignal.any([lifecycle,signal])}
|
|
961
|
+
const [provider,credentials]=await Promise.all([context.client.provider.get({providerID:model.providerID,location:location()},options),context.client.credential.list(options)])
|
|
962
|
+
let connection=provider.data.id===model.providerID?providerAccountIdentity(provider.data,credentials):{status:"unavailable"}
|
|
963
|
+
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)
|
|
964
|
+
const active=integration.data.connections[0]
|
|
965
|
+
if(active?.type==="env"&&!active.status)
|
|
966
|
+
connection={status:"ready",id:`env:${active.name}`,method:"api",providerID:provider.data.canonical??provider.data.id
|
|
967
|
+
}}
|
|
968
|
+
if(disposed||signal.aborted||epoch!==generation||key(model)!==target)
|
|
969
|
+
return{status:"unavailable"}
|
|
970
|
+
save(target,connection)
|
|
971
|
+
return connection
|
|
972
|
+
} catch{if(!disposed&&!signal.aborted&&epoch===generation)
|
|
973
|
+
save(target,{status:"unavailable"})
|
|
974
|
+
return{status:"unavailable"}}})()
|
|
975
|
+
pending={key:target,generation:epoch,promise}
|
|
976
|
+
try{return await promise
|
|
977
|
+
} finally{if(pending?.promise===promise)
|
|
978
|
+
pending=undefined
|
|
979
|
+
}}
|
|
980
|
+
function invalidate(){generation++
|
|
981
|
+
setConnections({})
|
|
982
|
+
if(lastModel&&!disposed)
|
|
983
|
+
read(lastModel)
|
|
984
|
+
}
|
|
985
|
+
const events=["credential.switched","credential.updated","integration.updated","provider.updated","server.connected"]
|
|
986
|
+
const unsubscribes=events.map((event)=>context.data.on(event,()=>untrack2(invalidate)))
|
|
987
|
+
return{current(model){lastModel=model
|
|
988
|
+
return{...model,hostConnection:connections()[key(model)]??{status:"loading"}}},async refresh(model){await read(model)
|
|
989
|
+
},async validate(model,signal){const actual=await read(model,signal)
|
|
990
|
+
const expected=model.hostConnection
|
|
991
|
+
return actual.status==="ready"&&expected?.status==="ready"&&actual.id===expected.id&&actual.accountID===expected.accountID&&(!actual.accountID||actual.accountID===model.accountID)
|
|
992
|
+
},dispose(){disposed=true
|
|
993
|
+
generation++
|
|
994
|
+
for(const unsubscribe of unsubscribes)
|
|
995
|
+
unsubscribe()
|
|
996
|
+
}}}
|
|
997
|
+
function bindCodexAccount(model,preferences){const connection=model.hostConnection
|
|
998
|
+
if(model.providerID!=="openai"&&connection?.providerID!=="openai"||connection?.method==="api")
|
|
999
|
+
return{...model,accountID:undefined,accountSource:undefined}
|
|
1000
|
+
if(connection&&connection.status!=="ready")
|
|
1001
|
+
return{...model,accountID:undefined,accountSource:undefined}
|
|
1002
|
+
const accountID=connection?.accountID??preferences.codexAccountForConnection(model.providerID,connection?.id,model.modelID)
|
|
1003
|
+
const accountSource=connection?.accountID?"host":"binding"
|
|
1004
|
+
return{...model,accountID,accountSource:accountID?accountSource:undefined}}
|
|
1005
|
+
function selectedV2Model(context){const route=context.ui.router.current()
|
|
1006
|
+
const model=context.ui.model.current()
|
|
1007
|
+
if(route.type!=="session"||!model)
|
|
1008
|
+
return
|
|
1009
|
+
return{sessionID:route.sessionID,providerID:model.providerID,modelID:model.modelID,...model.variant&&{variant:model.variant}}}
|
|
1010
|
+
function createProviderQuotaIntegration(api,preferences,version,context){const selectModel=()=>context?selectedV2Model(context):selectedV1Model(api)
|
|
1011
|
+
const accountSource=context?createOpenCodeV2AccountSource(context,api.lifecycle.signal):undefined
|
|
1012
|
+
const codex=createCodexQuotaAdapter(createCodexAppServerClient({version}),accountSource?.validate)
|
|
1013
|
+
const native=context?createNativeQuotaAdapter(context):undefined
|
|
1014
|
+
const controller=createProviderLimitsController([codex,...native?[native.adapter]:[]],()=>{const model=selectModel()
|
|
1015
|
+
return model?bindCodexAccount(accountSource?.current(model)??model,preferences):undefined
|
|
1016
|
+
},api.lifecycle.signal,accountSource&&(async ()=>{const model=selectModel()
|
|
1017
|
+
if(model)
|
|
1018
|
+
await accountSource.refresh(model)
|
|
1019
|
+
}))
|
|
1020
|
+
native?.onUpdated((providerID,modelID)=>{const selected=selectModel()
|
|
1021
|
+
if(selected?.providerID===providerID&&selected.modelID===modelID)
|
|
1022
|
+
controller.refresh()
|
|
1023
|
+
})
|
|
1024
|
+
return{controller,codex,accountSource,selectModel,capabilities:native?.capabilities??(async ()=>providerQuotaCatalog())
|
|
1025
|
+
}}
|
|
1026
|
+
export{ProviderLimitsSection,bindCodexAccount,createCodexAppServerClient,createCodexQuotaAdapter,createOpenCodeV2AccountSource,createProviderLimitsController,createProviderQuotaIntegration,selectedV1Model
|
|
1027
|
+
}
|