opencode-navigator 0.26.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"
@@ -216,6 +216,8 @@ const rightRank=BUCKET_RANK[right.bucket]??1
216
216
  return leftRank-rightRank||left.bucket.localeCompare(right.bucket)||left.name.localeCompare(right.name)
217
217
  })
218
218
  }
219
+ function serverKey(current,name){return JSON.stringify([current.key,name])
220
+ }
219
221
  function bulkDescription(operation,action){if(operation.action==="preset")
220
222
  return `apply ${operation.preset} MCP preset`
221
223
  if(operation.group)
@@ -228,12 +230,8 @@ return Object.keys(presets).sort((left,right)=>left.localeCompare(right)).find((
228
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")
229
231
  })
230
232
  }
231
- function statusError(status){if(!status||typeof status!=="object"||!("error" in status))
232
- return
233
- const error=status.error
234
- return typeof error==="string"?error:undefined
235
- }
236
- function createMcpController(api,persist,preferences,source){const [snapshots,setSnapshots]=createSignal2({})
233
+ function createMcpModel(options,persist,preferences){const{source}=options
234
+ const [snapshots,setSnapshots]=createSignal2({})
237
235
  const [mutationCounts,setMutationCounts]=createSignal2({})
238
236
  const [bulkStates,setBulkStates]=createSignal2({})
239
237
  const [selectedPresets,setSelectedPresets]=createSignal2({})
@@ -243,8 +241,8 @@ const mutationRetries=new Map
243
241
  const bulkGenerations=new Map
244
242
  const bulkRetries=new Map
245
243
  const selectedPresetStates=new Map
246
- const requests=createRequestState(api.lifecycle?.signal)
247
- const serverRequests=createRequestState(api.lifecycle?.signal)
244
+ const requests=createRequestState(options.signal)
245
+ const serverRequests=createRequestState(options.signal)
248
246
  let activation=0
249
247
  let activeTarget
250
248
  function changeMutationCount(current,offset){setMutationCounts((values)=>{const next=Math.max(0,(values[current.key]??0) + offset)
@@ -268,10 +266,7 @@ serverRequests.abortAll()
268
266
  refreshing.clear()
269
267
  activeTarget=current.key
270
268
  }
271
- function target(){const location=currentLocation(api)
272
- return{key:location.key,scope:preferencesScope({directory:location.routing.directory,worktree:api.state.path.worktree}),routing:location.routing
273
- }}
274
- function serverKey(current,name){return JSON.stringify([current.key,name])
269
+ function target(){return options.target()
275
270
  }
276
271
  function mutating(current=target()){return(mutationCounts()[current.key]??0)>0
277
272
  }
@@ -306,7 +301,7 @@ const failed=state.failed.filter((candidate)=>candidate!==name)
306
301
  bulkRetries.set(current.key,(bulkRetries.get(current.key)??[]).filter((candidate)=>candidate.name!==name))
307
302
  return{...values,[current.key]:{...state,status:failed.length>0?"error":"ready",failed}}})
308
303
  }
309
- 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)
310
305
  }
311
306
  async function refresh(current=target(),force=false){const pending=refreshing.get(current.key)
312
307
  if(pending&&!force)
@@ -314,7 +309,7 @@ return pending
314
309
  const requestState=requests.start(current.key,"refresh MCP servers",current.key in snapshots(),force)
315
310
  if(!requestState)
316
311
  return pending
317
- 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())
318
313
  setSnapshots((value)=>({...value,[current.key]:items}))
319
314
  if(requestState.isCurrent())
320
315
  reconcileSelectedPreset(current,items)
@@ -337,7 +332,7 @@ const requestState=serverRequests.start(key,operation,false)
337
332
  if(!requestState)
338
333
  return mutations.get(key)
339
334
  changeMutationCount(current,1)
340
- 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()
341
336
  mutationRetries.delete(key)
342
337
  clearBulkFailure(current,name)
343
338
  }).catch((error)=>{requestState.fail(error)
@@ -385,14 +380,14 @@ if(desired==="disabled"&&action==="disconnect"){await changeServer(current,item.
385
380
  }
386
381
  if(desired==="enabled"&&action==="connect"){await changeServer(current,item.name,"connect","restore MCP preference",false)
387
382
  }}))
388
- const failed=api.lifecycle?.signal.aborted?[]:results.flatMap((result,index)=>result.status==="rejected"&&!isAbortError(result.reason)?[items[index].name]:[])
389
- 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(", ")}`
390
385
  })
391
386
  }
392
387
  if(generation===activation&&target().key===current.key)
393
388
  await refresh(current,true)
394
389
  }
395
- function reconnect(current=target()){return retryBackgroundRefresh(()=>activate(current),{signal:api.lifecycle?.signal})
390
+ function reconnect(current=target()){return retryBackgroundRefresh(()=>activate(current),{signal:options.signal})
396
391
  }
397
392
  function bulkState(current=target()){return bulkStates()[current.key]??{action:"connect",status:"idle",completed:0,total:0,failed:[]
398
393
  }}
@@ -430,7 +425,7 @@ setBulkStates((values)=>({...values,[current.key]:{...operation,status:failed.le
430
425
  if(failed.length>0&&activeTarget===current.key){let action=operation.action==="preset"?`apply ${operation.preset}`:operation.action
431
426
  if(operation.group)
432
427
  action += ` ${operation.group}`
433
- 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(", ")}`
434
429
  })
435
430
  }}
436
431
  return{list,refresh,reconnect,activate,deactivate(current){if(activeTarget!==current.key)
@@ -497,6 +492,14 @@ return
497
492
  const changes=bulkRetries.get(current.key)??[]
498
493
  return changeMany({action:state.action,...state.preset&&{preset:state.preset},...state.group&&{group:state.group}},changes,current)
499
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
+ }
500
503
 
501
504
 
502
505
 
@@ -1195,12 +1198,16 @@ _$insert(_el$,_$createComponent(Suspense,{get children(){return _$createComponen
1195
1198
  return _el$
1196
1199
  })()
1197
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
+ }
1198
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"]
1199
1206
  }
1200
- var terminalIconIDs=Object.keys(glyphs)
1207
+ var terminalIconIDs=[...ICON_CHOICE_IDS]
1201
1208
  function terminalIcon(id,style){return(glyphs[id]??glyphs.bolt)[style==="nerd"?0:1]
1202
1209
  }
1203
- function terminalIconLabel(id){return id.replaceAll("-"," ")
1210
+ function terminalIconLabel(id){return iconChoiceLabel(id)
1204
1211
  }
1205
1212
  function McpErrorDialog(props){return _$createComponent(SelectionMenu,{get api(){return props.api
1206
1213
  },get title(){return `MCP error \xB7 ${props.name}`
@@ -1209,6 +1216,9 @@ function McpErrorDialog(props){return _$createComponent(SelectionMenu,{get api()
1209
1216
  },onSelect:()=>{}})
1210
1217
  }
1211
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:{}}}
1212
1222
  function codexBindingKey(target){return JSON.stringify([target.providerID,target.connectionID?{connectionID:target.connectionID}:null])
1213
1223
  }
1214
1224
  function legacyKey(key){try{const value=JSON.parse(key)
@@ -1333,6 +1343,83 @@ function serializePortableSettings(layout,mcp,behavior){return `${JSON.stringify
1333
1343
  }, undefined, 2)}
1334
1344
  `
1335
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
+ }}}
1336
1423
  function toolbarPinKey(pin){return pin.kind==="action"?`action:${pin.id}`:`skill:${pin.location}`
1337
1424
  }
1338
1425
  function identifier(value){return typeof value==="string"&&/^[a-z][a-z\d-]{0,47}$/u.test(value)
@@ -1569,14 +1656,6 @@ order=[...layer.layout.order]
1569
1656
  Object.assign(desiredMcpStates,layer.desiredMcpStates)
1570
1657
  }
1571
1658
  return{behavior,layout:{sections,expanded,order},desiredMcpStates}}
1572
- import {randomUUID} from "crypto"
1573
- import {link,mkdir,readFile,readdir,rename,rm,unlink,writeFile,readFile as readFile2} from "fs/promises"
1574
- import {join,dirname,join as join2,join as join3} from "path"
1575
- var LOCK_TIMEOUT_MS=3000
1576
- function errno(error,code){return error instanceof Error&&"code" in error&&error.code===code
1577
- }
1578
- function delay(milliseconds){return new Promise((resolve)=>setTimeout(resolve,milliseconds))
1579
- }
1580
1659
  function parseSkillConfirmations(value){if(!Array.isArray(value))
1581
1660
  return []
1582
1661
  return [...new Set(value.filter((item)=>typeof item==="string"&&item.length>0))].sort()
@@ -1723,153 +1802,8 @@ return{global:target.kind==="global"?scope:current.global,worktrees,user:{...ski
1723
1802
  },...pendingResetAttempt&&{pendingResetAttempt},...typeof (update.user?.autoApprovePermissions??current.user.autoApprovePermissions)==="boolean"&&{autoApprovePermissions:update.user?.autoApprovePermissions??current.user.autoApprovePermissions
1724
1803
  }}}}
1725
1804
  function targetForLegacyScope(scope){return scope==="global"?{kind:"global"}:{kind:"worktree",key:scope}}
1726
- function createPreferencesStore(stateDirectory){const directory=join(stateDirectory,LEGACY_PLUGIN_ID)
1727
- const file=join(directory,"preferences.json")
1728
- const lock=join(directory,"preferences.lock")
1729
- let writes=Promise.resolve()
1730
- async function readLockOwner(){const source=await readFile(lock,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
1731
- return
1732
- throw error
1733
- })
1734
- if(source===undefined)
1735
- return
1736
- try{const value=JSON.parse(source)
1737
- if(typeof value.token!=="string"||typeof value.pid!=="number")
1738
- return
1739
- return{token:value.token,pid:value.pid}} catch{return
1740
- }}
1741
- function processIsAlive(pid){try{process.kill(pid,0)
1742
- return true
1743
- } catch(error){return !errno(error,"ESRCH")
1744
- }}
1745
- async function moveLockToTombstone(suffix){const tombstone=`${lock}.${suffix}.${randomUUID()}`
1746
- try{await rename(lock,tombstone)
1747
- } catch(error){if(errno(error,"ENOENT"))
1748
- return false
1749
- throw error
1750
- }
1751
- await rm(tombstone,{recursive:true,force:true})
1752
- return true
1753
- }
1754
- async function liveRecoveryMarkers(prefix="preferences.lock.recover."){const entries=await readdir(directory)
1755
- const markers=[]
1756
- for(const entry of entries){if(!entry.startsWith(prefix)||entry.endsWith(".candidate"))
1757
- continue
1758
- const path=join(directory,entry)
1759
- const markerOwner=await readFile(path,"utf8").then((source)=>JSON.parse(source)).catch(()=>{})
1760
- if(typeof markerOwner?.pid==="number"&&processIsAlive(markerOwner.pid))
1761
- markers.push(path)
1762
- else
1763
- await unlink(path).catch((error)=>{if(!errno(error,"ENOENT"))
1764
- throw error
1765
- })
1766
- }
1767
- return markers.sort()
1768
- }
1769
- async function reapStaleLock(owner){const token=Buffer.from(owner.token).toString("base64url")
1770
- const prefix=`preferences.lock.recover.${token}.`
1771
- const recovery=join(directory,`${prefix}${Date.now().toString().padStart(13, "0")}.${process.pid}.${randomUUID()}`)
1772
- const markers=await liveRecoveryMarkers(prefix)
1773
- if(markers.length>0)
1774
- return false
1775
- const candidate=`${recovery}.candidate`
1776
- await writeFile(candidate,JSON.stringify({token:owner.token,pid:process.pid}),{flag:"wx",mode:384})
1777
- try{await rename(candidate,recovery)
1778
- await delay(10)
1779
- const liveMarkers=await liveRecoveryMarkers(prefix)
1780
- if(liveMarkers[0]!==recovery)
1781
- return false
1782
- const current=await readLockOwner()
1783
- if(current?.token!==owner.token||processIsAlive(current.pid))
1784
- return false
1785
- return moveLockToTombstone("stale")
1786
- } finally{await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
1787
- throw error
1788
- })
1789
- await unlink(recovery).catch((error)=>{if(!errno(error,"ENOENT"))
1790
- throw error
1791
- })
1792
- }}
1793
- async function tryAcquireLock(deadline){const token=randomUUID()
1794
- const candidate=`${lock}.${token}.candidate`
1795
- let acquired=false
1796
- let handedOff=false
1797
- await writeFile(candidate,JSON.stringify({token,pid:process.pid}),{flag:"wx",mode:384})
1798
- try{try{await link(candidate,lock)
1799
- acquired=true
1800
- } catch(error){if(!errno(error,"EEXIST"))
1801
- throw error
1802
- const owner=await readLockOwner()
1803
- if(!owner||processIsAlive(owner.pid))
1804
- return
1805
- await reapStaleLock(owner)
1806
- return
1807
- }
1808
- while(true){const markers=await liveRecoveryMarkers()
1809
- if(markers.length===0)
1810
- break
1811
- if(Date.now()>=deadline)
1812
- return
1813
- await delay(10)
1814
- }
1815
- const currentOwner=await readLockOwner()
1816
- if(currentOwner?.token!==token)
1817
- return
1818
- handedOff=true
1819
- return async ()=>{const current=await readLockOwner()
1820
- if(current?.token!==token)
1821
- throw new Error("Refusing to release a preferences lock owned by another process")
1822
- await moveLockToTombstone("released")
1823
- }} finally{if(acquired&&!handedOff){const current=await readLockOwner()
1824
- if(current?.token===token)
1825
- await moveLockToTombstone("released")
1826
- }
1827
- await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
1828
- throw error
1829
- })
1830
- }}
1831
- async function acquireLock(){await mkdir(directory,{recursive:true,mode:448})
1832
- const deadline=Date.now() + LOCK_TIMEOUT_MS
1833
- while(true){const release=await tryAcquireLock(deadline)
1834
- if(release)
1835
- return release
1836
- if(Date.now()>=deadline)
1837
- throw new Error(`Timed out waiting for preferences lock: ${lock}`)
1838
- await delay(10)
1839
- }}
1840
- async function withLock(operation){const release=await acquireLock()
1841
- try{return await operation()
1842
- } finally{await release()
1843
- }}
1844
- async function read(){const source=await readFile(file,"utf8").catch((error)=>{if(errno(error,"ENOENT"))
1845
- return
1846
- throw error
1847
- })
1848
- if(source===undefined)
1849
- return
1850
- try{return parsePreferencesDocument(JSON.parse(source))
1851
- } catch(error){if(error instanceof SyntaxError)
1852
- throw new Error(`Malformed preferences JSON: ${file}`,{cause:error})
1853
- throw error
1854
- }}
1855
- async function write(value){const temporary=`${file}.${process.pid}.${randomUUID()}.tmp`
1856
- try{await writeFile(temporary,`${JSON.stringify(value)}
1857
- `,{mode:384})
1858
- await rename(temporary,file)
1859
- } finally{await unlink(temporary).catch((error)=>{if(!errno(error,"ENOENT"))
1860
- throw error
1861
- })
1862
- }}
1863
- function enqueue(operation){const result=writes.then(operation)
1864
- writes=result.catch(()=>{})
1865
- return result
1805
+ function skillKey(skill){return skill.location
1866
1806
  }
1867
- return{load(){return withLock(async ()=>await read()??emptyPreferencesDocument())
1868
- },update(update){return enqueue(()=>withLock(async ()=>{const current=await read()??emptyPreferencesDocument()
1869
- await write(applyPreferencesUpdate(current,update))
1870
- }))
1871
- },async flush(){await writes
1872
- }}}
1873
1807
  function swap(items,first,second){const value=items[first]
1874
1808
  items[first]=items[second]
1875
1809
  items[second]=value
@@ -1883,9 +1817,8 @@ return{behavior:scope.behavior,layout:scope.layout,desiredMcpStates:scope.mcp
1883
1817
  }}
