opencode-navigator 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.26.0] - 2026-11-06
4
+
5
+ - Prevent background retries from accumulating cancellation listeners and preferences loading from waiting indefinitely on recovery markers.
6
+ - Show exact active Subagent durations from OpenCode 2's native execution start time, preserving the observation fallback when timing is unavailable. Keep interrupted executions marked as cancelled in recent history.
7
+
3
8
  ## [0.25.0] - 2026-10-31
4
9
 
5
10
  - Offer Enable Todo when OpenCode 2's native tool is disabled, with an existing server-config picker, exact change preview and separate confirmation. Preserve unrelated JSONC content and refuse stale targets; distinguish disabled Todo from an enabled empty list.
package/HOST_API.md CHANGED
@@ -9,9 +9,9 @@ define the boundary; internal host components are not plugin APIs.
9
9
  | Capability | OpenCode 1.19.5 | OpenCode 2.2.5 |
10
10
  | --- | --- | --- |
11
11
  | Todo | Supported host Todo source/events | Public message cache, completed `todowrite` and revert boundary |
12
- | Skills | Host discovery and prompt insertion | Native Skill cache and `session.skill` with exact ID |
13
- | MCP | Host status and connection API | Native cache, sync/invalidate and connect/disconnect |
14
- | Subagents | Supported child/status API and events | Native session cache/status and typed execution events |
12
+ | Skills | Host discovery and prompt insertion | Native Skill cache and `experimental.session.skill` with exact ID |
13
+ | MCP | Host status and connection API | Native cache, sync/invalidate and `experimental.mcp.connect/disconnect` |
14
+ | Subagents | Supported child/status API and events | Native session cache/status, execution start time and typed execution events |
15
15
  | Commands | Public keymap | Public owned layers, targets, priorities, modes, discovery and dispatch |
16
16
  | Dialogs/toasts | Public dialog components and mounting | Public show/prompt/confirm/set/clear and toast |
17
17
  | Model/variant | Saved session model and host pickers | Reactive prompt model, variant API and host pickers |
@@ -31,10 +31,10 @@ justify bypassing a native API.
31
31
  | Bug-report transport and external browser launch | Neither supported public TUI contract provides GitHub issue creation or an external browser opener. Direct sending uses the user's authenticated `gh` without reading credential files; a `type: Bug` repository form handles browser reports. Bounded argv-based OS launch has a visible link/clipboard fallback on remote or headless hosts. | Equivalent public host issue/browser APIs preserving authentication ownership, editable draft, real issue type and confirmation semantics. |
32
32
  | Literal Ctrl+, hook on 2.x | Public plugin bindings are strings; the comma expander splits literal commas into alternatives. The named parser does not normalize `comma` to the actual comma event. Dispatch still uses the native command API. | A documented literal-comma binding passing the real Settings smoke. |
33
33
  | Subagent child discovery | Published TUI Data caches sessions but has no complete children-sync operation. Initial/manual/reconnect discovery uses the public paginated API and syncs unknown IDs into the host cache. | A complete children-sync/query on the supplied public Data interface. |
34
- | Subagent observation history and retry details | Native status and cached transcript retry data are used first; recent runs, pre-observation clock markers, errors and unloaded-child retry detail remain observation history. | Equivalent public observed-run/retry history without loading every transcript. |
34
+ | Subagent observation history and retry details | Native status, `SessionInfo.time.busy` and cached retry data are used first. Valid current-execution start times give exact active durations without a pre-observation marker; missing/invalid timing keeps the observed fallback. Recent runs, errors and unloaded-child retry details remain observation history. The prior execution's `idle`/`outcome` never completes a currently running execution. | Equivalent public observed-run/retry history without loading every transcript. |
35
35
  | Read-only Todo projection | Native completed tool calls and revert are public; the host's `sessionTodos` helper is not exported. No direct CRUD/priority API exists. | A public equivalent projection; editing also requires an explicitly selected interaction and supported API. |
36
36
  | Native Todo enablement | `config.get` exposes ordered sources, but `experimental.config.update` supports only shell. The setup feature previews and confirms a JSONC leaf edit in an existing discovered source through public server file APIs. File reads are location-confined, so selected-file IO is routed to its server-side parent; the original session/location remains the stale-target guard. Reads immediately before writes reject changed previews, and readback verifies the file and effective flag. The public write API has no CAS, so this is not an atomic transaction against concurrent external writers; uncertain outcomes require Refresh before retry. No model, shell or Todo event is used. | A public Todo config patch API preserving source precedence, unrelated JSONC content and conditional-write semantics. |
37
- | Console/Go request lifecycle and projection | `client.usage.limits` is public, but the internal usage cache and budget selectors are not in published Plugin Context. Workspace IDs are not provider account IDs. | Equivalent public cache and selector on the plugin interface. |
37
+ | Console/Go request lifecycle and projection | `client.experimental.usage.limits` is public, but the internal usage cache and budget selectors are not in published Plugin Context. Workspace IDs are not provider account IDs. | Equivalent public cache and selector on the plugin interface. |
38
38
  | External quotas / reset credits | Console/Go is not a universal provider API and has no Codex reset-credit consume operation. Sources stay documented, account-bound and portable. Native auth/request errors never enable a bypass reader. | Host APIs matching provider/account/units/resets and idempotent consume semantics. |
39
39
  | Pinned-file snapshot installer | Native package updates do not implement the recognized versioned wrapper/staging layout. Preserve installation source and scope. | A public atomic file-snapshot update API with equivalent verification. |
40
40
  | Corner masks and product UI | Multiline shape, three-cell controls, filters, groups, presets and pins are product features. Paint hooks only draw; scroll restoration runs outside paint. | Equivalent public primitives preserving geometry, behavior and fallback mode. |
