opencode-sesh 0.7.0 → 0.7.2

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/AGENTS.md CHANGED
@@ -160,9 +160,11 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
160
160
  opens locally). Works for background agents and older panels too.
161
161
  - **Workspace naming.** The poll renames this workspace to the current
162
162
  session title (`cmux workspace rename <id> --title …`), skipping titles that
163
- are still the pre-first-turn placeholder. `bin/sesh-cmux-sync.sh` does the
164
- same for every workspace at once. The session title is the source of truth;
165
- a manual cmux rename is overwritten on the next session-title change.
163
+ must never name a workspace: the pre-first-turn placeholder
164
+ (`/^New session\b/`) and plugin sentinels (`ghost-hidden`, which
165
+ opencode-ghost titles its internal sessions). `bin/sesh-cmux-sync.sh` does
166
+ the same for every workspace at once. The session title is the source of
167
+ truth; a manual cmux rename is overwritten on the next session-title change.
166
168
  These are the only cmux calls in the panel; keep them behind the
167
169
  `CMUX_WORKSPACE_ID` guard.
168
170
  - **Picker:** a custom dialog (not `DialogSelect`) grouped by
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.2] - 2026-09-24
11
+
12
+ ### Fixed
13
+
14
+ - Switching to a session from the picker (`ctrl+o` / `/sesh`), the home
15
+ "Recent sessions" list, or the needs-input picker now runs the cmux
16
+ click-to-focus bridge: a session already live in another cmux workspace
17
+ focuses that workspace instead of opening a duplicate here. Previously only
18
+ sidebar rows did this.
19
+
20
+ ### Changed
21
+
22
+ - Every open path shares one cmux-aware handler, and the cmux surface scan is
23
+ cached briefly so the additional paths add no noticeable delay. Terminals
24
+ outside cmux are unaffected — with no `CMUX_WORKSPACE_ID` no cmux call is
25
+ made, now asserted by a test.
26
+ ## [0.7.1] - 2026-09-24
27
+
28
+ ### Fixed
29
+
30
+ - Workspace naming no longer pushes the `ghost-hidden` sentinel that the
31
+ opencode-ghost plugin uses for its internal sessions. The placeholder check
32
+ was also corrected, so `New session - …` titles are skipped as intended
33
+ rather than slipping through.
10
34
  ## [0.7.0] - 2026-09-24
11
35
 
12
36
  ### Added
@@ -22,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
46
  - Click-to-focus now reads cmux's per-surface resume record (`checkpoint_id`)
23
47
  instead of heartbeat files, so it also finds background agents and panes
24
48
  running an older panel, and has no files to sweep.
49
+
25
50
  ## [0.6.0] - 2026-09-24
26
51
 
27
52
  ### Added
@@ -293,7 +318,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
293
318
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
294
319
  CI.
295
320
 