1884
1818
  function mergeSessionLayout(base,session){const order=session.order??base?.order
1885
1819
  return{sections:{...base?.sections,...session.sections},expanded:{...base?.expanded,...session.expanded},...order&&{order:[...order]}}}
1886
- 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
1887
- },layout:{sections:defaults.sections,expanded:DEFAULT_SECTION_EXPANSION,order:defaults.sectionOrder??[...SIDEBAR_SECTIONS]
1888
- },desiredMcpStates:{}}
1820
+ function createPreferencesModel(ports,defaults,store,configuredInput={}){let configured=configuredInput
1821
+ const builtIns=builtInPreferences(defaults)
1889
1822
  const [resolved,setResolved]=createSignal5(resolvePreferences({builtIns,pluginOptions:configured}))
1890
1823
  const [skippedSkills,setSkippedSkills]=createSignal5(new Set)
1891
1824
  const [onboardingCompleted,setOnboardingCompleted]=createSignal5(false)
@@ -1915,17 +1848,15 @@ let mcpPresetReconciliation=Promise.resolve()
1915
1848
  let updateRevision=0
1916
1849
  let reloading
1917
1850
  let isPersistenceWarningShown=false
1918
- function skillKey(skill){return skill.location
1919
- }
1920
1851
  function showPersistenceWarning(cause){if(isPersistenceWarningShown)
1921
1852
  return
1922
1853
  isPersistenceWarningShown=true
1923
- 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"
1924
1855
  })
1925
1856
  }
1926
1857
  function persist(request){request.catch(showPersistenceWarning)
1927
1858
  }
1928
- 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)
1929
1860
  } catch{return false
1930
1861
  }}
1931
1862
  function resolveTarget(target,session=true){const worktree=target.kind==="worktree"?document.worktrees[target.key]:undefined
@@ -1951,13 +1882,16 @@ return{...configured.workspaceProfiles,...document.user.workspaceProfiles}}
1951
1882
  function scopeSnapshot(target=selectedTarget()){const scope=target.kind==="global"?document.global:document.worktrees[target.key]
1952
1883
  return JSON.stringify({layout:scope?.layout,mcp:scope?.mcp,behavior:scope?.behavior})
1953
1884
  }
1954
- function update(change){updateRevision++
1885
+ function update(change,wait=false){updateRevision++
1955
1886
  document=applyPreferencesUpdate(document,change)
1956
- if(isHydrated)
1957
- persist(store.update(change))
1958
- else
1887
+ let request
1888
+ if(isHydrated){request=store.update(change)
1889
+ persist(request)
1890
+ } else
1959
1891
  pendingUpdates.push(change)
1960
1892
  refreshResolved()
1893
+ if(wait)
1894
+ return request??Promise.resolve()
1961
1895
  }
1962
1896
  function applyDocument(value){document=value
1963
1897
  setSkippedSkills(new Set(value.user.skippedSkillConfirmations))
@@ -2028,13 +1962,6 @@ function setSessionLayout(layout){setTargetSessionLayout(activeTarget(),layout)
2028
1962
  }
2029
1963
  function setSelectedSessionLayout(layout,replace=false){setTargetSessionLayout(selectedTarget(),layout,replace)
2030
1964
  }
2031
- function normalizePresetName(value){const name=value.trim()
2032
- if(!name)
2033
- throw new Error("Enter a preset name")
2034
- if(name.length>64)
2035
- throw new Error("Preset names can contain at most 64 characters")
2036
- return name
2037
- }
2038
1965
  function currentLayoutPreset(){const layout=selectedLayoutResolved().layout
2039
1966
  return{sections:{...layout.sections},expanded:{...layout.expanded},order:[...layout.order]
2040
1967
  }}
@@ -2058,7 +1985,7 @@ isApplied=!name
2058
1985
  else if(mcpPreset.operation==="rename"){isApplied=name===mcpPreset.name&&(mcpPreset.previousName===mcpPreset.name||!actual[mcpPreset.previousName])
2059
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)
2060
1987
  }
2061
- 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"
2062
1989
  })
2063
1990
  }}).catch(showPersistenceWarning)
2064
1991
  }
@@ -2094,13 +2021,14 @@ return
2094
2021
  setAutoApprovePermissions(value)
2095
2022
  update({user:{autoApprovePermissions:value}})
2096
2023
  }
2024
+ function codexBindingUpdate(providerID,connectionID,accountID){setCodexAccountBindings((bindings)=>changeCodexBinding(bindings,{providerID,connectionID},accountID))
2025
+ return{user:{codexAccountBinding:{providerID,connectionID,accountID}}}}
2097
2026
  function changeCodexConnectionBinding(providerID,connectionID,accountID){if(!isHydrated){load().then(()=>changeCodexConnectionBinding(providerID,connectionID,accountID))
2098
2027
  return
2099
2028
  }
2100
2029
  if(!providerID)
2101
2030
  return
2102
- setCodexAccountBindings((bindings)=>changeCodexBinding(bindings,{providerID,connectionID},accountID))
2103
- update({user:{codexAccountBinding:{providerID,connectionID,accountID}}})
2031
+ update(codexBindingUpdate(providerID,connectionID,accountID))
2104
2032
  }
2105
2033
  function toggleFavoriteMcpServer(name){if(!isHydrated){load().then(()=>toggleFavoriteMcpServer(name))
2106
2034
  return
@@ -2126,10 +2054,18 @@ next.add(id)
2126
2054
  setFavoriteQuickActions(next)
2127
2055
  update({user:{favoriteQuickAction:{id,favorite:next.has(id)}}})
2128
2056
  }
2129
- 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()
2130
2064
  return document.user.toolbarPins??[]
2131
2065
  },changeToolbarPin(change){requireHydration()
2132
2066
  update({user:{toolbarPin:change}})
2067
+ },saveToolbarPin(change){requireHydration()
2068
+ return update({user:{toolbarPin:change}},true)
2133
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()
2134
2070
  return resolveTarget(targetForScope(scope),false).behavior.startInChat
2135
2071
  },cornerFont:()=>resolved().behavior.cornerFont,lspIconStyle:()=>resolved().behavior.lspIconStyle,rowDensity:()=>resolved().behavior.rowDensity,toolbarOverflow:()=>resolved().behavior.toolbarOverflow,selectedToolbarOverflow:()=>selectedResolved().behavior.toolbarOverflow,toggleToolbarOverflow(){load()
@@ -2183,7 +2119,16 @@ throw new Error("MCP preset no longer exists")
2183
2119
  update({user:{workspaceProfile:{layout,mcp}}})
2184
2120
  },favoriteSkills,favoriteMcpServers,favoriteQuickActions,toggleFavoriteMcpServer,toggleFavoriteQuickAction,recentSkills,recentQuickActions,mcpServerGroups,setMcpServerGroup(name,value){setMcpServerGroups(assignGroup(mcpServerGroups(),name,value))
2185
2121
  update({user:{mcpServerGroup:{name,group:value}}})
2186
- },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()
2187
2132
  await store.update({user:{pendingResetAttempt:attempt}})