@@ -55,7 +55,7 @@ exercise migration only; they do not extend the supported host matrix.
55
55
  The published 2.2.5 TUI context, plugin, index and Solid declarations are identical
56
56
  to 2.2.0. OpenTUI 0.8 retains the drawing-only paint-hook contract; renderer tests,
57
57
  nested dialogs and frame-level typing probes run against the updated runtime.
58
- Development uses Bun 1.4.4 and OpenTUI 0.8.1, leaving Solid/OpenTUI external so the
58
+ Development uses Bun 1.4.4 and OpenTUI 0.8.2, leaving Solid/OpenTUI external so the
59
59
  host owns its runtime. The 1.x host ships older runtime modules; its actual smoke
60
60
  must pass rather than injecting the development modules into it.
61
61
 
@@ -67,13 +67,22 @@ renderer/FFI support. CI and package verification use Node 26.12.1.
67
67
  ### Deliberate Compatibility Pins
68
68
 
69
69
  - `web-tree-sitter` stays at **0.25.10**, the exact peer required by OpenTUI
70
- 0.8.0. Upgrade it when the supported host runtime publishes a compatible peer
70
+ 0.8.2. Upgrade it when the supported host runtime publishes a compatible peer
71
71
  contract; the independent 0.27 line is not a compatible substitution.
72
- - `typescript` is **7.0.2** throughout the resolved project graph. The shared
73
- preset's legacy compiler dependency is overridden to TS7; its incompatible
74
- SonarJS and typescript-eslint JS plugins are not loaded. Native replacements,
75
- retained compatible plugins and the explicitly approved coverage loss are
76
- documented in [Lint coverage](LINT_COVERAGE.md). No TS6 compatibility copy is used.
72
+ - `typescript` is **6.0.3**, selected to match the complete published
73
+ `@st1ggy/linter-config` **10.0.0** preset. SonarJS and typescript-eslint JS
74
+ plugins remain enabled without the former TS7 projection or override.
75
+ [Lint coverage](LINT_COVERAGE.md) records the current setup and historical exclusions.
76
+ - Oxlint **1.90.0**, Oxfmt **0.75.0** and oxlint-tsgolint **7.0.2005** match the
77
+ preset's exact peer versions. Advance them when the preset supports newer engines.
78
+ - OpenTUI's transitive `bun-ffi-structs` **0.3.1** declares the outdated compiler
79
+ peer `typescript: ^5`. A root-only Bun metadata patch expands it to `^5 || ^6`;
80
+ no runtime bytes or declarations change. Strict declaration compilation with
81
+ library checks enabled, packing regressions and the resolved compiler graph
82
+ verify the selected TS6 use. Remove the patch when upstream accepts TS6.
83
+ Bun patches apply only in this repository, not transitively in consumer projects
84
+ or the separately generated snapshot runtime manifest. Those installations may
85
+ still report the upstream peer warning; this is not an upstream fix.
77
86
  - Bun remains pinned at **1.4.4**, the current stable release. Direct dependencies
78
87
  otherwise use their latest compatible stable releases; transitive updates stay
79
88
  within upstream ranges. Runtime peers remain host-owned and are not bundled.
package/LINT_COVERAGE.md CHANGED
@@ -1,39 +1,37 @@
1
- # TypeScript 7 Lint Coverage
1
+ # Lint Coverage
2
2
 
3
- Project type checking uses stable TypeScript 7. The lockfile resolves every
4
- TypeScript dependency, including the shared preset's dependency tree, to that
5
- version. There is no TypeScript 6 compatibility installation.
3
+ Project type checking uses stable TypeScript 6.0.3, matching the compiler API
4
+ used by the published shared preset. The user selected this compatibility-first
5
+ setup instead of extending the previous TypeScript 7 coverage exclusions.
6
6
 
7
7
  Oxlint uses `@st1ggy/linter-config/solid-ox` with type-aware analysis. Oxfmt uses
8
- `@st1ggy/linter-config/oxfmt-common`. `scripts/native-lint-config.ts` adapts the
9
- published preset without editing installed packages. The user explicitly approved
10
- the uncovered SonarJS checks below after reviewing the TS7 incompatibility.
8
+ `@st1ggy/linter-config/oxfmt-common`. The version 10 preset is extended directly,
9
+ including its SonarJS and typescript-eslint JS plugins. No compiler-compatibility
10
+ projection filters its rules. Deliberate project exceptions remain documented
11
+ beside their individual settings in `oxlint.config.ts`.
11
12
 
12
13
  ## Preserved Checks
13
14
 
14
15
  - Native ESLint, TypeScript, import and Unicorn rules remain enabled.
15
- - The compatible Solid, ESLint-core, import-x, stylistic and Unicorn JS plugins
16
- remain enabled. There is no blanket removal of JS plugins.
17
- - `typescript-js/array-type` and `typescript-js/prefer-as-const` use their native
18
- `typescript/` equivalents. The legacy typescript-eslint plugin is not loaded.
19
- - Six SonarJS checks use native ESLint equivalents: `block-scoped-var`,
20
- `no-delete-var`, `no-fallthrough`, `no-labels`, `no-unused-vars` and
21
- `no-useless-catch`. Existing native options take precedence.
16
+ - All published Solid, ESLint-core, import-x, stylistic, Unicorn, SonarJS and
17
+ typescript-eslint JS plugins remain enabled.
18
+ - Promise handling and strict equality follow the version 10 preset.
19
+ - Bun 1.4.4 can return undefined from `rejects`/`resolves` matchers for already
20
+ settled promises. Tests normalize matcher results through `Promise.resolve`
21
+ before awaiting, preserving pending assertions and satisfying both the native
22
+ thenable rule and SonarJS assertion handling without disabling either rule.
22
23
  - Oxfmt still checks formatting separately. This migration does not configure