296
- [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.0...HEAD
321
+ [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.2...HEAD
322
+ [0.7.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.1...v0.7.2
323
+ [0.7.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.0...v0.7.1
297
324
  [0.7.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.6.0...v0.7.0
298
325
  [0.6.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.5.0...v0.6.0
299
326
  [0.5.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.4.0...v0.5.0
@@ -111,7 +111,10 @@ FINAL=$(printf '%s' "$PAIRS" | "$JQ_BIN" -c \
111
111
  | ($current[0] | map({key: .workspace, value: .title}) | from_entries) as $shown
112
112
  | [ .[]
113
113
  | ($byId[.session] // "" | gsub("^ +| +$"; "")) as $t
114
- | select($t != "" and ($t | test("^New session\\b"; "i") | not))
114
+ | select($t != "")
115
+ # Never push a placeholder or a plugin sentinel (opencode-ghost titles its
116
+ # internal sessions "ghost-hidden").
117
+ | select(($t | test("^New session\\b|^ghost-hidden$"; "i") | not))
115
118
  | select(($shown[.workspace] // "") != $t)
116
119
  | . + {title: $t} ]')
117
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
6
6
  "license": "MIT",
@@ -177,9 +177,10 @@ function pinnedFirst(entries: Entry[], pins: Pins): Entry[] {
177
177
  // dialog). Native `/sessions` and session_list (<leader>l) stay untouched.
178
178
  const BASE_MODE = "base"
179
179
  const POLL_MS = 15_000
180
- // opencode names a session only after its first turn; until then the title is
181
- // a placeholder that should not become a cmux workspace name.
182
- const PLACEHOLDER_TITLE = /^New session\b/i
180
+ // Titles that must never become a cmux workspace name: opencode's pre-first-
181
+ // turn placeholder, and the sentinel other plugins (opencode-ghost) use for
182
+ // internal sessions so their scaffolding rows stay out of the UI.
183
+ const PLACEHOLDER_TITLE = /^New session\b|^ghost-hidden$/i
183
184
 
184
185
  function shortDir(dir: string, home: string): string {
185
186
  if (!dir || dir === "/") return "other"
@@ -436,6 +437,18 @@ async function cmuxSurfaces(): Promise<CmuxSurface[]> {
436
437
  return surfaces
437
438
  }
438
439
 
440
+ // The bridge walks `cmux tree` plus one `surface resume show` per surface, and
441
+ // switching sessions is frequent while the surface set changes rarely, so reuse
442
+ // a recent scan for a moment instead of re-walking cmux on every open.
443
+ let cmuxSurfacesCache: { at: number; surfaces: CmuxSurface[] } | undefined
444
+ async function cmuxSurfacesCached(): Promise<CmuxSurface[]> {
445
+ const now = Date.now()
446
+ if (cmuxSurfacesCache && now - cmuxSurfacesCache.at < 1500) return cmuxSurfacesCache.surfaces
447
+ const surfaces = await cmuxSurfaces()
448
+ cmuxSurfacesCache = { at: now, surfaces }
449
+ return surfaces
450
+ }
451
+
439
452
  // Pure selection: the workspace to focus for a session, or undefined to open
440
453
  // locally. Prefers a live surface in a different workspace; ignores the
441
454
  // caller's own surface and workspace (focusing where you already are is a
@@ -451,6 +464,26 @@ function pickFocusWorkspace(surfaces: CmuxSurface[], sessionID: string): string
451
464
  return undefined
452
465
  }
453
466
 
467
+ // Open a session from anywhere in the panel, cmux-aware: if another cmux
468
+ // workspace is already running it, focus that workspace instead of opening a
469
+ // duplicate here; otherwise open it locally. Every switch path (sidebar, home
470
+ // list, picker, needs-input picker) goes through here so none of them silently
471
+ // skips the bridge. cmux calls only run inside cmux.
472
+ async function openSessionEntry(api: TuiPluginApi, entry: Entry): Promise<void> {
473
+ if (process.env.CMUX_WORKSPACE_ID) {
474
+ const target = pickFocusWorkspace(await cmuxSurfacesCached(), entry.id)
475
+ if (target) {
476
+ try {
477
+ await promisify(execFile)("cmux", ["workspace", "select", target])
478
+ return
479
+ } catch {
480
+ // cmux unavailable; fall through to a local open
481
+ }
482
+ }
483
+ }
484
+ api.route.navigate("session", { sessionID: entry.id })
485
+ }
486
+
454
487
  function addTranscript(index: Map<string, string>, sid: string, data: string): void {
455
488
  try {
456
489
  const part = JSON.parse(data) as { type?: unknown; text?: unknown }
@@ -852,23 +885,9 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
852
885
  setSectionCollapsed((value) => !value)
853
886
  }
854
887
 
855
- // Open a session. If a cmux surface is already running it in another
856
- // workspace, focus that workspace instead of opening a duplicate here;
857
- // otherwise open it locally. `cmux` calls only run inside cmux.
858
- const openSession = async (entry: Entry) => {
859
- if (process.env.CMUX_WORKSPACE_ID) {
860
- const target = pickFocusWorkspace(await cmuxSurfaces(), entry.id)
861
- if (target) {
862
- try {
863
- await promisify(execFile)("cmux", ["workspace", "select", target])
864
- return
865
- } catch {
866
- // cmux unavailable; fall through to a local open
867
- }
868
- }
869
- }
870
- props.api.route.navigate("session", { sessionID: entry.id })
871
- }
888
+ // Sidebar rows open through the shared cmux-aware path, so they focus the
889
+ // workspace already running the session instead of opening a duplicate.
890
+ const openSession = (entry: Entry) => openSessionEntry(props.api, entry)
872
891
 
873
892
  // Flat order of the rows a cursor can land on, so keyboard navigation and
874
893
  // mouse hover resolve to the same row.
@@ -1448,7 +1467,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1448
1467
  backgroundColor={entry.id === hovered() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
1449
1468
  onMouseOver={() => setHovered(entry.id)}
1450
1469
  onMouseOut={() => setHovered(undefined)}
1451
- onMouseDown={() => props.api.route.navigate("session", { sessionID: entry.id })}
1470
+ onMouseDown={() => void openSessionEntry(props.api, entry)}
1452
1471
  >
1453
1472
  <text flexShrink={0} style={{ fg: theme().textMuted }}>
1454
1473
  ○
@@ -1752,7 +1771,7 @@ const tui: TuiPlugin = async (api) => {
1752
1771
  if (!target) return
1753
1772
  cleanup()
1754
1773
  api.ui.dialog.clear()
1755
- api.route.navigate("session", { sessionID: target.id })
1774
+ void openSessionEntry(api, target)
1756
1775
  }
1757
1776
 
1758
1777
  const disposeNav = api.keymap.registerLayer({
@@ -2153,7 +2172,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
2153
2172
  if (!target) return
2154
2173
  disposeNav()
2155
2174
  api.ui.dialog.clear()
2156
- api.route.navigate("session", { sessionID: target.entry.id })
2175
+ void openSessionEntry(api, target.entry)
2157
2176
  }
2158
2177
  const disposeNav = api.keymap.registerLayer({
2159
2178
  bindings: [