2188
2133
  const current=await store.load()
2189
2134
  setPendingResetAttempt(current.user.pendingResetAttempt)
@@ -2236,6 +2181,9 @@ setSessionLayout({order})
2236
2181
  },toggleSelectedSection(name){load()
2237
2182
  const layout=resolveTarget(selectedTarget()).layout
2238
2183
  setSelectedSessionLayout({sections:{[name]:!layout.sections[name]}})
2184
+ },toggleSelectedSectionExpanded(name){load()
2185
+ const layout=resolveTarget(selectedTarget()).layout
2186
+ setSelectedSessionLayout({expanded:{[name]:!layout.expanded[name]}})
2239
2187
  },moveSelectedSection(name,direction){load()
2240
2188
  const layout=resolveTarget(selectedTarget()).layout
2241
2189
  const order=[...layout.order]
@@ -2248,76 +2196,8 @@ setSelectedSessionLayout({order})
2248
2196
  },previewLayoutPreset(preset){return layoutPresetPreview(selectedLayoutResolved().layout,preset)
2249
2197
  },applyLayoutPreset(preset){load()
2250
2198
  setSelectedSessionLayout(layoutPresetPreview(selectedLayoutResolved().layout,preset).next,true)
2251
- },saveLayoutPreset(value){load()
2252
- const name=normalizePresetName(value)
2253
- if(Object.keys(layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2254
- }
2255
- if(Object.keys(layoutPresets()).length>=50)
2256
- throw new Error("You can save at most 50 layout presets")
2257
- const layout=currentLayoutPreset()
2258
- persistLayoutPresets({...layoutPresets(),[name]:layout},{name,layout})
2259
- return name
2260
- },updateLayoutPreset(name){if(!layoutPresets()[name])
2261
- return false
2262
- const layout=currentLayoutPreset()
2263
- persistLayoutPresets({...layoutPresets(),[name]:layout},{name,layout})
2264
- return true
2265
- },renameLayoutPreset(current,value){const name=normalizePresetName(value)
2266
- const preset=layoutPresets()[current]
2267
- if(!preset)
2268
- throw new Error("Layout preset no longer exists")
2269
- if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(layoutPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2270
- }
2271
- const next={...layoutPresets()}
2272
- delete next[current]
2273
- next[name]=preset
2274
- persistLayoutPresets(next,{name,layout:preset,previousName:current})
2275
- return name
2276
- },deleteLayoutPreset(name){if(!layoutPresets()[name])
2277
- return false
2278
- const next={...layoutPresets()}
2279
- delete next[name]
2280
- persistLayoutPresets(next,{name})
2281
- return true
2282
- },saveMcpPreset(value,states){requireHydration()
2283
- if(Object.keys(states).length===0)
2284
- throw new Error("There are no MCP servers to save")
2285
- const name=normalizePresetName(value)
2286
- if(Object.keys(mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2287
- }
2288
- if(Object.keys(mcpPresets()).length>=50)
2289
- throw new Error("You can save at most 50 MCP presets")
2290
- const preset={...states}
2291
- persistMcpPresets({...mcpPresets(),[name]:preset},{operation:"save",name,states:preset})
2292
- return name
2293
- },updateMcpPreset(name,states){requireHydration()
2294
- if(!mcpPresets()[name])
2295
- return false
2296
- if(Object.keys(states).length===0)
2297
- throw new Error("There are no MCP servers to save")
2298
- const preset={...states}
2299
- persistMcpPresets({...mcpPresets(),[name]:preset},{operation:"update",name,states:preset})
2300
- return true
2301
- },renameMcpPreset(current,value){requireHydration()
2302
- const name=normalizePresetName(value)
2303
- const preset=mcpPresets()[current]
2304
- if(!preset)
2305
- throw new Error("MCP preset no longer exists")
2306
- if(name.toLocaleLowerCase()!==current.toLocaleLowerCase()&&Object.keys(mcpPresets()).some((candidate)=>candidate.toLocaleLowerCase()===name.toLocaleLowerCase())){throw new Error("A preset with this name already exists")
2307
- }
2308
- const next={...mcpPresets()}
2309
- delete next[current]
2310
- next[name]=preset
2311
- persistMcpPresets(next,{operation:"rename",name,previousName:current})
2312
- return name
2313
- },deleteMcpPreset(name){requireHydration()
2314
- if(!mcpPresets()[name])
2315
- return false
2316
- const next={...mcpPresets()}
2317
- delete next[name]
2318
- persistMcpPresets(next,{operation:"delete",name})
2319
- return true
2320
- },async saveLayoutAsDefault(){await load()
2199
+ },...createPreferencePresetActions({load,requireHydration,layoutPresets,mcpPresets,currentLayoutPreset,persistLayoutPresets,persistMcpPresets
2200
+ }),async saveLayoutAsDefault(){await load()
2321
2201
  const target=selectedTarget()
2322
2202
  const layout=resolveTarget(target).layout
2323
2203
  updateRevision++
@@ -2380,6 +2260,9 @@ update({target:targetForScope(scope),mcp:{name,state}})
2380
2260
  },setDesiredMcpStates(scope,states){load()
2381
2261
  update({target:targetForScope(scope),mcp:{states}})
2382
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
+ }
2383
2266
  function record2(value){if(!value||typeof value!=="object"||Array.isArray(value))
2384
2267
  return
2385
2268
  return value
@@ -2414,17 +2297,173 @@ function legacyDefaults(config){const{sections,sectionOrder,...behavior}=config
2414
2297
  return{behavior,layout:{sections,expanded:{},...sectionOrder&&{order:sectionOrder}}}}
2415
2298
  function configuredDefaultsFromPluginOptions(options){return mergeConfiguredDefaults(legacyDefaults(pluginConfig(options)),parseConfiguredDefaults(options??{}))
2416
2299
  }
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
+ }}}
2417
2455
 
2418
- function createSkillController(api,source){const [skills,setSkills]=createSignal6({})
2456
+ function createSkillModel(options){const{source,lifecycle}=options
2457
+ const [skills,setSkills]=createSignal6({})
2419
2458
  const refreshing=new Map
2420
2459
  const targets=new Map
2421
- const requests=createRequestState(api.lifecycle.signal)
2422
- const promptRequests=createRequestState(api.lifecycle.signal)
2460
+ const requests=createRequestState(lifecycle.signal)
2461
+ const promptRequests=createRequestState(lifecycle.signal)
2423
2462
  let activeTarget
2424
2463
  let leases=new Set
2425
- function target(){return currentLocation(api)
2464
+ function target(){return options.target()
2426
2465
  }
2427
- 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]??[]
2428
2467
  }
2429
2468
  function error(current=target()){return requests.state(current.key).error?.message
2430
2469
  }
@@ -2435,8 +2474,8 @@ targets.set(current.key,current)
2435
2474
  const requestState=requests.start(current.key,"refresh skills",current.key in skills(),force)
2436
2475
  if(!requestState)
2437
2476
  return pending
2438
- 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))
2439
- 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())
2440
2479
  setSkills((value)=>({...value,[current.key]:items}))
2441
2480
  requestState.succeed()
2442
2481
  return items
@@ -2449,9 +2488,9 @@ refreshing.delete(current.key)
2449
2488
  refreshing.set(current.key,request)
2450
2489
  return request
2451
2490
  }
2452
- 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(()=>{})
2453
2492
  }})
2454
- api.lifecycle.onDispose(unsubscribe)
2493
+ lifecycle.onDispose(unsubscribe)
2455
2494
  return{list,error,refresh,target,state(current=target()){return requests.state(current.key)
2456
2495
  },retry(current=target()){return refresh(current,true)
2457
2496
  },activate(current=target()){targets.set(current.key,current)
@@ -2477,15 +2516,24 @@ throw new DOMException("Sidebar context changed","AbortError")
2477
2516
  const request=promptRequests.start(current.key,"insert skill",false,true)
2478
2517
  if(!request)
2479
2518
  return false
2480
- 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)
2481
2520
  if(!request.isCurrent())
2482
2521
  return false
2483
2522
  request.succeed()
2484
- return result.data
2523
+ return result??false
2485
2524
  } catch(error_){request.fail(error_)
2486
2525
  throw error_
2487
2526
  } finally{request.finish()
2488
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
+ }
2489
2537
  var BUCKET_RANK2={Favorites:0,Ungrouped:2}