23
24
  Stylelint for the CSS-free TUI project.
24
25
 
25
- ## Approved Coverage Loss
26
+ ## Historical TypeScript 7 Coverage Exclusions
26
27
 
27
- The published preset configures 161 SonarJS rule names. Six have the direct
28
- native mappings above. The following 155 preset checks are not loaded after
29
- removing the TS6-dependent SonarJS plugin. Some overlap with other retained rules,
30
- but that is not claimed as complete semantic equivalence. The list includes
31
- checks for frameworks and services this project does not use; it describes the
32
- preset, not 155 distinct bugs or applicable findings in Navigator.
28
+ Navigator 0.25.0 used native substitutions for six SonarJS rules and excluded
29
+ the following 155 rule names from preset 8 for TS7 compatibility. This list is
30
+ historical, not the current configuration. Preset 10 now supplies its complete
31
+ rule set, subject only to the documented project-specific exceptions.
33
32
 
34
- In particular, native cyclomatic complexity is not a replacement for SonarJS
35
- cognitive complexity. Identical-function detection, several security/regex
36
- checks and SonarJS-specific test heuristics lose their dedicated coverage.
33
+ Native cyclomatic complexity was not considered a replacement for SonarJS
34
+ cognitive complexity. The compiler-driven exclusions are no longer applied.
37
35
 
38
36
  ```text
39
37
  sonarjs/assertions-in-test-cases
@@ -193,9 +191,9 @@ sonarjs/x-powered-by
193
191
  sonarjs/xml-parser-xxe
194
192
  ```
195
193
 
196
- ## Removing The Adapter
194
+ ## Future Compiler Upgrades
197
195
 
198
- Replace the compatibility projection when the shared preset offers a TS7-only
199
- stack with equivalent native rules or compatible public plugin APIs. Keep the
200
- coverage inventory explicit when upgrading that preset; do not label a removed
201
- check as preserved merely because a similarly named rule exists.
196
+ The compatibility projection has been removed. Reconsider TypeScript 7 only when
197
+ the complete published lint stack supports its compiler API. Keep coverage
198
+ explicit; do not label a removed check as preserved merely because a similarly
199
+ named rule exists.
package/README.md CHANGED
@@ -936,9 +936,11 @@ tasks before pending tasks. Finished contains separate Completed and Cancelled
936
936
  groups. The selected view is temporary and resets to All when changing sessions.
937
937
  The header always reports completed tasks against the full task count.
938
938
 
939
- Subagents show observed run duration, retry countdowns, and execution errors. An
940
- already-running child discovered by the sidebar uses a clock icon (`~` in Text
941
- fallback) because its exact start time is unknown. Recent keeps the last 10 observed
939
+ Subagents show run duration, retry countdowns, and execution errors. OpenCode 2
940
+ uses its public execution-start timestamp for exact active durations, including
941
+ children already running when Navigator opens. Missing or invalid native timing
942
+ and OpenCode 1 retain observed durations. An already-running child whose exact
943
+ start is unknown has a clock icon (`~` in Text fallback). Recent keeps the last 10 observed
942
944
  finished runs per parent session in memory across navigation, until OpenCode exits.
943
945
  An idle transition is labelled Finished, not a guarantee of successful task
944
946
  completion; errors and cancellations retain their own labels. A worker timeout
@@ -974,7 +976,7 @@ session or workspace dismisses the entire search dialog chain.
974
976
 
975
977
  ## Scripts
976
978
 
977
- Development uses Bun 1.4.4 and stable TypeScript 7. Standalone Node/OpenTUI paths
979
+ Development uses Bun 1.4.4 and stable TypeScript 6.0.3. Standalone Node/OpenTUI paths
978
980
  require Node.js 26.4 or newer; CI pins Node 26.12.1. Install dependencies with
979
981
  `bun install --frozen-lockfile`.
980
982
 
@@ -984,9 +986,11 @@ Oxlint uses the `solid-ox` preset from
984
986
  OpenTUI callbacks, SDK compatibility, and test fixtures. Type-aware analysis uses
985
987
  `oxlint-tsgolint`; `tsconfig.lint.json` covers application code, tests, build scripts,
986
988
  and configuration files. Formatter exclusions are maintained in `oxfmt.config.ts`.
987
- The resolved dependency tree contains TS7 only. The approved removal of
988
- incompatible SonarJS checks and native replacements is documented in
989
- [Lint coverage](LINT_COVERAGE.md); compatible Solid and other JS checks remain.
989
+ The complete version 10 preset includes SonarJS and typescript-eslint checks;
990
+ the former TS7 compatibility projection is removed. See [Lint coverage](LINT_COVERAGE.md)
991
+ for current exceptions and historical exclusions. A tested root-only Bun metadata
992
+ patch corrects the legacy TypeScript 5 peer in OpenTUI's FFI helper for this repo.
993
+ Consumer installations retain upstream metadata; see [Host APIs](HOST_API.md).
990
994
 
