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.
package/dist/tui.js CHANGED
@@ -3,7 +3,6 @@ import {insert as _$insert,setProp as _$setProp,createElement as _$createElement
3
3
 
4
4
 
5
5
 
6
- import {createSignal as createSignal2,createSignal,createEffect,onCleanup,createContext,useContext,createContext as createContext2,createSignal as createSignal3,onCleanup as onCleanup2,useContext as useContext2,createEffect as createEffect2,splitProps,untrack,splitProps as splitProps2,For,createEffect as createEffect3,onCleanup as onCleanup3,Show,createEffect as createEffect4,Show as Show2,splitProps as splitProps3,createEffect as createEffect5,createSignal as createSignal4,onCleanup as onCleanup4,onMount,Suspense,lazy,createSignal as createSignal5,createSignal as createSignal6,onCleanup as onCleanup5,Show as Show3,batch,createSignal as createSignal8,createSignal as createSignal7,createEffect as createEffect6,createSignal as createSignal9,onCleanup as onCleanup6,createSignal as createSignal10,createSignal as createSignal11,Suspense as Suspense2,lazy as lazy2,Index,onCleanup as onCleanup7,createSignal as createSignal12,onCleanup as onCleanup8,createEffect as createEffect8,onCleanup as onCleanup10,batch as batch2,createEffect as createEffect7,createMemo,createSignal as createSignal13,onCleanup as onCleanup9,onMount as onMount2,For as For2,For as For3,Show as Show4,createMemo as createMemo2,Suspense as Suspense3,lazy as lazy3,For as For6,createMemo as createMemo4,onMount as onMount4,For as For5,createMemo as createMemo3,onMount as onMount3,Show as Show6,createEffect as createEffect9,createSignal as createSignal14,onCleanup as onCleanup11,Show as Show5,For as For4,For as For7,createEffect as createEffect10,createMemo as createMemo5,onCleanup as onCleanup12,createEffect as createEffect11,createMemo as createMemo6,untrack as untrack2,Suspense as Suspense4,lazy as lazy4,onCleanup as onCleanup13,createEffect as createEffect12,onCleanup as onCleanup14,createSignal as createSignal15,createMemo as createMemo7,createSignal as createSignal16,createSignal as createSignal17,lazy as lazy5,Show as Show9,For as For8,Show as Show8,Show as Show7,createEffect as createEffect13,onCleanup as onCleanup15,onMount as onMount5,onCleanup as onCleanup16,onMount as onMount6,For as For9,createSignal as createSignal18,createSignal as createSignal19,onCleanup as onCleanup17,createSignal as createSignal20,Show as Show10,createSignal as createSignal22,createSignal as createSignal21,onCleanup as onCleanup18,For as For10,Show as Show13,createMemo as createMemo9,createComputed,createMemo as createMemo8,createSignal as createSignal23,untrack as untrack3,Show as Show11,createComputed as createComputed2,untrack as untrack4,Show as Show12,createSignal as createSignal25,createEffect as createEffect14,createSignal as createSignal24,on,createMemo as createMemo11,createSignal as createSignal28,createMemo as createMemo10,For as For11,Show as Show17,Show as Show14,Show as Show15,createSignal as createSignal26,createContext as createContext3,useContext as useContext3,Show as Show16,createSignal as createSignal27,onCleanup as onCleanup19,Show as Show23,createSignal as createSignal29,untrack as untrack5,createSignal as createSignal30,createSignal as createSignal31,createEffect as createEffect15,For as For13,Show as Show18,For as For12,createSignal as createSignal32,Show as Show19,Show as Show20,Show as Show21,Show as Show22,For as For14,Show as Show25,createMemo as createMemo13,Show as Show24,createMemo as createMemo12,createSignal as createSignal33,createContext as createContext4,useContext as useContext4,Show as Show26,Suspense as Suspense5,createEffect as createEffect16,onCleanup as onCleanup20,untrack as untrack6,For as For20,Match,Switch,createMemo as createMemo19,onCleanup as onCleanup26,For as For15,Show as Show28,createMemo as createMemo15,createSignal as createSignal35,createEffect as createEffect17,createMemo as createMemo14,onCleanup as onCleanup21,untrack as untrack7,Show as Show27,createEffect as createEffect18,createSignal as createSignal34,onCleanup as onCleanup22,For as For16,Show as Show30,createSignal as createSignal36,createEffect as createEffect19,createMemo as createMemo16,onCleanup as onCleanup23,untrack as untrack8,Show as Show29,For as For18,Show as Show33,createEffect as createEffect20,createMemo as createMemo17,createSignal as createSignal37,onCleanup as onCleanup24,untrack as untrack9,For as For17,Show as Show32,Show as Show31,Show as Show38,createEffect as createEffect21,createMemo as createMemo18,createSignal as createSignal38,onCleanup as onCleanup25,untrack as untrack10,Show as Show34,For as For19,Show as Show36,Show as Show35,Show as Show37,createEffect as createEffect22,createMemo as createMemo20,onCleanup as onCleanup27,untrack as untrack11,Show as Show39,createMemo as createMemo21,onCleanup as onCleanup28,createEffect as createEffect23,createMemo as createMemo22,on as on2,onCleanup as onCleanup29,createRoot,createSignal as createSignal39,getOwner,createSignal as createSignal40,onCleanup as onCleanup30,onMount as onMount7} from "solid-js"
7
6
  var SIDEBAR_SECTIONS=["limits","todo","subagents","skills","quick_actions","lsp","mcp"]
8
7
  var QUICK_ACTION_IDS=["session.rename","session.timeline","session.copy","session.export","session.compact","session.list","session.new","session.fork","messages.copy","session.first","session.last","session.messages_last_user","session.message.next","session.message.previous","permission.mode"]
9
8
  function preferencesScope(path){return path.worktree||path.directory||"global"
@@ -85,6 +84,7 @@ const session=sessionID?api.state.session.get(sessionID):undefined
85
84
  const directory=session?.directory??api.state.path.directory
86
85
  const workspace=session?.workspaceID
87
86
  return{key:JSON.stringify([directory,workspace??null]),routing:{directory,...workspace&&{workspace}}}}
87
+ import {createSignal as createSignal2,createSignal,createEffect,onCleanup,createContext,useContext,createContext as createContext2,createSignal as createSignal3,onCleanup as onCleanup2,useContext as useContext2,createEffect as createEffect2,splitProps,untrack,splitProps as splitProps2,For,createEffect as createEffect3,onCleanup as onCleanup3,Show,createEffect as createEffect4,Show as Show2,splitProps as splitProps3,createEffect as createEffect5,createSignal as createSignal4,onCleanup as onCleanup4,onMount,Suspense,lazy,batch,createSignal as createSignal5,createSignal as createSignal6,onCleanup as onCleanup5,Show as Show3,batch as batch2,createSignal as createSignal8,createSignal as createSignal7,createEffect as createEffect6,createSignal as createSignal9,onCleanup as onCleanup6,createSignal as createSignal10,createSignal as createSignal11,Suspense as Suspense2,lazy as lazy2,Index,onCleanup as onCleanup7,createEffect as createEffect8,onCleanup as onCleanup10,batch as batch3,createEffect as createEffect7,createMemo,createSignal as createSignal13,onCleanup as onCleanup9,onMount as onMount2,createSignal as createSignal12,onCleanup as onCleanup8,For as For2,For as For3,Show as Show4,createMemo as createMemo2,Suspense as Suspense3,lazy as lazy3,For as For6,createMemo as createMemo4,onMount as onMount4,For as For5,createMemo as createMemo3,onMount as onMount3,Show as Show6,createEffect as createEffect9,createSignal as createSignal14,onCleanup as onCleanup11,Show as Show5,For as For4,For as For7,createEffect as createEffect10,createMemo as createMemo5,onCleanup as onCleanup12,createEffect as createEffect11,createMemo as createMemo6,untrack as untrack2,Suspense as Suspense4,lazy as lazy4,onCleanup as onCleanup13,createEffect as createEffect12,onCleanup as onCleanup14,createSignal as createSignal15,createMemo as createMemo7,createSignal as createSignal16,createSignal as createSignal17,lazy as lazy5,Show as Show9,For as For8,Show as Show8,Show as Show7,createEffect as createEffect13,onCleanup as onCleanup15,onMount as onMount5,onCleanup as onCleanup16,onMount as onMount6,For as For9,createSignal as createSignal18,createSignal as createSignal19,onCleanup as onCleanup17,createSignal as createSignal20,Show as Show10,createSignal as createSignal22,createSignal as createSignal21,onCleanup as onCleanup18,For as For10,Show as Show13,createMemo as createMemo9,createComputed,createMemo as createMemo8,createSignal as createSignal23,untrack as untrack3,Show as Show11,createComputed as createComputed2,untrack as untrack4,Show as Show12,createSignal as createSignal25,createEffect as createEffect14,createSignal as createSignal24,on,createMemo as createMemo11,createSignal as createSignal28,createMemo as createMemo10,For as For11,Show as Show17,Show as Show14,Show as Show15,createSignal as createSignal26,createContext as createContext3,useContext as useContext3,Show as Show16,createSignal as createSignal27,onCleanup as onCleanup19,Show as Show23,createSignal as createSignal29,untrack as untrack5,createSignal as createSignal30,createSignal as createSignal31,createEffect as createEffect15,For as For13,Show as Show18,For as For12,createSignal as createSignal32,Show as Show19,Show as Show20,Show as Show21,Show as Show22,createSignal as createSignal33,createEffect as createEffect16,createMemo as createMemo12,on as on2,onCleanup as onCleanup20,For as For14,Show as Show25,createMemo as createMemo14,Show as Show24,createMemo as createMemo13,createSignal as createSignal34,createContext as createContext4,useContext as useContext4,Show as Show26,Suspense as Suspense5,createEffect as createEffect17,onCleanup as onCleanup21,untrack as untrack6,For as For20,Match,Switch,createMemo as createMemo20,onCleanup as onCleanup27,For as For15,Show as Show28,createMemo as createMemo16,createSignal as createSignal36,createEffect as createEffect18,createMemo as createMemo15,onCleanup as onCleanup22,untrack as untrack7,Show as Show27,createEffect as createEffect19,createSignal as createSignal35,onCleanup as onCleanup23,For as For16,Show as Show30,createSignal as createSignal37,createEffect as createEffect20,createMemo as createMemo17,onCleanup as onCleanup24,untrack as untrack8,Show as Show29,For as For18,Show as Show33,createEffect as createEffect21,createMemo as createMemo18,createSignal as createSignal38,onCleanup as onCleanup25,untrack as untrack9,For as For17,Show as Show32,Show as Show31,Show as Show38,createEffect as createEffect22,createMemo as createMemo19,createSignal as createSignal39,onCleanup as onCleanup26,untrack as untrack10,Show as Show34,For as For19,Show as Show36,Show as Show35,Show as Show37,createEffect as createEffect23,createMemo as createMemo21,onCleanup as onCleanup28,untrack as untrack11,Show as Show39,createMemo as createMemo22,onCleanup as onCleanup29,createRoot,createSignal as createSignal40,getOwner,createSignal as createSignal41,onCleanup as onCleanup30,onMount as onMount7} from "solid-js"
88
88
  function mcpToggleAction(status){if(status==="pending")
89
89
  return
90
90
  return status==="connected"?"disconnect":"connect"
@@ -179,10 +179,14 @@ setStates((values)=>({...values,[target]:{status:"error",error:requestError(caus
179
179
  return{state,start,abort,abortAll}}
180
180
  function wait(delay,signal){return new Promise((resolve,reject)=>{if(signal?.aborted)
181
181
  return reject(signal.reason??new DOMException("Aborted","AbortError"))
182
- const timer=setTimeout(resolve,delay)
183
- signal?.addEventListener("abort",()=>{clearTimeout(timer)
184
- reject(signal.reason??new DOMException("Aborted","AbortError"))
185
- },{once:true})
182
+ const abort=()=>{clearTimeout(timer)
183
+ signal?.removeEventListener("abort",abort)
184
+ reject(signal?.reason??new DOMException("Aborted","AbortError"))
185
+ }
186
+ const timer=setTimeout(()=>{signal?.removeEventListener("abort",abort)
187
+ resolve()
188
+ },delay)
189
+ signal?.addEventListener("abort",abort,{once:true})
186
190
  })
187
191
  }
188
192
  async function retryBackgroundRefresh(refresh,options={}){const delays=options.delays??[250,1000]
@@ -212,6 +216,8 @@ const rightRank=BUCKET_RANK[right.bucket]??1
212
216
  return leftRank-rightRank||left.bucket.localeCompare(right.bucket)||left.name.localeCompare(right.name)
213
217
  })
214
218
  }
219
+ function serverKey(current,name){return JSON.stringify([current.key,name])
220
+ }
215
221
  function bulkDescription(operation,action){if(operation.action==="preset")
216
222
  return `apply ${operation.preset} MCP preset`
217
223
  if(operation.group)
@@ -224,12 +230,8 @@ return Object.keys(presets).sort((left,right)=>left.localeCompare(right)).find((
224
230
  return Object.keys(states).length===items.length&&items.every((item)=>item.status==="connected"&&states[item.name]==="enabled"||item.status==="disabled"&&states[item.name]==="disabled")
225
231
  })
226
232
  }
227
- function statusError(status){if(!status||typeof status!=="object"||!("error" in status))
228
- return
229
- const error=status.error
230
- return typeof error==="string"?error:undefined
231
- }
232
- function createMcpController(api,persist,preferences,source){const [snapshots,setSnapshots]=createSignal2({})
233
+ function createMcpModel(options,persist,preferences){const{source}=options
234
+ const [snapshots,setSnapshots]=createSignal2({})
233
235
  const [mutationCounts,setMutationCounts]=createSignal2({})
234
236
  const [bulkStates,setBulkStates]=createSignal2({})
235
237
  const [selectedPresets,setSelectedPresets]=createSignal2({})
@@ -239,8 +241,8 @@ const mutationRetries=new Map
239
241
  const bulkGenerations=new Map
240
242
  const bulkRetries=new Map
241
243
  const selectedPresetStates=new Map
242
- const requests=createRequestState(api.lifecycle?.signal)
243
- const serverRequests=createRequestState(api.lifecycle?.signal)
244
+ const requests=createRequestState(options.signal)
245
+ const serverRequests=createRequestState(options.signal)
244
246
  let activation=0
245
247
  let activeTarget
246
248
  function changeMutationCount(current,offset){setMutationCounts((values)=>{const next=Math.max(0,(values[current.key]??0) + offset)
@@ -264,10 +266,7 @@ serverRequests.abortAll()
264
266
  refreshing.clear()
265
267
  activeTarget=current.key
266
268
  }
267
- function target(){const location=currentLocation(api)
268
- return{key:location.key,scope:preferencesScope({directory:location.routing.directory,worktree:api.state.path.worktree}),routing:location.routing
269
- }}
270
- function serverKey(current,name){return JSON.stringify([current.key,name])
269
+ function target(){return options.target()
271
270
  }
272
271
  function mutating(current=target()){return(mutationCounts()[current.key]??0)>0
273
272
  }
@@ -302,7 +301,7 @@ const failed=state.failed.filter((candidate)=>candidate!==name)
302
301
  bulkRetries.set(current.key,(bulkRetries.get(current.key)??[]).filter((candidate)=>candidate.name!==name))
303
302
  return{...values,[current.key]:{...state,status:failed.length>0?"error":"ready",failed}}})
304
303
  }
305
- function list(current=target()){return source?source.list(current):snapshots()[current.key]??api.state.mcp()
304
+ function list(current=target()){return options.cache==="source"?source.list(current):snapshots()[current.key]??source.list(current)
306
305
  }
307
306
  async function refresh(current=target(),force=false){const pending=refreshing.get(current.key)
308
307
  if(pending&&!force)
@@ -310,7 +309,7 @@ return pending
310
309
  const requestState=requests.start(current.key,"refresh MCP servers",current.key in snapshots(),force)
311
310
  if(!requestState)
312
311
  return pending
313
- const request=(source?source.refresh(current,force,requestState.signal):api.client.mcp.status(current.routing,{throwOnError:true,signal:requestState.signal}).then((result)=>Object.entries(result.data??{}).map(([name,status])=>({name,status:status.status,error:statusError(status)})).sort((a,b)=>a.name.localeCompare(b.name)))).then((items)=>{if(!source&&requestState.isCurrent())
312
+ const request=source.refresh(current,force,requestState.signal).then((items)=>{if(options.cache==="model"&&requestState.isCurrent())
314
313
  setSnapshots((value)=>({...value,[current.key]:items}))
315
314
  if(requestState.isCurrent())
316
315
  reconcileSelectedPreset(current,items)
@@ -333,7 +332,7 @@ const requestState=serverRequests.start(key,operation,false)
333
332
  if(!requestState)
334
333
  return mutations.get(key)
335
334
  changeMutationCount(current,1)
336
- const request=(action==="disconnect"?api.client.mcp.disconnect({name,...current.routing},{throwOnError:true,signal:requestState.signal}):api.client.mcp.connect({name,...current.routing},{throwOnError:true,signal:requestState.signal})).then(()=>{requestState.succeed()
335
+ const request=options.change(current,name,action,requestState.signal).then(()=>{requestState.succeed()
337
336
  mutationRetries.delete(key)
338
337
  clearBulkFailure(current,name)
339
338
  }).catch((error)=>{requestState.fail(error)
@@ -381,14 +380,14 @@ if(desired==="disabled"&&action==="disconnect"){await changeServer(current,item.
381
380
  }
382
381
  if(desired==="enabled"&&action==="connect"){await changeServer(current,item.name,"connect","restore MCP preference",false)
383
382
  }}))
384
- const failed=api.lifecycle?.signal.aborted?[]:results.flatMap((result,index)=>result.status==="rejected"&&!isAbortError(result.reason)?[items[index].name]:[])
385
- if(failed.length>0){api.ui.toast({variant:"warning",title:"MCP preferences",message:`Could not restore: ${failed.join(", ")}`,duration:4000
383
+ const failed=options.signal?.aborted?[]:results.flatMap((result,index)=>result.status==="rejected"&&!isAbortError(result.reason)?[items[index].name]:[])
384
+ if(failed.length>0){options.notifications.show({variant:"warning",title:"MCP preferences",message:`Could not restore: ${failed.join(", ")}`
386
385
  })
387
386
  }
388
387
  if(generation===activation&&target().key===current.key)
389
388
  await refresh(current,true)
390
389
  }
391
- function reconnect(current=target()){return retryBackgroundRefresh(()=>activate(current),{signal:api.lifecycle?.signal})
390
+ function reconnect(current=target()){return retryBackgroundRefresh(()=>activate(current),{signal:options.signal})
392
391
  }
393
392
  function bulkState(current=target()){return bulkStates()[current.key]??{action:"connect",status:"idle",completed:0,total:0,failed:[]
394
393
  }}
@@ -426,7 +425,7 @@ setBulkStates((values)=>({...values,[current.key]:{...operation,status:failed.le
426
425
  if(failed.length>0&&activeTarget===current.key){let action=operation.action==="preset"?`apply ${operation.preset}`:operation.action
427
426
  if(operation.group)
428
427
  action += ` ${operation.group}`
429
- api.ui.toast({variant:"warning",title:"MCP servers",message:`Could not ${action}: ${failed.join(", ")}`,duration:4000
428
+ options.notifications.show({variant:"warning",title:"MCP servers",message:`Could not ${action}: ${failed.join(", ")}`
430
429
  })
431
430
  }}
432
431
  return{list,refresh,reconnect,activate,deactivate(current){if(activeTarget!==current.key)
@@ -493,6 +492,14 @@ return
493
492
  const changes=bulkRetries.get(current.key)??[]
494
493
  return changeMany({action:state.action,...state.preset&&{preset:state.preset},...state.group&&{group:state.group}},changes,current)
495
494
  }}}
495
+ function createMcpController(api,persist,preferences,source){return createMcpModel({target(){const location=currentLocation(api)
496
+ return{...location,scope:preferencesScope({directory:location.routing.directory,worktree:api.state.path.worktree})
497
+ }},signal:api.lifecycle?.signal,notifications:{show:(input)=>api.ui.toast({...input,duration:4000})},cache:source?"source":"model",source:source??{list:()=>api.state.mcp(),async refresh(target,_force,signal){const result=await api.client.mcp.status(target.routing,{throwOnError:true,signal})
498
+ return Object.entries(result.data??{}).map(([name,status])=>({name,status:status.status,error:"error" in status?status.error:undefined
499
+ })).sort((left,right)=>left.name.localeCompare(right.name))
500
+ }},async change(target,name,action,signal){await api.client.mcp[action]({name,...target.routing},{throwOnError:true,signal})
501
+ }},persist,preferences)
502
+ }
496
503
 
497
504
 
498
505
 
@@ -1191,12 +1198,16 @@ _$insert(_el$,_$createComponent(Suspense,{get children(){return _$createComponen
1191
1198
  return _el$
1192
1199
  })()
1193
1200
  }
1201
+ var ICON_CHOICE_IDS=["plus","check","circle","star","pin","bolt","book","search","code","terminal","gear","shield","lock","unlock","eye","clock","refresh","history","copy","export","edit","fork","list","play","pause","stop","rocket","bug","beaker","wand","cloud","server","folder","file","heart","lightbulb","arrow-up","arrow-down","warning","close"]
1202
+ var choices=new Set(ICON_CHOICE_IDS)
1203
+ function iconChoiceLabel(id){return id.replaceAll("-"," ")
1204
+ }
1194
1205
  var glyphs={plus:["\uEA60","+"],check:["\uEAB2","v"],circle:["\uEAEE","o"],star:["\uEA6A","*"],pin:["\uEBAB","^"],bolt:["\uEA86","!"],book:["\uEAA4","B"],search:["\uEA6D","?"],code:["\uEAC4","#"],terminal:["\uEA85",">"],gear:["\uEB51","G"],shield:["\uEBB3","S"],lock:["\uEA75","L"],unlock:["\uEB74","U"],eye:["\uEA70","E"],clock:["\uEA82","T"],refresh:["\uEAF5","R"],history:["\uEA82","H"],copy:["\uEBCC","C"],export:["\uEBAC","X"],edit:["\uEA73","/"],fork:["\uEB9E","F"],list:["\uEBEB","="],play:["\uEB2C",">"],pause:["\uEAD1","|"],stop:["\uEAD7","s"],rocket:["\uEB44","r"],bug:["\uEAAF","b"],beaker:["\uEA79","t"],wand:["\uEAB6","w"],cloud:["\uEABA","c"],server:["\uEB50","s"],folder:["\uEA83","D"],file:["\uEA7B","f"],heart:["\uEB05","h"],lightbulb:["\uEA61","i"],"arrow-up":["\uEAA1","^"],"arrow-down":["\uEA9A","v"],warning:["\uEA6C","!"],close:["\uEA76","x"]
1195
1206
  }
1196
- var terminalIconIDs=Object.keys(glyphs)
1207
+ var terminalIconIDs=[...ICON_CHOICE_IDS]
1197
1208
  function terminalIcon(id,style){return(glyphs[id]??glyphs.bolt)[style==="nerd"?0:1]
1198
1209
  }
1199
- function terminalIconLabel(id){return id.replaceAll("-"," ")
1210
+ function terminalIconLabel(id){return iconChoiceLabel(id)
1200
1211
  }
1201
1212
  function McpErrorDialog(props){return _$createComponent(SelectionMenu,{get api(){return props.api
1202
1213
  },get title(){return `MCP error \xB7 ${props.name}`
@@ -1205,6 +1216,9 @@ function McpErrorDialog(props){return _$createComponent(SelectionMenu,{get api()
1205
1216
  },onSelect:()=>{}})
1206
1217
  }
1207
1218
 
1219
+ function builtInPreferences(defaults){return{behavior:{toggleKey:defaults.toggleKey,focusKey:defaults.focusKey,searchKey:defaults.searchKey,persistMcp:defaults.persistMcp,limitsRefreshMinutes:defaults.limitsRefreshMinutes,startInChat:defaults.startInChat,showSessionTitle:defaults.showSessionTitle,showSessionDate:defaults.showSessionDate,cornerFont:defaults.cornerFont,lspIconStyle:defaults.lspIconStyle,rowDensity:defaults.rowDensity,toolbarOverflow:defaults.toolbarOverflow,sectionItemLimits:defaults.sectionItemLimits,quickActionOrder:defaults.quickActionOrder,quickActionVisibility:defaults.quickActionVisibility
1220
+ },layout:{sections:defaults.sections,expanded:DEFAULT_SECTION_EXPANSION,order:defaults.sectionOrder??[...SIDEBAR_SECTIONS]
1221
+ },desiredMcpStates:{}}}
1208
1222
  function codexBindingKey(target){return JSON.stringify([target.providerID,target.connectionID?{connectionID:target.connectionID}:null])
1209
1223
  }
1210
1224
  function legacyKey(key){try{const value=JSON.parse(key)
@@ -1329,6 +1343,83 @@ function serializePortableSettings(layout,mcp,behavior){return `${JSON.stringify
1329
1343
  }, undefined, 2)}
1330
1344
  `
1331
1345
  }
1346
+ function normalizePresetName(value){const name=value.trim()
1347
+ if(!name)
1348
+ throw new Error("Enter a preset name")
1349
+ if(name.length>64)
1350
+ throw new Error("Preset names can contain at most 64 characters")
1351
+ return name
1352
+ }
1353
+ function createPreferencePresetActions(input){return{saveLayoutPreset(value){input.load()
1354
+ const name=normalizePresetName(value)
1355
+ if(Object.keys(input.layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase()))
1356
+ throw new Error("A preset with this name already exists")
1357
+ if(Object.keys(input.layoutPresets()).length>=50)
1358
+ throw new Error("You can save at most 50 layout presets")
1359
+ const layout=input.currentLayoutPreset()
1360
+ input.persistLayoutPresets({...input.layoutPresets(),[name]:layout},{name,layout})
1361
+ return name
1362
+ },updateLayoutPreset(name){if(!input.layoutPresets()[name])
1363
+ return false
1364
+ const layout=input.currentLayoutPreset()
1365
+ input.persistLayoutPresets({...input.layoutPresets(),[name]:layout},{name,layout})
1366
+ return true
1367
+ },renameLayoutPreset(current,value){const name=normalizePresetName(value)
1368
+ const preset=input.layoutPresets()[current]
1369
+ if(!preset)
1370
+ throw new Error("Layout preset no longer exists")
1371
+ if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(input.layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase()))
1372
+ throw new Error("A preset with this name already exists")
1373
+ const next={...input.layoutPresets()}
1374
+ delete next[current]
1375
+ next[name]=preset
1376
+ input.persistLayoutPresets(next,{name,layout:preset,previousName:current})
1377
+ return name
1378
+ },deleteLayoutPreset(name){if(!input.layoutPresets()[name])
1379
+ return false
1380
+ const next={...input.layoutPresets()}
1381
+ delete next[name]
1382
+ input.persistLayoutPresets(next,{name})
1383
+ return true
1384
+ },saveMcpPreset(value,states){input.requireHydration()
1385
+ if(Object.keys(states).length===0)
1386
+ throw new Error("There are no MCP servers to save")
1387
+ const name=normalizePresetName(value)
1388
+ if(Object.keys(input.mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase()))
1389
+ throw new Error("A preset with this name already exists")
1390
+ if(Object.keys(input.mcpPresets()).length>=50)
1391
+ throw new Error("You can save at most 50 MCP presets")
1392
+ const preset={...states}
1393
+ input.persistMcpPresets({...input.mcpPresets(),[name]:preset},{operation:"save",name,states:preset})
1394
+ return name
1395
+ },updateMcpPreset(name,states){input.requireHydration()
1396
+ if(!input.mcpPresets()[name])
1397
+ return false
1398
+ if(Object.keys(states).length===0)
1399
+ throw new Error("There are no MCP servers to save")
1400
+ const preset={...states}
1401
+ input.persistMcpPresets({...input.mcpPresets(),[name]:preset},{operation:"update",name,states:preset})
1402
+ return true
1403
+ },renameMcpPreset(current,value){input.requireHydration()
1404
+ const name=normalizePresetName(value)
1405
+ const preset=input.mcpPresets()[current]
1406
+ if(!preset)
1407
+ throw new Error("MCP preset no longer exists")
1408
+ if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(input.mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase()))
1409
+ throw new Error("A preset with this name already exists")
1410
+ const next={...input.mcpPresets()}
1411
+ delete next[current]
1412
+ next[name]=preset
1413
+ input.persistMcpPresets(next,{operation:"rename",name,previousName:current})
1414
+ return name
1415
+ },deleteMcpPreset(name){input.requireHydration()
1416
+ if(!input.mcpPresets()[name])
1417
+ return false
1418
+ const next={...input.mcpPresets()}
1419
+ delete next[name]
1420
+ input.persistMcpPresets(next,{operation:"delete",name})
1421
+ return true
1422
+ }}}
1332
1423
  function toolbarPinKey(pin){return pin.kind==="action"?`action:${pin.id}`:`skill:${pin.location}`
1333
1424
  }
1334
1425
  function identifier(value){return typeof value==="string"&&/^[a-z][a-z\d-]{0,47}$/u.test(value)
@@ -1565,14 +1656,6 @@ order=[...layer.layout.order]
1565
1656
  Object.assign(desiredMcpStates,layer.desiredMcpStates)
1566
1657
  }
1567
1658
  return{behavior,layout:{sections,expanded,order},desiredMcpStates}}
1568
- import {randomUUID} from "crypto"
1569
- import {link,mkdir,readFile,readdir,rename,rm,unlink,writeFile,readFile as readFile2} from "fs/promises"
1570
- import {join,dirname,join as join2,join as join3} from "path"
1571
- var LOCK_TIMEOUT_MS=3000
1572
- function errno(error,code){return error instanceof Error&&"code" in error&&error.code===code
1573
- }
1574
- function delay(milliseconds){return new Promise((resolve)=>setTimeout(resolve,milliseconds))
1575
- }
1576
1659
  function parseSkillConfirmations(value){if(!Array.isArray(value))
1577
1660
  return []
1578
1661
  return [...new Set(value.filter((item)=>typeof item==="string"&&item.length>0))].sort()
@@ -1604,7 +1687,7 @@ behavior.quickActionVisibility=quickActionVisibility
1604
1687
  const mcpUpdate={...update.mcp?.states,...update.mcp?.name&&update.mcp.state&&{[update.mcp.name]:update.mcp.state}}
1605
1688
  const mcp=parseDesiredMcpStates({...!update.clearMcp&&existing.mcp,...mcpUpdate
1606
1689
  })
1607
- const scope={...Object.keys(behavior).length>0&&{behavior},...(update.layout||!update.clearLayout&&existing.layout)&&{layout:parseSectionLayout(update.layout??existing.layout)
1690
+ const scope={...Object.keys(behavior).length>0&&{behavior},...(update.layout??(!update.clearLayout&&existing.layout))&&{layout:parseSectionLayout(update.layout??existing.layout)
1608
1691
  },...Object.keys(mcp).length>0&&{mcp}}
1609
1692
  const worktrees={...current.worktrees}
1610
1693
  if(target.kind==="worktree"){if(isEmptyScope(scope))
@@ -1719,143 +1802,8 @@ return{global:target.kind==="global"?scope:current.global,worktrees,user:{...ski
1719
1802
  },...pendingResetAttempt&&{pendingResetAttempt},...typeof (update.user?.autoApprovePermissions??current.user.autoApprovePermissions)==="boolean"&&{autoApprovePermissions:update.user?.autoApprovePermissions??current.user.autoApprovePermissions
1720
1803
  }}}}
1721
1804
  function targetForLegacyScope(scope){return scope==="global"?{kind:"global"}:{kind:"worktree",key:scope}}
1722
- function createPreferencesStore(stateDirectory){const directory=join(stateDirectory,LEGACY_PLUGIN_ID)
1723
- const file=join(directory,"preferences.json")
1724
- const lock=join(directory,"preferences.lock")
1725
- let writes=Promise.resolve()
1726
- async function readLockOwner(){const source=await readFile(lock,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
1727
- return
1728
- throw error
1729
- })
1730
- if(source===undefined)
1731
- return
1732
- try{const value=JSON.parse(source)
1733
- if(typeof value.token!=="string"||typeof value.pid!=="number")
1734
- return
1735
- return{token:value.token,pid:value.pid}} catch{return
1736
- }}
1737
- function processIsAlive(pid){try{process.kill(pid,0)
1738
- return true
1739
- } catch(error){return !errno(error,"ESRCH")
1740
- }}
1741
- async function moveLockToTombstone(suffix){const tombstone=`${lock}.${suffix}.${randomUUID()}`
1742
- try{await rename(lock,tombstone)
1743
- } catch(error){if(errno(error,"ENOENT"))
1744
- return false
1745
- throw error
1746
- }
1747
- await rm(tombstone,{recursive:true,force:true})
1748
- return true
1749
- }
1750
- async function liveRecoveryMarkers(prefix="preferences.lock.recover."){const entries=await readdir(directory)
1751
- const markers=[]
1752
- for(const entry of entries){if(!entry.startsWith(prefix)||entry.endsWith(".candidate"))
1753
- continue
1754
- const path=join(directory,entry)
1755
- const markerOwner=await readFile(path,"utf8").then((source)=>JSON.parse(source)).catch(()=>{})
1756
- if(typeof markerOwner?.pid==="number"&&processIsAlive(markerOwner.pid))
1757
- markers.push(path)
1758
- else
1759
- await unlink(path).catch((error)=>{if(!errno(error,"ENOENT"))
1760
- throw error
1761
- })
1762
- }
1763
- return markers.sort()
1764
- }
1765
- async function reapStaleLock(owner){const token=Buffer.from(owner.token).toString("base64url")
1766
- const prefix=`preferences.lock.recover.${token}.`
1767
- const recovery=join(directory,`${prefix}${Date.now().toString().padStart(13, "0")}.${process.pid}.${randomUUID()}`)
1768
- const markers=await liveRecoveryMarkers(prefix)
1769
- if(markers.length>0)
1770
- return false
1771
- const candidate=`${recovery}.candidate`
1772
- await writeFile(candidate,JSON.stringify({token:owner.token,pid:process.pid}),{flag:"wx",mode:384})
1773
- try{await rename(candidate,recovery)
1774
- await delay(10)
1775
- const liveMarkers=await liveRecoveryMarkers(prefix)
1776
- if(liveMarkers[0]!==recovery)
1777
- return false
1778
- const current=await readLockOwner()
1779
- if(current?.token!==owner.token||processIsAlive(current.pid))
1780
- return false
1781
- return moveLockToTombstone("stale")
1782
- } finally{await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
1783
- throw error
1784
- })
1785
- await unlink(recovery).catch((error)=>{if(!errno(error,"ENOENT"))
1786
- throw error
1787
- })
1788
- }}
1789
- async function tryAcquireLock(){const token=randomUUID()
1790
- const candidate=`${lock}.${token}.candidate`
1791
- await writeFile(candidate,JSON.stringify({token,pid:process.pid}),{flag:"wx",mode:384})
1792
- try{try{await link(candidate,lock)
1793
- } catch(error){if(!errno(error,"EEXIST"))
1794
- throw error
1795
- const owner=await readLockOwner()
1796
- if(!owner||processIsAlive(owner.pid))
1797
- return
1798
- await reapStaleLock(owner)
1799
- return
1800
- }
1801
- while(true){const markers=await liveRecoveryMarkers()
1802
- if(markers.length===0)
1803
- break
1804
- await delay(10)
1805
- }
1806
- const currentOwner=await readLockOwner()
1807
- if(currentOwner?.token!==token)
1808
- return
1809
- return async ()=>{const current=await readLockOwner()
1810
- if(current?.token!==token)
1811
- throw new Error("Refusing to release a preferences lock owned by another process")
1812
- await moveLockToTombstone("released")
1813
- }} finally{await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
1814
- throw error
1815
- })
1816
- }}
1817
- async function acquireLock(){await mkdir(directory,{recursive:true,mode:448})
1818
- const deadline=Date.now() + LOCK_TIMEOUT_MS
1819
- while(true){const release=await tryAcquireLock()
1820
- if(release)
1821
- return release
1822
- if(Date.now()>=deadline)
1823
- throw new Error(`Timed out waiting for preferences lock: ${lock}`)
1824
- await delay(10)
1825
- }}
1826
- async function withLock(operation){const release=await acquireLock()
1827
- try{return await operation()
1828
- } finally{await release()
1829
- }}
1830
- async function read(){const source=await readFile(file,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
1831
- return
1832
- throw error
1833
- })
1834
- if(source===undefined)
1835
- return
1836
- try{return parsePreferencesDocument(JSON.parse(source))
1837
- } catch(error){if(error instanceof SyntaxError)
1838
- throw new Error(`Malformed preferences JSON: ${file}`,{cause:error})
1839
- throw error
1840
- }}
1841
- async function write(value){const temporary=`${file}.${process.pid}.${randomUUID()}.tmp`
1842
- try{await writeFile(temporary,`${JSON.stringify(value)}
1843
- `,{mode:384})
1844
- await rename(temporary,file)
1845
- } finally{await unlink(temporary).catch((error)=>{if(!errno(error,"ENOENT"))
1846
- throw error
1847
- })
1848
- }}
1849
- function enqueue(operation){const result=writes.then(operation)
1850
- writes=result.catch(()=>{})
1851
- return result
1805
+ function skillKey(skill){return skill.location
1852
1806
  }
1853
- return{load(){return withLock(async ()=>await read()??emptyPreferencesDocument())
1854
- },update(update){return enqueue(()=>withLock(async ()=>{const current=await read()??emptyPreferencesDocument()
1855
- await write(applyPreferencesUpdate(current,update))
1856
- }))
1857
- },async flush(){await writes
1858
- }}}
1859
1807
  function swap(items,first,second){const value=items[first]
1860
1808
  items[first]=items[second]
1861
1809
  items[second]=value
@@ -1869,9 +1817,8 @@ return{behavior:scope.behavior,layout:scope.layout,desiredMcpStates:scope.mcp
1869
1817
  }}
1870
1818
  function mergeSessionLayout(base,session){const order=session.order??base?.order
1871
1819
  return{sections:{...base?.sections,...session.sections},expanded:{...base?.expanded,...session.expanded},...order&&{order:[...order]}}}
1872
- function createPreferencesController(api,defaults,store,configured={}){const builtIns={behavior:{toggleKey:defaults.toggleKey,focusKey:defaults.focusKey,searchKey:defaults.searchKey,persistMcp:defaults.persistMcp,limitsRefreshMinutes:defaults.limitsRefreshMinutes,startInChat:defaults.startInChat,showSessionTitle:defaults.showSessionTitle,showSessionDate:defaults.showSessionDate,cornerFont:defaults.cornerFont,lspIconStyle:defaults.lspIconStyle,rowDensity:defaults.rowDensity,toolbarOverflow:defaults.toolbarOverflow,sectionItemLimits:defaults.sectionItemLimits,quickActionOrder:defaults.quickActionOrder,quickActionVisibility:defaults.quickActionVisibility
1873
- },layout:{sections:defaults.sections,expanded:DEFAULT_SECTION_EXPANSION,order:defaults.sectionOrder??[...SIDEBAR_SECTIONS]
1874
- },desiredMcpStates:{}}
1820
+ function createPreferencesModel(ports,defaults,store,configuredInput={}){let configured=configuredInput
1821
+ const builtIns=builtInPreferences(defaults)
1875
1822
  const [resolved,setResolved]=createSignal5(resolvePreferences({builtIns,pluginOptions:configured}))
1876
1823
  const [skippedSkills,setSkippedSkills]=createSignal5(new Set)
1877
1824
  const [onboardingCompleted,setOnboardingCompleted]=createSignal5(false)
@@ -1901,17 +1848,15 @@ let mcpPresetReconciliation=Promise.resolve()
1901
1848
  let updateRevision=0
1902
1849
  let reloading
1903
1850
  let isPersistenceWarningShown=false
1904
- function skillKey(skill){return skill.location
1905
- }
1906
1851
  function showPersistenceWarning(cause){if(isPersistenceWarningShown)
1907
1852
  return
1908
1853
  isPersistenceWarningShown=true
1909
- api.ui.toast({variant:"warning",title:"Navigator settings",message:cause instanceof Error?cause.message:"Navigator settings could not be saved",duration:4000
1854
+ ports.notifications.show({variant:"warning",title:"Navigator settings",message:cause instanceof Error?cause.message:"Navigator settings could not be saved"
1910
1855
  })
1911
1856
  }
1912
1857
  function persist(request){request.catch(showPersistenceWarning)
1913
1858
  }
1914
- function validKey(value){try{return api.keymap?.parseKeySequence?api.keymap.parseKeySequence(value).length>0:Boolean(value.trim())
1859
+ function validKey(value){try{return ports.validKey(value)
1915
1860
  } catch{return false
1916
1861
  }}
1917
1862
  function resolveTarget(target,session=true){const worktree=target.kind==="worktree"?document.worktrees[target.key]:undefined
@@ -1937,13 +1882,16 @@ return{...configured.workspaceProfiles,...document.user.workspaceProfiles}}
1937
1882
  function scopeSnapshot(target=selectedTarget()){const scope=target.kind==="global"?document.global:document.worktrees[target.key]
1938
1883
  return JSON.stringify({layout:scope?.layout,mcp:scope?.mcp,behavior:scope?.behavior})
1939
1884
  }
1940
- function update(change){updateRevision++
1885
+ function update(change,wait=false){updateRevision++
1941
1886
  document=applyPreferencesUpdate(document,change)
1942
- if(isHydrated)
1943
- persist(store.update(change))
1944
- else
1887
+ let request
1888
+ if(isHydrated){request=store.update(change)
1889
+ persist(request)
1890
+ } else
1945
1891
  pendingUpdates.push(change)
1946
1892
  refreshResolved()
1893
+ if(wait)
1894
+ return request??Promise.resolve()
1947
1895
  }
1948
1896
  function applyDocument(value){document=value
1949
1897
  setSkippedSkills(new Set(value.user.skippedSkillConfirmations))
@@ -2014,13 +1962,6 @@ function setSessionLayout(layout){setTargetSessionLayout(activeTarget(),layout)
2014
1962
  }
2015
1963
  function setSelectedSessionLayout(layout,replace=false){setTargetSessionLayout(selectedTarget(),layout,replace)
2016
1964
  }
2017
- function normalizePresetName(value){const name=value.trim()
2018
- if(!name)
2019
- throw new Error("Enter a preset name")
2020
- if(name.length>64)
2021
- throw new Error("Preset names can contain at most 64 characters")
2022
- return name
2023
- }
2024
1965
  function currentLayoutPreset(){const layout=selectedLayoutResolved().layout
2025
1966
  return{sections:{...layout.sections},expanded:{...layout.expanded},order:[...layout.order]
2026
1967
  }}
@@ -2044,7 +1985,7 @@ isApplied=!name
2044
1985
  else if(mcpPreset.operation==="rename"){isApplied=name===mcpPreset.name&&(mcpPreset.previousName===mcpPreset.name||!actual[mcpPreset.previousName])
2045
1986
  } else{isApplied=Boolean(name)&&Object.keys(actual[name]).length===Object.keys(mcpPreset.states).length&&Object.entries(mcpPreset.states).every(([server,state])=>actual[name][server]===state)
2046
1987
  }
2047
- if(!isApplied){api.ui.toast({variant:"warning",title:"MCP presets",message:"Preset changed in another OpenCode instance; reloaded saved presets",duration:4000
1988
+ if(!isApplied){ports.notifications.show({variant:"warning",title:"MCP presets",message:"Preset changed in another OpenCode instance; reloaded saved presets"
2048
1989
  })
2049
1990
  }}).catch(showPersistenceWarning)
2050
1991
  }
@@ -2080,13 +2021,14 @@ return
2080
2021
  setAutoApprovePermissions(value)
2081
2022
  update({user:{autoApprovePermissions:value}})
2082
2023
  }
2024
+ function codexBindingUpdate(providerID,connectionID,accountID){setCodexAccountBindings((bindings)=>changeCodexBinding(bindings,{providerID,connectionID},accountID))
2025
+ return{user:{codexAccountBinding:{providerID,connectionID,accountID}}}}
2083
2026
  function changeCodexConnectionBinding(providerID,connectionID,accountID){if(!isHydrated){load().then(()=>changeCodexConnectionBinding(providerID,connectionID,accountID))
2084
2027
  return
2085
2028
  }
2086
2029
  if(!providerID)
2087
2030
  return
2088
- setCodexAccountBindings((bindings)=>changeCodexBinding(bindings,{providerID,connectionID},accountID))
2089
- update({user:{codexAccountBinding:{providerID,connectionID,accountID}}})
2031
+ update(codexBindingUpdate(providerID,connectionID,accountID))
2090
2032
  }
2091
2033
  function toggleFavoriteMcpServer(name){if(!isHydrated){load().then(()=>toggleFavoriteMcpServer(name))
2092
2034
  return
@@ -2112,10 +2054,18 @@ next.add(id)
2112
2054
  setFavoriteQuickActions(next)
2113
2055
  update({user:{favoriteQuickAction:{id,favorite:next.has(id)}}})
2114
2056
  }
2115
- return{sections:()=>resolved().layout.sections,toolbarPins(){revision()
2057
+ return{setConfiguredDefaults(next){batch(()=>{configured=next
2058
+ setLayoutPresets({...configured.layoutPresets,...document.user.layoutPresets})
2059
+ setMcpPresets({...configured.mcpPresets,...document.user.mcpPresets})
2060
+ setMcpServerGroups({...configured.mcpServerGroups,...document.user.mcpServerGroups})
2061
+ refreshResolved()
2062
+ })
2063
+ },sections:()=>resolved().layout.sections,toolbarPins(){revision()
2116
2064
  return document.user.toolbarPins??[]
2117
2065
  },changeToolbarPin(change){requireHydration()
2118
2066
  update({user:{toolbarPin:change}})
2067
+ },saveToolbarPin(change){requireHydration()
2068
+ return update({user:{toolbarPin:change}},true)
2119
2069
  },expanded:()=>resolved().layout.expanded,sectionOrder:()=>resolved().layout.order,toggleKey:()=>resolved().behavior.toggleKey,focusKey:()=>resolved().behavior.focusKey,searchKey:()=>resolved().behavior.searchKey,persistMcp:()=>resolved().behavior.persistMcp,limitsRefreshMinutes:()=>resolved().behavior.limitsRefreshMinutes,showSessionTitle:()=>resolved().behavior.showSessionTitle,showSessionDate:()=>resolved().behavior.showSessionDate,startInChatForScope:(scope)=>{revision()
2120
2070
  return resolveTarget(targetForScope(scope),false).behavior.startInChat
2121
2071
  },cornerFont:()=>resolved().behavior.cornerFont,lspIconStyle:()=>resolved().behavior.lspIconStyle,rowDensity:()=>resolved().behavior.rowDensity,toolbarOverflow:()=>resolved().behavior.toolbarOverflow,selectedToolbarOverflow:()=>selectedResolved().behavior.toolbarOverflow,toggleToolbarOverflow(){load()
@@ -2169,7 +2119,16 @@ throw new Error("MCP preset no longer exists")
2169
2119
  update({user:{workspaceProfile:{layout,mcp}}})
2170
2120
  },favoriteSkills,favoriteMcpServers,favoriteQuickActions,toggleFavoriteMcpServer,toggleFavoriteQuickAction,recentSkills,recentQuickActions,mcpServerGroups,setMcpServerGroup(name,value){setMcpServerGroups(assignGroup(mcpServerGroups(),name,value))
2171
2121
  update({user:{mcpServerGroup:{name,group:value}}})
2172
- },skillGroups,setSkillGroup:changeSkillGroup,autoApprovePermissions,setAutoApprovePermissions:changeAutoApprovePermissions,codexAccountForModel:(providerID,modelID)=>codexBindingAccount(codexAccountBindings(),{providerID},modelID),setCodexAccountBinding:(providerID,_modelID,accountID)=>changeCodexConnectionBinding(providerID,undefined,accountID),codexAccountForConnection:(providerID,connectionID,modelID)=>codexBindingAccount(codexAccountBindings(),{providerID,connectionID},modelID),setCodexConnectionBinding:changeCodexConnectionBinding,pendingResetAttempt,async beginResetAttempt(attempt){await load()
2122
+ },skillGroups,setSkillGroup:changeSkillGroup,autoApprovePermissions,setAutoApprovePermissions:changeAutoApprovePermissions,codexAccountForModel:(providerID,modelID)=>codexBindingAccount(codexAccountBindings(),{providerID},modelID),setCodexAccountBinding:(providerID,_modelID,accountID)=>changeCodexConnectionBinding(providerID,undefined,accountID),codexAccountForConnection:(providerID,connectionID,modelID)=>codexBindingAccount(codexAccountBindings(),{providerID,connectionID},modelID),setCodexConnectionBinding:changeCodexConnectionBinding,async saveCodexConnectionBinding(providerID,connectionID,accountID){requireHydration()
2123
+ if(!providerID||!accountID)
2124
+ throw new Error("A provider and verified account are required")
2125
+ updateRevision++
2126
+ const request=store.update({user:{codexAccountBinding:{providerID,connectionID,accountID}}})
2127
+ persist(request)
2128
+ await request
2129
+ document=applyPreferencesUpdate(document,codexBindingUpdate(providerID,connectionID,accountID))
2130
+ refreshResolved()
2131
+ },pendingResetAttempt,async beginResetAttempt(attempt){await load()
2173
2132
  await store.update({user:{pendingResetAttempt:attempt}})
2174
2133
  const current=await store.load()
2175
2134
  setPendingResetAttempt(current.user.pendingResetAttempt)
@@ -2222,6 +2181,9 @@ setSessionLayout({order})
2222
2181
  },toggleSelectedSection(name){load()
2223
2182
  const layout=resolveTarget(selectedTarget()).layout
2224
2183
  setSelectedSessionLayout({sections:{[name]:!layout.sections[name]}})
2184
+ },toggleSelectedSectionExpanded(name){load()
2185
+ const layout=resolveTarget(selectedTarget()).layout
2186
+ setSelectedSessionLayout({expanded:{[name]:!layout.expanded[name]}})
2225
2187
  },moveSelectedSection(name,direction){load()
2226
2188
  const layout=resolveTarget(selectedTarget()).layout
2227
2189
  const order=[...layout.order]
@@ -2234,76 +2196,8 @@ setSelectedSessionLayout({order})
2234
2196
  },previewLayoutPreset(preset){return layoutPresetPreview(selectedLayoutResolved().layout,preset)
2235
2197
  },applyLayoutPreset(preset){load()
2236
2198
  setSelectedSessionLayout(layoutPresetPreview(selectedLayoutResolved().layout,preset).next,true)
2237
- },saveLayoutPreset(value){load()
2238
- const name=normalizePresetName(value)
2239
- if(Object.keys(layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2240
- }
2241
- if(Object.keys(layoutPresets()).length>=50)
2242
- throw new Error("You can save at most 50 layout presets")
2243
- const layout=currentLayoutPreset()
2244
- persistLayoutPresets({...layoutPresets(),[name]:layout},{name,layout})
2245
- return name
2246
- },updateLayoutPreset(name){if(!layoutPresets()[name])
2247
- return false
2248
- const layout=currentLayoutPreset()
2249
- persistLayoutPresets({...layoutPresets(),[name]:layout},{name,layout})
2250
- return true
2251
- },renameLayoutPreset(current,value){const name=normalizePresetName(value)
2252
- const preset=layoutPresets()[current]
2253
- if(!preset)
2254
- throw new Error("Layout preset no longer exists")
2255
- if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2256
- }
2257
- const next={...layoutPresets()}
2258
- delete next[current]
2259
- next[name]=preset
2260
- persistLayoutPresets(next,{name,layout:preset,previousName:current})
2261
- return name
2262
- },deleteLayoutPreset(name){if(!layoutPresets()[name])
2263
- return false
2264
- const next={...layoutPresets()}
2265
- delete next[name]
2266
- persistLayoutPresets(next,{name})
2267
- return true
2268
- },saveMcpPreset(value,states){requireHydration()
2269
- if(Object.keys(states).length===0)
2270
- throw new Error("There are no MCP servers to save")
2271
- const name=normalizePresetName(value)
2272
- if(Object.keys(mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2273
- }
2274
- if(Object.keys(mcpPresets()).length>=50)
2275
- throw new Error("You can save at most 50 MCP presets")
2276
- const preset={...states}
2277
- persistMcpPresets({...mcpPresets(),[name]:preset},{operation:"save",name,states:preset})
2278
- return name
2279
- },updateMcpPreset(name,states){requireHydration()
2280
- if(!mcpPresets()[name])
2281
- return false
2282
- if(Object.keys(states).length===0)
2283
- throw new Error("There are no MCP servers to save")
2284
- const preset={...states}
2285
- persistMcpPresets({...mcpPresets(),[name]:preset},{operation:"update",name,states:preset})
2286
- return true
2287
- },renameMcpPreset(current,value){requireHydration()
2288
- const name=normalizePresetName(value)
2289
- const preset=mcpPresets()[current]
2290
- if(!preset)
2291
- throw new Error("MCP preset no longer exists")
2292
- if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2293
- }
2294
- const next={...mcpPresets()}
2295
- delete next[current]
2296
- next[name]=preset
2297
- persistMcpPresets(next,{operation:"rename",name,previousName:current})
2298
- return name
2299
- },deleteMcpPreset(name){requireHydration()
2300
- if(!mcpPresets()[name])
2301
- return false
2302
- const next={...mcpPresets()}
2303
- delete next[name]
2304
- persistMcpPresets(next,{operation:"delete",name})
2305
- return true
2306
- },async saveLayoutAsDefault(){await load()
2199
+ },...createPreferencePresetActions({load,requireHydration,layoutPresets,mcpPresets,currentLayoutPreset,persistLayoutPresets,persistMcpPresets
2200
+ }),async saveLayoutAsDefault(){await load()
2307
2201
  const target=selectedTarget()
2308
2202
  const layout=resolveTarget(target).layout
2309
2203
  updateRevision++
@@ -2366,6 +2260,9 @@ update({target:targetForScope(scope),mcp:{name,state}})
2366
2260
  },setDesiredMcpStates(scope,states){load()
2367
2261
  update({target:targetForScope(scope),mcp:{states}})
2368
2262
  }}}
2263
+ function createPreferencesController(api,defaults,store,configured={}){return createPreferencesModel({notifications:{show:(input)=>api.ui.toast({...input,duration:4000})},validKey:(value)=>api.keymap?.parseKeySequence?api.keymap.parseKeySequence(value).length>0:Boolean(value.trim())
2264
+ },defaults,store,configured)
2265
+ }
2369
2266
  function record2(value){if(!value||typeof value!=="object"||Array.isArray(value))
2370
2267
  return
2371
2268
  return value
@@ -2400,17 +2297,173 @@ function legacyDefaults(config){const{sections,sectionOrder,...behavior}=config
2400
2297
  return{behavior,layout:{sections,expanded:{},...sectionOrder&&{order:sectionOrder}}}}
2401
2298
  function configuredDefaultsFromPluginOptions(options){return mergeConfiguredDefaults(legacyDefaults(pluginConfig(options)),parseConfiguredDefaults(options??{}))
2402
2299
  }
2403
-
2404
- function createSkillController(api,source){const [skills,setSkills]=createSignal6({})
2405
- const refreshing=new Map
2406
- const targets=new Map
2407
- const requests=createRequestState(api.lifecycle.signal)
2408
- const promptRequests=createRequestState(api.lifecycle.signal)
2409
- let activeTarget
2300
+ import {randomUUID} from "crypto"
2301
+ import {link,mkdir,readFile,readdir,rename,rm,unlink,writeFile,readFile as readFile2} from "fs/promises"
2302
+ import {join,dirname,join as join2,join as join3} from "path"
2303
+ var LOCK_TIMEOUT_MS=3000
2304
+ function errno(error,code){return error instanceof Error&&"code" in error&&error.code===code
2305
+ }
2306
+ function delay(milliseconds){return new Promise((resolve)=>setTimeout(resolve,milliseconds))
2307
+ }
2308
+ function createPreferencesStore(stateDirectory){const directory=join(stateDirectory,LEGACY_PLUGIN_ID)
2309
+ const file=join(directory,"preferences.json")
2310
+ const lock=join(directory,"preferences.lock")
2311
+ let writes=Promise.resolve()
2312
+ async function readLockOwner(){const source=await readFile(lock,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
2313
+ return
2314
+ throw error
2315
+ })
2316
+ if(source===undefined)
2317
+ return
2318
+ try{const value=JSON.parse(source)
2319
+ if(typeof value.token!=="string"||typeof value.pid!=="number")
2320
+ return
2321
+ return{token:value.token,pid:value.pid}} catch{return
2322
+ }}
2323
+ function processIsAlive(pid){try{process.kill(pid,0)
2324
+ return true
2325
+ } catch(error){return !errno(error,"ESRCH")
2326
+ }}
2327
+ async function moveLockToTombstone(suffix){const tombstone=`${lock}.${suffix}.${randomUUID()}`
2328
+ try{await rename(lock,tombstone)
2329
+ } catch(error){if(errno(error,"ENOENT"))
2330
+ return false
2331
+ throw error
2332
+ }
2333
+ await rm(tombstone,{recursive:true,force:true})
2334
+ return true
2335
+ }
2336
+ async function liveRecoveryMarkers(prefix="preferences.lock.recover."){const entries=await readdir(directory)
2337
+ const markers=[]
2338
+ for(const entry of entries){if(!entry.startsWith(prefix)||entry.endsWith(".candidate"))
2339
+ continue
2340
+ const path=join(directory,entry)
2341
+ const markerOwner=await readFile(path,"utf8").then((source)=>JSON.parse(source)).catch(()=>{})
2342
+ if(typeof markerOwner?.pid==="number"&&processIsAlive(markerOwner.pid))
2343
+ markers.push(path)
2344
+ else
2345
+ await unlink(path).catch((error)=>{if(!errno(error,"ENOENT"))
2346
+ throw error
2347
+ })
2348
+ }
2349
+ return markers.sort()
2350
+ }
2351
+ async function reapStaleLock(owner){const token=Buffer.from(owner.token).toString("base64url")
2352
+ const prefix=`preferences.lock.recover.${token}.`
2353
+ const recovery=join(directory,`${prefix}${Date.now().toString().padStart(13, "0")}.${process.pid}.${randomUUID()}`)
2354
+ const markers=await liveRecoveryMarkers(prefix)
2355
+ if(markers.length>0)
2356
+ return false
2357
+ const candidate=`${recovery}.candidate`
2358
+ await writeFile(candidate,JSON.stringify({token:owner.token,pid:process.pid}),{flag:"wx",mode:384})
2359
+ try{await rename(candidate,recovery)
2360
+ await delay(10)
2361
+ const liveMarkers=await liveRecoveryMarkers(prefix)
2362
+ if(liveMarkers[0]!==recovery)
2363
+ return false
2364
+ const current=await readLockOwner()
2365
+ if(current?.token!==owner.token||processIsAlive(current.pid))
2366
+ return false
2367
+ return moveLockToTombstone("stale")
2368
+ } finally{await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
2369
+ throw error
2370
+ })
2371
+ await unlink(recovery).catch((error)=>{if(!errno(error,"ENOENT"))
2372
+ throw error
2373
+ })
2374
+ }}
2375
+ async function tryAcquireLock(deadline){const token=randomUUID()
2376
+ const candidate=`${lock}.${token}.candidate`
2377
+ let acquired=false
2378
+ let handedOff=false
2379
+ await writeFile(candidate,JSON.stringify({token,pid:process.pid}),{flag:"wx",mode:384})
2380
+ try{try{await link(candidate,lock)
2381
+ acquired=true
2382
+ } catch(error){if(!errno(error,"EEXIST"))
2383
+ throw error
2384
+ const owner=await readLockOwner()
2385
+ if(!owner||processIsAlive(owner.pid))
2386
+ return
2387
+ await reapStaleLock(owner)
2388
+ return
2389
+ }
2390
+ while(true){const markers=await liveRecoveryMarkers()
2391
+ if(markers.length===0)
2392
+ break
2393
+ if(Date.now()>=deadline)
2394
+ return
2395
+ await delay(10)
2396
+ }
2397
+ const currentOwner=await readLockOwner()
2398
+ if(currentOwner?.token!==token)
2399
+ return
2400
+ handedOff=true
2401
+ return async ()=>{const current=await readLockOwner()
2402
+ if(current?.token!==token)
2403
+ throw new Error("Refusing to release a preferences lock owned by another process")
2404
+ await moveLockToTombstone("released")
2405
+ }} finally{if(acquired&&!handedOff){const current=await readLockOwner()
2406
+ if(current?.token===token)
2407
+ await moveLockToTombstone("released")
2408
+ }
2409
+ await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
2410
+ throw error
2411
+ })
2412
+ }}
2413
+ async function acquireLock(){await mkdir(directory,{recursive:true,mode:448})
2414
+ const deadline=Date.now() + LOCK_TIMEOUT_MS
2415
+ while(true){const release=await tryAcquireLock(deadline)
2416
+ if(release)
2417
+ return release
2418
+ if(Date.now()>=deadline)
2419
+ throw new Error(`Timed out waiting for preferences lock: ${lock}`)
2420
+ await delay(10)
2421
+ }}
2422
+ async function withLock(operation){const release=await acquireLock()
2423
+ try{return await operation()
2424
+ } finally{await release()
2425
+ }}
2426
+ async function read(){const source=await readFile(file,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
2427
+ return
2428
+ throw error
2429
+ })
2430
+ if(source===undefined)
2431
+ return
2432
+ try{return parsePreferencesDocument(JSON.parse(source))
2433
+ } catch(error){if(error instanceof SyntaxError)
2434
+ throw new Error(`Malformed preferences JSON: ${file}`,{cause:error})
2435
+ throw error
2436
+ }}
2437
+ async function write(value){const temporary=`${file}.${process.pid}.${randomUUID()}.tmp`
2438
+ try{await writeFile(temporary,`${JSON.stringify(value)}
2439
+ `,{mode:384})
2440
+ await rename(temporary,file)
2441
+ } finally{await unlink(temporary).catch((error)=>{if(!errno(error,"ENOENT"))
2442
+ throw error
2443
+ })
2444
+ }}
2445
+ function enqueue(operation){const result=writes.then(operation)
2446
+ writes=result.catch(()=>{})
2447
+ return result
2448
+ }
2449
+ return{load(){return withLock(async ()=>await read()??emptyPreferencesDocument())
2450
+ },update(update){return enqueue(()=>withLock(async ()=>{const current=await read()??emptyPreferencesDocument()
2451
+ await write(applyPreferencesUpdate(current,update))
2452
+ }))
2453
+ },async flush(){await writes
2454
+ }}}
2455
+
2456
+ function createSkillModel(options){const{source,lifecycle}=options
2457
+ const [skills,setSkills]=createSignal6({})
2458
+ const refreshing=new Map
2459
+ const targets=new Map
2460
+ const requests=createRequestState(lifecycle.signal)
2461
+ const promptRequests=createRequestState(lifecycle.signal)
2462
+ let activeTarget
2410
2463
  let leases=new Set
2411
- function target(){return currentLocation(api)
2464
+ function target(){return options.target()
2412
2465
  }
2413
- function list(current=target()){return source?source.list(current):skills()[current.key]??[]
2466
+ function list(current=target()){return options.cache==="source"?source.list(current):skills()[current.key]??[]
2414
2467
  }
2415
2468
  function error(current=target()){return requests.state(current.key).error?.message
2416
2469
  }
@@ -2421,8 +2474,8 @@ targets.set(current.key,current)
2421
2474
  const requestState=requests.start(current.key,"refresh skills",current.key in skills(),force)
2422
2475
  if(!requestState)
2423
2476
  return pending
2424
- const request=(source?source.refresh(current,force,requestState.signal).then((data)=>({data})):api.client.app.skills(current.routing,{throwOnError:true,signal:requestState.signal})).then((result)=>{const items=[...result.data??[]].sort((a,b)=>a.name.localeCompare(b.name))
2425
- if(!source&&requestState.isCurrent())
2477
+ const request=source.refresh(current,force,requestState.signal).then((data)=>{const items=[...data].sort((a,b)=>a.name.localeCompare(b.name))
2478
+ if(options.cache==="model"&&requestState.isCurrent())
2426
2479
  setSkills((value)=>({...value,[current.key]:items}))
2427
2480
  requestState.succeed()
2428
2481
  return items
@@ -2435,9 +2488,9 @@ refreshing.delete(current.key)
2435
2488
  refreshing.set(current.key,request)
2436
2489
  return request
2437
2490
  }
2438
- const unsubscribe=api.event.on("server.connected",()=>{for(const current of targets.values()){retryBackgroundRefresh(()=>refresh(current),{signal:api.lifecycle.signal}).catch(()=>{})
2491
+ const unsubscribe=options.onReconnect(()=>{for(const current of targets.values()){retryBackgroundRefresh(()=>refresh(current),{signal:lifecycle.signal}).catch(()=>{})
2439
2492
  }})
2440
- api.lifecycle.onDispose(unsubscribe)
2493
+ lifecycle.onDispose(unsubscribe)
2441
2494
  return{list,error,refresh,target,state(current=target()){return requests.state(current.key)
2442
2495
  },retry(current=target()){return refresh(current,true)
2443
2496
  },activate(current=target()){targets.set(current.key,current)
@@ -2463,15 +2516,24 @@ throw new DOMException("Sidebar context changed","AbortError")
2463
2516
  const request=promptRequests.start(current.key,"insert skill",false,true)
2464
2517
  if(!request)
2465
2518
  return false
2466
- try{const result=source&&selected?{data:await source.use(current,selected,request.signal)}:await api.client.tui.appendPrompt({...current.routing,text:`/${name} `},{throwOnError:true,signal:request.signal})
2519
+ try{const result=selected?await source.use(current,selected,request.signal):await options.useNamed?.(current,name,request.signal)
2467
2520
  if(!request.isCurrent())
2468
2521
  return false
2469
2522
  request.succeed()
2470
- return result.data
2523
+ return result??false
2471
2524
  } catch(error_){request.fail(error_)
2472
2525
  throw error_
2473
2526
  } finally{request.finish()
2474
2527
  }}}}
2528
+ function createSkillController(api,source){const useNamed=async (target,name,signal)=>{const result=await api.client.tui.appendPrompt({...target.routing,text:`/${name} `},{throwOnError:true,signal})
2529
+ return result.data??false
2530
+ }
2531
+ return createSkillModel({lifecycle:api.lifecycle,target:()=>currentLocation(api),cache:source?"source":"model",source:source??{list:()=>[],async refresh(target,_force,signal){const result=await api.client.app.skills(target.routing,{throwOnError:true,signal})
2532
+ return result.data??[]
2533
+ },use:(target,skill,signal)=>useNamed(target,skill.name,signal)
2534
+ },onReconnect:(listener)=>api.event.on("server.connected",listener),useNamed
2535
+ })
2536
+ }
2475
2537
  var BUCKET_RANK2={Favorites:0,Ungrouped:2}
2476
2538
  function buildSkillGroupedView(items,favorites,groups,recent){return items.map((item)=>({...item,assignedGroup:groups[item.location],bucket:favorites.has(item.location)?"Favorites":groups[item.location]??"Ungrouped"
2477
2539
  })).sort((left,right)=>{const rank=(BUCKET_RANK2[left.bucket]??1)-(BUCKET_RANK2[right.bucket]??1)
@@ -2725,34 +2787,40 @@ return "Skill source changed"
2725
2787
  return "Multiple sources share this skill name"
2726
2788
  return
2727
2789
  }
2728
- function activateSkill(input){input={...input,skill:{...input.skill
2729
- }}
2790
+ function createSkillActivation(input){const skill={...input.skill}
2730
2791
  const target=input.controller.target()
2731
- const route=JSON.stringify(input.api.route.current)
2732
- const current=()=>target.key===input.controller.target().key&&route===JSON.stringify(input.api.route.current)
2733
- const report=(error)=>{if(!isAbortError(error))
2734
- input.api.ui.toast({variant:"error",title:"Skills",message:String(error),duration:5000
2735
- })
2736
- }
2737
- function check(){if(!current())
2792
+ function check(){if(!input.current()||target.key!==input.controller.target().key)
2738
2793
  throw new DOMException("Skill target changed; choose it again","AbortError")
2739
- const reason=skillUnavailable(input.controller,input.skill)
2794
+ const reason=skillUnavailable(input.controller,skill)
2740
2795
  if(reason)
2741
2796
  throw new Error(reason)
2742
2797
  }
2743
- async function insert(skip=false){try{check()
2798
+ return{skill,check,shouldConfirm:()=>input.access.shouldConfirmSkill(skill),async run(skip=false){check()
2744
2799
  if(skip)
2745
- input.access.skipSkillConfirmation(input.skill)
2800
+ input.access.skipSkillConfirmation(skill)
2746
2801
  input.beforeInsert?.()
2747
- if(await input.controller.use(target,input.skill.name,input.skill))
2748
- await input.access.recordSkillUse?.(input.skill)
2802
+ const inserted=await input.controller.use(target,skill.name,skill)
2803
+ if(inserted)
2804
+ await input.access.recordSkillUse?.(skill)
2805
+ return inserted
2806
+ }}}
2807
+ function activateSkill(input){input={...input,skill:{...input.skill
2808
+ }}
2809
+ const route=JSON.stringify(input.api.route.current)
2810
+ const activation=createSkillActivation({...input,current:()=>route===JSON.stringify(input.api.route.current)
2811
+ })
2812
+ const report=(error)=>{if(!isAbortError(error))
2813
+ input.api.ui.toast({variant:"error",title:"Skills",message:String(error),duration:5000
2814
+ })
2815
+ }
2816
+ async function insert(skip=false){try{await activation.run(skip)
2749
2817
  } catch(error){report(error)
2750
2818
  }}
2751
- try{check()
2819
+ try{activation.check()
2752
2820
  } catch(error){report(error)
2753
2821
  return
2754
2822
  }
2755
- if(!input.access.shouldConfirmSkill(input.skill)){insert()
2823
+ if(!activation.shouldConfirm()){insert()
2756
2824
  return
2757
2825
  }
2758
2826
  input.dialogs.open(()=>_$createComponent(SkillDialog,{get api(){return input.api
@@ -2775,7 +2843,7 @@ return value
2775
2843
  }
2776
2844
  function changed(){setRevision((value)=>value + 1)
2777
2845
  }
2778
- return{observeStatus(key,sessionID,status,at=now()){const value=state(key)
2846
+ return{observeStatus(key,sessionID,status,at=now(),nativeStart){const value=state(key)
2779
2847
  const run=value.active.get(sessionID)
2780
2848
  const isKnown=value.known.has(sessionID)
2781
2849
  value.known.add(sessionID)
@@ -2783,10 +2851,17 @@ if(status.type==="idle"){if(!run)
2783
2851
  return
2784
2852
  value.active.delete(sessionID)
2785
2853
  value.recent=[{...run,finishedAt:Math.max(at,run.startedAt),outcome:run.outcome??"finished"},...value.recent].sort((a,b)=>b.finishedAt-a.finishedAt||a.sessionID.localeCompare(b.sessionID)).slice(0,10)
2786
- } else{if(run)
2854
+ } else{const exact=typeof nativeStart==="number"&&Number.isFinite(nativeStart)&&nativeStart>0&&nativeStart<=at?nativeStart:undefined
2855
+ if(run){if(exact===undefined||run.nativeStartedAt!==undefined&&exact<=run.nativeStartedAt)
2856
+ return
2857
+ const next=exact>run.startedAt?{sessionID}:run
2858
+ value.active.set(sessionID,{...next,startedAt:exact,nativeStartedAt:exact,startedBeforeObservation:false
2859
+ })
2860
+ changed()
2787
2861
  return
2862
+ }
2788
2863
  value.recent=value.recent.filter((item)=>item.sessionID!==sessionID)
2789
- value.active.set(sessionID,{sessionID,startedAt:at,startedBeforeObservation:!isKnown})
2864
+ value.active.set(sessionID,{sessionID,startedAt:exact??at,startedBeforeObservation:exact===undefined&&!isKnown,...exact!==undefined&&{nativeStartedAt:exact}})
2790
2865
  }
2791
2866
  changed()
2792
2867
  },observeError(key,sessionID,error,at=now()){if(!sessionID)
@@ -2798,7 +2873,7 @@ return
2798
2873
  const isCancelled=error?.name==="MessageAbortedError"
2799
2874
  const data=error&&"data" in error?error.data:undefined
2800
2875
  const fallbackMessage=isCancelled?"Session aborted":"Session error"
2801
- const message=data&&"message" in data&&typeof data.message==="string"?data.message:fallbackMessage
2876
+ const message=data&&typeof data==="object"&&"message" in data&&typeof data.message==="string"?data.message:fallbackMessage
2802
2877
  const next={...run,startedAt:Math.min(run.startedAt,at),outcome:isCancelled?"cancelled":"error",errorMessage:message
2803
2878
  }
2804
2879
  if(value.active.has(sessionID))
@@ -2821,7 +2896,7 @@ return targets.get(key)?.recent??[]
2821
2896
  var POLL_INTERVAL_MS=5000
2822
2897
  function activeStatus(status){return status?.type==="busy"||status?.type==="retry"
2823
2898
  }
2824
- function createSubagentController(api,options={}){const source=options.source
2899
+ function createSubagentModel(options){const source=options.source
2825
2900
  const now=options.now??Date.now
2826
2901
  const history=createSubagentHistory({now})
2827
2902
  const [children,setChildren]=createSignal8({})
@@ -2832,14 +2907,13 @@ const [unavailable,setUnavailable]=createSignal8({})
2832
2907
  const targets=new Map
2833
2908
  const remoteFailures=new Map
2834
2909
  const remoteRetryAt=new Map
2835
- const requests=createRequestState(api.lifecycle.signal)
2910
+ const requests=createRequestState(options.lifecycle.signal)
2836
2911
  let activeTarget
2912
+ let leases=new Set
2837
2913
  let pollTimer
2838
- const snapshots=()=>source?Object.fromEntries([...targets].map(([key,current])=>[key,source.list(current.parentID)])):children()
2839
- function target(parentID){const parent=api.state.session.get(parentID)
2840
- const directory=parent?.directory??api.state.path.directory
2841
- const workspace=parent?.workspaceID
2842
- return{key:JSON.stringify([parentID,directory,workspace??null]),parentID,routing:{directory,...workspace&&{workspace}}}}
2914
+ const snapshots=()=>options.cache==="source"?Object.fromEntries([...targets].map(([key,current])=>[key,source.list(current.parentID)])):children()
2915
+ function target(parentID){return options.target(parentID)
2916
+ }
2843
2917
  function applySession(items,parentID,mutation){const next=items.filter((item)=>item.id!==(mutation.type==="upsert"?mutation.info.id:mutation.sessionID))
2844
2918
  if(mutation.type==="upsert"&&mutation.info.parentID===parentID)
2845
2919
  next.push(mutation.info)
@@ -2854,18 +2928,19 @@ history.remove(key,sessionID)
2854
2928
  }
2855
2929
  for(const journal of journals)
2856
2930
  journal.sessions.push(mutation)
2857
- if(!source)
2931
+ if(options.cache==="model")
2858
2932
  setChildren((current)=>Object.fromEntries(Object.entries(current).map(([key,items])=>[key,applySession(items,targets.get(key).parentID,mutation)])))
2859
2933
  }
2860
- function recordStatus(input){const mutation={...input,at:now(),observed:new Set}
2934
+ function recordStatus(input){const mutation={...input,at:now(),startedAt:source?.startedAt?.(input.sessionID),observed:new Set
2935
+ }
2861
2936
  for(const journal of journals)
2862
2937
  journal.statuses.push(mutation)
2863
- batch(()=>{for(const [key,items] of Object.entries(snapshots())){if(items.every((item)=>item.id!==mutation.sessionID))
2938
+ batch2(()=>{for(const [key,items] of Object.entries(snapshots())){if(items.every((item)=>item.id!==mutation.sessionID))
2864
2939
  continue
2865
- history.observeStatus(key,mutation.sessionID,mutation.status,mutation.at)
2940
+ history.observeStatus(key,mutation.sessionID,mutation.status,mutation.at,mutation.startedAt)
2866
2941
  mutation.observed.add(key)
2867
2942
  }
2868
- if(!source)
2943
+ if(options.cache==="model")
2869
2944
  setStatuses((current)=>Object.fromEntries(Object.entries(current).map(([key,value])=>[key,applyStatus(value,mutation)])))
2870
2945
  })
2871
2946
  }
@@ -2891,14 +2966,13 @@ if(!requestState){journals.delete(journal)
2891
2966
  return pending
2892
2967
  }
2893
2968
  const signal=AbortSignal.any([requestState.signal,AbortSignal.timeout(5000)])
2894
- const request=(source?source.refresh(current.parentID,signal).then(()=>[{data:source.list(current.parentID)},{data:Object.fromEntries(source.list(current.parentID).map((item)=>[item.id,source.status(item.id)]))
2895
- }]):Promise.all([api.client.session.children({sessionID:current.parentID,...current.routing},{throwOnError:true,signal}),api.client.session.status(current.routing,{throwOnError:true,signal})])).then(async ([childResult,statusResult])=>{let nextChildren=childResult.data??[]
2969
+ const request=options.snapshot(current,signal).then(async (snapshot)=>{let nextChildren=snapshot.sessions
2896
2970
  for(const mutation of journal.sessions)
2897
2971
  nextChildren=applySession(nextChildren,current.parentID,mutation)
2898
- let nextStatuses={...statusResult.data}
2972
+ let nextStatuses={...snapshot.statuses}
2899
2973
  const missing={}
2900
- const remoteStatuses=source?[]:await Promise.all(nextChildren.map((session)=>{const key=JSON.stringify([current.key,session.id])
2901
- return(remoteRetryAt.get(key)??0)<=now()?fetchDevTeamStatus(session,requestState.signal):{sessionID:session.id,failed:true,cooldown:true,status:undefined}}))
2974
+ const remoteStatuses=options.remoteStatus?await Promise.all(nextChildren.map(async (session)=>{const key=JSON.stringify([current.key,session.id])
2975
+ return(remoteRetryAt.get(key)??0)<=now()?options.remoteStatus(session,requestState.signal):{sessionID:session.id,failed:true,cooldown:true,status:undefined}})):[]
2902
2976
  if(!requestState.isCurrent())
2903
2977
  return
2904
2978
  for(const result of remoteStatuses){if(!result)
@@ -2924,17 +2998,17 @@ for(const mutation of journal.sessions)
2924
2998
  nextChildren=applySession(nextChildren,current.parentID,mutation)
2925
2999
  for(const mutation of journal.statuses)
2926
3000
  nextStatuses=applyStatus(nextStatuses,mutation)
2927
- if(requestState.isCurrent()){batch(()=>{for(const session of nextChildren){const mutations=journal.statuses.filter((mutation)=>mutation.sessionID===session.id)
3001
+ if(requestState.isCurrent()){batch2(()=>{for(const session of nextChildren){const mutations=journal.statuses.filter((mutation)=>mutation.sessionID===session.id)
2928
3002
  if(mutations.length>0){for(const mutation of mutations){if(!mutation.observed.has(current.key))
2929
- history.observeStatus(current.key,session.id,mutation.status,mutation.at)
3003
+ history.observeStatus(current.key,session.id,mutation.status,mutation.at,mutation.startedAt)
2930
3004
  }} else if(!missing[session.id]){const status=nextStatuses[session.id]??{type:"idle"}
2931
3005
  nextStatuses[session.id]=status
2932
- history.observeStatus(current.key,session.id,status)
3006
+ history.observeStatus(current.key,session.id,status,now(),source?.startedAt?.(session.id))
2933
3007
  }
2934
3008
  for(const mutation of journal.errors){if(mutation.sessionID===session.id&&!mutation.observed.has(current.key))
2935
3009
  history.observeError(current.key,session.id,mutation.error,mutation.at)
2936
3010
  }}
2937
- if(!source){setChildren((value)=>({...value,[current.key]:nextChildren}))
3011
+ if(options.cache==="model"){setChildren((value)=>({...value,[current.key]:nextChildren}))
2938
3012
  setStatuses((value)=>({...value,[current.key]:nextStatuses}))
2939
3013
  }
2940
3014
  setUnavailable((value)=>({...value,[current.key]:missing}))
@@ -2954,8 +3028,8 @@ return request
2954
3028
  function refresh(parentID,force=false){return refreshTarget(target(parentID),force)
2955
3029
  }
2956
3030
  function list(parentID){const current=target(parentID)
2957
- const currentStatuses=source?Object.fromEntries(source.list(parentID).map((item)=>[item.id,source.status(item.id)])):statuses()[current.key]??{}
2958
- return(snapshots()[current.key]??[]).map((session)=>({session,status:currentStatuses[session.id]??(unavailable()[current.key]?.[session.id]?undefined:api.state.session.status(session.id))
3031
+ const currentStatuses=options.cache==="source"?Object.fromEntries(source.list(parentID).map((item)=>[item.id,source.status(item.id)])):statuses()[current.key]??{}
3032
+ return(snapshots()[current.key]??[]).map((session)=>({session,status:currentStatuses[session.id]??(unavailable()[current.key]?.[session.id]?undefined:source.status(session.id))
2959
3033
  })).filter((item)=>activeStatus(item.status)).map((item)=>({...item,run:history.active(current.key,item.session.id),unavailable:unavailable()[current.key]?.[item.session.id]
2960
3034
  }))
2961
3035
  }
@@ -2965,14 +3039,25 @@ return history.recent(current.key).flatMap((run)=>{const session=sessions.get(ru
2965
3039
  return session?[{session,status:{type:"idle"},run,unavailable:false}]:[]
2966
3040
  })
2967
3041
  }
2968
- const unsubscribe=[api.event.on("session.created",(event)=>recordSession({type:"upsert",info:event.properties.info})),api.event.on("session.updated",(event)=>recordSession({type:"upsert",info:event.properties.info})),api.event.on("session.deleted",(event)=>{recordSession({type:"remove",sessionID:event.properties.sessionID})
2969
- recordStatus({sessionID:event.properties.sessionID,status:{type:"idle"}})
2970
- }),api.event.on("session.status",(event)=>recordStatus(event.properties)),api.event.on("session.error",(event)=>recordError(event.properties)),api.event.on("session.idle",(event)=>recordStatus({sessionID:event.properties.sessionID,status:{type:"idle"}})),api.event.on("server.connected",()=>{const current=activeTarget?targets.get(activeTarget):undefined
3042
+ const unsubscribe=options.subscribe((event)=>{switch(event.type){case "upsert":{recordSession(event)
3043
+ break
3044
+ }
3045
+ case "remove":{recordSession(event)
3046
+ recordStatus({sessionID:event.sessionID,status:{type:"idle"}})
3047
+ break
3048
+ }
3049
+ case "status":{recordStatus(event)
3050
+ break
3051
+ }
3052
+ case "error":{recordError(event)
3053
+ break
3054
+ }
3055
+ case "reconnect":{const current=activeTarget?targets.get(activeTarget):undefined
2971
3056
  if(current)
2972
- retryBackgroundRefresh(()=>refreshTarget(current),{signal:api.lifecycle.signal}).catch(()=>{})
2973
- })]
2974
- api.lifecycle.onDispose(()=>{for(const dispose of unsubscribe)
2975
- dispose()
3057
+ retryBackgroundRefresh(()=>refreshTarget(current),{signal:options.lifecycle.signal}).catch(()=>{})
3058
+ break
3059
+ }}})
3060
+ options.lifecycle.onDispose(()=>{unsubscribe()
2976
3061
  if(pollTimer)
2977
3062
  clearTimeout(pollTimer)
2978
3063
  })
@@ -2985,15 +3070,18 @@ clearTimeout(pollTimer)
2985
3070
  requests.abortAll()
2986
3071
  refreshing.clear()
2987
3072
  activeTarget=current.key
2988
- const poll=()=>{if(activeTarget!==current.key||api.lifecycle.signal.aborted)
3073
+ leases=new Set
3074
+ const poll=()=>{if(activeTarget!==current.key||options.lifecycle.signal.aborted)
2989
3075
  return
2990
3076
  refreshTarget(current).catch(()=>{})
2991
3077
  pollTimer=setTimeout(poll,POLL_INTERVAL_MS)
2992
3078
  }
2993
- if(!source)
3079
+ if(options.cache==="model")
2994
3080
  pollTimer=setTimeout(poll,POLL_INTERVAL_MS)
2995
3081
  }
2996
- return()=>{if(activeTarget!==current.key)
3082
+ const lease=Symbol(current.key)
3083
+ leases.add(lease)
3084
+ return()=>{if(activeTarget!==current.key||!leases.delete(lease)||leases.size>0)
2997
3085
  return
2998
3086
  activeTarget=undefined
2999
3087
  if(pollTimer)
@@ -3001,7 +3089,7 @@ clearTimeout(pollTimer)
3001
3089
  pollTimer=undefined
3002
3090
  requests.abortAll()
3003
3091
  refreshing.clear()
3004
- }},open(sessionID){api.route.navigate("session",{sessionID})
3092
+ }},open(sessionID){options.open(sessionID)
3005
3093
  }}}
3006
3094
  async function fetchDevTeamStatus(session,signal){const metadata=session.metadata
3007
3095
  const devTeam=metadata&&typeof metadata.devTeam==="object"?metadata.devTeam:undefined
@@ -3022,10 +3110,26 @@ return{sessionID:session.id,failed:true}
3022
3110
  const status=statuses[session.id]
3023
3111
  if(status!==undefined&&(!status||typeof status!=="object"||!("type" in status)||!["idle","busy","retry"].includes(String(status.type))))
3024
3112
  return{sessionID:session.id,failed:true}
3025
- if(status&&typeof status==="object"&&"type" in status&&status.type==="retry"&&(!("next" in status)||typeof status.next!=="number"||!Number.isFinite(status.next)||!("attempt" in status)||typeof status.attempt!=="number"||!("message" in status)||typeof status.message!=="string")){return{sessionID:session.id,failed:true}}
3113
+ if(status&&typeof status==="object"&&"type" in status&&status.type==="retry"&&(!("next" in status)||typeof status.next!=="number"||!Number.isFinite(status.next)||!("attempt" in status)||typeof status.attempt!=="number"||!("message" in status)||typeof status.message!=="string"))
3114
+ return{sessionID:session.id,failed:true}
3026
3115
  return{sessionID:session.id,status}} catch{if(signal.aborted)
3027
3116
  return
3028
3117
  return{sessionID:session.id,failed:true}}}
3118
+ function createSubagentController(api,options={}){const source=options.source
3119
+ return createSubagentModel({now:options.now,lifecycle:api.lifecycle,cache:source?"source":"model",source:source??{list:()=>[],status:(id)=>api.state.session.status(id)??{type:"idle"},refresh:async ()=>{}},target(parentID){const parent=api.state.session.get(parentID)
3120
+ const directory=parent?.directory??api.state.path.directory
3121
+ const workspace=parent?.workspaceID
3122
+ return{key:JSON.stringify([parentID,directory,workspace??null]),parentID,routing:{directory,...workspace&&{workspace}}}},async snapshot(target,signal){if(source){await source.refresh(target.parentID,signal)
3123
+ const sessions=source.list(target.parentID)
3124
+ return{sessions,statuses:Object.fromEntries(sessions.map((session)=>[session.id,source.status(session.id)]))
3125
+ }}
3126
+ const [children,statuses]=await Promise.all([api.client.session.children({sessionID:target.parentID,...target.routing},{throwOnError:true,signal}),api.client.session.status(target.routing,{throwOnError:true,signal})])
3127
+ return{sessions:children.data??[],statuses:statuses.data??{}}},remoteStatus:source?undefined:fetchDevTeamStatus,subscribe(listener){const unsubscribe=[api.event.on("session.created",(event)=>listener({type:"upsert",info:event.properties.info})),api.event.on("session.updated",(event)=>listener({type:"upsert",info:event.properties.info})),api.event.on("session.deleted",(event)=>listener({type:"remove",sessionID:event.properties.sessionID})),api.event.on("session.status",(event)=>listener({type:"status",...event.properties})),api.event.on("session.error",(event)=>listener({type:"error",...event.properties})),api.event.on("session.idle",(event)=>listener({type:"status",sessionID:event.properties.sessionID,status:{type:"idle"}})),api.event.on("server.connected",()=>listener({type:"reconnect"}))]
3128
+ return()=>{for(const dispose of unsubscribe)
3129
+ dispose()
3130
+ }},open:(sessionID)=>api.route.navigate("session",{sessionID})
3131
+ })
3132
+ }
3029
3133
 
3030
3134
  function filterSubagentView(rows,mode,query){const needle=query.trim().toLocaleLowerCase()
3031
3135
  return rows.filter((row)=>{if(mode==="active"&&row.group!=="Active")
@@ -3063,30 +3167,32 @@ onCleanup6(()=>clearInterval(timer))
3063
3167
  return now
3064
3168
  }
3065
3169
 
3066
- function createTodoController(api){const [sessions,setSessions]=createSignal10({})
3170
+ function createTodoModel(options){const [sessions,setSessions]=createSignal10({})
3067
3171
  const refreshing=new Map
3068
3172
  const revisions=new Map
3069
- const requests=createRequestState(api.lifecycle.signal)
3173
+ const requests=createRequestState(options.lifecycle.signal)
3070
3174
  let activeTarget
3071
- function target(sessionID){const session=api.state.session.get(sessionID)
3072
- const directory=session?.directory??api.state.path.directory
3073
- const workspace=session?.workspaceID
3074
- return{key:JSON.stringify([sessionID,directory,workspace??null]),sessionID,routing:{directory,...workspace&&{workspace}}}}
3175
+ let leases=new Set
3176
+ function target(sessionID){return options.target(sessionID)
3177
+ }
3075
3178
  function set(current,todos){setSessions((value)=>({...value,[current.key]:todos}))
3076
3179
  }
3077
3180
  function list(sessionID){const current=target(sessionID)
3078
- return sessions()[current.key]??api.state.session.todo(sessionID)
3181
+ return options.cache==="source"?options.source.list(sessionID):sessions()[current.key]??options.source.list(sessionID)
3079
3182
  }
3080
- async function refresh(sessionID,force=false){const current=target(sessionID)
3183
+ function refresh(sessionID,force=false){if(options.lifecycle.signal.aborted)
3184
+ return Promise.resolve([])
3185
+ const current=target(sessionID)
3081
3186
  const pending=refreshing.get(current.key)
3082
3187
  if(pending&&!force)
3083
3188
  return pending
3084
- const revision=revisions.get(sessionID)??0
3085
- const requestState=requests.start(current.key,"refresh todos",current.key in sessions(),force)
3189
+ const revision=revisions.get(current.key)??0
3190
+ const requestState=requests.start(current.key,"refresh todos",current.key in sessions()||Boolean(options.hasValue?.(sessionID)),force)
3086
3191
  if(!requestState)
3087
3192
  return pending
3088
- const request=api.client.session.todo({sessionID,...current.routing},{throwOnError:true,signal:requestState.signal}).then((result)=>{const todos=result.data??[]
3089
- if(requestState.isCurrent()&&(revisions.get(sessionID)??0)===revision)
3193
+ const request=Promise.try(()=>options.source.refresh(sessionID,requestState.signal)).then((todos)=>{if(!requestState.isCurrent())
3194
+ return []
3195
+ if(options.cache==="model"&&(revisions.get(current.key)??0)===revision)
3090
3196
  set(current,todos)
3091
3197
  requestState.succeed()
3092
3198
  return todos
@@ -3099,50 +3205,134 @@ refreshing.delete(current.key)
3099
3205
  refreshing.set(current.key,request)
3100
3206
  return request
3101
3207
  }
3102
- const unsubscribe=api.event.on("todo.updated",(event)=>{const sessionID=event.properties.sessionID
3103
- revisions.set(sessionID,(revisions.get(sessionID)??0) + 1)
3104
- set(target(sessionID),event.properties.todos)
3208
+ const unsubscribe=options.source.subscribe?.((sessionID,todos)=>{const current=target(sessionID)
3209
+ revisions.set(current.key,(revisions.get(current.key)??0) + 1)
3210
+ set(current,todos)
3211
+ })
3212
+ if(unsubscribe)
3213
+ options.lifecycle.onDispose(unsubscribe)
3214
+ return{target,list,refresh,state(sessionID){return requests.state(target(sessionID).key)
3215
+ },retry(sessionID){return refresh(sessionID,true)
3216
+ },activate(sessionID){const key=target(sessionID).key
3217
+ if(activeTarget!==key){requests.abortAll()
3218
+ refreshing.clear()
3219
+ activeTarget=key
3220
+ leases=new Set
3221
+ }
3222
+ const lease=Symbol(key)
3223
+ leases.add(lease)
3224
+ return()=>{if(activeTarget!==key||!leases.delete(lease)||leases.size>0)
3225
+ return
3226
+ activeTarget=undefined
3227
+ requests.abortAll()
3228
+ refreshing.clear()
3229
+ }}}}
3230
+ function createTodoController(api){const target=(sessionID)=>{const session=api.state.session.get(sessionID)
3231
+ const directory=session?.directory??api.state.path.directory
3232
+ const workspace=session?.workspaceID
3233
+ return{key:JSON.stringify([sessionID,directory,workspace??null]),sessionID,routing:{directory,...workspace&&{workspace}}}}
3234
+ return createTodoModel({lifecycle:api.lifecycle,target,cache:"model",source:{list:(sessionID)=>api.state.session.todo(sessionID),async refresh(sessionID,signal){const result=await api.client.session.todo({sessionID,...target(sessionID).routing},{throwOnError:true,signal})
3235
+ return result.data??[]
3236
+ },subscribe:(listener)=>api.event.on("todo.updated",(event)=>listener(event.properties.sessionID,event.properties.todos))
3237
+ }})
3238
+ }
3239
+
3240
+ function buildTodoView(todos,mode){const active=[...todos.filter((item)=>item.status==="in_progress"),...todos.filter((item)=>item.status==="pending")]
3241
+ const completed=todos.filter((item)=>item.status==="completed")
3242
+ const cancelled=todos.filter((item)=>item.status==="cancelled")
3243
+ const other=todos.filter((item)=>!["in_progress","pending","completed","cancelled"].includes(item.status))
3244
+ const groups=[{title:"Active",items:mode==="finished"?[]:active},{title:"Completed",items:mode==="active"?[]:completed},{title:"Cancelled",items:mode==="active"?[]:cancelled},{title:"Other",items:mode==="all"?other:[]}].filter((group)=>group.items.length)
3245
+ return{groups,rows:groups.flatMap((group)=>group.items.map((item)=>({item,group:group.title}))),counts:{all:todos.length,active:active.length,finished:completed.length + cancelled.length,completed:completed.length
3246
+ }}}
3247
+
3248
+
3249
+
3250
+
3251
+
3252
+ var HelpView=lazy2(async ()=>{const path=import.meta.url.includes("/dist/tui.js")?"./sidebar-shortcuts.js":"../../../sidebar-shortcuts"
3253
+ const module=await import(path)
3254
+ return{default:module.KeyboardHelpDialog
3255
+ }})
3256
+ function KeyboardHelpDialog(props){return(()=>{var _el$=_$createElement("box")
3257
+ _$insert(_el$,_$createComponent(Suspense2,{get children(){return _$createComponent(HelpView,props)
3258
+ }}))
3259
+ return _el$
3260
+ })()
3261
+ }
3262
+ function openKeyboardHelp(api,iconStyle=()=>"nerd",multilineCorners=()=>true,shortcuts=[]){createDialogStack(api,iconStyle,()=>true,multilineCorners).open(()=>_$createComponent(KeyboardHelpDialog,{api,shortcuts
3263
+ }))
3264
+ }
3265
+
3266
+
3267
+
3268
+
3269
+
3270
+
3271
+
3272
+
3273
+
3274
+
3275
+
3276
+ var QUICK_ACTION_METADATA=[["\uEA73","Rename"],["\uEA82","Timeline"],["\uEBCC","Copy transcript"],["\uEBAC","Export"],["\uEAF5","Compact"],["\uEBEB","Switch session"],["\uEA60","New session"],["\uEB9E","Fork session"],["\uEBCC","Copy last response"],["\uEAA1","First message"],["\uEA9A","Last message"],["\uEA82","Last user message"],["\uEA9A","Next message"],["\uEAA1","Previous message"],["\uEBB3","Auto-approve permissions"]]
3277
+ var QUICK_ACTIONS=QUICK_ACTION_IDS.map((command,index)=>({command,icon:QUICK_ACTION_METADATA[index][0],label:QUICK_ACTION_METADATA[index][1]
3278
+ }))
3279
+ function quickActionRequiresSession(command){return command!=="session.list"&&command!=="session.new"&&command!=="permission.mode"
3280
+ }
3281
+ function orderQuickActionIds(order,favorites){return [...order.filter((id)=>favorites.has(id)),...order.filter((id)=>!favorites.has(id))]
3282
+ }
3283
+ var SEARCH_GROUPS=["Skills","Subagents","MCP","Actions"]
3284
+ var SEARCH_SECTIONS={Skills:"skills",Subagents:"subagents",MCP:"mcp",Actions:"quick_actions"
3285
+ }
3286
+ function buildSearchCandidates(input){const recent=new Map(input.recentSkills.map((location,index)=>[location,index]))
3287
+ const skills=[...input.skills].sort((a,b)=>Number(input.favoriteSkills.has(b.location))-Number(input.favoriteSkills.has(a.location))||(recent.get(a.location)??Infinity)-(recent.get(b.location)??Infinity)||a.name.localeCompare(b.name))
3288
+ const mcp=[...input.mcp].sort((a,b)=>Number(input.favoriteMcp.has(b.name))-Number(input.favoriteMcp.has(a.name))||a.name.localeCompare(b.name))
3289
+ const results=[...skills.map((skill)=>({id:`skill:${skill.location || skill.name}`,group:"Skills",title:skill.name,description:[input.skillGroups?.[skill.location],skill.description?.trim()||skill.location].filter(Boolean).join(" \xB7 "),keywords:`${skill.location} ${input.skillGroups?.[skill.location] ?? ""}`,skill
3290
+ })),...input.subagents.map((item)=>({id:`subagent:${item.session.id}`,group:"Subagents",title:item.session.title,description:[item.run?.outcome??item.status.type,item.run?.errorMessage].filter(Boolean).join(" \xB7 "),keywords:item.session.id,sessionID:item.session.id
3291
+ })),...mcp.map((item)=>({id:`mcp:${item.name}`,group:"MCP",title:item.name,description:[input.mcpGroups?.[item.name],mcpToggleAction2(item.status)==="disconnect"?"Disconnect":"Connect",item.status,input.mcpErrorMode==="details"?undefined:item.error].filter(Boolean).join(" \xB7 "),keywords:input.mcpGroups?.[item.name],disabled:input.mcpBusy||!mcpToggleAction2(item.status)?"MCP operation in progress":undefined,server:item.name,...input.mcpErrorMode==="details"&&item.error&&{information:{title:`MCP \xB7 ${item.name}`,body:`Status: ${item.status}
3292
+
3293
+ ${item.error}`}}})),...orderQuickActionIds(input.actionOrder,input.favoriteQuickActions??new Set).flatMap((id)=>{const action=QUICK_ACTIONS.find((item)=>item.command===id)
3294
+ return action?[{id:`action:${id}`,group:"Actions",title:input.actionLabel?.(action)??action.label,description:id,command:id,disabled:input.actionDisabledReason?.(action)??(quickActionRequiresSession(action.command)&&!input.hasSession?"Requires an open session":undefined)
3295
+ }]:[]
3296
+ })]
3297
+ return [...new Map(results.map((item)=>[item.id,item])).values()]
3298
+ }
3299
+ function fuzzyScore(needle,value){const text=value.toLocaleLowerCase()
3300
+ const position=text.indexOf(needle)
3301
+ if(text===needle)
3302
+ return 2000
3303
+ if(position===0)
3304
+ return 1500
3305
+ if(position!==-1)
3306
+ return 1000-position
3307
+ let cursor=0
3308
+ let gaps=0
3309
+ for(const char of needle){const index=text.indexOf(char,cursor)
3310
+ if(index===-1)
3311
+ return -1
3312
+ gaps += index-cursor
3313
+ cursor=index + char.length
3314
+ }
3315
+ return Math.max(0,500-gaps)
3316
+ }
3317
+ function searchResults(candidates,query){const terms=query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)
3318
+ const scored=candidates.flatMap((item,index)=>{let score=0
3319
+ for(const term of terms){const title=fuzzyScore(term,item.title)
3320
+ const detail=fuzzyScore(term,`${item.description} ${item.keywords ?? ""}`)
3321
+ if(title<0&&detail<0)
3322
+ return []
3323
+ score += Math.max(title>=0?title:-Infinity,detail>=0?detail-250:-Infinity)
3324
+ }
3325
+ return [{item,score,index}]
3105
3326
  })
3106
- api.lifecycle.onDispose(unsubscribe)
3107
- return{target,list,refresh,state(sessionID){return requests.state(target(sessionID).key)
3108
- },retry(sessionID){return refresh(sessionID,true)
3109
- },activate(sessionID){const key=target(sessionID).key
3110
- if(activeTarget!==key){requests.abortAll()
3111
- refreshing.clear()
3112
- activeTarget=key
3327
+ scored.sort((a,b)=>SEARCH_GROUPS.indexOf(a.item.group)-SEARCH_GROUPS.indexOf(b.item.group)||b.score-a.score||a.index-b.index)
3328
+ return scored.map(({item})=>item)
3113
3329
  }
3114
- return()=>{if(activeTarget!==key)
3115
- return
3116
- activeTarget=undefined
3117
- requests.abortAll()
3118
- refreshing.clear()
3119
- }}}}
3120
-
3121
- function buildTodoView(todos,mode){const active=[...todos.filter((item)=>item.status==="in_progress"),...todos.filter((item)=>item.status==="pending")]
3122
- const completed=todos.filter((item)=>item.status==="completed")
3123
- const cancelled=todos.filter((item)=>item.status==="cancelled")
3124
- const other=todos.filter((item)=>!["in_progress","pending","completed","cancelled"].includes(item.status))
3125
- const groups=[{title:"Active",items:mode==="finished"?[]:active},{title:"Completed",items:mode==="active"?[]:completed},{title:"Cancelled",items:mode==="active"?[]:cancelled},{title:"Other",items:mode==="all"?other:[]}].filter((group)=>group.items.length)
3126
- return{groups,rows:groups.flatMap((group)=>group.items.map((item)=>({item,group:group.title}))),counts:{all:todos.length,active:active.length,finished:completed.length + cancelled.length,completed:completed.length
3127
- }}}
3128
-
3129
-
3130
-
3330
+ function searchContext(api){const route=api.route.current
3331
+ const sessionID=route.name==="session"&&"params" in route&&typeof route.params?.sessionID==="string"?route.params.sessionID:undefined
3332
+ const location=currentLocation(api)
3333
+ return{sessionID,location,key:JSON.stringify([route.name,sessionID??null,location.key])}}
3131
3334
 
3132
3335
 
3133
- var HelpView=lazy2(async ()=>{const path=import.meta.url.includes("/dist/tui.js")?"./sidebar-shortcuts.js":"../../../sidebar-shortcuts"
3134
- const module=await import(path)
3135
- return{default:module.KeyboardHelpDialog
3136
- }})
3137
- function KeyboardHelpDialog(props){return(()=>{var _el$=_$createElement("box")
3138
- _$insert(_el$,_$createComponent(Suspense2,{get children(){return _$createComponent(HelpView,props)
3139
- }}))
3140
- return _el$
3141
- })()
3142
- }
3143
- function openKeyboardHelp(api,iconStyle=()=>"nerd",multilineCorners=()=>true,shortcuts=[]){createDialogStack(api,iconStyle,()=>true,multilineCorners).open(()=>_$createComponent(KeyboardHelpDialog,{api,shortcuts
3144
- }))
3145
- }
3146
3336
 
3147
3337
 
3148
3338
 
@@ -3170,11 +3360,6 @@ function supportsTitleVisibility(api){return hostCapabilities(api).titleVisibili
3170
3360
  }
3171
3361
  function supportsPermissionMode(api){return hostCapabilities(api).permissionMode!==false
3172
3362
  }
3173
- var QUICK_ACTION_METADATA=[["\uEA73","Rename"],["\uEA82","Timeline"],["\uEBCC","Copy transcript"],["\uEBAC","Export"],["\uEAF5","Compact"],["\uEBEB","Switch session"],["\uEA60","New session"],["\uEB9E","Fork session"],["\uEBCC","Copy last response"],["\uEAA1","First message"],["\uEA9A","Last message"],["\uEA82","Last user message"],["\uEA9A","Next message"],["\uEAA1","Previous message"],["\uEBB3","Auto-approve permissions"]]
3174
- var QUICK_ACTIONS=QUICK_ACTION_IDS.map((command,index)=>({command,icon:QUICK_ACTION_METADATA[index][0],label:QUICK_ACTION_METADATA[index][1]
3175
- }))
3176
- function quickActionRequiresSession(command){return command!=="session.list"&&command!=="session.new"&&command!=="permission.mode"
3177
- }
3178
3363
  function quickActionLabel(api,action){if(action.command!=="permission.mode")
3179
3364
  return action.label
3180
3365
  const command=api.keymap.getCommands?.({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
@@ -3186,8 +3371,6 @@ if(unsubscribe)
3186
3371
  onCleanup8(unsubscribe)
3187
3372
  return revision
3188
3373
  }
3189
- function orderQuickActionIds(order,favorites){return [...order.filter((id)=>favorites.has(id)),...order.filter((id)=>!favorites.has(id))]
3190
- }
3191
3374
  function quickActionDisabledReason(api,action){const route=api.route.current
3192
3375
  if(quickActionRequiresSession(action.command)&&route.name!=="session")
3193
3376
  return "Requires an open session"
@@ -3225,69 +3408,6 @@ return mode?"on":"off"
3225
3408
  }
3226
3409
  return sources.get(api)?.(id)
3227
3410
  }
3228
- var SEARCH_GROUPS=["Skills","Subagents","MCP","Actions"]
3229
- var SEARCH_SECTIONS={Skills:"skills",Subagents:"subagents",MCP:"mcp",Actions:"quick_actions"
3230
- }
3231
- function searchContext(api){const route=api.route.current
3232
- const sessionID=route.name==="session"&&"params" in route&&typeof route.params?.sessionID==="string"?route.params.sessionID:undefined
3233
- const location=currentLocation(api)
3234
- return{sessionID,location,key:JSON.stringify([route.name,sessionID??null,location.key])}}
3235
- function buildSearchCandidates(input){const recent=new Map(input.recentSkills.map((location,index)=>[location,index]))
3236
- const skills=[...input.skills].sort((a,b)=>Number(input.favoriteSkills.has(b.location))-Number(input.favoriteSkills.has(a.location))||(recent.get(a.location)??Infinity)-(recent.get(b.location)??Infinity)||a.name.localeCompare(b.name))
3237
- const mcp=[...input.mcp].sort((a,b)=>Number(input.favoriteMcp.has(b.name))-Number(input.favoriteMcp.has(a.name))||a.name.localeCompare(b.name))
3238
- const results=[...skills.map((skill)=>({id:`skill:${skill.location || skill.name}`,group:"Skills",title:skill.name,description:[input.skillGroups?.[skill.location],skill.description?.trim()||skill.location].filter(Boolean).join(" \xB7 "),keywords:`${skill.location} ${input.skillGroups?.[skill.location] ?? ""}`,skill
3239
- })),...input.subagents.map((item)=>({id:`subagent:${item.session.id}`,group:"Subagents",title:item.session.title,description:[item.run?.outcome??item.status.type,item.run?.errorMessage].filter(Boolean).join(" \xB7 "),keywords:item.session.id,sessionID:item.session.id
3240
- })),...mcp.map((item)=>({id:`mcp:${item.name}`,group:"MCP",title:item.name,description:[input.mcpGroups?.[item.name],mcpToggleAction2(item.status)==="disconnect"?"Disconnect":"Connect",item.status,item.error].filter(Boolean).join(" \xB7 "),keywords:input.mcpGroups?.[item.name],disabled:input.mcpBusy||!mcpToggleAction2(item.status)?"MCP operation in progress":undefined,server:item.name
3241
- })),...orderQuickActionIds(input.actionOrder,input.favoriteQuickActions??new Set).flatMap((id)=>{const action=QUICK_ACTIONS.find((item)=>item.command===id)
3242
- return action?[{id:`action:${id}`,group:"Actions",title:input.actionLabel?.(action)??action.label,description:id,command:id,disabled:input.actionDisabledReason?.(action)??(quickActionRequiresSession(action.command)&&!input.hasSession?"Requires an open session":undefined)
3243
- }]:[]
3244
- })]
3245
- return [...new Map(results.map((item)=>[item.id,item])).values()]
3246
- }
3247
- function fuzzyScore(needle,value){const text=value.toLocaleLowerCase()
3248
- const position=text.indexOf(needle)
3249
- if(text===needle)
3250
- return 2000
3251
- if(position===0)
3252
- return 1500
3253
- if(position!==-1)
3254
- return 1000-position
3255
- let cursor=0
3256
- let gaps=0
3257
- for(const char of needle){const index=text.indexOf(char,cursor)
3258
- if(index===-1)
3259
- return -1
3260
- gaps += index-cursor
3261
- cursor=index + char.length
3262
- }
3263
- return Math.max(0,500-gaps)
3264
- }
3265
- function searchResults(candidates,query){const terms=query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)
3266
- const scored=candidates.flatMap((item,index)=>{let score=0
3267
- for(const term of terms){const title=fuzzyScore(term,item.title)
3268
- const detail=fuzzyScore(term,`${item.description} ${item.keywords ?? ""}`)
3269
- if(title<0&&detail<0)
3270
- return []
3271
- score += Math.max(title>=0?title:-Infinity,detail>=0?detail-250:-Infinity)
3272
- }
3273
- return [{item,score,index}]
3274
- })
3275
- scored.sort((a,b)=>SEARCH_GROUPS.indexOf(a.item.group)-SEARCH_GROUPS.indexOf(b.item.group)||b.score-a.score||a.index-b.index)
3276
- return scored.map(({item})=>item)
3277
- }
3278
-
3279
-
3280
-
3281
-
3282
-
3283
-
3284
-
3285
-
3286
-
3287
-
3288
-
3289
-
3290
-
3291
3411
  function mixColor(from,to,amount){return RGBA3.fromValues(from.r + (to.r-from.r)*amount,from.g + (to.g-from.g)*amount,from.b + (to.b-from.b)*amount)
3292
3412
  }
3293
3413
  function createSearchDialogController(props){const context=searchContext(props.api)
@@ -3373,7 +3493,7 @@ function selectTab(tab){if(tab!==activeTab()){tabViews.set(activeTab(),{selected
3373
3493
  })
3374
3494
  const saved=tabViews.get(tab)
3375
3495
  pendingScroll=saved?.scrollTop??0
3376
- batch2(()=>{setActiveTab(tab)
3496
+ batch3(()=>{setActiveTab(tab)
3377
3497
  setSelectedID(saved?.selectedID)
3378
3498
  })
3379
3499
  }
@@ -3678,14 +3798,17 @@ clearTimeout(timer)
3678
3798
  return _$createElement("box")
3679
3799
  }
3680
3800
 
3681
- function createMcpPresetPreview(controller,preferences,name){const target=controller.target()
3801
+ var alwaysCurrent=()=>true
3802
+ function createMcpPresetPreview(controller,preferences,name,isCurrent=alwaysCurrent){const target=controller.target()
3682
3803
  const valid=()=>{const current=controller.target()
3683
- return current.key===target.key&&current.scope===target.scope
3804
+ return isCurrent()&&current.key===target.key&&current.scope===target.scope
3684
3805
  }
3685
3806
  const preset=createMemo2(()=>preferences.mcpPresets()[name])
3686
- const preview=createMemo2(()=>mcpPresetPreview(controller.list(target),preset()??{}))
3807
+ const preview=createMemo2(()=>mcpPresetPreview(valid()?controller.list(target):[],preset()??{}))
3687
3808
  const state=createMemo2(()=>controller.state(target))
3688
- const blocked=createMemo2(()=>{if(!preset())
3809
+ const blocked=createMemo2(()=>{if(!valid())
3810
+ return "Session or server changed; reopen the preview."
3811
+ if(!preset())
3689
3812
  return "Preset no longer exists."
3690
3813
  if(controller.mutating(target))
3691
3814
  return "Wait for the current MCP operation to finish."
@@ -4375,7 +4498,7 @@ return _$createElement("box")
4375
4498
 
4376
4499
  function UpgradeNotesBinding(props){let pending
4377
4500
  let disposed=false
4378
- onMount6(async ()=>{const previous=await props.preferences.claimNavigatorUpgrade(props.version)
4501
+ async function load(){const previous=await props.preferences.claimNavigatorUpgrade(props.version)
4379
4502
  if(!previous||disposed)
4380
4503
  return
4381
4504
  try{let show=function(){if(disposed||props.api.lifecycle.signal?.aborted)
@@ -4395,7 +4518,8 @@ const entries=changesSince(await (props.read??readBundledChangelog)(),previous,p
4395
4518
  if(entries.length===0||disposed)
4396
4519
  return
4397
4520
  show()
4398
- } catch{}})
4521
+ } catch{}}
4522
+ onMount6(()=>void load().catch(()=>{}))
4399
4523
  onCleanup16(()=>{disposed=true
4400
4524
  clearTimeout(pending)
4401
4525
  })
@@ -5606,6 +5730,8 @@ return _$createComponent(SectionWithHeaderAction,{get api(){return props.api
5606
5730
 
5607
5731
 
5608
5732
 
5733
+ var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"]);
5734
+
5609
5735
 
5610
5736
 
5611
5737
 
@@ -5663,6 +5789,36 @@ return _$createComponent(SectionWithHeaderAction,{get api(){return props.api
5663
5789
 
5664
5790
 
5665
5791
 
5792
+
5793
+ var scheduleInterval=(callback,milliseconds)=>{const timer=setInterval(callback,milliseconds)
5794
+ return()=>clearInterval(timer)
5795
+ }
5796
+ function createLimitsRefresh(options,schedule=scheduleInterval){let disposed=false
5797
+ const selectedKey=createMemo12(()=>JSON.stringify(options.selectedModel?.()))
5798
+ const targetKey=createMemo12(()=>JSON.stringify(options.controller.current().model))
5799
+ const interval=createMemo12(()=>(parseLimitsRefreshMinutes(options.refreshMinutes?.())??DEFAULT_LIMITS_REFRESH_MINUTES)*60000)
5800
+ createEffect16(on2(selectedKey,()=>{const model=options.selectedModel?.()
5801
+ if(model&&options.accountSource)
5802
+ options.accountSource.refresh(model).catch(()=>{})
5803
+ }))
5804
+ createEffect16(on2(targetKey,()=>{const{model,adapter}=options.controller.current()
5805
+ if(model&&adapter)
5806
+ options.controller.refresh()
5807
+ }))
5808
+ async function refresh(){if(disposed)
5809
+ return
5810
+ const target=targetKey()
5811
+ const model=options.selectedModel?.()
5812
+ if(model&&options.accountSource)
5813
+ await options.accountSource.refresh(model)
5814
+ if(!disposed&&target===targetKey())
5815
+ await options.controller.refresh()
5816
+ }
5817
+ createEffect16(on2(interval,(milliseconds)=>{onCleanup20(schedule(()=>void refresh().catch(()=>{}),milliseconds))
5818
+ }))
5819
+ onCleanup20(()=>{disposed=true
5820
+ })
5821
+ }
5666
5822
 
5667
5823
 
5668
5824
 
@@ -5716,12 +5872,12 @@ function QuickActionRow(props){const{api,interaction
5716
5872
  const icons=useIcons()
5717
5873
  const toolbar=useToolbar()
5718
5874
  const theme=()=>api.theme.current
5719
- const [width,setWidth]=createSignal33(0)
5720
- const shortcut=createMemo12(()=>{const bindings=api.keymap.getCommandBindings({visibility:"registered",commands:[props.action.command]
5875
+ const [width,setWidth]=createSignal34(0)
5876
+ const shortcut=createMemo13(()=>{const bindings=api.keymap.getCommandBindings({visibility:"registered",commands:[props.action.command]
5721
5877
  })
5722
5878
  return api.keys.formatBindings(bindings.get(props.action.command))
5723
5879
  })
5724
- const visibleShortcut=createMemo12(()=>{const value=shortcut()
5880
+ const visibleShortcut=createMemo13(()=>{const value=shortcut()
5725
5881
  return value&&width()>=Bun.stringWidth(props.action.label) + Bun.stringWidth(value) + (toolbar?11:8)?value:undefined
5726
5882
  })
5727
5883
  function run(){if(props.action.command==="session.new"&&props.onNewSession?.())
@@ -5785,7 +5941,7 @@ return _el$4
5785
5941
  }})
5786
5942
  }
5787
5943
  function QuickActionsSection(props){const commandRevision=createQuickActionRevision(props.api)
5788
- const actions=createMemo13(()=>{commandRevision()
5944
+ const actions=createMemo14(()=>{commandRevision()
5789
5945
  return orderQuickActionIds(props.preferences.quickActionOrder?.()??QUICK_ACTIONS.map((action)=>action.command),props.preferences.favoriteQuickActions?.()??new Set).flatMap((id)=>{const action=QUICK_ACTIONS.find((candidate)=>candidate.command===id)
5790
5946
  return action&&props.preferences.quickActionVisible?.(id)!==false?[{...action,label:quickActionLabel(props.api,action),disabled:quickActionDisabledReason(props.api,action)
5791
5947
  }]:[]
@@ -5869,13 +6025,13 @@ item.activate(event)
5869
6025
 
5870
6026
 
5871
6027
  function SidebarToggleBinding(props){const shortcuts=createSidebarShortcutMode(props.api)
5872
- createEffect16(()=>{const key=props.preferences.toggleKey()
6028
+ createEffect17(()=>{const key=props.preferences.toggleKey()
5873
6029
  untrack6(()=>shortcuts.bind(key))
5874
6030
  })
5875
- createEffect16(()=>{if(shortcuts.active()&&(props.api.route.current.name!=="session"||props.api.ui.dialog.open||props.api.mode.current()==="modal"))
6031
+ createEffect17(()=>{if(shortcuts.active()&&(props.api.route.current.name!=="session"||props.api.ui.dialog.open||props.api.mode.current()==="modal"))
5876
6032
  untrack6(shortcuts.cancel)
5877
6033
  })
5878
- onCleanup20(shortcuts.dispose)
6034
+ onCleanup21(shortcuts.dispose)
5879
6035
  return(()=>{var _el$=_$createElement("box")
5880
6036
  _$setProp(_el$,"position","absolute")
5881
6037
  _$setProp(_el$,"top",0)
@@ -5897,11 +6053,11 @@ const unregisterCommands=props.api.keymap.registerLayer({commands:[...commands,.
5897
6053
  })=>({name:name.replace(PLUGIN_ID,()=>LEGACY_PLUGIN_ID),run,enabled
5898
6054
  }))]
5899
6055
  })
5900
- onCleanup20(unregisterCommands)
5901
- createEffect16(()=>{const unregister=props.api.keymap.registerLayer({bindings:[{key:props.preferences.focusKey(),cmd:FOCUS_COMMAND
6056
+ onCleanup21(unregisterCommands)
6057
+ createEffect17(()=>{const unregister=props.api.keymap.registerLayer({bindings:[{key:props.preferences.focusKey(),cmd:FOCUS_COMMAND
5902
6058
  }]
5903
6059
  })
5904
- onCleanup20(unregister)
6060
+ onCleanup21(unregister)
5905
6061
  })
5906
6062
  return _$createElement("box")
5907
6063
  }
@@ -5953,6 +6109,26 @@ rows.push(visible)
5953
6109
  }
5954
6110
  return{columns,overflow,rows,...widths&&{sizes}}}
5955
6111
 
6112
+ var PRIVATE_KEYS=new Set(["favoriteSkills","favoriteMcpServers","favoriteQuickActions","skillGroups","recentSkills","recentQuickActions","skippedSkillConfirmations","onboardingCompleted","codexAccountBindings","pendingResetAttempt","lastNavigatorVersion","previousNavigatorVersion","worktrees","user"]);
6113
+ function createPinnedToolbarModel(ports){function view(pin){const action=pin.kind==="action"?ports.action(pin.id):undefined
6114
+ const skill=pin.kind==="skill"?ports.skill(pin):undefined
6115
+ return{label:action?.label??(pin.kind==="skill"?pin.name:pin.id),stateLabel:action?.stateLabel,icons:toolbarPinIcons(pin,action?.state),text:pin.text,reason:action?.reason??skill?.reason
6116
+ }}
6117
+ function run(key){const pin=ports.pins().find((item)=>toolbarPinKey(item)===key)
6118
+ if(!pin)
6119
+ return
6120
+ const item=view(pin)
6121
+ if(item.reason){ports.unavailable(item.label,item.reason)
6122
+ return
6123
+ }
6124
+ if(pin.kind==="action"){ports.runAction(pin.id)
6125
+ return
6126
+ }
6127
+ const skill=ports.skill(pin).skill
6128
+ if(skill)
6129
+ ports.runSkill(skill)
6130
+ }
6131
+ return{pins:ports.pins,view,run}}
5956
6132
  function toolbarPinWidth(pin){const states=Object.keys({...pin.stateIcons,...pin.stateIconLists})
5957
6133
  const count=Math.max(toolbarPinIcons(pin).length,...states.map((state)=>toolbarPinIcons(pin,state).length))
5958
6134
  const iconsWidth=Math.max(0,count*2-1)
@@ -5961,49 +6137,34 @@ return 2 + Math.max(1,iconsWidth + (count&&pin.text?1:0) + Bun.stringWidth(pin.t
5961
6137
  function createToolbarModel(props){const icons=useIcons()
5962
6138
  const revision=createQuickActionRevision(props.api)
5963
6139
  const pins=()=>props.preferences.toolbarPins?.()??[]
5964
- const needsSkills=createMemo14(()=>pins().some((pin)=>pin.kind==="skill"))
5965
- const target=createMemo14(()=>props.skills.target())
5966
- createEffect17(()=>{if(!needsSkills())
6140
+ const needsSkills=createMemo15(()=>pins().some((pin)=>pin.kind==="skill"))
6141
+ const target=createMemo15(()=>props.skills.target())
6142
+ createEffect18(()=>{if(!needsSkills())
5967
6143
  return
5968
6144
  const current=target()
5969
6145
  const release=untrack7(()=>props.skills.activate(current))
5970
- onCleanup21(release)
6146
+ onCleanup22(release)
5971
6147
  untrack7(()=>{props.skills.refresh(current).catch(()=>{})
5972
6148
  })
5973
6149
  })
5974
- function view(pin){revision()
5975
- if(pin.kind==="action"){const action=QUICK_ACTIONS.find((item)=>item.command===pin.id)
5976
- const state=quickActionState(props.api,pin.id)
5977
- const stateLabel=quickActionStates(props.api,pin.id).find((item)=>item.id===state)?.label
5978
- return{label:quickActionLabel(props.api,action),stateLabel,icons:toolbarPinIcons(pin,state),text:pin.text,reason:quickActionDisabledReason(props.api,action)
5979
- }}
5980
- const skill=props.skills.list(target()).find((item)=>item.location===pin.location&&item.name===pin.name)
6150
+ const model=createPinnedToolbarModel({pins,action(id){revision()
6151
+ const action=QUICK_ACTIONS.find((item)=>item.command===id)
6152
+ const state=quickActionState(props.api,id)
6153
+ return{label:quickActionLabel(props.api,action),state,stateLabel:quickActionStates(props.api,id).find((item)=>item.id===state)?.label,reason:quickActionDisabledReason(props.api,action)
6154
+ }},skill(pin){const skill=props.skills.list(target()).find((item)=>item.location===pin.location&&item.name===pin.name)
5981
6155
  const request=props.skills.state(target())
5982
6156
  const unavailable=request.status==="loading"?"Loading Skills":"Skill is unavailable in this workspace"
5983
- return{label:pin.name,icons:toolbarPinIcons(pin),text:pin.text,stateLabel:undefined,reason:request.error?.message??(skill?skillUnavailable(props.skills,skill):unavailable)
5984
- }}
5985
- function run(key){const pin=pins().find((item)=>toolbarPinKey(item)===key)
5986
- if(!pin)
5987
- return
5988
- const item=view(pin)
5989
- if(item.reason){props.api.ui.toast({variant:"warning",title:item.label,message:item.reason})
5990
- return
5991
- }
5992
- if(pin.kind==="action"){if(pin.id==="session.new"&&props.onNewSession?.())
6157
+ return{skill,reason:request.error?.message??(skill?skillUnavailable(props.skills,skill):unavailable)}},unavailable:(title,message)=>props.api.ui.toast({variant:"warning",title,message}),runAction(id){if(id==="session.new"&&props.onNewSession?.())
5993
6158
  return
5994
- const result=props.interaction.dispatchFromReturnTarget(pin.id)
6159
+ const result=props.interaction.dispatchFromReturnTarget(id)
5995
6160
  if(!result.ok)
5996
- props.api.ui.toast({variant:"warning",title:item.label,message:`Command is ${result.reason}`})
5997
- return
5998
- }
5999
- const skill=props.skills.list(target()).find((candidate)=>candidate.location===pin.location&&candidate.name===pin.name)
6000
- if(!skill)
6001
- return
6002
- const route=JSON.stringify(props.api.route.current)
6161
+ props.api.ui.toast({variant:"warning",title:quickActionLabel(props.api,QUICK_ACTIONS.find((item)=>item.command===id)),message:`Command is ${result.reason}`
6162
+ })
6163
+ },runSkill(skill){const route=JSON.stringify(props.api.route.current)
6003
6164
  const dialogs=createDialogStack(props.api,icons.style,()=>route===JSON.stringify(props.api.route.current),icons.multilineCorners)
6004
6165
  activateSkill({api:props.api,controller:props.skills,access:props.preferences,skill,dialogs})
6005
- }
6006
- return{pins,view,run,width:toolbarPinWidth}}
6166
+ }})
6167
+ return{...model,width:toolbarPinWidth}}
6007
6168
 
6008
6169
 
6009
6170
 
@@ -6013,13 +6174,13 @@ return{pins,view,run,width:toolbarPinWidth}}
6013
6174
 
6014
6175
  function ToolbarButton(props){const icons=useIcons()
6015
6176
  const foreground=()=>props.reason?props.api.theme.current.textMuted:item.foregroundColor()
6016
- const [hovered,setHovered]=createSignal34(false)
6177
+ const [hovered,setHovered]=createSignal35(false)
6017
6178
  const item=useSidebarItem(props.api,props.interaction,{id:props.id,position:()=>({section:0,row:props.row + 1,column:props.column
6018
6179
  }),activate:props.onRun
6019
6180
  },()=>props.reason?props.api.theme.current.textMuted:props.api.theme.current.accent,"control")
6020
6181
  const content=()=>[props.icons.map((icon)=>terminalIcon(icon,icons.style())).join(" "),props.text].filter(Boolean).join(" ")
6021
- createEffect18(()=>props.onDetails(props.id,item.focused()||hovered()))
6022
- onCleanup22(()=>props.onDetails(props.id,false))
6182
+ createEffect19(()=>props.onDetails(props.id,item.focused()||hovered()))
6183
+ onCleanup23(()=>props.onDetails(props.id,false))
6023
6184
  return _$createComponent(Show27,{get when(){return _$memo(()=>!!(props.width===3&&props.icons.length===1))()&&!props.text
6024
6185
  },get fallback(){return _$createComponent(SelectionBox,{get id(){return props.id
6025
6186
  },ref(r$){var _ref$2=item.ref
@@ -6060,17 +6221,17 @@ setHovered(false)
6060
6221
  }
6061
6222
  function SidebarToolbar(props){const model=createToolbarModel(props)
6062
6223
  const icons=useIcons()
6063
- const [width,setWidth]=createSignal35(3)
6064
- const [detail,setDetail]=createSignal35()
6065
- const keys=createMemo15(()=>model.pins().map((pin)=>toolbarPinKey(pin)))
6066
- const widths=createMemo15(()=>new Map(model.pins().map((pin)=>[toolbarPinKey(pin),model.width(pin)])))
6067
- const layout=createMemo15(()=>toolbarLayout(keys(),width(),props.preferences.toolbarOverflow?.()??"menu",widths()))
6068
- const rows=createMemo15(()=>layout().rows.map((_,index)=>index))
6069
- const byKey=createMemo15(()=>new Map(model.pins().map((pin)=>[toolbarPinKey(pin),pin])))
6224
+ const [width,setWidth]=createSignal36(3)
6225
+ const [detail,setDetail]=createSignal36()
6226
+ const keys=createMemo16(()=>model.pins().map((pin)=>toolbarPinKey(pin)))
6227
+ const widths=createMemo16(()=>new Map(model.pins().map((pin)=>[toolbarPinKey(pin),model.width(pin)])))
6228
+ const layout=createMemo16(()=>toolbarLayout(keys(),width(),props.preferences.toolbarOverflow?.()??"menu",widths()))
6229
+ const rows=createMemo16(()=>layout().rows.map((_,index)=>index))
6230
+ const byKey=createMemo16(()=>new Map(model.pins().map((pin)=>[toolbarPinKey(pin),pin])))
6070
6231
  const view=(key)=>{const pin=byKey().get(key)
6071
6232
  return pin&&model.view(pin)
6072
6233
  }
6073
- const details=createMemo15(()=>{if(detail()==="toolbar:more")
6234
+ const details=createMemo16(()=>{if(detail()==="toolbar:more")
6074
6235
  return "More pinned actions and Skills"
6075
6236
  const item=view(detail()??"")
6076
6237
  return item?[item.label,item.stateLabel,item.reason].filter(Boolean).join(" \xB7 "):undefined
@@ -6143,18 +6304,18 @@ return _el$
6143
6304
 
6144
6305
 
6145
6306
 
6146
- function createSkillSectionModel(props,query){const target=createMemo16(()=>props.controller.target())
6147
- const recent=createMemo16(()=>new Map((props.preferences.recentSkills?.()??[]).map((location,index)=>[location,index])))
6148
- const groups=createMemo16(()=>props.preferences.skillGroups?.()??{})
6149
- const grouped=createMemo16(()=>Object.keys(groups()).length>0)
6150
- const list=createMemo16(()=>buildSkillGroupedView(props.controller.list(target()),props.preferences.favoriteSkills?.()??new Set,groups(),recent()))
6151
- const filtered=createMemo16(()=>list().filter((item)=>matchesFilter(query(),item.name,item.description,item.assignedGroup)))
6307
+ function createSkillSectionModel(props,query){const target=createMemo17(()=>props.controller.target())
6308
+ const recent=createMemo17(()=>new Map((props.preferences.recentSkills?.()??[]).map((location,index)=>[location,index])))
6309
+ const groups=createMemo17(()=>props.preferences.skillGroups?.()??{})
6310
+ const grouped=createMemo17(()=>Object.keys(groups()).length>0)
6311
+ const list=createMemo17(()=>buildSkillGroupedView(props.controller.list(target()),props.preferences.favoriteSkills?.()??new Set,groups(),recent()))
6312
+ const filtered=createMemo17(()=>list().filter((item)=>matchesFilter(query(),item.name,item.description,item.assignedGroup)))
6152
6313
  const visibility=createListVisibility({items:filtered,limit:()=>props.preferences.sectionItemLimit?.("skills")??0,resetKey:()=>JSON.stringify([target().key,query()])
6153
6314
  })
6154
- const state=createMemo16(()=>props.controller.state(target()))
6155
- createEffect19(()=>{const current=target()
6315
+ const state=createMemo17(()=>props.controller.state(target()))
6316
+ createEffect20(()=>{const current=target()
6156
6317
  const deactivate=untrack8(()=>props.controller.activate?.(current)??(()=>{}))
6157
- onCleanup23(deactivate)
6318
+ onCleanup24(deactivate)
6158
6319
  untrack8(()=>void props.controller.refresh(current).catch(()=>{}))
6159
6320
  })
6160
6321
  async function useSkill(item){try{const isInserted=await props.controller.use(target(),item.name)
@@ -6215,7 +6376,7 @@ return _el$
6215
6376
  }
6216
6377
  function SkillsSection(props){const dialogs=useDialogs(props.api)
6217
6378
  const toolbar=useToolbar()
6218
- const [query,setQuery]=createSignal36("")
6379
+ const [query,setQuery]=createSignal37("")
6219
6380
  const model=createSkillSectionModel(props,query)
6220
6381
  const navigationSection=()=>props.navigationSection??3
6221
6382
  function selectSkill(item){activateSkill({api:props.api,controller:props.controller,access:props.preferences,dialogs,skill:item,onConfigure:toolbar?(navigation)=>toolbar.configure({kind:"skill",name:item.name,location:item.location,icon:"book"
@@ -6292,34 +6453,34 @@ return _el$
6292
6453
 
6293
6454
 
6294
6455
 
6295
- function createSubagentSectionModel(props){const list=createMemo17(()=>props.controller.list(props.sessionID))
6296
- const recent=createMemo17(()=>props.controller.recent?.(props.sessionID)??[])
6297
- const rows=createMemo17(()=>buildSubagentView(list(),recent()))
6298
- const [query,setQuery]=createSignal37("")
6299
- const [mode,setMode]=createSignal37("all")
6300
- const counts=createMemo17(()=>countSubagentView(rows(),query()))
6301
- const tabs=createMemo17(()=>["all","active","recent",...counts().errors>0?["errors"]:[]])
6302
- const filtered=createMemo17(()=>filterSubagentView(rows(),mode(),query()))
6303
- const parentID=createMemo17(()=>props.api.state?.session?.get(props.sessionID)?.parentID)
6304
- const targetKey=createMemo17(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6305
- createEffect20(()=>{targetKey()
6456
+ function createSubagentSectionModel(props){const list=createMemo18(()=>props.controller.list(props.sessionID))
6457
+ const recent=createMemo18(()=>props.controller.recent?.(props.sessionID)??[])
6458
+ const rows=createMemo18(()=>buildSubagentView(list(),recent()))
6459
+ const [query,setQuery]=createSignal38("")
6460
+ const [mode,setMode]=createSignal38("all")
6461
+ const counts=createMemo18(()=>countSubagentView(rows(),query()))
6462
+ const tabs=createMemo18(()=>["all","active","recent",...counts().errors>0?["errors"]:[]])
6463
+ const filtered=createMemo18(()=>filterSubagentView(rows(),mode(),query()))
6464
+ const parentID=createMemo18(()=>props.api.state?.session?.get(props.sessionID)?.parentID)
6465
+ const targetKey=createMemo18(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6466
+ createEffect21(()=>{targetKey()
6306
6467
  setQuery("")
6307
6468
  setMode("all")
6308
6469
  })
6309
- createEffect20(()=>{if(mode()!=="all"&&counts()[mode()]===0)
6470
+ createEffect21(()=>{if(mode()!=="all"&&counts()[mode()]===0)
6310
6471
  setMode("all")
6311
6472
  })
6312
6473
  const visibility=createListVisibility({items:filtered,limit:()=>props.preferences.sectionItemLimit?.("subagents")??0,resetKey:()=>JSON.stringify([targetKey(),query(),mode()])
6313
6474
  })
6314
- const clockKey=createMemo17(()=>props.preferences.expanded().subagents&&visibility.visible().some((item)=>item.status.type!=="idle")?targetKey():undefined)
6475
+ const clockKey=createMemo18(()=>props.preferences.expanded().subagents&&visibility.visible().some((item)=>item.status.type!=="idle")?targetKey():undefined)
6315
6476
  const now=createSubagentClock(clockKey)
6316
- const byId=createMemo17(()=>new Map(visibility.visible().map((item)=>[item.session.id,item])))
6317
- const ids=createMemo17(()=>visibility.visible().map((item)=>item.session.id))
6318
- const state=createMemo17(()=>props.controller.state(props.sessionID))
6319
- createEffect20(()=>{const sessionID=props.sessionID
6477
+ const byId=createMemo18(()=>new Map(visibility.visible().map((item)=>[item.session.id,item])))
6478
+ const ids=createMemo18(()=>visibility.visible().map((item)=>item.session.id))
6479
+ const state=createMemo18(()=>props.controller.state(props.sessionID))
6480
+ createEffect21(()=>{const sessionID=props.sessionID
6320
6481
  targetKey()
6321
6482
  const deactivate=untrack9(()=>props.controller.activate?.(sessionID)??(()=>{}))
6322
- onCleanup24(deactivate)
6483
+ onCleanup25(deactivate)
6323
6484
  untrack9(()=>void props.controller.refresh(sessionID).catch(()=>{}))
6324
6485
  })
6325
6486
  return{list,recent,rows,query,setQuery,mode,setMode,counts,tabs,filtered,parentID,visibility,now,byId,ids,state
@@ -6706,28 +6867,28 @@ return _el$2
6706
6867
  return _el$
6707
6868
  }})
6708
6869
  }
6709
- function TodoSection(props){const list=createMemo18(()=>props.controller.list(props.sessionID))
6870
+ function TodoSection(props){const list=createMemo19(()=>props.controller.list(props.sessionID))
6710
6871
  const icons=useIcons()
6711
6872
  const unavailable=()=>hostCapabilityUnavailable(props.api,"todo")
6712
6873
  const managed=()=>("guidance" in props.controller)?props.controller:undefined
6713
6874
  const native=()=>("source" in props.controller)&&props.controller.source==="native"
6714
6875
  const setup=()=>("setup" in props.controller)?props.controller.setup:undefined
6715
6876
  const setupState=()=>setup()?.state(props.sessionID)
6716
- const [mode,setMode]=createSignal38("all")
6717
- const targetKey=createMemo18(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6718
- createEffect21(()=>{targetKey()
6877
+ const [mode,setMode]=createSignal39("all")
6878
+ const targetKey=createMemo19(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6879
+ createEffect22(()=>{targetKey()
6719
6880
  setMode("all")
6720
6881
  })
6721
- const view=createMemo18(()=>buildTodoView(list(),mode()))
6882
+ const view=createMemo19(()=>buildTodoView(list(),mode()))
6722
6883
  const visibility=createListVisibility({items:()=>view().rows,limit:()=>props.preferences.sectionItemLimit?.("todo")??0,resetKey:()=>JSON.stringify([targetKey(),mode()])
6723
6884
  })
6724
- const state=createMemo18(()=>props.controller.state(props.sessionID))
6725
- createEffect21(()=>{const sessionID=props.sessionID
6885
+ const state=createMemo19(()=>props.controller.state(props.sessionID))
6886
+ createEffect22(()=>{const sessionID=props.sessionID
6726
6887
  targetKey()
6727
6888
  if(unavailable())
6728
6889
  return
6729
6890
  const deactivate=untrack10(()=>props.controller.activate?.(sessionID)??(()=>{}))
6730
- onCleanup25(deactivate)
6891
+ onCleanup26(deactivate)
6731
6892
  untrack10(()=>void props.controller.refresh(sessionID).catch(()=>{}))
6732
6893
  untrack10(()=>void setup()?.refresh(sessionID).catch(()=>{}))
6733
6894
  })
@@ -6796,8 +6957,8 @@ return _el$4
6796
6957
  }})
6797
6958
  }
6798
6959
  function SidebarContent(props){const sections=props.preferences.sections
6799
- const visibleSections=createMemo19(()=>props.preferences.sectionOrder().filter((section)=>sections()[section]&&supportsSidebarSection(props.api,section)))
6800
- onCleanup26(()=>props.interaction.setContentRoot(undefined))
6960
+ const visibleSections=createMemo20(()=>props.preferences.sectionOrder().filter((section)=>sections()[section]&&supportsSidebarSection(props.api,section)))
6961
+ onCleanup27(()=>props.interaction.setContentRoot(undefined))
6801
6962
  return _$createComponent(ToolbarProvider,{get api(){return props.api
6802
6963
  },get preferences(){return props.preferences
6803
6964
  },get skills(){return props.skills
@@ -6848,8 +7009,8 @@ return _el$
6848
7009
  }
6849
7010
 
6850
7011
 
6851
- function McpPersistence(props){const persist=createMemo20(()=>props.controller.persist())
6852
- createEffect22(()=>{if(!props.api.state.ready)
7012
+ function McpPersistence(props){const persist=createMemo21(()=>props.controller.persist())
7013
+ createEffect23(()=>{if(!props.api.state.ready)
6853
7014
  return
6854
7015
  persist()
6855
7016
  const route=props.api.route.current
@@ -6857,12 +7018,12 @@ const params="params" in route?route.params:undefined
6857
7018
  if(typeof params?.sessionID!=="string")
6858
7019
  return
6859
7020
  const current=props.controller.target()
6860
- onCleanup27(()=>props.controller.deactivate(current))
7021
+ onCleanup28(()=>props.controller.deactivate(current))
6861
7022
  untrack11(()=>void props.controller.activate(current).catch(()=>{}))
6862
7023
  })
6863
7024
  return _$createElement("box")
6864
7025
  }
6865
- function PreferencesPersistence(props){createEffect22(()=>{const location=currentLocation(props.api)
7026
+ function PreferencesPersistence(props){createEffect23(()=>{const location=currentLocation(props.api)
6866
7027
  untrack11(()=>props.controller.setActiveScope(preferencesScope({directory:location.routing.directory,worktree:props.api.state.path.worktree
6867
7028
  })))
6868
7029
  props.controller.load()
@@ -6870,7 +7031,7 @@ props.controller.load()
6870
7031
  return _$createElement("box")
6871
7032
  }
6872
7033
  function FirstRunWizardPersistence(props){let isChecked=false
6873
- createEffect22(()=>{if(isChecked)
7034
+ createEffect23(()=>{if(isChecked)
6874
7035
  return
6875
7036
  isChecked=true
6876
7037
  showFirstRunWizard(props.api,props.preferences).catch(()=>{})
@@ -6888,7 +7049,7 @@ return _$createElement("box")
6888
7049
 
6889
7050
  function SidebarTitle(props){const icons=useIcons()
6890
7051
  const theme=()=>props.api.theme.current
6891
- const created=createMemo21(()=>{const value=props.api.state.session.get(props.sessionID)?.time?.created
7052
+ const created=createMemo22(()=>{const value=props.api.state.session.get(props.sessionID)?.time?.created
6892
7053
  return value===undefined?undefined:`Created ${new Date(value).toLocaleDateString("en", {
6893
7054
  dateStyle: "medium"
6894
7055
  })}`
@@ -6897,7 +7058,7 @@ const settingsId=`${PLUGIN_ID}.settings`
6897
7058
  const settings=useSidebarItem(props.api,props.interaction,{id:settingsId,position:{section:0,row:0,column:0
6898
7059
  },activate:()=>openSettings(props.api,props.preferences,undefined,props.mcp,props.skills)
6899
7060
  },()=>theme().textMuted,"control")
6900
- onCleanup28(()=>props.interaction.setTitleRoot(undefined))
7061
+ onCleanup29(()=>props.interaction.setTitleRoot(undefined))
6901
7062
  return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("box"),_el$3=_$createElement("box")
6902
7063
  _$insertNode(_el$,_el$2)
6903
7064
  _$use((node)=>props.interaction.setTitleRoot(node),_el$)
@@ -6957,40 +7118,10 @@ const [user,project]=await Promise.all([safe(userPath?readSource(userPath,()=>re
6957
7118
  return mergeConfiguredDefaults(configuredDefaultsFromPluginOptions(options),user,project)
6958
7119
  }
6959
7120
 
6960
-
6961
- var scheduleInterval=(callback,milliseconds)=>{const timer=setInterval(callback,milliseconds)
6962
- return()=>clearInterval(timer)
6963
- }
6964
- function createLimitsRefresh(options,schedule=scheduleInterval){let disposed=false
6965
- const selectedKey=createMemo22(()=>JSON.stringify(options.selectedModel?.()))
6966
- const targetKey=createMemo22(()=>JSON.stringify(options.controller.current().model))
6967
- const interval=createMemo22(()=>(parseLimitsRefreshMinutes(options.refreshMinutes?.())??DEFAULT_LIMITS_REFRESH_MINUTES)*60000)
6968
- createEffect23(on2(selectedKey,()=>{const model=options.selectedModel?.()
6969
- if(model&&options.accountSource)
6970
- options.accountSource.refresh(model).catch(()=>{})
6971
- }))
6972
- createEffect23(on2(targetKey,()=>{const{model,adapter}=options.controller.current()
6973
- if(model&&adapter)
6974
- options.controller.refresh()
6975
- }))
6976
- async function refresh(){if(disposed)
6977
- return
6978
- const target=targetKey()
6979
- const model=options.selectedModel?.()
6980
- if(model&&options.accountSource)
6981
- await options.accountSource.refresh(model)
6982
- if(!disposed&&target===targetKey())
6983
- await options.controller.refresh()
6984
- }
6985
- createEffect23(on2(interval,(milliseconds)=>{onCleanup29(schedule(()=>void refresh().catch(()=>{}),milliseconds))
6986
- }))
6987
- onCleanup29(()=>{disposed=true
6988
- })
6989
- }
6990
7121
  function LimitsPersistence(props){createLimitsRefresh(props)
6991
7122
  return _$createElement("box")
6992
7123
  }
6993
- function bindNavigatorAbout(api,status,updates,notes){const dispose=registerNavigatorAboutServices(api,{navigatorVersion:"0.25.0",openCodeVersion:api.app.version,checkState:()=>status.navigatorCheckState?.()??{status:"idle"},checking:()=>status.navigatorCheckState?.().status==="checking",updating:updates.navigatorUpdating,installed:updates.navigatorInstalled,check:()=>status.refreshNavigator?.()??Promise.resolve("unavailable"),update(dialogs){const target=status.navigatorUpdate()
7124
+ function bindNavigatorAbout(api,status,updates,notes){const dispose=registerNavigatorAboutServices(api,{navigatorVersion:"0.27.0",openCodeVersion:api.app.version,checkState:()=>status.navigatorCheckState?.()??{status:"idle"},checking:()=>status.navigatorCheckState?.().status==="checking",updating:updates.navigatorUpdating,installed:updates.navigatorInstalled,check:()=>status.refreshNavigator?.()??Promise.resolve("unavailable"),update(dialogs){const target=status.navigatorUpdate()
6994
7125
  if(target)
6995
7126
  updates.navigator(target,dialogs)
6996
7127
  },changelog:async ()=>notes.parseChangelog(await notes.readBundledChangelog())
@@ -7013,11 +7144,11 @@ const candidates=context.data.location.skill.list({directory})?.filter((skill)=>
7013
7144
  const id=candidates.length===1?candidates[0].id:undefined
7014
7145
  if(route.type!=="session"||session?.location.directory!==directory||!id)
7015
7146
  return{data:false}
7016
- await context.client.session.skill({sessionID:route.sessionID,id},{signal:options.signal})
7147
+ await context.client.experimental.session.skill({sessionID:route.sessionID,id},{signal:options.signal})
7017
7148
  return{data:true}}},mcp:{async status(input,options={}){const response=await context.client.mcp.list(location(input),{signal:options.signal})
7018
7149
  const data=Object.fromEntries(response.data.map((server)=>[server.name,server.status]))
7019
- return{data}},async connect(input,options={}){await context.client.mcp.connect({server:input.name,...location(input)},{signal:options.signal})
7020
- return{data:undefined}},async disconnect(input,options={}){await context.client.mcp.disconnect({server:input.name,...location(input)},{signal:options.signal})
7150
+ return{data}},async connect(input,options={}){await context.client.experimental.mcp.connect({server:input.name,...location(input)},{signal:options.signal})
7151
+ return{data:undefined}},async disconnect(input,options={}){await context.client.experimental.mcp.disconnect({server:input.name,...location(input)},{signal:options.signal})
7021
7152
  return{data:undefined}}}}}}
7022
7153
  var TERMINAL_EVENTS=new Set(["session.execution.succeeded","session.execution.failed","session.execution.interrupted"])
7023
7154
  function executionStatus(name,event){const sessionID=event.data.sessionID
@@ -7037,7 +7168,7 @@ return ["session.status","session.retry.scheduled",...executions]
7037
7168
  if(name==="session.idle")
7038
7169
  return ["session.idle",...executions.slice(1)]
7039
7170
  if(name==="session.error")
7040
- return ["session.execution.failed"]
7171
+ return ["session.execution.failed","session.execution.interrupted"]
7041
7172
  if(name==="mcp.tools.changed")
7042
7173
  return ["mcp.status.changed","mcp.resources.changed"]
7043
7174
  if(["session.created","session.deleted","server.connected","installation.update-available"].includes(name))
@@ -7082,7 +7213,7 @@ if(name==="session.deleted"){emit({sessionID})
7082
7213
  return
7083
7214
  }
7084
7215
  if(name==="session.error"){const error=data.error
7085
- emit({sessionID,error:error&&{name:error.type??"Error",data:{message:error.message??"Session error"}}})
7216
+ emit({sessionID,error:event.type==="session.execution.interrupted"?{name:"MessageAbortedError",data:{message:"Session aborted"}}:error&&{name:error.type??"Error",data:{message:error.message??"Session error"}}})
7086
7217
  return
7087
7218
  }
7088
7219
  const info=context.data.session.get(sessionID)
@@ -7126,7 +7257,7 @@ context.keymap.dispatch(command.id)
7126
7257
  context.renderer.keyInput.on("keypress",onKeypress)
7127
7258
  const keymap={registerLayer(layer){const id=++sequence
7128
7259
  const owner=getOwner()
7129
- const [active,setActive]=createSignal39(true)
7260
+ const [active,setActive]=createSignal40(true)
7130
7261
  const install=()=>{context.keymap.layer(()=>({mode:typeof layer.mode==="string"?layer.mode:"base",priority:layer.priority,enabled:()=>active()&&enabled(layer.enabled),target:layer.target?()=>layer.target:undefined,commands:v2LayerCommands(id,layer,()=>true,run,context.keymap.dispatch)
7131
7262
  }))
7132
7263
  }
@@ -7171,7 +7302,7 @@ cleanups.clear()
7171
7302
  context.renderer.keyInput.off("keypress",onKeypress)
7172
7303
  }}}
7173
7304
 
7174
- function createV2SessionAdapter(context){const [retryDetails,setRetryDetails]=createSignal40({})
7305
+ function createV2SessionAdapter(context){const [retryDetails,setRetryDetails]=createSignal41({})
7175
7306
  const projections=new WeakMap
7176
7307
  function normalize(info){const directory=info.location.directory
7177
7308
  const workspaceID=context.location?.directory===directory?context.location.workspaceID:undefined
@@ -7243,7 +7374,7 @@ function DialogPrompt(props){return _$createComponent(SingleLinePrompt,_$mergePr
7243
7374
  function DialogConfirm(props){let active=true
7244
7375
  onCleanup30(()=>{active=false
7245
7376
  })
7246
- onMount7(async ()=>{const confirmed=await context.ui.dialog.confirm({title:props.title,message:props.message
7377
+ async function confirm(){const confirmed=await context.ui.dialog.confirm({title:props.title,message:props.message
7247
7378
  })
7248
7379
  if(!active)
7249
7380
  return
@@ -7251,6 +7382,10 @@ if(confirmed)
7251
7382
  props.onConfirm?.()
7252
7383
  else
7253
7384
  props.onCancel?.()
7385
+ }
7386
+ onMount7(()=>{confirm().catch(()=>{if(active)
7387
+ props.onCancel?.()
7388
+ })
7254
7389
  })
7255
7390
  return _$createElement("box")
7256
7391
  }
@@ -7334,48 +7469,18 @@ return parseTodos(item.state.input.todos)
7334
7469
  }}
7335
7470
  return []
7336
7471
  }
7337
- function createNativeTodoController(context,api){const requests=createRequestState(api.lifecycle.signal)
7338
- const pending=new Map
7339
- let activeTarget
7340
- function target(sessionID){const session=api.state.session.get(sessionID)
7472
+ function createNativeTodoController(context,api){function target(sessionID){const session=api.state.session.get(sessionID)
7341
7473
  const directory=session?.directory??api.state.path.directory
7342
7474
  const workspace=session?.workspaceID
7343
7475
  return{key:JSON.stringify([sessionID,directory,workspace??null]),sessionID,routing:{directory,...workspace&&{workspace}}}}
7344
7476
  function list(sessionID){return nativeTodos(context.data.session.message.list(sessionID),context.data.session.get(sessionID)?.revert?.messageID)
7345
7477
  }
7346
- function refresh(sessionID,force=false){if(api.lifecycle.signal.aborted)
7347
- return Promise.resolve([])
7348
- const current=target(sessionID)
7349
- const existing=pending.get(current.key)
7350
- if(existing&&!force)
7351
- return existing
7352
- const requestState=requests.start(current.key,"refresh native todos",context.data.session.message.list(sessionID).length>0,force)
7353
- if(!requestState)
7354
- return existing
7355
- const request=Promise.try(()=>Promise.all([context.data.session.sync(sessionID),context.data.session.message.sync(sessionID)])).then(()=>{if(!requestState.isCurrent())
7356
- return []
7357
- requestState.succeed()
7478
+ return{source:"native",...createTodoModel({lifecycle:api.lifecycle,target,cache:"source",hasValue:(sessionID)=>context.data.session.message.list(sessionID).length>0,source:{list,async refresh(sessionID,signal){signal.throwIfAborted()
7479
+ await Promise.all([context.data.session.sync(sessionID),context.data.session.message.sync(sessionID)])
7480
+ signal.throwIfAborted()
7358
7481
  return list(sessionID)
7359
- }).catch((error)=>{requestState.fail(error)
7360
- throw error
7361
- }).finally(()=>{requestState.finish()
7362
- if(pending.get(current.key)===request)
7363
- pending.delete(current.key)
7364
- })
7365
- pending.set(current.key,request)
7366
- return request
7367
- }
7368
- return{source:"native",target,list,refresh,state:(sessionID)=>requests.state(target(sessionID).key),retry:(sessionID)=>refresh(sessionID,true),activate(sessionID){const key=target(sessionID).key
7369
- if(activeTarget!==key){requests.abortAll()
7370
- pending.clear()
7371
- activeTarget=key
7372
- }
7373
- return()=>{if(activeTarget!==key)
7374
- return
7375
- activeTarget=undefined
7376
- requests.abortAll()
7377
- pending.clear()
7378
- }}}}
7482
+ }}})
7483
+ }}
7379
7484
  function collectionLocation(target){return{directory:target.routing.directory??"",...target.routing.workspace&&{workspaceID:target.routing.workspace}}}
7380
7485
  async function syncCollection(collection,target,force,signal){signal.throwIfAborted()
7381
7486
  const ref=collectionLocation(target)
@@ -7384,9 +7489,9 @@ collection.invalidate(ref)
7384
7489
  await collection.sync(ref)
7385
7490
  signal.throwIfAborted()
7386
7491
  }
7387
- function createV2McpSource(context){const collection=context.data.location.mcp.server
7492
+ function createNativeMcpSource(data){const collection=()=>data().location.mcp.server
7388
7493
  const projections=new WeakMap
7389
- const list=(target)=>(collection.list(collectionLocation(target))??[]).map((server)=>{let item=projections.get(server)
7494
+ const list=(target)=>(collection().list(collectionLocation(target))??[]).map((server)=>{let item=projections.get(server)
7390
7495
  if(!item){item={get name(){return server.name
7391
7496
  },get title(){return server.title
7392
7497
  },get status(){return server.status.status
@@ -7396,12 +7501,17 @@ projections.set(server,item)
7396
7501
  }
7397
7502
  return item
7398
7503
  })
7399
- return{list,async refresh(target,force,signal){await syncCollection(collection,target,force,signal)
7504
+ return{list,async refresh(target,force,signal){const current=collection()
7505
+ await syncCollection(current,target,force,signal)
7506
+ if(collection()!==current)
7507
+ throw new DOMException("OpenCode connection changed","AbortError")
7400
7508
  return list(target)
7401
7509
  }}}
7402
- function createV2SkillSource(context){const projections=new WeakMap
7403
- const collection=context.data.location.skill
7404
- const list=(target)=>(collection.list(collectionLocation(target))??[]).map((skill)=>{let item=projections.get(skill)
7510
+ function createV2McpSource(context){return createNativeMcpSource(()=>context.data)
7511
+ }
7512
+ function createNativeSkillSource(input){const projections=new WeakMap
7513
+ const collection=()=>input.data().location.skill
7514
+ const list=(target)=>(collection().list(collectionLocation(target))??[]).map((skill)=>{let item=projections.get(skill)
7405
7515
  if(!item){item={get id(){return skill.id
7406
7516
  },get name(){return skill.name
7407
7517
  },get description(){return skill.description
@@ -7412,28 +7522,49 @@ projections.set(skill,item)
7412
7522
  }
7413
7523
  return item
7414
7524
  })
7415
- return{list,async refresh(target,force,signal){await syncCollection(collection,target,force,signal)
7525
+ return{list,async refresh(target,force,signal){const current=collection()
7526
+ await syncCollection(current,target,force,signal)
7527
+ if(collection()!==current)
7528
+ throw new DOMException("OpenCode connection changed","AbortError")
7416
7529
  return list(target)
7417
7530
  },async use(target,skill,signal){signal.throwIfAborted()
7418
- const route=context.ui.router.current()
7419
- const session=route.type==="session"&&context.data.session.get(route.sessionID)
7420
7531
  const actual=list(target).find((item)=>item.id===skill.id&&item.location===skill.location)
7421
- if(route.type!=="session"||!session||session.location.directory!==target.routing.directory||!actual?.id)
7532
+ if(!actual?.id)
7422
7533
  throw new Error("Skill target changed; select the source again")
7423
- await context.client.session.skill({sessionID:route.sessionID,id:actual.id},{signal})
7424
- return !signal.aborted
7534
+ return input.use(target,actual,signal)
7425
7535
  }}}
7426
- function createV2SubagentSource(context,api){return{list:(parentID)=>context.data.session.list().filter((item)=>item.parentID===parentID).flatMap((item)=>{const value=api.state.session.get(item.id)
7427
- return value?[value]:[]
7428
- }),status:(id)=>api.state.session.status(id)??{type:"idle"},async refresh(parentID,signal){signal.throwIfAborted()
7536
+ function createV2SkillSource(context){return createNativeSkillSource({data:()=>context.data,async use(target,skill,signal){const route=context.ui.router.current()
7537
+ const session=route.type==="session"&&context.data.session.get(route.sessionID)
7538
+ if(route.type!=="session"||!session||session.location.directory!==target.routing.directory||!skill.id)
7539
+ throw new Error("Skill target changed; select the source again")
7540
+ await context.client.experimental.session.skill({sessionID:route.sessionID,id:skill.id},{signal})
7541
+ return !signal.aborted
7542
+ }})
7543
+ }
7544
+ function createNativeSubagentSource(input){return{list:(parentID)=>input.sessions().list().filter((item)=>item.parentID===parentID).flatMap((item)=>{const projected=input.project(item)
7545
+ return projected?[projected]:[]
7546
+ }),status:input.status,startedAt(id){const sessions=input.sessions()
7547
+ if(sessions.status(id)!=="running")
7548
+ return
7549
+ const time=sessions.get(id)?.time
7550
+ if(!time?.busy||time.idle!==undefined&&time.idle>=time.busy)
7551
+ return
7552
+ return time.busy
7553
+ },async refresh(parentID,signal){signal.throwIfAborted()
7554
+ const client=input.client()
7555
+ const sessions=input.sessions()
7429
7556
  let cursor
7430
- do{const page=await context.client.session.list(cursor?{cursor,limit:100}:{parentID,limit:100},{signal
7431
- })
7432
- await Promise.all(page.data.filter((item)=>!context.data.session.get(item.id)).map((item)=>context.data.session.sync(item.id)))
7557
+ do{const page=await client.session.list(cursor?{cursor,limit:100}:{parentID,limit:100},{signal})
7558
+ signal.throwIfAborted()
7559
+ if(input.client()!==client||input.sessions()!==sessions)
7560
+ throw new DOMException("Subagent connection changed","AbortError")
7561
+ await Promise.all(page.data.filter((item)=>!sessions.get(item.id)).map((item)=>sessions.sync(item.id)))
7433
7562
  cursor=page.cursor.next??undefined
7434
7563
  signal.throwIfAborted()
7435
7564
  } while(cursor)
7436
7565
  }}}
7566
+ function createV2SubagentSource(context,api){return createNativeSubagentSource({sessions:()=>context.data.session,client:()=>context.client,project:(info)=>api.state.session.get(info.id),status:(id)=>api.state.session.status(id)??{type:"idle"}})
7567
+ }
7437
7568
  function createOpenCodeV2Sources(context,api){return{skills:createV2SkillSource(context),mcp:createV2McpSource(context),subagents:createV2SubagentSource(context,api)
7438
7569
  }}
7439
7570
  function loadNavigatorUpdates(){const path=import.meta.url.includes("/dist/tui.js")?"./navigator-updates.js":"../navigator-updates"
@@ -7455,7 +7586,7 @@ const limitsPath=import.meta.url.includes("/dist/tui.js")?"./provider-limits.js"
7455
7586
  const limitsModule=await import(limitsPath)
7456
7587
  const LimitsSection=limitsModule.ProviderLimitsSection
7457
7588
  const{controller:limits,codex,accountSource,selectModel,modelSelection
7458
- }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.25.0",hostContext)
7589
+ }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.27.0",hostContext)
7459
7590
  const mcp=createMcpController(api,preferences.persistMcp,preferences,nativeSources?.mcp)
7460
7591
  const interaction=createSidebarInteraction(api,()=>openKeyboardHelp(api,preferences.lspIconStyle,preferences.cornerFont,sidebarShortcutActions(api)))
7461
7592
  const startup=createStartupSessionController(api,preferences)
@@ -7484,7 +7615,7 @@ api.slots.register({order:100,slots:{app(){return _$createComponent(IconProvider
7484
7615
  },interaction,skills,subagents,mcp
7485
7616
  }),_$createComponent(FirstRunWizardPersistence,{api,preferences
7486
7617
  }),_$createComponent(StartupSessionBinding,{api,controller:startup
7487
- }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.25.0"
7618
+ }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.27.0"
7488
7619
  }),_$createComponent(McpPersistence,{api,controller:mcp
7489
7620
  })]
7490
7621
  }})
@@ -7518,13 +7649,13 @@ return result.data?.type==="text"?result.data.content:undefined
7518
7649
  const updateServices=await loadNavigatorUpdates()
7519
7650
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV1VersionUpdater
7520
7651
  }=updateServices
7521
- const versionStatus=createVersionStatus(api,"0.25.0")
7522
- const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.25.0"))
7652
+ const versionStatus=createVersionStatus(api,"0.27.0")
7653
+ const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.27.0"))
7523
7654
  bindNavigatorAbout(api,versionStatus,versionUpdates,updateServices)
7524
7655
  api.slots.register({order:90,slots:{sidebar_footer(_context,props){return _$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7525
7656
  },get multilineCorners(){return preferences.cornerFont
7526
7657
  },get children(){return _$createComponent(VersionFooter,{api,get sessionID(){return props.session_id
7527
- },navigatorVersion:"0.25.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7658
+ },navigatorVersion:"0.27.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7528
7659
  },get onNavigatorUpdate(){return versionUpdates.navigator
7529
7660
  }})
7530
7661
  }})
@@ -7541,8 +7672,8 @@ return new TextDecoder().decode(await context.client.file.read({location:context
7541
7672
  const updateServices=await loadNavigatorUpdates()
7542
7673
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV2VersionUpdater
7543
7674
  }=updateServices
7544
- const versionStatus=createVersionStatus(adapter.api,"0.25.0")
7545
- const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.25.0"))
7675
+ const versionStatus=createVersionStatus(adapter.api,"0.27.0")
7676
+ const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.27.0"))
7546
7677
  bindNavigatorAbout(adapter.api,versionStatus,versionUpdates,updateServices)
7547
7678
  const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7548
7679
  },get multilineCorners(){return preferences.cornerFont
@@ -7550,7 +7681,7 @@ const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$create
7550
7681
  _$setProp(_el$,"flexDirection","row")
7551
7682
  _$setProp(_el$,"justifyContent","space-between")
7552
7683
  _$insert(_el$,_$createComponent(VersionSummary,{get api(){return adapter.api
7553
- },navigatorVersion:"0.25.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7684
+ },navigatorVersion:"0.27.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7554
7685
  },get onNavigatorUpdate(){return versionUpdates.navigator
7555
7686
  }}),null)
7556
7687
  _$insert(_el$,_$createComponent(SettingsFooterButton,{get api(){return adapter.api