2490
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"
2491
2539
  })).sort((left,right)=>{const rank=(BUCKET_RANK2[left.bucket]??1)-(BUCKET_RANK2[right.bucket]??1)
@@ -2739,34 +2787,40 @@ return "Skill source changed"
2739
2787
  return "Multiple sources share this skill name"
2740
2788
  return
2741
2789
  }
2742
- function activateSkill(input){input={...input,skill:{...input.skill
2743
- }}
2790
+ function createSkillActivation(input){const skill={...input.skill}
2744
2791
  const target=input.controller.target()
2745
- const route=JSON.stringify(input.api.route.current)
2746
- const current=()=>target.key===input.controller.target().key&&route===JSON.stringify(input.api.route.current)
2747
- const report=(error)=>{if(!isAbortError(error))
2748
- input.api.ui.toast({variant:"error",title:"Skills",message:String(error),duration:5000
2749
- })
2750
- }
2751
- function check(){if(!current())
2792
+ function check(){if(!input.current()||target.key!==input.controller.target().key)
2752
2793
  throw new DOMException("Skill target changed; choose it again","AbortError")
2753
- const reason=skillUnavailable(input.controller,input.skill)
2794
+ const reason=skillUnavailable(input.controller,skill)
2754
2795
  if(reason)
2755
2796
  throw new Error(reason)
2756
2797
  }
2757
- async function insert(skip=false){try{check()
2798
+ return{skill,check,shouldConfirm:()=>input.access.shouldConfirmSkill(skill),async run(skip=false){check()
2758
2799
  if(skip)
2759
- input.access.skipSkillConfirmation(input.skill)
2800
+ input.access.skipSkillConfirmation(skill)
2760
2801
  input.beforeInsert?.()
2761
- if(await input.controller.use(target,input.skill.name,input.skill))
2762
- 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)
2763
2817
  } catch(error){report(error)
2764
2818
  }}
2765
- try{check()
2819
+ try{activation.check()
2766
2820
  } catch(error){report(error)
2767
2821
  return
2768
2822
  }
2769
- if(!input.access.shouldConfirmSkill(input.skill)){insert()
2823
+ if(!activation.shouldConfirm()){insert()
2770
2824
  return
2771
2825
  }
2772
2826
  input.dialogs.open(()=>_$createComponent(SkillDialog,{get api(){return input.api
@@ -2819,7 +2873,7 @@ return
2819
2873
  const isCancelled=error?.name==="MessageAbortedError"
2820
2874
  const data=error&&"data" in error?error.data:undefined
2821
2875
  const fallbackMessage=isCancelled?"Session aborted":"Session error"
2822
- 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
2823
2877
  const next={...run,startedAt:Math.min(run.startedAt,at),outcome:isCancelled?"cancelled":"error",errorMessage:message
2824
2878
  }
2825
2879
  if(value.active.has(sessionID))
@@ -2842,7 +2896,7 @@ return targets.get(key)?.recent??[]
2842
2896
  var POLL_INTERVAL_MS=5000
2843
2897
  function activeStatus(status){return status?.type==="busy"||status?.type==="retry"
2844
2898
  }
2845
- function createSubagentController(api,options={}){const source=options.source
2899
+ function createSubagentModel(options){const source=options.source
2846
2900
  const now=options.now??Date.now
2847
2901
  const history=createSubagentHistory({now})
2848
2902
  const [children,setChildren]=createSignal8({})
@@ -2853,14 +2907,13 @@ const [unavailable,setUnavailable]=createSignal8({})
2853
2907
  const targets=new Map
2854
2908
  const remoteFailures=new Map
2855
2909
  const remoteRetryAt=new Map
2856
- const requests=createRequestState(api.lifecycle.signal)
2910
+ const requests=createRequestState(options.lifecycle.signal)
2857
2911
  let activeTarget
2912
+ let leases=new Set
2858
2913
  let pollTimer
2859
- const snapshots=()=>source?Object.fromEntries([...targets].map(([key,current])=>[key,source.list(current.parentID)])):children()
2860
- function target(parentID){const parent=api.state.session.get(parentID)
2861
- const directory=parent?.directory??api.state.path.directory
2862
- const workspace=parent?.workspaceID
2863
- 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
+ }
2864
2917
  function applySession(items,parentID,mutation){const next=items.filter((item)=>item.id!==(mutation.type==="upsert"?mutation.info.id:mutation.sessionID))
2865
2918
  if(mutation.type==="upsert"&&mutation.info.parentID===parentID)
2866
2919
  next.push(mutation.info)
@@ -2875,19 +2928,19 @@ history.remove(key,sessionID)
2875
2928
  }
2876
2929
  for(const journal of journals)
2877
2930
  journal.sessions.push(mutation)
2878
- if(!source)
2931
+ if(options.cache==="model")
2879
2932
  setChildren((current)=>Object.fromEntries(Object.entries(current).map(([key,items])=>[key,applySession(items,targets.get(key).parentID,mutation)])))
2880
2933
  }
2881
2934
  function recordStatus(input){const mutation={...input,at:now(),startedAt:source?.startedAt?.(input.sessionID),observed:new Set
2882
2935
  }
2883
2936
  for(const journal of journals)
2884
2937
  journal.statuses.push(mutation)
2885
- 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))
2886
2939
  continue
2887
2940
  history.observeStatus(key,mutation.sessionID,mutation.status,mutation.at,mutation.startedAt)
2888
2941
  mutation.observed.add(key)
2889
2942
  }
2890
- if(!source)
2943
+ if(options.cache==="model")
2891
2944
  setStatuses((current)=>Object.fromEntries(Object.entries(current).map(([key,value])=>[key,applyStatus(value,mutation)])))
2892
2945
  })
2893
2946
  }
@@ -2913,14 +2966,13 @@ if(!requestState){journals.delete(journal)
2913
2966
  return pending
2914
2967
  }
2915
2968
  const signal=AbortSignal.any([requestState.signal,AbortSignal.timeout(5000)])
2916
- 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)]))
2917
- }]):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
2918
2970
  for(const mutation of journal.sessions)
2919
2971
  nextChildren=applySession(nextChildren,current.parentID,mutation)
2920
- let nextStatuses={...statusResult.data}
2972
+ let nextStatuses={...snapshot.statuses}
2921
2973
  const missing={}
2922
- const remoteStatuses=source?[]:await Promise.all(nextChildren.map(async (session)=>{const key=JSON.stringify([current.key,session.id])
2923
- 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}})):[]
2924
2976
  if(!requestState.isCurrent())
2925
2977
  return
2926
2978
  for(const result of remoteStatuses){if(!result)
@@ -2946,7 +2998,7 @@ for(const mutation of journal.sessions)
2946
2998
  nextChildren=applySession(nextChildren,current.parentID,mutation)
2947
2999
  for(const mutation of journal.statuses)
2948
3000
  nextStatuses=applyStatus(nextStatuses,mutation)
2949
- 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)
2950
3002
  if(mutations.length>0){for(const mutation of mutations){if(!mutation.observed.has(current.key))
2951
3003
  history.observeStatus(current.key,session.id,mutation.status,mutation.at,mutation.startedAt)
2952
3004
  }} else if(!missing[session.id]){const status=nextStatuses[session.id]??{type:"idle"}
@@ -2956,7 +3008,7 @@ history.observeStatus(current.key,session.id,status,now(),source?.startedAt?.(se
2956
3008
  for(const mutation of journal.errors){if(mutation.sessionID===session.id&&!mutation.observed.has(current.key))
2957
3009
  history.observeError(current.key,session.id,mutation.error,mutation.at)
2958
3010
  }}
2959
- if(!source){setChildren((value)=>({...value,[current.key]:nextChildren}))
3011
+ if(options.cache==="model"){setChildren((value)=>({...value,[current.key]:nextChildren}))
2960
3012
  setStatuses((value)=>({...value,[current.key]:nextStatuses}))
2961
3013
  }
2962
3014
  setUnavailable((value)=>({...value,[current.key]:missing}))
@@ -2976,8 +3028,8 @@ return request
2976
3028
  function refresh(parentID,force=false){return refreshTarget(target(parentID),force)
2977
3029
  }
2978
3030
  function list(parentID){const current=target(parentID)
2979
- const currentStatuses=source?Object.fromEntries(source.list(parentID).map((item)=>[item.id,source.status(item.id)])):statuses()[current.key]??{}
2980
- 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))
2981
3033
  })).filter((item)=>activeStatus(item.status)).map((item)=>({...item,run:history.active(current.key,item.session.id),unavailable:unavailable()[current.key]?.[item.session.id]
2982
3034
  }))
2983
3035
  }
@@ -2987,14 +3039,25 @@ return history.recent(current.key).flatMap((run)=>{const session=sessions.get(ru
2987
3039
  return session?[{session,status:{type:"idle"},run,unavailable:false}]:[]
2988
3040
  })
2989
3041
  }
2990
- 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})
2991
- recordStatus({sessionID:event.properties.sessionID,status:{type:"idle"}})
2992
- }),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
2993
3056
  if(current)
2994
- retryBackgroundRefresh(()=>refreshTarget(current),{signal:api.lifecycle.signal}).catch(()=>{})
2995
- })]
2996
- api.lifecycle.onDispose(()=>{for(const dispose of unsubscribe)
2997
- dispose()
3057
+ retryBackgroundRefresh(()=>refreshTarget(current),{signal:options.lifecycle.signal}).catch(()=>{})
3058
+ break
3059
+ }}})
3060
+ options.lifecycle.onDispose(()=>{unsubscribe()
2998
3061
  if(pollTimer)
2999
3062
  clearTimeout(pollTimer)
3000
3063
  })
@@ -3007,15 +3070,18 @@ clearTimeout(pollTimer)
3007
3070
  requests.abortAll()
3008
3071
  refreshing.clear()
3009
3072
  activeTarget=current.key
3010
- 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)
3011
3075
  return
3012
3076
  refreshTarget(current).catch(()=>{})
3013
3077
  pollTimer=setTimeout(poll,POLL_INTERVAL_MS)
3014
3078
  }
3015
- if(!source)
3079
+ if(options.cache==="model")
3016
3080
  pollTimer=setTimeout(poll,POLL_INTERVAL_MS)
3017
3081
  }
3018
- 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)
3019
3085
  return
3020
3086
  activeTarget=undefined
3021
3087
  if(pollTimer)
@@ -3023,7 +3089,7 @@ clearTimeout(pollTimer)
3023
3089
  pollTimer=undefined
3024
3090
  requests.abortAll()
3025
3091
  refreshing.clear()
3026
- }},open(sessionID){api.route.navigate("session",{sessionID})
3092
+ }},open(sessionID){options.open(sessionID)
3027
3093
  }}}
3028
3094
  async function fetchDevTeamStatus(session,signal){const metadata=session.metadata
3029
3095
  const devTeam=metadata&&typeof metadata.devTeam==="object"?metadata.devTeam:undefined
@@ -3044,10 +3110,26 @@ return{sessionID:session.id,failed:true}
3044
3110
  const status=statuses[session.id]
3045
3111
  if(status!==undefined&&(!status||typeof status!=="object"||!("type" in status)||!["idle","busy","retry"].includes(String(status.type))))
3046
3112
  return{sessionID:session.id,failed:true}
3047
- 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}
3048
3115
  return{sessionID:session.id,status}} catch{if(signal.aborted)
3049
3116
  return
3050
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
+ }
3051
3133
 
3052
3134
  function filterSubagentView(rows,mode,query){const needle=query.trim().toLocaleLowerCase()
3053
3135
  return rows.filter((row)=>{if(mode==="active"&&row.group!=="Active")
@@ -3085,30 +3167,32 @@ onCleanup6(()=>clearInterval(timer))
3085
3167
  return now
3086
3168
  }
3087
3169
 
3088
- function createTodoController(api){const [sessions,setSessions]=createSignal10({})
3170
+ function createTodoModel(options){const [sessions,setSessions]=createSignal10({})
3089
3171
  const refreshing=new Map
3090
3172
  const revisions=new Map
3091
- const requests=createRequestState(api.lifecycle.signal)
3173
+ const requests=createRequestState(options.lifecycle.signal)
3092
3174
  let activeTarget
3093
- function target(sessionID){const session=api.state.session.get(sessionID)
3094
- const directory=session?.directory??api.state.path.directory
3095
- const workspace=session?.workspaceID
3096
- 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
+ }
3097
3178
  function set(current,todos){setSessions((value)=>({...value,[current.key]:todos}))
3098
3179
  }
3099
3180
  function list(sessionID){const current=target(sessionID)
3100
- 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)
3101
3182
  }
3102
- 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)
3103
3186
  const pending=refreshing.get(current.key)
3104
3187
  if(pending&&!force)
3105
3188
  return pending
3106
- const revision=revisions.get(sessionID)??0
3107
- 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)
3108
3191
  if(!requestState)
3109
3192
  return pending
3110
- const request=api.client.session.todo({sessionID,...current.routing},{throwOnError:true,signal:requestState.signal}).then((result)=>{const todos=result.data??[]
3111
- 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)
3112
3196
  set(current,todos)
3113
3197
  requestState.succeed()
3114
3198
  return todos
@@ -3121,24 +3205,37 @@ refreshing.delete(current.key)
3121
3205
  refreshing.set(current.key,request)
3122
3206
  return request
3123
3207
  }
3124
- const unsubscribe=api.event.on("todo.updated",(event)=>{const sessionID=event.properties.sessionID
3125
- revisions.set(sessionID,(revisions.get(sessionID)??0) + 1)
3126
- 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)
3127
3211
  })