991
995
  ```sh
992
996
  bun run test
@@ -14,8 +14,9 @@ pending.delete(id)
14
14
  }}
15
15
  function write(message){if(!child?.stdin||typeof child.stdin==="number")
16
16
  throw new Error("Codex app-server is unavailable")
17
- child.stdin.write(`${JSON.stringify(message)}
18
- `)
17
+ Promise.resolve(child.stdin.write(`${JSON.stringify(message)}
18
+ `)).catch((error)=>{failAll(error instanceof Error?error:new Error("Codex app-server write failed"))
19
+ })
19
20
  }
20
21
  function accept(line){let message
21
22
  try{message=JSON.parse(line)
@@ -677,13 +678,13 @@ setRevision((value)=>value + 1)
677
678
  }),context.data.on("server.connected",()=>{generation++
678
679
  setRevision((value)=>value + 1)
679
680
  })]
680
- const adapter={id:"opencode-usage",name:"OpenCode workspace limits",supports:(model)=>typeof context.client.usage?.limits==="function"&&provider(model)?.integrationID==="opencode",async read(model,signal){signal.throwIfAborted()
681
+ const adapter={id:"opencode-usage",name:"OpenCode workspace limits",supports:(model)=>typeof context.client.experimental?.usage?.limits==="function"&&provider(model)?.integrationID==="opencode",async read(model,signal){signal.throwIfAborted()
681
682
  const epoch=generation
682
683
  const target=key()
683
684
  const activeProvider=provider(model)
684
685
  if(!activeProvider||activeProvider.integrationID!=="opencode")
685
686
  throw new Error("Native usage provider changed")
686
- try{const result=await context.client.usage.limits({location:{directory:location().directory}},{signal})
687
+ try{const result=await context.client.experimental.usage.limits({location:{directory:location().directory}},{signal})
687
688
  if(epoch!==generation||target!==key()||signal.aborted||result.location.directory!==location().directory)
688
689
  throw new DOMException("Workspace usage target changed","AbortError")
689
690
  if(!result.data)
@@ -1062,7 +1063,7 @@ return _$createComponent13(Section,{get api(){return props.api
1062
1063
  },get navigationSection(){return props.navigationSection
1063
1064
  },get credits(){return(snapshot()?.bankedResets?.availableCount??0)>0||Boolean(props.preferences.pendingResetAttempt())
1064
1065
  },get refreshing(){return ["loading","refreshing"].includes(props.controller.state().status)
1065
- },get canRefresh(){return Boolean(model()&&(props.controller.current().adapter||model()?.hostConnection?.status==="unavailable"))
1066
+ },get canRefresh(){return Boolean(model()&&(props.controller.current().adapter??model()?.hostConnection?.status==="unavailable"))
1066
1067
  },onCredits:openCredits,onRefresh:()=>void props.controller.refresh(true)
1067
1068
  })
1068
1069
  },get open(){return props.preferences.expanded().limits
@@ -2,9 +2,9 @@
2
2
  "version": 1,
3
3
  "dependencies": {
4
4
  "@opencode/plugin": "2.2.5",
5
- "@opentui/core": "0.8.1",
6
- "@opentui/keymap": "0.8.1",
7
- "@opentui/solid": "0.8.1",
5
+ "@opentui/core": "0.8.2",
6
+ "@opentui/keymap": "0.8.2",
7
+ "@opentui/solid": "0.8.2",
8
8
  "solid-js": "1.9.18"
9
9
  }
10
10
  }
@@ -49,8 +49,6 @@ let child
49
49
  try{child=Bun.spawn([...command],{stdin:"pipe",stdout:"pipe",stderr:"ignore",env:environment?{...process.env,...environment}:undefined
50
50
  })
51
51
  } catch{return{code:-1,stdout:""}}
52
- child.stdin.write(input)
53
- child.stdin.end()
54
52
  let timer
55
53
  const chunks=[]
56
54
  const output=()=>new TextDecoder().decode(Buffer.concat(chunks))
@@ -77,7 +75,11 @@ chunks.push(part.value)
77
75
  }
78
76
  return{code:await child.exited,stdout:output()}} finally{reader.releaseLock()
79
77
  }}
80
- try{return await Promise.race([read(),cancelled])
78
+ const run=async ()=>{await child.stdin.write(input)
79
+ await child.stdin.end()
80
+ return read()
81
+ }
82
+ try{return await Promise.race([run(),cancelled])
81
83
  } catch{return{code:-1,stdout:""}} finally{clearTimeout(timer)
82
84
  signal?.removeEventListener("abort",cancel)
83
85
  }}
package/dist/tui.js CHANGED
@@ -179,10 +179,14 @@ setStates((values)=>({...values,[target]:{status:"error",error:requestError(caus
179
179
  return{state,start,abort,abortAll}}
180
180
  function wait(delay,signal){return new Promise((resolve,reject)=>{if(signal?.aborted)
181
181
  return reject(signal.reason??new DOMException("Aborted","AbortError"))
182
- const timer=setTimeout(resolve,delay)
183
- signal?.addEventListener("abort",()=>{clearTimeout(timer)
184
- reject(signal.reason??new DOMException("Aborted","AbortError"))
185
- },{once:true})
182
+ const abort=()=>{clearTimeout(timer)
183
+ signal?.removeEventListener("abort",abort)
184
+ reject(signal?.reason??new DOMException("Aborted","AbortError"))
185
+ }
186
+ const timer=setTimeout(()=>{signal?.removeEventListener("abort",abort)
187
+ resolve()
188
+ },delay)
189
+ signal?.addEventListener("abort",abort,{once:true})
186
190
  })
187
191
  }
188
192
  async function retryBackgroundRefresh(refresh,options={}){const delays=options.delays??[250,1000]
@@ -1604,7 +1608,7 @@ behavior.quickActionVisibility=quickActionVisibility
1604
1608
  const mcpUpdate={...update.mcp?.states,...update.mcp?.name&&update.mcp.state&&{[update.mcp.name]:update.mcp.state}}
1605
1609
  const mcp=parseDesiredMcpStates({...!update.clearMcp&&existing.mcp,...mcpUpdate
1606
1610
  })
1607
- const scope={...Object.keys(behavior).length>0&&{behavior},...(update.layout||!update.clearLayout&&existing.layout)&&{layout:parseSectionLayout(update.layout??existing.layout)
1611
+ const scope={...Object.keys(behavior).length>0&&{behavior},...(update.layout??(!update.clearLayout&&existing.layout))&&{layout:parseSectionLayout(update.layout??existing.layout)
1608
1612
  },...Object.keys(mcp).length>0&&{mcp}}
1609
1613
  const worktrees={...current.worktrees}
1610
1614
  if(target.kind==="worktree"){if(isEmptyScope(scope))
@@ -1786,10 +1790,13 @@ await unlink(recovery).catch((error)=>{if(!errno(error,"ENOENT"))
1786
1790
  throw error
1787
1791
  })
1788
1792
  }}
1789
- async function tryAcquireLock(){const token=randomUUID()
1793
+ async function tryAcquireLock(deadline){const token=randomUUID()
1790
1794
  const candidate=`${lock}.${token}.candidate`
1795
+ let acquired=false
1796
+ let handedOff=false
1791
1797
  await writeFile(candidate,JSON.stringify({token,pid:process.pid}),{flag:"wx",mode:384})
1792
1798
  try{try{await link(candidate,lock)
1799
+ acquired=true
1793
1800
  } catch(error){if(!errno(error,"EEXIST"))
1794
1801
  throw error
1795
1802
  const owner=await readLockOwner()
@@ -1801,22 +1808,29 @@ return
1801
1808
  while(true){const markers=await liveRecoveryMarkers()
1802
1809
  if(markers.length===0)
1803
1810
  break
1811
+ if(Date.now()>=deadline)
1812
+ return
1804
1813
  await delay(10)
1805
1814
  }
1806
1815
  const currentOwner=await readLockOwner()
1807
1816
  if(currentOwner?.token!==token)
1808
1817
  return
1818
+ handedOff=true
1809
1819
  return async ()=>{const current=await readLockOwner()
1810
1820
  if(current?.token!==token)
1811
1821
  throw new Error("Refusing to release a preferences lock owned by another process")
1812
1822
  await moveLockToTombstone("released")
1813
- }} finally{await unlink(candidate).catch((error)=>{if(!errno(error,"ENOENT"))
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"))
1814
1828
  throw error
1815
1829
  })
1816
1830
  }}
1817
1831
  async function acquireLock(){await mkdir(directory,{recursive:true,mode:448})
1818
1832
  const deadline=Date.now() + LOCK_TIMEOUT_MS
1819
- while(true){const release=await tryAcquireLock()
1833
+ while(true){const release=await tryAcquireLock(deadline)
1820
1834
  if(release)
1821
1835
  return release
1822
1836
  if(Date.now()>=deadline)
@@ -2775,7 +2789,7 @@ return value
2775
2789
  }
2776
2790
  function changed(){setRevision((value)=>value + 1)
2777
2791
  }
2778
- return{observeStatus(key,sessionID,status,at=now()){const value=state(key)
2792
+ return{observeStatus(key,sessionID,status,at=now(),nativeStart){const value=state(key)
2779
2793
  const run=value.active.get(sessionID)
2780
2794
  const isKnown=value.known.has(sessionID)
2781
2795
  value.known.add(sessionID)
@@ -2783,10 +2797,17 @@ if(status.type==="idle"){if(!run)
2783
2797
  return
2784
2798
  value.active.delete(sessionID)
2785
2799
  value.recent=[{...run,finishedAt:Math.max(at,run.startedAt),outcome:run.outcome??"finished"},...value.recent].sort((a,b)=>b.finishedAt-a.finishedAt||a.sessionID.localeCompare(b.sessionID)).slice(0,10)
2786
- } else{if(run)
2800
+ } else{const exact=typeof nativeStart==="number"&&Number.isFinite(nativeStart)&&nativeStart>0&&nativeStart<=at?nativeStart:undefined
2801
+ if(run){if(exact===undefined||run.nativeStartedAt!==undefined&&exact<=run.nativeStartedAt)
2802
+ return
2803
+ const next=exact>run.startedAt?{sessionID}:run
2804
+ value.active.set(sessionID,{...next,startedAt:exact,nativeStartedAt:exact,startedBeforeObservation:false
2805
+ })
2806
+ changed()
2787
2807
  return
2808
+ }
2788
2809
  value.recent=value.recent.filter((item)=>item.sessionID!==sessionID)
2789
- value.active.set(sessionID,{sessionID,startedAt:at,startedBeforeObservation:!isKnown})
2810
+ value.active.set(sessionID,{sessionID,startedAt:exact??at,startedBeforeObservation:exact===undefined&&!isKnown,...exact!==undefined&&{nativeStartedAt:exact}})
2790
2811
  }
2791
2812
  changed()
2792
2813
  },observeError(key,sessionID,error,at=now()){if(!sessionID)
@@ -2857,12 +2878,13 @@ journal.sessions.push(mutation)
2857
2878
  if(!source)
2858
2879
  setChildren((current)=>Object.fromEntries(Object.entries(current).map(([key,items])=>[key,applySession(items,targets.get(key).parentID,mutation)])))
2859
2880
  }
2860
- function recordStatus(input){const mutation={...input,at:now(),observed:new Set}
2881
+ function recordStatus(input){const mutation={...input,at:now(),startedAt:source?.startedAt?.(input.sessionID),observed:new Set
2882
+ }
2861
2883
  for(const journal of journals)
2862
2884
  journal.statuses.push(mutation)
2863
2885
  batch(()=>{for(const [key,items] of Object.entries(snapshots())){if(items.every((item)=>item.id!==mutation.sessionID))
2864
2886
  continue
2865
- history.observeStatus(key,mutation.sessionID,mutation.status,mutation.at)
2887
+ history.observeStatus(key,mutation.sessionID,mutation.status,mutation.at,mutation.startedAt)
2866
2888
  mutation.observed.add(key)
2867
2889
  }
2868
2890
  if(!source)
@@ -2897,7 +2919,7 @@ for(const mutation of journal.sessions)
2897
2919
  nextChildren=applySession(nextChildren,current.parentID,mutation)
2898
2920
  let nextStatuses={...statusResult.data}
2899
2921
  const missing={}
2900
- const remoteStatuses=source?[]:await Promise.all(nextChildren.map((session)=>{const key=JSON.stringify([current.key,session.id])
2922
+ const remoteStatuses=source?[]:await Promise.all(nextChildren.map(async (session)=>{const key=JSON.stringify([current.key,session.id])
2901
2923
  return(remoteRetryAt.get(key)??0)<=now()?fetchDevTeamStatus(session,requestState.signal):{sessionID:session.id,failed:true,cooldown:true,status:undefined}}))
2902
2924
  if(!requestState.isCurrent())
2903
2925
  return
@@ -2926,10 +2948,10 @@ for(const mutation of journal.statuses)
2926
2948
  nextStatuses=applyStatus(nextStatuses,mutation)
2927
2949
  if(requestState.isCurrent()){batch(()=>{for(const session of nextChildren){const mutations=journal.statuses.filter((mutation)=>mutation.sessionID===session.id)
2928
2950
  if(mutations.length>0){for(const mutation of mutations){if(!mutation.observed.has(current.key))
2929
- history.observeStatus(current.key,session.id,mutation.status,mutation.at)
2951
+ history.observeStatus(current.key,session.id,mutation.status,mutation.at,mutation.startedAt)
2930
2952
  }} else if(!missing[session.id]){const status=nextStatuses[session.id]??{type:"idle"}
2931
2953
  nextStatuses[session.id]=status
2932
- history.observeStatus(current.key,session.id,status)
2954
+ history.observeStatus(current.key,session.id,status,now(),source?.startedAt?.(session.id))
2933
2955
  }
2934
2956
  for(const mutation of journal.errors){if(mutation.sessionID===session.id&&!mutation.observed.has(current.key))
2935
2957
  history.observeError(current.key,session.id,mutation.error,mutation.at)
@@ -4375,7 +4397,7 @@ return _$createElement("box")
4375
4397
 
4376
4398
  function UpgradeNotesBinding(props){let pending
4377
4399
  let disposed=false
4378
- onMount6(async ()=>{const previous=await props.preferences.claimNavigatorUpgrade(props.version)
4400
+ async function load(){const previous=await props.preferences.claimNavigatorUpgrade(props.version)
4379
4401
  if(!previous||disposed)
4380
4402
  return
4381
4403
  try{let show=function(){if(disposed||props.api.lifecycle.signal?.aborted)
@@ -4395,7 +4417,8 @@ const entries=changesSince(await (props.read??readBundledChangelog)(),previous,p
4395
4417
  if(entries.length===0||disposed)
4396
4418
  return
4397
4419
  show()
4398
- } catch{}})
4420
+ } catch{}}
4421
+ onMount6(()=>void load().catch(()=>{}))
4399
4422
  onCleanup16(()=>{disposed=true
4400
4423
  clearTimeout(pending)
4401
4424
  })
@@ -6990,7 +7013,7 @@ onCleanup29(()=>{disposed=true
6990
7013
  function LimitsPersistence(props){createLimitsRefresh(props)
6991
7014
  return _$createElement("box")
6992
7015
  }
6993
- function bindNavigatorAbout(api,status,updates,notes){const dispose=registerNavigatorAboutServices(api,{navigatorVersion:"0.25.0",openCodeVersion:api.app.version,checkState:()=>status.navigatorCheckState?.()??{status:"idle"},checking:()=>status.navigatorCheckState?.().status==="checking",updating:updates.navigatorUpdating,installed:updates.navigatorInstalled,check:()=>status.refreshNavigator?.()??Promise.resolve("unavailable"),update(dialogs){const target=status.navigatorUpdate()
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()
6994
7017
  if(target)
6995
7018
  updates.navigator(target,dialogs)
6996
7019
  },changelog:async ()=>notes.parseChangelog(await notes.readBundledChangelog())
@@ -7013,11 +7036,11 @@ const candidates=context.data.location.skill.list({directory})?.filter((skill)=>
7013
7036
  const id=candidates.length===1?candidates[0].id:undefined
7014
7037
  if(route.type!=="session"||session?.location.directory!==directory||!id)
7015
7038
  return{data:false}
7016
- await context.client.session.skill({sessionID:route.sessionID,id},{signal:options.signal})
7039
+ await context.client.experimental.session.skill({sessionID:route.sessionID,id},{signal:options.signal})
7017
7040
  return{data:true}}},mcp:{async status(input,options={}){const response=await context.client.mcp.list(location(input),{signal:options.signal})
7018
7041
  const data=Object.fromEntries(response.data.map((server)=>[server.name,server.status]))
7019
- return{data}},async connect(input,options={}){await context.client.mcp.connect({server:input.name,...location(input)},{signal:options.signal})
7020
- return{data:undefined}},async disconnect(input,options={}){await context.client.mcp.disconnect({server:input.name,...location(input)},{signal:options.signal})
7042
+ return{data}},async connect(input,options={}){await context.client.experimental.mcp.connect({server:input.name,...location(input)},{signal:options.signal})
7043
+ return{data:undefined}},async disconnect(input,options={}){await context.client.experimental.mcp.disconnect({server:input.name,...location(input)},{signal:options.signal})
7021
7044
  return{data:undefined}}}}}}
7022
7045
  var TERMINAL_EVENTS=new Set(["session.execution.succeeded","session.execution.failed","session.execution.interrupted"])
7023
7046
  function executionStatus(name,event){const sessionID=event.data.sessionID
@@ -7037,7 +7060,7 @@ return ["session.status","session.retry.scheduled",...executions]
7037
7060
  if(name==="session.idle")
7038
7061
  return ["session.idle",...executions.slice(1)]
7039
7062
  if(name==="session.error")
7040
- return ["session.execution.failed"]
7063
+ return ["session.execution.failed","session.execution.interrupted"]
7041
7064
  if(name==="mcp.tools.changed")
7042
7065
  return ["mcp.status.changed","mcp.resources.changed"]
7043
7066
  if(["session.created","session.deleted","server.connected","installation.update-available"].includes(name))
@@ -7082,7 +7105,7 @@ if(name==="session.deleted"){emit({sessionID})
7082
7105
  return
7083
7106
  }
7084
7107
  if(name==="session.error"){const error=data.error
7085
- emit({sessionID,error:error&&{name:error.type??"Error",data:{message:error.message??"Session error"}}})
7108
+ emit({sessionID,error:event.type==="session.execution.interrupted"?{name:"MessageAbortedError",data:{message:"Session aborted"}}:error&&{name:error.type??"Error",data:{message:error.message??"Session error"}}})
7086
7109
  return
7087
7110
  }
7088
7111
  const info=context.data.session.get(sessionID)
@@ -7243,7 +7266,7 @@ function DialogPrompt(props){return _$createComponent(SingleLinePrompt,_$mergePr
7243
7266
  function DialogConfirm(props){let active=true
7244
7267
  onCleanup30(()=>{active=false
7245
7268
  })
7246
- onMount7(async ()=>{const confirmed=await context.ui.dialog.confirm({title:props.title,message:props.message
7269
+ async function confirm(){const confirmed=await context.ui.dialog.confirm({title:props.title,message:props.message
7247
7270
  })
7248
7271
  if(!active)
7249
7272
  return
@@ -7251,6 +7274,10 @@ if(confirmed)
7251
7274
  props.onConfirm?.()
7252
7275
  else
7253
7276
  props.onCancel?.()
7277
+ }
7278
+ onMount7(()=>{confirm().catch(()=>{if(active)
7279
+ props.onCancel?.()
7280
+ })
7254
7281
  })
7255
7282
  return _$createElement("box")
7256
7283
  }
@@ -7420,12 +7447,18 @@ const session=route.type==="session"&&context.data.session.get(route.sessionID)
7420
7447
  const actual=list(target).find((item)=>item.id===skill.id&&item.location===skill.location)
7421
7448
  if(route.type!=="session"||!session||session.location.directory!==target.routing.directory||!actual?.id)
7422
7449
  throw new Error("Skill target changed; select the source again")
7423
- await context.client.session.skill({sessionID:route.sessionID,id:actual.id},{signal})
7450
+ await context.client.experimental.session.skill({sessionID:route.sessionID,id:actual.id},{signal})
7424
7451
  return !signal.aborted
7425
7452
  }}}
7426
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)
7427
7454
  return value?[value]:[]
7428
- }),status:(id)=>api.state.session.status(id)??{type:"idle"},async refresh(parentID,signal){signal.throwIfAborted()
7455
+ }),status:(id)=>api.state.session.status(id)??{type:"idle"},startedAt(id){if(context.data.session.status(id)!=="running")
7456
+ return
7457
+ const time=context.data.session.get(id)?.time
7458
+ if(!time?.busy||time.idle!==undefined&&time.idle>=time.busy)
7459
+ return
7460
+ return time.busy
7461
+ },async refresh(parentID,signal){signal.throwIfAborted()
7429
7462
  let cursor
7430
7463
  do{const page=await context.client.session.list(cursor?{cursor,limit:100}:{parentID,limit:100},{signal
7431
7464
  })
@@ -7455,7 +7488,7 @@ const limitsPath=import.meta.url.includes("/dist/tui.js")?"./provider-limits.js"
7455
7488
  const limitsModule=await import(limitsPath)
7456
7489
  const LimitsSection=limitsModule.ProviderLimitsSection
7457
7490
  const{controller:limits,codex,accountSource,selectModel,modelSelection
7458
- }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.25.0",hostContext)
7491
+ }=limitsModule.createProviderQuotaIntegration(api,preferences,"0.26.0",hostContext)
7459
7492
  const mcp=createMcpController(api,preferences.persistMcp,preferences,nativeSources?.mcp)
7460
7493
  const interaction=createSidebarInteraction(api,()=>openKeyboardHelp(api,preferences.lspIconStyle,preferences.cornerFont,sidebarShortcutActions(api)))
7461
7494
  const startup=createStartupSessionController(api,preferences)
@@ -7484,7 +7517,7 @@ api.slots.register({order:100,slots:{app(){return _$createComponent(IconProvider
7484
7517
  },interaction,skills,subagents,mcp
7485
7518
  }),_$createComponent(FirstRunWizardPersistence,{api,preferences
7486
7519
  }),_$createComponent(StartupSessionBinding,{api,controller:startup
7487
- }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.25.0"
7520
+ }),_$createComponent(UpgradeNotesBinding,{api,preferences,version:"0.26.0"
7488
7521
  }),_$createComponent(McpPersistence,{api,controller:mcp
7489
7522
  })]
7490
7523
  }})
@@ -7518,13 +7551,13 @@ return result.data?.type==="text"?result.data.content:undefined
7518
7551
  const updateServices=await loadNavigatorUpdates()
7519
7552
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV1VersionUpdater
7520
7553
  }=updateServices
7521
- const versionStatus=createVersionStatus(api,"0.25.0")
7522
- const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.25.0"))
7554
+ const versionStatus=createVersionStatus(api,"0.26.0")
7555
+ const versionUpdates=createVersionUpdateActions(api,versionStatus,createOpenCodeV1VersionUpdater(api,meta),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.26.0"))
7523
7556
  bindNavigatorAbout(api,versionStatus,versionUpdates,updateServices)
7524
7557
  api.slots.register({order:90,slots:{sidebar_footer(_context,props){return _$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7525
7558
  },get multilineCorners(){return preferences.cornerFont
7526
7559
  },get children(){return _$createComponent(VersionFooter,{api,get sessionID(){return props.session_id
7527
- },navigatorVersion:"0.25.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7560
+ },navigatorVersion:"0.26.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7528
7561
  },get onNavigatorUpdate(){return versionUpdates.navigator
7529
7562
  }})
7530
7563
  }})
@@ -7541,8 +7574,8 @@ return new TextDecoder().decode(await context.client.file.read({location:context
7541
7574
  const updateServices=await loadNavigatorUpdates()
7542
7575
  const{createVersionStatus,createVersionUpdateActions,createOpenCodeV2VersionUpdater
7543
7576
  }=updateServices
7544
- const versionStatus=createVersionStatus(adapter.api,"0.25.0")
7545
- const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.25.0"))
7577
+ const versionStatus=createVersionStatus(adapter.api,"0.26.0")
7578
+ const versionUpdates=createVersionUpdateActions(adapter.api,versionStatus,createOpenCodeV2VersionUpdater(context),()=>preferences.rememberNavigatorVersionBeforeUpdate("0.26.0"))
7546
7579
  bindNavigatorAbout(adapter.api,versionStatus,versionUpdates,updateServices)
7547
7580
  const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$createComponent(IconProvider,{get style(){return preferences.lspIconStyle
7548
7581
  },get multilineCorners(){return preferences.cornerFont
@@ -7550,7 +7583,7 @@ const disposeFooter=context.ui.slot({append:"sidebar.footer",render:()=>_$create
7550
7583
  _$setProp(_el$,"flexDirection","row")
7551
7584
  _$setProp(_el$,"justifyContent","space-between")
7552
7585
  _$insert(_el$,_$createComponent(VersionSummary,{get api(){return adapter.api
7553
- },navigatorVersion:"0.25.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7586
+ },navigatorVersion:"0.26.0",status:versionStatus,get onOpenCodeUpdate(){return versionUpdates.openCode
7554
7587
  },get onNavigatorUpdate(){return versionUpdates.navigator
7555
7588
  }}),null)
7556
7589
  _$insert(_el$,_$createComponent(SettingsFooterButton,{get api(){return adapter.api
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-navigator",
4
- "version": "0.25.0",
4
+ "version": "0.26.0",
5
5
  "description": "Search, navigate, and manage OpenCode sessions from the TUI",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.4.4",
@@ -30,6 +30,7 @@
30
30
  "server.js",
31
31
  "rpc.js",
32
32
  "assets",
33
+ "patches",
33
34
  "scripts/install-font.mjs",
34
35
  "scripts/update-local-snapshot.mjs",
35
36
  "scripts/snapshot-runtime.mjs",
@@ -80,7 +81,7 @@
80
81
  "test:e2e": "bun run build && bash scripts/smoke-opencode.sh && bash scripts/smoke-opencode-v2.sh",
81
82
  "test:e2e:v1": "bun run build && bash scripts/smoke-opencode.sh",
82
83
  "test:e2e:v2": "bun run build && bash scripts/smoke-opencode-v2.sh",
83
- "typecheck": "tsc --noEmit",
84
+ "typecheck": "tsc --noEmit && tsc --project tsconfig.ffi.json",
84
85
  "check": "bun run test && bun run typecheck && bun run lint && bun run bundle:size && bun run format:check"
85
86
  },
86
87
  "peerDependencies": {
@@ -111,10 +112,10 @@
111
112
  "@opencode-ai/plugin": "1.19.5",
112
113
  "@opencode-ai/sdk": "1.19.5",
113
114
  "@opencode/plugin": "2.2.5",
114
- "@opentui/core": "0.8.1",
115
- "@opentui/keymap": "0.8.1",
116
- "@opentui/solid": "0.8.1",
117
- "@st1ggy/linter-config": "8.0.0",
115
+ "@opentui/core": "0.8.2",
116
+ "@opentui/keymap": "0.8.2",
117
+ "@opentui/solid": "0.8.2",
118
+ "@st1ggy/linter-config": "10.0.0",
118
119
  "@types/bun": "1.4.4",
119
120
  "eslint-plugin-solid": "0.18.1",
120
121
  "jsonc-parser": "3.3.1",
@@ -122,13 +123,10 @@
122
123
  "oxlint": "1.90.0",
123
124
  "oxlint-tsgolint": "7.0.2005",
124
125
  "solid-js": "1.9.18",
125
- "typescript": "7.0.2",
126
+ "typescript": "6.0.3",
126
127
  "web-tree-sitter": "0.25.10"
127
128
  },
128
129
  "optionalDependencies": {
129
130
  "@github/copilot-sdk": "1.0.28"
130
- },
131
- "overrides": {
132
- "typescript": "7.0.2"
133
131
  }
134
132
  }