3128
- api.lifecycle.onDispose(unsubscribe)
3212
+ if(unsubscribe)
3213
+ options.lifecycle.onDispose(unsubscribe)
3129
3214
  return{target,list,refresh,state(sessionID){return requests.state(target(sessionID).key)
3130
3215
  },retry(sessionID){return refresh(sessionID,true)
3131
3216
  },activate(sessionID){const key=target(sessionID).key
3132
3217
  if(activeTarget!==key){requests.abortAll()
3133
3218
  refreshing.clear()
3134
3219
  activeTarget=key
3220
+ leases=new Set
3135
3221
  }
3136
- return()=>{if(activeTarget!==key)
3222
+ const lease=Symbol(key)
3223
+ leases.add(lease)
3224
+ return()=>{if(activeTarget!==key||!leases.delete(lease)||leases.size>0)
3137
3225
  return
3138
3226
  activeTarget=undefined
3139
3227
  requests.abortAll()
3140
3228
  refreshing.clear()
3141
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
+ }
3142
3239
 
3143
3240
  function buildTodoView(todos,mode){const active=[...todos.filter((item)=>item.status==="in_progress"),...todos.filter((item)=>item.status==="pending")]
3144
3241
  const completed=todos.filter((item)=>item.status==="completed")
@@ -3176,91 +3273,24 @@ function openKeyboardHelp(api,iconStyle=()=>"nerd",multilineCorners=()=>true,sho
3176
3273
 
3177
3274
 
3178
3275
 
3179
-
3180
- var defaults={todo:true,lsp:true}
3181
- var capabilities=new WeakMap
3182
- function setHostCapabilities(api,value){capabilities.set(api,value)
3183
- }
3184
- function hostCapabilities(api){return capabilities.get(api)??defaults
3185
- }
3186
- function hostCapabilityUnavailable(api,section){return section==="todo"||section==="lsp"?hostCapabilities(api).unavailable?.[section]:undefined
3187
- }
3188
- function supportsSidebarSection(api,section){const current=hostCapabilities(api)
3189
- return section!=="todo"&&section!=="lsp"?true:current[section]
3190
- }
3191
- function supportsTitleVisibility(api){return hostCapabilities(api).titleVisibility!==false
3192
- }
3193
- function supportsPermissionMode(api){return hostCapabilities(api).permissionMode!==false
3194
- }
3195
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"]]
3196
3277
  var QUICK_ACTIONS=QUICK_ACTION_IDS.map((command,index)=>({command,icon:QUICK_ACTION_METADATA[index][0],label:QUICK_ACTION_METADATA[index][1]
3197
3278
  }))
3198
3279
  function quickActionRequiresSession(command){return command!=="session.list"&&command!=="session.new"&&command!=="permission.mode"
3199
3280
  }
3200
- function quickActionLabel(api,action){if(action.command!=="permission.mode")
3201
- return action.label
3202
- const command=api.keymap.getCommands?.({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
3203
- return typeof command?.title==="string"?command.title:action.label
3204
- }
3205
- function createQuickActionRevision(api){const [revision,setRevision]=createSignal12(0)
3206
- const unsubscribe=api.keymap.on?.("state",()=>setRevision((value)=>value + 1))
3207
- if(unsubscribe)
3208
- onCleanup8(unsubscribe)
3209
- return revision
3210
- }
3211
3281
  function orderQuickActionIds(order,favorites){return [...order.filter((id)=>favorites.has(id)),...order.filter((id)=>!favorites.has(id))]
3212
3282
  }
3213
- function quickActionDisabledReason(api,action){const route=api.route.current
3214
- if(quickActionRequiresSession(action.command)&&route.name!=="session")
3215
- return "Requires an open session"
3216
- const getCommands=api.keymap.getCommands?.bind(api.keymap)
3217
- if(!getCommands)
3218
- return
3219
- const command=getCommands({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
3220
- if(!command)
3221
- return hostCapabilities(api).commandsReachableOnly?"Unavailable in the current session":"Unavailable in this OpenCode version"
3222
- const enabled=command.enabled
3223
- if(enabled===false||typeof enabled==="function"&&!enabled())
3224
- return "Unavailable in the current session"
3225
- return
3226
- }
3227
- var sources=new WeakMap
3228
- function setQuickActionStateSource(api,source){sources.set(api,source)
3229
- }
3230
- function currentPermissionMode(api){const title=api.keymap.getCommands?.({visibility:"registered"}).find((command)=>command.name==="permission.mode")?.title
3231
- if(typeof title!=="string")
3232
- return
3233
- if(title.startsWith("Disable auto-approve permissions"))
3234
- return true
3235
- return title.startsWith("Enable auto-approve permissions")?false:undefined
3236
- }
3237
- function quickActionStates(api,id){if(id==="permission.mode"&&currentPermissionMode(api)!==undefined)
3238
- return [{id:"on",label:"Auto-approve on"},{id:"off",label:"Auto-approve off"}]
3239
- if(id==="session.compact"&&sources.has(api))
3240
- return [{id:"idle",label:"Idle"},{id:"compacting",label:"Compacting"}]
3241
- return []
3242
- }
3243
- function quickActionState(api,id){if(id==="permission.mode"){const mode=currentPermissionMode(api)
3244
- if(mode===undefined)
3245
- return
3246
- return mode?"on":"off"
3247
- }
3248
- return sources.get(api)?.(id)
3249
- }
3250
3283
  var SEARCH_GROUPS=["Skills","Subagents","MCP","Actions"]
3251
3284
  var SEARCH_SECTIONS={Skills:"skills",Subagents:"subagents",MCP:"mcp",Actions:"quick_actions"
3252
3285
  }
3253
- function searchContext(api){const route=api.route.current
3254
- const sessionID=route.name==="session"&&"params" in route&&typeof route.params?.sessionID==="string"?route.params.sessionID:undefined
3255
- const location=currentLocation(api)
3256
- return{sessionID,location,key:JSON.stringify([route.name,sessionID??null,location.key])}}
3257
3286
  function buildSearchCandidates(input){const recent=new Map(input.recentSkills.map((location,index)=>[location,index]))
3258
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))
3259
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))
3260
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
3261
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
3262
- })),...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
3263
- })),...orderQuickActionIds(input.actionOrder,input.favoriteQuickActions??new Set).flatMap((id)=>{const action=QUICK_ACTIONS.find((item)=>item.command===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)
3264
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)
3265
3295
  }]:[]
3266
3296
  })]
@@ -3297,6 +3327,11 @@ return [{item,score,index}]
3297
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)
3298
3328
  return scored.map(({item})=>item)
3299
3329
  }
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])}}
3334
+
3300
3335
 
3301
3336
 
3302
3337
 
@@ -3310,6 +3345,69 @@ return scored.map(({item})=>item)
3310
3345
 
3311
3346
 
3312
3347
 
3348
+ var defaults={todo:true,lsp:true}
3349
+ var capabilities=new WeakMap
3350
+ function setHostCapabilities(api,value){capabilities.set(api,value)
3351
+ }
3352
+ function hostCapabilities(api){return capabilities.get(api)??defaults
3353
+ }
3354
+ function hostCapabilityUnavailable(api,section){return section==="todo"||section==="lsp"?hostCapabilities(api).unavailable?.[section]:undefined
3355
+ }
3356
+ function supportsSidebarSection(api,section){const current=hostCapabilities(api)
3357
+ return section!=="todo"&&section!=="lsp"?true:current[section]
3358
+ }
3359
+ function supportsTitleVisibility(api){return hostCapabilities(api).titleVisibility!==false
3360
+ }
3361
+ function supportsPermissionMode(api){return hostCapabilities(api).permissionMode!==false
3362
+ }
3363
+ function quickActionLabel(api,action){if(action.command!=="permission.mode")
3364
+ return action.label
3365
+ const command=api.keymap.getCommands?.({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
3366
+ return typeof command?.title==="string"?command.title:action.label
3367
+ }
3368
+ function createQuickActionRevision(api){const [revision,setRevision]=createSignal12(0)
3369
+ const unsubscribe=api.keymap.on?.("state",()=>setRevision((value)=>value + 1))
3370
+ if(unsubscribe)
3371
+ onCleanup8(unsubscribe)
3372
+ return revision
3373
+ }
3374
+ function quickActionDisabledReason(api,action){const route=api.route.current
3375
+ if(quickActionRequiresSession(action.command)&&route.name!=="session")
3376
+ return "Requires an open session"
3377
+ const getCommands=api.keymap.getCommands?.bind(api.keymap)
3378
+ if(!getCommands)
3379
+ return
3380
+ const command=getCommands({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
3381
+ if(!command)
3382
+ return hostCapabilities(api).commandsReachableOnly?"Unavailable in the current session":"Unavailable in this OpenCode version"
3383
+ const enabled=command.enabled
3384
+ if(enabled===false||typeof enabled==="function"&&!enabled())
3385
+ return "Unavailable in the current session"
3386
+ return
3387
+ }
3388
+ var sources=new WeakMap
3389
+ function setQuickActionStateSource(api,source){sources.set(api,source)
3390
+ }
3391
+ function currentPermissionMode(api){const title=api.keymap.getCommands?.({visibility:"registered"}).find((command)=>command.name==="permission.mode")?.title
3392
+ if(typeof title!=="string")
3393
+ return
3394
+ if(title.startsWith("Disable auto-approve permissions"))
3395
+ return true
3396
+ return title.startsWith("Enable auto-approve permissions")?false:undefined
3397
+ }
3398
+ function quickActionStates(api,id){if(id==="permission.mode"&&currentPermissionMode(api)!==undefined)
3399
+ return [{id:"on",label:"Auto-approve on"},{id:"off",label:"Auto-approve off"}]
3400
+ if(id==="session.compact"&&sources.has(api))
3401
+ return [{id:"idle",label:"Idle"},{id:"compacting",label:"Compacting"}]
3402
+ return []
3403
+ }
3404
+ function quickActionState(api,id){if(id==="permission.mode"){const mode=currentPermissionMode(api)
3405
+ if(mode===undefined)
3406
+ return
3407
+ return mode?"on":"off"
3408
+ }
3409
+ return sources.get(api)?.(id)
3410
+ }
3313
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)
3314
3412
  }
3315
3413
  function createSearchDialogController(props){const context=searchContext(props.api)
@@ -3395,7 +3493,7 @@ function selectTab(tab){if(tab!==activeTab()){tabViews.set(activeTab(),{selected
3395
3493
  })
3396
3494
  const saved=tabViews.get(tab)
3397
3495
  pendingScroll=saved?.scrollTop??0
3398
- batch2(()=>{setActiveTab(tab)
3496
+ batch3(()=>{setActiveTab(tab)
3399
3497
  setSelectedID(saved?.selectedID)
3400
3498
  })
3401
3499
  }
@@ -3700,14 +3798,17 @@ clearTimeout(timer)
3700
3798
  return _$createElement("box")
3701
3799
  }
3702
3800
 
3703
- 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()
3704
3803
  const valid=()=>{const current=controller.target()
3705
- return current.key===target.key&&current.scope===target.scope
3804
+ return isCurrent()&&current.key===target.key&&current.scope===target.scope
3706
3805
  }
3707
3806
  const preset=createMemo2(()=>preferences.mcpPresets()[name])
3708
- const preview=createMemo2(()=>mcpPresetPreview(controller.list(target),preset()??{}))
3807
+ const preview=createMemo2(()=>mcpPresetPreview(valid()?controller.list(target):[],preset()??{}))
3709
3808
  const state=createMemo2(()=>controller.state(target))
3710
- const blocked=createMemo2(()=>{if(!preset())
3809
+ const blocked=createMemo2(()=>{if(!valid())
3810
+ return "Session or server changed; reopen the preview."
3811
+ if(!preset())
3711
3812
  return "Preset no longer exists."
3712
3813
  if(controller.mutating(target))
3713
3814
  return "Wait for the current MCP operation to finish."
@@ -5629,6 +5730,9 @@ return _$createComponent(SectionWithHeaderAction,{get api(){return props.api
5629
5730
 
5630
5731
 
5631
5732
 
5733
+ var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"]);
5734
+
5735
+
5632
5736
 
5633
5737
 
5634
5738
 
@@ -5686,6 +5790,35 @@ return _$createComponent(SectionWithHeaderAction,{get api(){return props.api
5686
5790
 
5687
5791
 
5688
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
+ }
5689
5822
 
5690
5823
 
5691
5824
 
@@ -5739,12 +5872,12 @@ function QuickActionRow(props){const{api,interaction
5739
5872
  const icons=useIcons()
5740
5873
  const toolbar=useToolbar()
5741
5874
  const theme=()=>api.theme.current
5742
- const [width,setWidth]=createSignal33(0)
5743
- 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]
5744
5877
  })
5745
5878
  return api.keys.formatBindings(bindings.get(props.action.command))
5746
5879
  })
5747
- const visibleShortcut=createMemo12(()=>{const value=shortcut()
5880
+ const visibleShortcut=createMemo13(()=>{const value=shortcut()
5748
5881
  return value&&width()>=Bun.stringWidth(props.action.label) + Bun.stringWidth(value) + (toolbar?11:8)?value:undefined
5749
5882
  })
5750
5883
  function run(){if(props.action.command==="session.new"&&props.onNewSession?.())
@@ -5808,7 +5941,7 @@ return _el$4
5808
5941
  }})
5809
5942
  }
5810
5943
  function QuickActionsSection(props){const commandRevision=createQuickActionRevision(props.api)
5811
- const actions=createMemo13(()=>{commandRevision()
5944
+ const actions=createMemo14(()=>{commandRevision()
5812
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)
5813
5946
  return action&&props.preferences.quickActionVisible?.(id)!==false?[{...action,label:quickActionLabel(props.api,action),disabled:quickActionDisabledReason(props.api,action)
5814
5947
  }]:[]
@@ -5892,13 +6025,13 @@ item.activate(event)
5892
6025
 
5893
6026
 
5894
6027
  function SidebarToggleBinding(props){const shortcuts=createSidebarShortcutMode(props.api)
5895
- createEffect16(()=>{const key=props.preferences.toggleKey()
6028
+ createEffect17(()=>{const key=props.preferences.toggleKey()
5896
6029
  untrack6(()=>shortcuts.bind(key))
5897
6030
  })
5898
- 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"))
5899
6032
  untrack6(shortcuts.cancel)
5900
6033
  })
5901
- onCleanup20(shortcuts.dispose)
6034
+ onCleanup21(shortcuts.dispose)
5902
6035
  return(()=>{var _el$=_$createElement("box")
5903
6036
  _$setProp(_el$,"position","absolute")
5904
6037
  _$setProp(_el$,"top",0)
@@ -5920,11 +6053,11 @@ const unregisterCommands=props.api.keymap.registerLayer({commands:[...commands,.
5920
6053
  })=>({name:name.replace(PLUGIN_ID,()=>LEGACY_PLUGIN_ID),run,enabled
5921
6054
  }))]
5922
6055
  })
5923
- onCleanup20(unregisterCommands)
5924
- 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
5925
6058
  }]
5926
6059
  })
5927
- onCleanup20(unregister)
6060
+ onCleanup21(unregister)
5928
6061
  })
5929
6062
  return _$createElement("box")
5930
6063
  }
@@ -5976,6 +6109,26 @@ rows.push(visible)
5976
6109
  }
5977
6110
  return{columns,overflow,rows,...widths&&{sizes}}}
5978
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}}
5979
6132
  function toolbarPinWidth(pin){const states=Object.keys({...pin.stateIcons,...pin.stateIconLists})
5980
6133
  const count=Math.max(toolbarPinIcons(pin).length,...states.map((state)=>toolbarPinIcons(pin,state).length))
5981
6134
  const iconsWidth=Math.max(0,count*2-1)
@@ -5984,49 +6137,34 @@ return 2 + Math.max(1,iconsWidth + (count&&pin.text?1:0) + Bun.stringWidth(pin.t
5984
6137
  function createToolbarModel(props){const icons=useIcons()
5985
6138
  const revision=createQuickActionRevision(props.api)
5986
6139
  const pins=()=>props.preferences.toolbarPins?.()??[]
5987
- const needsSkills=createMemo14(()=>pins().some((pin)=>pin.kind==="skill"))
5988
- const target=createMemo14(()=>props.skills.target())
5989
- createEffect17(()=>{if(!needsSkills())
6140
+ const needsSkills=createMemo15(()=>pins().some((pin)=>pin.kind==="skill"))
6141
+ const target=createMemo15(()=>props.skills.target())
6142
+ createEffect18(()=>{if(!needsSkills())
5990
6143
  return
5991
6144
  const current=target()
5992
6145
  const release=untrack7(()=>props.skills.activate(current))
5993
- onCleanup21(release)
6146
+ onCleanup22(release)
5994
6147
  untrack7(()=>{props.skills.refresh(current).catch(()=>{})
5995
6148
  })
5996
6149
  })
5997
- function view(pin){revision()
5998
- if(pin.kind==="action"){const action=QUICK_ACTIONS.find((item)=>item.command===pin.id)
5999
- const state=quickActionState(props.api,pin.id)
6000
- const stateLabel=quickActionStates(props.api,pin.id).find((item)=>item.id===state)?.label
6001
- return{label:quickActionLabel(props.api,action),stateLabel,icons:toolbarPinIcons(pin,state),text:pin.text,reason:quickActionDisabledReason(props.api,action)
6002
- }}
6003
- 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)
6004
6155
  const request=props.skills.state(target())
6005
6156
  const unavailable=request.status==="loading"?"Loading Skills":"Skill is unavailable in this workspace"
6006
- return{label:pin.name,icons:toolbarPinIcons(pin),text:pin.text,stateLabel:undefined,reason:request.error?.message??(skill?skillUnavailable(props.skills,skill):unavailable)
6007
- }}
6008
- function run(key){const pin=pins().find((item)=>toolbarPinKey(item)===key)
6009
- if(!pin)
6010
- return
6011
- const item=view(pin)
6012
- if(item.reason){props.api.ui.toast({variant:"warning",title:item.label,message:item.reason})
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?.())
6013
6158
  return
6014
- }
6015
- if(pin.kind==="action"){if(pin.id==="session.new"&&props.onNewSession?.())
6016
- return
6017
- const result=props.interaction.dispatchFromReturnTarget(pin.id)
6159
+ const result=props.interaction.dispatchFromReturnTarget(id)
6018
6160
  if(!result.ok)
6019
- props.api.ui.toast({variant:"warning",title:item.label,message:`Command is ${result.reason}`})
6020
- return
6021
- }
6022
- const skill=props.skills.list(target()).find((candidate)=>candidate.location===pin.location&&candidate.name===pin.name)
6023
- if(!skill)
6024
- return
6025
- 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)
6026
6164
  const dialogs=createDialogStack(props.api,icons.style,()=>route===JSON.stringify(props.api.route.current),icons.multilineCorners)
6027
6165
  activateSkill({api:props.api,controller:props.skills,access:props.preferences,skill,dialogs})
6028
- }
6029
- return{pins,view,run,width:toolbarPinWidth}}
6166
+ }})
6167
+ return{...model,width:toolbarPinWidth}}
6030
6168
 
6031
6169
 
6032
6170
 
@@ -6036,13 +6174,13 @@ return{pins,view,run,width:toolbarPinWidth}}
6036
6174
 
6037
6175
  function ToolbarButton(props){const icons=useIcons()
6038
6176
  const foreground=()=>props.reason?props.api.theme.current.textMuted:item.foregroundColor()
6039
- const [hovered,setHovered]=createSignal34(false)
6177
+ const [hovered,setHovered]=createSignal35(false)
6040
6178
  const item=useSidebarItem(props.api,props.interaction,{id:props.id,position:()=>({section:0,row:props.row + 1,column:props.column
6041
6179
  }),activate:props.onRun
6042
6180
  },()=>props.reason?props.api.theme.current.textMuted:props.api.theme.current.accent,"control")
6043
6181
  const content=()=>[props.icons.map((icon)=>terminalIcon(icon,icons.style())).join(" "),props.text].filter(Boolean).join(" ")
6044
- createEffect18(()=>props.onDetails(props.id,item.focused()||hovered()))
6045
- onCleanup22(()=>props.onDetails(props.id,false))
6182
+ createEffect19(()=>props.onDetails(props.id,item.focused()||hovered()))
6183
+ onCleanup23(()=>props.onDetails(props.id,false))
6046
6184
  return _$createComponent(Show27,{get when(){return _$memo(()=>!!(props.width===3&&props.icons.length===1))()&&!props.text
6047
6185
  },get fallback(){return _$createComponent(SelectionBox,{get id(){return props.id
6048
6186
  },ref(r$){var _ref$2=item.ref
@@ -6083,17 +6221,17 @@ setHovered(false)
6083
6221
  }
6084
6222
  function SidebarToolbar(props){const model=createToolbarModel(props)
6085
6223
  const icons=useIcons()
6086
- const [width,setWidth]=createSignal35(3)
6087
- const [detail,setDetail]=createSignal35()
6088
- const keys=createMemo15(()=>model.pins().map((pin)=>toolbarPinKey(pin)))
6089
- const widths=createMemo15(()=>new Map(model.pins().map((pin)=>[toolbarPinKey(pin),model.width(pin)])))
6090
- const layout=createMemo15(()=>toolbarLayout(keys(),width(),props.preferences.toolbarOverflow?.()??"menu",widths()))
6091
- const rows=createMemo15(()=>layout().rows.map((_,index)=>index))
6092
- 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])))
6093
6231
  const view=(key)=>{const pin=byKey().get(key)
6094
6232
  return pin&&model.view(pin)
6095
6233
  }
6096
- const details=createMemo15(()=>{if(detail()==="toolbar:more")
6234
+ const details=createMemo16(()=>{if(detail()==="toolbar:more")
6097
6235
  return "More pinned actions and Skills"
6098
6236
  const item=view(detail()??"")
6099
6237
  return item?[item.label,item.stateLabel,item.reason].filter(Boolean).join(" \xB7 "):undefined
@@ -6166,18 +6304,18 @@ return _el$
6166
6304
 
6167
6305
 
6168
6306
 
6169
- function createSkillSectionModel(props,query){const target=createMemo16(()=>props.controller.target())
6170
- const recent=createMemo16(()=>new Map((props.preferences.recentSkills?.()??[]).map((location,index)=>[location,index])))
6171
- const groups=createMemo16(()=>props.preferences.skillGroups?.()??{})
6172
- const grouped=createMemo16(()=>Object.keys(groups()).length>0)
6173
- const list=createMemo16(()=>buildSkillGroupedView(props.controller.list(target()),props.preferences.favoriteSkills?.()??new Set,groups(),recent()))
6174
- 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)))
6175
6313
  const visibility=createListVisibility({items:filtered,limit:()=>props.preferences.sectionItemLimit?.("skills")??0,resetKey:()=>JSON.stringify([target().key,query()])
6176
6314
  })
6177
- const state=createMemo16(()=>props.controller.state(target()))
6178
- createEffect19(()=>{const current=target()
6315
+ const state=createMemo17(()=>props.controller.state(target()))
6316
+ createEffect20(()=>{const current=target()
6179
6317
  const deactivate=untrack8(()=>props.controller.activate?.(current)??(()=>{}))
6180
- onCleanup23(deactivate)
6318
+ onCleanup24(deactivate)
6181
6319
  untrack8(()=>void props.controller.refresh(current).catch(()=>{}))
6182
6320
  })
6183
6321
  async function useSkill(item){try{const isInserted=await props.controller.use(target(),item.name)
@@ -6238,7 +6376,7 @@ return _el$
6238
6376
  }
6239
6377
  function SkillsSection(props){const dialogs=useDialogs(props.api)
6240
6378
  const toolbar=useToolbar()
6241
- const [query,setQuery]=createSignal36("")
6379
+ const [query,setQuery]=createSignal37("")
6242
6380
  const model=createSkillSectionModel(props,query)
6243
6381
  const navigationSection=()=>props.navigationSection??3
6244
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"
@@ -6315,34 +6453,34 @@ return _el$
6315
6453
 
6316
6454
 
6317
6455
 
6318
- function createSubagentSectionModel(props){const list=createMemo17(()=>props.controller.list(props.sessionID))
6319
- const recent=createMemo17(()=>props.controller.recent?.(props.sessionID)??[])
6320
- const rows=createMemo17(()=>buildSubagentView(list(),recent()))
6321
- const [query,setQuery]=createSignal37("")
6322
- const [mode,setMode]=createSignal37("all")
6323
- const counts=createMemo17(()=>countSubagentView(rows(),query()))
6324
- const tabs=createMemo17(()=>["all","active","recent",...counts().errors>0?["errors"]:[]])
6325
- const filtered=createMemo17(()=>filterSubagentView(rows(),mode(),query()))
6326
- const parentID=createMemo17(()=>props.api.state?.session?.get(props.sessionID)?.parentID)
6327
- const targetKey=createMemo17(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6328
- 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()
6329
6467
  setQuery("")
6330
6468
  setMode("all")
6331
6469
  })
6332
- createEffect20(()=>{if(mode()!=="all"&&counts()[mode()]===0)
6470
+ createEffect21(()=>{if(mode()!=="all"&&counts()[mode()]===0)
6333
6471
  setMode("all")
6334
6472
  })
6335
6473
  const visibility=createListVisibility({items:filtered,limit:()=>props.preferences.sectionItemLimit?.("subagents")??0,resetKey:()=>JSON.stringify([targetKey(),query(),mode()])
6336
6474
  })
6337
- 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)
6338
6476
  const now=createSubagentClock(clockKey)
6339
- const byId=createMemo17(()=>new Map(visibility.visible().map((item)=>[item.session.id,item])))
6340
- const ids=createMemo17(()=>visibility.visible().map((item)=>item.session.id))
6341
- const state=createMemo17(()=>props.controller.state(props.sessionID))
6342
- 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
6343
6481
  targetKey()
6344
6482
  const deactivate=untrack9(()=>props.controller.activate?.(sessionID)??(()=>{}))
6345
- onCleanup24(deactivate)
6483
+ onCleanup25(deactivate)
6346
6484
  untrack9(()=>void props.controller.refresh(sessionID).catch(()=>{}))
6347
6485
  })
6348
6486
  return{list,recent,rows,query,setQuery,mode,setMode,counts,tabs,filtered,parentID,visibility,now,byId,ids,state
@@ -6729,28 +6867,28 @@ return _el$2
6729
6867
  return _el$
6730
6868
  }})
6731
6869
  }
6732
- function TodoSection(props){const list=createMemo18(()=>props.controller.list(props.sessionID))
6870
+ function TodoSection(props){const list=createMemo19(()=>props.controller.list(props.sessionID))
6733
6871
  const icons=useIcons()
6734
6872
  const unavailable=()=>hostCapabilityUnavailable(props.api,"todo")
6735
6873
  const managed=()=>("guidance" in props.controller)?props.controller:undefined
6736
6874
  const native=()=>("source" in props.controller)&&props.controller.source==="native"
6737
6875
  const setup=()=>("setup" in props.controller)?props.controller.setup:undefined
6738
6876
  const setupState=()=>setup()?.state(props.sessionID)
6739
- const [mode,setMode]=createSignal38("all")
6740
- const targetKey=createMemo18(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6741
- createEffect21(()=>{targetKey()
6877
+ const [mode,setMode]=createSignal39("all")
6878
+ const targetKey=createMemo19(()=>props.controller.target?.(props.sessionID).key??props.sessionID)
6879
+ createEffect22(()=>{targetKey()
6742
6880
  setMode("all")
6743
6881
  })
6744
- const view=createMemo18(()=>buildTodoView(list(),mode()))
6882
+ const view=createMemo19(()=>buildTodoView(list(),mode()))
6745
6883
  const visibility=createListVisibility({items:()=>view().rows,limit:()=>props.preferences.sectionItemLimit?.("todo")??0,resetKey:()=>JSON.stringify([targetKey(),mode()])
6746
6884
  })
6747
- const state=createMemo18(()=>props.controller.state(props.sessionID))
6748
- createEffect21(()=>{const sessionID=props.sessionID
6885
+ const state=createMemo19(()=>props.controller.state(props.sessionID))
6886
+ createEffect22(()=>{const sessionID=props.sessionID
6749
6887
  targetKey()
6750
6888
  if(unavailable())
6751
6889
  return
6752
6890
  const deactivate=untrack10(()=>props.controller.activate?.(sessionID)??(()=>{}))
6753
- onCleanup25(deactivate)
6891
+ onCleanup26(deactivate)
6754
6892
  untrack10(()=>void props.controller.refresh(sessionID).catch(()=>{}))
6755
6893
  untrack10(()=>void setup()?.refresh(sessionID).catch(()=>{}))
6756
6894
  })
@@ -6819,8 +6957,8 @@ return _el$4
6819
6957
  }})
6820
6958
  }
6821
6959
  function SidebarContent(props){const sections=props.preferences.sections
6822
- const visibleSections=createMemo19(()=>props.preferences.sectionOrder().filter((section)=>sections()[section]&&supportsSidebarSection(props.api,section)))
6823
- 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))
6824
6962
  return _$createComponent(ToolbarProvider,{get api(){return props.api
6825
6963
  },get preferences(){return props.preferences
6826
6964
  },get skills(){return props.skills
@@ -6871,8 +7009,8 @@ return _el$
6871
7009
  }
6872
7010
 
6873
7011
 
6874
- function McpPersistence(props){const persist=createMemo20(()=>props.controller.persist())
6875
- createEffect22(()=>{if(!props.api.state.ready)
7012
+ function McpPersistence(props){const persist=createMemo21(()=>props.controller.persist())
7013
+ createEffect23(()=>{if(!props.api.state.ready)
6876
7014
  return
6877
7015
  persist()
6878
7016
  const route=props.api.route.current
@@ -6880,12 +7018,12 @@ const params="params" in route?route.params:undefined
6880
7018
  if(typeof params?.sessionID!=="string")
6881
7019
  return
6882
7020
  const current=props.controller.target()
6883
- onCleanup27(()=>props.controller.deactivate(current))
7021
+ onCleanup28(()=>props.controller.deactivate(current))
6884
7022
  untrack11(()=>void props.controller.activate(current).catch(()=>{}))
6885
7023
  })
6886
7024
  return _$createElement("box")
6887
7025
  }
6888
- function PreferencesPersistence(props){createEffect22(()=>{const location=currentLocation(props.api)
7026
+ function PreferencesPersistence(props){createEffect23(()=>{const location=currentLocation(props.api)
6889
7027
  untrack11(()=>props.controller.setActiveScope(preferencesScope({directory:location.routing.directory,worktree:props.api.state.path.worktree
6890
7028
  })))
6891
7029
  props.controller.load()
@@ -6893,7 +7031,7 @@ props.controller.load()
6893
7031
  return _$createElement("box")
6894
7032
  }
6895
7033
  function FirstRunWizardPersistence(props){let isChecked=false
6896
- createEffect22(()=>{if(isChecked)
7034
+ createEffect23(()=>{if(isChecked)
6897
7035
  return
6898
7036
  isChecked=true
6899
7037
  showFirstRunWizard(props.api,props.preferences).catch(()=>{})
@@ -6911,7 +7049,7 @@ return _$createElement("box")
6911
7049
 
6912
7050
  function SidebarTitle(props){const icons=useIcons()
6913
7051
  const theme=()=>props.api.theme.current
6914
- 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
6915
7053
  return value===undefined?undefined:`Created ${new Date(value).toLocaleDateString("en", {
6916
7054
  dateStyle: "medium"
6917
7055
  })}`
@@ -6920,7 +7058,7 @@ const settingsId=`${PLUGIN_ID}.settings`
6920
7058
  const settings=useSidebarItem(props.api,props.interaction,{id:settingsId,position:{section:0,row:0,column:0
6921
7059
  },activate:()=>openSettings(props.api,props.preferences,undefined,props.mcp,props.skills)
6922
7060
  },()=>theme().textMuted,"control")
6923
- onCleanup28(()=>props.interaction.setTitleRoot(undefined))
7061
+ onCleanup29(()=>props.interaction.setTitleRoot(undefined))
6924
7062
  return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("box"),_el$3=_$createElement("box")
6925
7063
  _$insertNode(_el$,_el$2)
6926
7064
  _$use((node)=>props.interaction.setTitleRoot(node),_el$)
@@ -6980,40 +7118,10 @@ const [user,project]=await Promise.all([safe(userPath?readSource(userPath,()=>re
6980
7118
  return mergeConfiguredDefaults(configuredDefaultsFromPluginOptions(options),user,project)
6981
7119
  }
6982
7120
 
6983
-
6984
- var scheduleInterval=(callback,milliseconds)=>{const timer=setInterval(callback,milliseconds)
6985
- return()=>clearInterval(timer)
6986
- }
6987
- function createLimitsRefresh(options,schedule=scheduleInterval){let disposed=false
6988
- const selectedKey=createMemo22(()=>JSON.stringify(options.selectedModel?.()))
6989
- const targetKey=createMemo22(()=>JSON.stringify(options.controller.current().model))
6990
- const interval=createMemo22(()=>(parseLimitsRefreshMinutes(options.refreshMinutes?.())??DEFAULT_LIMITS_REFRESH_MINUTES)*60000)
6991
- createEffect23(on2(selectedKey,()=>{const model=options.selectedModel?.()
6992
- if(model&&options.accountSource)
6993
- options.accountSource.refresh(model).catch(()=>{})
6994
- }))
6995
- createEffect23(on2(targetKey,()=>{const{model,adapter}=options.controller.current()
6996
- if(model&&adapter)
6997
- options.controller.refresh()
6998
- }))
6999
- async function refresh(){if(disposed)
7000
- return
7001
- const target=targetKey()
7002
- const model=options.selectedModel?.()
7003
- if(model&&options.accountSource)
7004
- await options.accountSource.refresh(model)
7005
- if(!disposed&&target===targetKey())
7006
- await options.controller.refresh()
7007
- }
7008
- createEffect23(on2(interval,(milliseconds)=>{onCleanup29(schedule(()=>void refresh().catch(()=>{}),milliseconds))
7009
- }))
7010
- onCleanup29(()=>{disposed=true
7011
- })
7012
- }
7013
7121
  function LimitsPersistence(props){createLimitsRefresh(props)
7014
7122
  return _$createElement("box")
7015
7123
  }
7016
- function bindNavigatorAbout(api,status,updates,notes){const dispose=registerNavigatorAboutServices(api,{navigatorVersion:"0.26.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()
7017
7125
  if(target)
7018
7126
  updates.navigator(target,dialogs)
7019
7127
  },changelog:async ()=>notes.parseChangelog(await notes.readBundledChangelog())
@@ -7149,7 +7257,7 @@ context.keymap.dispatch(command.id)
7149
7257
  context.renderer.keyInput.on("keypress",onKeypress)
7150
7258
  const keymap={registerLayer(layer){const id=++sequence
7151
7259
  const owner=getOwner()
7152
- const [active,setActive]=createSignal39(true)
7260
+ const [active,setActive]=createSignal40(true)
7153
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)
7154
7262
  }))
7155
7263
  }
@@ -7194,7 +7302,7 @@ cleanups.clear()
7194
7302
  context.renderer.keyInput.off("keypress",onKeypress)
7195
7303
  }}}
7196
7304
 
7197
- function createV2SessionAdapter(context){const [retryDetails,setRetryDetails]=createSignal40({})
7305
+ function createV2SessionAdapter(context){const [retryDetails,setRetryDetails]=createSignal41({})
7198
7306
  const projections=new WeakMap
7199
7307
  function normalize(info){const directory=info.location.directory
7200
7308
  const workspaceID=context.location?.directory===directory?context.location.workspaceID:undefined
@@ -7361,48 +7469,18 @@ return parseTodos(item.state.input.todos)
7361
7469
  }}
7362
7470
  return []
7363
7471
  }
7364
- function createNativeTodoController(context,api){const requests=createRequestState(api.lifecycle.signal)
7365
- const pending=new Map
7366
- let activeTarget
7367
- 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)
7368
7473
  const directory=session?.directory??api.state.path.directory
7369
7474
  const workspace=session?.workspaceID
7370
7475
  return{key:JSON.stringify([sessionID,directory,workspace??null]),sessionID,routing:{directory,...workspace&&{workspace}}}}
7371
7476
  function list(sessionID){return nativeTodos(context.data.session.message.list(sessionID),context.data.session.get(sessionID)?.revert?.messageID)
7372
7477
  }
7373
- function refresh(sessionID,force=false){if(api.lifecycle.signal.aborted)
7374
- return Promise.resolve([])
7375
- const current=target(sessionID)
7376
- const existing=pending.get(current.key)
7377
- if(existing&&!force)
7378
- return existing
7379
- const requestState=requests.start(current.key,"refresh native todos",context.data.session.message.list(sessionID).length>0,force)
7380
- if(!requestState)
7381
- return existing
7382
- const request=Promise.try(()=>Promise.all([context.data.session.sync(sessionID),context.data.session.message.sync(sessionID)])).then(()=>{if(!requestState.isCurrent())
7383
- return []
7384
- 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()
7385
7481
  return list(sessionID)
7386
- }).catch((error)=>{requestState.fail(error)
7387
- throw error
7388
- }).finally(()=>{requestState.finish()
7389
- if(pending.get(current.key)===request)
7390
- pending.delete(current.key)
7391
- })
7392
- pending.set(current.key,request)
7393
- return request
7394
- }
7395
- 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
7396
- if(activeTarget!==key){requests.abortAll()
7397
- pending.clear()
7398
- activeTarget=key
7399
- }
7400
- return()=>{if(activeTarget!==key)
7401
- return
7402
- activeTarget=undefined
7403
- requests.abortAll()
7404
- pending.clear()
7405
- }}}}
7482
+ }}})
7483
+ }}
7406
7484
  function collectionLocation(target){return{directory:target.routing.directory??"",...target.routing.workspace&&{workspaceID:target.routing.workspace}}}
7407
7485
  async function syncCollection(collection,target,force,signal){signal.throwIfAborted()
7408
7486
  const ref=collectionLocation(target)
@@ -7411,9 +7489,9 @@ collection.invalidate(ref)
7411
7489
  await collection.sync(ref)
7412
7490
  signal.throwIfAborted()
7413
7491
  }
7414
- function createV2McpSource(context){const collection=context.data.location.mcp.server
7492
+ function createNativeMcpSource(data){const collection=()=>data().location.mcp.server
7415
7493
  const projections=new WeakMap
7416
- 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)
7417
7495
  if(!item){item={get name(){return server.name
7418
7496
  },get title(){return server.title
7419
7497
  },get status(){return server.status.status
@@ -7423,12 +7501,17 @@ projections.set(server,item)
7423
7501
  }
7424
7502
  return item
7425
7503
  })
7426
- 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")
7427
7508
  return list(target)
7428
7509
  }}}
7429
- function createV2SkillSource(context){const projections=new WeakMap
7430
- const collection=context.data.location.skill
7431
- 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)
7432
7515
  if(!item){item={get id(){return skill.id
7433
7516
  },get name(){return skill.name
7434
7517
  },get description(){return skill.description
@@ -7439,34 +7522,49 @@ projections.set(skill,item)
7439
7522
  }
7440
7523
  return item
7441
7524
  })
7442
- 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")
7443
7529
  return list(target)
7444
7530
  },async use(target,skill,signal){signal.throwIfAborted()
7445
- const route=context.ui.router.current()
7446
- const session=route.type==="session"&&context.data.session.get(route.sessionID)
7447
7531
  const actual=list(target).find((item)=>item.id===skill.id&&item.location===skill.location)
7448
- if(route.type!=="session"||!session||session.location.directory!==target.routing.directory||!actual?.id)
7532
+ if(!actual?.id)
7449
7533
  throw new Error("Skill target changed; select the source again")
7450
- await context.client.experimental.session.skill({sessionID:route.sessionID,id:actual.id},{signal})
7451
- return !signal.aborted
7534
+ return input.use(target,actual,signal)
7452
7535
  }}}
7453
- 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)
7454
- return value?[value]:[]
7455
- }),status:(id)=>api.state.session.status(id)??{type:"idle"},startedAt(id){if(context.data.session.status(id)!=="running")
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")
7456
7548
  return
7457
- const time=context.data.session.get(id)?.time
7549
+ const time=sessions.get(id)?.time
7458
7550
  if(!time?.busy||time.idle!==undefined&&time.idle>=time.busy)
7459
7551
  return
7460
7552
  return time.busy
7461
7553
  },async refresh(parentID,signal){signal.throwIfAborted()
7554
+ const client=input.client()
7555
+ const sessions=input.sessions()
7462
7556
  let cursor
7463
- do{const page=await context.client.session.list(cursor?{cursor,limit:100}:{parentID,limit:100},{signal
7464
- })
7465
- 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)))
7466
7562
  cursor=page.cursor.next??undefined
7467
7563
  signal.throwIfAborted()
7468
7564
  } while(cursor)
7469
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
+ }
7470
7568
  function createOpenCodeV2Sources(context,api){return{skills:createV2SkillSource(context),mcp:createV2McpSource(context),subagents:createV2SubagentSource(context,api)
7471
7569
  }}
7472
7570
  function loadNavigatorUpdates(){const path=import.meta.url.includes("/dist/tui.js")?"./navigator-updates.js":"../navigator-updates"
@@ -7488,7 +7586,7 @@ const limitsPath=import.meta.url.includes("/dist/tui.js")?"./provider-limits.js"
7488
7586
  const limitsModule=await import(limitsPath)
7489
7587
  const LimitsSection=limitsModule.ProviderLimitsSection
7490
7588
  const{controller:limits,codex,accountSource,selectModel,modelSelection
7491
- }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.26.0",hostContext)
7589
+ }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.27.0",hostContext)
7492
7590
  const mcp=createMcpController(api,preferences.persistMcp,preferences,nativeSources?.mcp)
7493
7591
  const interaction=createSidebarInteraction(api,()=>openKeyboardHelp(api,preferences.lspIconStyle,preferences.cornerFont,sidebarShortcutActions(api)))
7494
7592
  const startup=createStartupSessionController(api,preferences)
@@ -7517,7 +7615,7 @@ api.slots.register({order:100,slots:{app(){return _$createComponent(IconProvider
7517
7615
  },interaction,skills,subagents,mcp
7518
7616
  }),_$createComponent(FirstRunWizardPersistence,{api,preferences
7519
7617
  }),_$createComponent(StartupSessionBinding,{api,controller:startup
7520
- }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.26.0"
7618
+ }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.27.0"
7521
7619
  }),_$createComponent(McpPersistence,{api,controller:mcp
7522
7620
  })]
7523
7621
  }})
@@ -7551,13 +7649,13 @@ return result.data?.type==="text"?result.data.content:undefined
7551
7649
  const updateServices=await loadNavigatorUpdates()
7552
7650
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV1VersionUpdater
7553
7651
  }=updateServices
7554
- const versionStatus=createVersionStatus(api,"0.26.0")
7555
- const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.26.0"))
7652
+ const versionStatus=createVersionStatus(api,"0.27.0")
7653
+ const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.27.0"))
7556
7654
  bindNavigatorAbout(api,versionStatus,versionUpdates,updateServices)
7557
7655
  api.slots.register({order:90,slots:{sidebar_footer(_context,props){return _$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7558
7656
  },get multilineCorners(){return preferences.cornerFont
7559
7657
  },get children(){return _$createComponent(VersionFooter,{api,get sessionID(){return props.session_id
7560
- },navigatorVersion:"0.26.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7658
+ },navigatorVersion:"0.27.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7561
7659
  },get onNavigatorUpdate(){return versionUpdates.navigator
7562
7660
  }})
7563
7661
  }})
@@ -7574,8 +7672,8 @@ return new TextDecoder().decode(await context.client.file.read({location:context
7574
7672
  const updateServices=await loadNavigatorUpdates()
7575
7673
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV2VersionUpdater
7576
7674
  }=updateServices
7577
- const versionStatus=createVersionStatus(adapter.api,"0.26.0")
7578
- const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.26.0"))
7675
+ const versionStatus=createVersionStatus(adapter.api,"0.27.0")
7676
+ const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.27.0"))
7579
7677
  bindNavigatorAbout(adapter.api,versionStatus,versionUpdates,updateServices)
7580
7678
  const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7581
7679
  },get multilineCorners(){return preferences.cornerFont
@@ -7583,7 +7681,7 @@ const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$create
7583
7681
  _$setProp(_el$,"flexDirection","row")
7584
7682
  _$setProp(_el$,"justifyContent","space-between")
7585
7683
  _$insert(_el$,_$createComponent(VersionSummary,{get api(){return adapter.api
7586
- },navigatorVersion:"0.26.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7684
+ },navigatorVersion:"0.27.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7587
7685
  },get onNavigatorUpdate(){return versionUpdates.navigator
7588
7686
  }}),null)
7589
7687
  _$insert(_el$,_$createComponent(SettingsFooterButton,{get api(){return adapter.api