opencode-sesh 0.7.1 → 0.7.3

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
@@ -147,26 +147,6 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
147
147
  Sessions waiting on the user (unanswered questions, stuck runs) pin a
148
148
  **Needs input** group above the directories, with the count in the heading —
149
149
  same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`.
150
- - **cmux bridge (cmux-only, opt-out by absence).** When `CMUX_WORKSPACE_ID` is
151
- set the sidebar talks to the `cmux` CLI; outside cmux no cmux call is made and
152
- every click opens locally. Two behaviors, both keyed on cmux's *own* per-
153
- surface resume record (`cmux surface resume show` → `restore_record.
154
- checkpoint_id`), not on anything sesh writes:
155
- - **Click-to-focus.** `pickFocusWorkspace` (pure, covered by `tests/tui.mjs`)
156
- scans `cmux tree --json --all` plus each surface's checkpoint and, when
157
- another workspace runs the clicked session, runs
158
- `cmux workspace select <ref>` instead of `api.route.navigate`. The caller's
159
- own surface/workspace never match (a same-workspace focus is a no-op, so it
160
- opens locally). Works for background agents and older panels too.
161
- - **Workspace naming.** The poll renames this workspace to the current
162
- session title (`cmux workspace rename <id> --title …`), skipping titles that
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.
168
- These are the only cmux calls in the panel; keep them behind the
169
- `CMUX_WORKSPACE_ID` guard.
170
150
  - **Picker:** a custom dialog (not `DialogSelect`) grouped by
171
151
  project/directory, recency-ordered, resume via
172
152
  `api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
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.3] - 2026-09-24
11
+
12
+ ### Removed
13
+
14
+ - The cmux bridge, panel and CLI. Click-to-focus, the sidebar's workspace
15
+ renaming, and the `sesh cmux-sync` command (with `bin/sesh-cmux-sync.sh`) are
16
+ gone: cmux already tracks each surface's session and names workspaces, so sesh
17
+ no longer shells out to the `cmux` CLI at all.
18
+ ## [0.7.2] - 2026-09-24
19
+
20
+ ### Fixed
21
+
22
+ - Switching to a session from the picker (`ctrl+o` / `/sesh`), the home
23
+ "Recent sessions" list, or the needs-input picker now runs the cmux
24
+ click-to-focus bridge: a session already live in another cmux workspace
25
+ focuses that workspace instead of opening a duplicate here. Previously only
26
+ sidebar rows did this.
27
+
28
+ ### Changed
29
+
30
+ - Every open path shares one cmux-aware handler, and the cmux surface scan is
31
+ cached briefly so the additional paths add no noticeable delay. Terminals
32
+ outside cmux are unaffected — with no `CMUX_WORKSPACE_ID` no cmux call is
33
+ made, now asserted by a test.
10
34
  ## [0.7.1] - 2026-09-24
11
35
 
12
36
  ### Fixed
@@ -302,7 +326,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
302
326
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
303
327
  CI.
304
328
 
305
- [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.1...HEAD
329
+ [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.3...HEAD
330
+ [0.7.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.2...v0.7.3
331
+ [0.7.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.1...v0.7.2
306
332
  [0.7.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.0...v0.7.1
307
333
  [0.7.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.6.0...v0.7.0
308
334
  [0.6.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.5.0...v0.6.0
package/README.md CHANGED
@@ -145,10 +145,6 @@ per-project record.
145
145
  (`New session - …`) with the start of the session's first user message. Only
146
146
  placeholder titles are touched, and the same dry-run/`--yes` gating applies.
147
147
 
148
- `sesh cmux-sync [--dry-run] [--json]` names every [cmux](https://cmux.dev)
149
- workspace after the opencode session its pane is running, so the sidebar and the
150
- workspace list agree. cmux-only; a no-op without the cmux CLI.
151
-
152
148
  When searching, title matches outrank transcript-only matches and the current
153
149
  project rises, so the list answers "where was that thing I worked on" before it
154
150
  answers "what is newest". Pins still win outright.
@@ -192,10 +188,8 @@ Sessions waiting on you (unanswered agent questions, runs stuck mid-tool) pin
192
188
  themselves to a **Needs input** group above the directories, with the count in
193
189
  the heading; `sesh --needs-input` lists the same set in the terminal.
194
190
  Hovering a row while keyboard navigation is active selects it for the next
195
- shortcut. Clicking (or Enter on) a session opens it here — unless another cmux
196
- workspace is already running that session, in which case the sidebar focuses
197
- that workspace instead of opening a duplicate (and the workspace is kept named
198
- after its session). The full picker is one keystroke away.
191
+ shortcut. Clicking (or Enter on) a session opens it here. The full picker is one
192
+ keystroke away.
199
193
 
200
194
  The full picker pages through your whole global session list (no fixed window)
201
195
  and indexes transcript text for every session in the background, showing
package/bin/sesh CHANGED
@@ -34,9 +34,5 @@ case ${1:-} in
34
34
  shift
35
35
  exec bash "$SCRIPT_DIR/sesh-retitle.sh" "$@"
36
36
  ;;
37
- cmux-sync)
38
- shift
39
- exec bash "$SCRIPT_DIR/sesh-cmux-sync.sh" "$@"
40
- ;;
41
37
  esac
42
38
  exec "$SCRIPT_DIR/sesh.sh" "$@"
@@ -37,16 +37,9 @@ caller-side on purpose, so sesh stays terminal-agnostic.
37
37
 
38
38
  ## Keep workspace names in step
39
39
 
40
- The sidebar renames its workspace to the opencode session title as you work, and
41
- `sesh cmux-sync` does it for every workspace at once:
42
-
43
- ```bash
44
- sesh cmux-sync --dry-run # preview
45
- sesh cmux-sync # rename to match
46
- ```
47
-
48
- It reads cmux's own per-surface resume record, so it also names workspaces whose
49
- agent was started outside the sidebar.
40
+ sesh no longer renames workspaces — that's cmux's own concern (its auto-naming,
41
+ or a title-mirroring hook on the session title). The sidebar only shows the
42
+ session; naming stays caller-side like everything else here.
50
43
 
51
44
  ## Needs-input triage from cmux
52
45
 
package/install.sh CHANGED
@@ -235,7 +235,6 @@ for path in \
235
235
  bin/sesh.sh \
236
236
  bin/sesh-list.sh \
237
237
  bin/sesh-preview.sh \
238
- bin/sesh-cmux-sync.sh \
239
238
  bin/sesh-costs.sh \
240
239
  bin/sesh-delete.sh \
241
240
  bin/sesh-prune.sh \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
6
6
  "license": "MIT",
@@ -4,8 +4,6 @@ import type { Project, Session } from "@opencode-ai/sdk/v2"
4
4
  import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
5
5
  import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises"
6
6
  import { dirname, join } from "node:path"
7
- import { execFile } from "node:child_process"
8
- import { promisify } from "node:util"
9
7
  import { getTreeSitterClient, RGBA, SyntaxStyle, TextAttributes } from "@opentui/core"
10
8
 
11
9
  type ThemeColors = TuiPluginApi["theme"]["current"]
@@ -177,10 +175,6 @@ function pinnedFirst(entries: Entry[], pins: Pins): Entry[] {
177
175
  // dialog). Native `/sessions` and session_list (<leader>l) stay untouched.
178
176
  const BASE_MODE = "base"
179
177
  const POLL_MS = 15_000
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
184
178
 
185
179
  function shortDir(dir: string, home: string): string {
186
180
  if (!dir || dir === "/") return "other"
@@ -394,64 +388,6 @@ async function queryWaitingIds(db: any): Promise<Map<string, string>> {
394
388
  return out
395
389
  }
396
390
 
397
- // cmux bridge: which surface is running which agent session, so opening a
398
- // session that is live in another cmux workspace focuses that workspace
399
- // instead of opening a duplicate here. The truth is cmux's own resume record
400
- // (`checkpoint_id` per surface), not anything this panel writes — so it also
401
- // covers background agents and panes running an older panel. Outside cmux
402
- // (no CMUX_WORKSPACE_ID) no cmux call is made.
403
- type CmuxSurface = { ref: string; workspaceRef: string; here: boolean; checkpoint?: string }
404
-
405
- async function cmuxJson(args: string[]): Promise<any | undefined> {
406
- try {
407
- const { stdout } = await promisify(execFile)("cmux", args)
408
- return JSON.parse(stdout)
409
- } catch {
410
- return undefined
411
- }
412
- }
413
-
414
- async function cmuxSurfaces(): Promise<CmuxSurface[]> {
415
- const tree = await cmuxJson(["tree", "--json", "--all"])
416
- if (!tree) return []
417
- const surfaces: CmuxSurface[] = []
418
- for (const w of tree.windows ?? []) {
419
- for (const ws of w.workspaces ?? []) {
420
- if (typeof ws?.ref !== "string") continue
421
- for (const pane of ws.panes ?? []) {
422
- for (const s of pane.surfaces ?? []) {
423
- if (s?.type === "terminal" && typeof s.ref === "string") {
424
- surfaces.push({ ref: s.ref, workspaceRef: ws.ref, here: s.here === true })
425
- }
426
- }
427
- }
428
- }
429
- }
430
- await Promise.all(
431
- surfaces.map(async (surface) => {
432
- const info = await cmuxJson(["surface", "resume", "show", "--surface", surface.ref, "--json"])
433
- const checkpoint = info?.restore_record?.checkpoint_id
434
- if (typeof checkpoint === "string") surface.checkpoint = checkpoint
435
- }),
436
- )
437
- return surfaces
438
- }
439
-
440
- // Pure selection: the workspace to focus for a session, or undefined to open
441
- // locally. Prefers a live surface in a different workspace; ignores the
442
- // caller's own surface and workspace (focusing where you already are is a
443
- // no-op, so a local open is the better answer).
444
- function pickFocusWorkspace(surfaces: CmuxSurface[], sessionID: string): string | undefined {
445
- const self = surfaces.find((surface) => surface.here)
446
- for (const surface of surfaces) {
447
- if (self && surface.ref === self.ref) continue
448
- if (surface.checkpoint !== sessionID) continue
449
- if (self && surface.workspaceRef === self.workspaceRef) continue
450
- return surface.workspaceRef
451
- }
452
- return undefined
453
- }
454
-
455
391
  function addTranscript(index: Map<string, string>, sid: string, data: string): void {
456
392
  try {
457
393
  const part = JSON.parse(data) as { type?: unknown; text?: unknown }
@@ -725,7 +661,6 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
725
661
  const [pendingDelete, setPendingDelete] = createSignal<string>()
726
662
  const [waiting, setWaiting] = createSignal<Map<string, string>>(new Map())
727
663
  let confirmTimer: ReturnType<typeof setTimeout> | undefined
728
- let lastSyncedTitle: string | undefined
729
664
  const currentID = createMemo(() => {
730
665
  const route = props.api.route.current
731
666
  if (route.name !== "session") return undefined
@@ -761,20 +696,6 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
761
696
  } catch {
762
697
  // leave the previous waiting set in place rather than flashing
763
698
  }
764
- // Keep this cmux workspace named after the session it is showing, so the
765
- // sidebar and the workspace list agree. The session title is the source
766
- // of truth; a placeholder title is never pushed. cmux-only.
767
- try {
768
- const workspace = process.env.CMUX_WORKSPACE_ID
769
- const id = currentID()
770
- const title = id ? (props.api.state.session.get(id)?.title ?? "").trim() : ""
771
- if (workspace && title && !PLACEHOLDER_TITLE.test(title) && title !== lastSyncedTitle) {
772
- await promisify(execFile)("cmux", ["workspace", "rename", workspace, "--title", title])
773
- lastSyncedTitle = title
774
- }
775
- } catch {
776
- // renaming is best-effort; a failure leaves the workspace name alone
777
- }
778
699
  }
779
700
  void load()
780
701
  const clock = setInterval(() => void load(), POLL_MS)
@@ -853,23 +774,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
853
774
  setSectionCollapsed((value) => !value)
854
775
  }
855
776
 
856
- // Open a session. If a cmux surface is already running it in another
857
- // workspace, focus that workspace instead of opening a duplicate here;
858
- // otherwise open it locally. `cmux` calls only run inside cmux.
859
- const openSession = async (entry: Entry) => {
860
- if (process.env.CMUX_WORKSPACE_ID) {
861
- const target = pickFocusWorkspace(await cmuxSurfaces(), entry.id)
862
- if (target) {
863
- try {
864
- await promisify(execFile)("cmux", ["workspace", "select", target])
865
- return
866
- } catch {
867
- // cmux unavailable; fall through to a local open
868
- }
869
- }
870
- }
871
- props.api.route.navigate("session", { sessionID: entry.id })
872
- }
777
+ const openSession = (entry: Entry) => props.api.route.navigate("session", { sessionID: entry.id })
873
778
 
874
779
  // Flat order of the rows a cursor can land on, so keyboard navigation and
875
780
  // mouse hover resolve to the same row.
@@ -1,149 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Name each cmux workspace after the opencode session it is running.
3
- #
4
- # cmux records the session behind every terminal surface (its resume
5
- # `checkpoint_id`), so the mapping needs no bookkeeping: for each surface whose
6
- # checkpoint is an opencode session (`ses_…`), rename its workspace to that
7
- # session's current title. Titles are the opencode session title (the source of
8
- # truth) unless it is still the pre-first-turn placeholder, which is skipped.
9
- #
10
- # cmux-only: exits cleanly when the cmux CLI is unavailable. Renames are
11
- # lossless (a title), so there is no confirmation; --dry-run previews instead.
12
- #
13
- # Usage: sesh-cmux-sync.sh [--dry-run] [--json]
14
- set -euo pipefail
15
-
16
- OPENCODE_BIN=${SESH_OPENCODE:-opencode}
17
- CMUX_BIN=${SESH_CMUX:-cmux}
18
- dry_run=0
19
- emit_json=0
20
- while [ "$#" -gt 0 ]; do
21
- case "$1" in
22
- --dry-run) dry_run=1 ;;
23
- --json) emit_json=1 ;;
24
- --help) echo "usage: ${0##*/} [--dry-run] [--json]"; exit 0 ;;
25
- *) echo "unknown option: $1" >&2; exit 2 ;;
26
- esac
27
- shift
28
- done
29
-
30
- command -v "$CMUX_BIN" >/dev/null 2>&1 || { echo 'sesh: cmux is not available' >&2; exit 1; }
31
- export CMUX_QUIET=1
32
-
33
- resolve_db() {
34
- if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
35
- local from_cli=''
36
- if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
37
- from_cli=$("$OPENCODE_BIN" db path 2>/dev/null || true)
38
- fi
39
- if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
40
- if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
41
- return 1
42
- }
43
-
44
- SQLITE_BIN=''
45
- if [ -n "${SESH_SQLITE:-}" ]; then
46
- SQLITE_BIN=$SESH_SQLITE
47
- elif command -v sqlite3 >/dev/null 2>&1; then
48
- SQLITE_BIN=sqlite3
49
- fi
50
-
51
- JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
52
- [ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
53
-
54
- DB_PATH=$(resolve_db) || { echo 'sesh: session database unavailable' >&2; exit 1; }
55
- [ -f "$DB_PATH" ] || { echo 'sesh: session database unavailable' >&2; exit 1; }
56
-
57
- # 1. Every terminal surface cmux knows about, with its workspace.
58
- SURFACES=$("$CMUX_BIN" tree --json --all 2>/dev/null | "$JQ_BIN" -c '
59
- [ .windows[]?.workspaces[]? as $w
60
- | $w.panes[]?.surfaces[]?
61
- | select(.type == "terminal" and (.ref | type) == "string")
62
- | {surface: .ref, workspace: $w.ref} ]') || SURFACES='[]'
63
- [ -n "$SURFACES" ] || SURFACES='[]'
64
-
65
- # 2. The opencode session each surface is running (cmux resume checkpoint).
66
- PAIRS='[]'
67
- while IFS=$'\t' read -r surface workspace; do
68
- [ -n "$surface" ] || continue
69
- checkpoint=$("$CMUX_BIN" surface resume show --surface "$surface" --json 2>/dev/null \
70
- | "$JQ_BIN" -r '.restore_record.checkpoint_id // empty' 2>/dev/null || true)
71
- case "$checkpoint" in
72
- ses_[A-Za-z0-9]*) ;;
73
- *) continue ;;
74
- esac
75
- PAIRS=$(printf '%s' "$PAIRS" | "$JQ_BIN" -c \
76
- --arg ws "$workspace" --arg sid "$checkpoint" \
77
- '. + [{workspace: $ws, session: $sid}]')
78
- done < <(printf '%s' "$SURFACES" | "$JQ_BIN" -r '.[] | [.surface, .workspace] | @tsv')
79
-
80
- if [ "$(printf '%s' "$PAIRS" | "$JQ_BIN" 'length')" = 0 ]; then
81
- echo 'No cmux workspace is running an opencode session.'
82
- exit 0
83
- fi
84
-
85
- # 3. Session titles (one query) and current workspace titles.
86
- IN_LIST=''
87
- for id in $(printf '%s' "$PAIRS" | "$JQ_BIN" -r '.[].session'); do
88
- case "$id" in
89
- ses_[A-Za-z0-9]*) IN_LIST="$IN_LIST'$id'," ;;
90
- *) echo 'sesh: unexpected session id shape' >&2; exit 1 ;;
91
- esac
92
- done
93
- IN_LIST=${IN_LIST%,}
94
- if [ -n "$SQLITE_BIN" ]; then
95
- TITLE_ROWS=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "SELECT id, title FROM session WHERE id IN ($IN_LIST);")
96
- else
97
- TITLE_ROWS=$("$OPENCODE_BIN" db "SELECT id, title FROM session WHERE id IN ($IN_LIST);" --format json)
98
- fi
99
- [ -n "$TITLE_ROWS" ] || TITLE_ROWS='[]'
100
-
101
- CURRENT=$("$CMUX_BIN" workspace list --json 2>/dev/null | "$JQ_BIN" -c \
102
- '[.workspaces[] | {workspace: .ref, title: (.custom_title // .title // "")}]') || CURRENT='[]'
103
- [ -n "$CURRENT" ] || CURRENT='[]'
104
-
105
- # 4. Keep only the real renames: a non-placeholder session title that differs
106
- # from what the workspace already shows.
107
- FINAL=$(printf '%s' "$PAIRS" | "$JQ_BIN" -c \
108
- --slurpfile sessions <(printf '%s' "$TITLE_ROWS") \
109
- --slurpfile current <(printf '%s' "$CURRENT") '
110
- ($sessions[0] | map({key: .id, value: .title}) | from_entries) as $byId
111
- | ($current[0] | map({key: .workspace, value: .title}) | from_entries) as $shown
112
- | [ .[]
113
- | ($byId[.session] // "" | gsub("^ +| +$"; "")) as $t
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))
118
- | select(($shown[.workspace] // "") != $t)
119
- | . + {title: $t} ]')
120
-
121
- COUNT=$(printf '%s' "$FINAL" | "$JQ_BIN" 'length')
122
- if [ "$COUNT" = 0 ]; then
123
- echo 'Every cmux workspace already matches its session.'
124
- exit 0
125
- fi
126
-
127
- if [ "$emit_json" = 1 ]; then
128
- printf '%s\n' "$FINAL"
129
- exit 0
130
- fi
131
-
132
- printf '%s' "$FINAL" | "$JQ_BIN" -r '.[] | "\(.workspace)\t\(.title)"' | while IFS=$'\t' read -r ws title; do
133
- printf '%s → %s\n' "$ws" "$title"
134
- done
135
-
136
- if [ "$dry_run" = 1 ]; then
137
- printf 'Would rename %s workspace(s) (--dry-run: nothing changed).\n' "$COUNT" >&2
138
- exit 0
139
- fi
140
-
141
- renamed=0
142
- while IFS=$'\t' read -r ws title; do
143
- if "$CMUX_BIN" workspace rename "$ws" --title "$title" >/dev/null 2>&1; then
144
- renamed=$((renamed + 1))
145
- else
146
- echo "sesh: could not rename $ws" >&2
147
- fi
148
- done < <(printf '%s' "$FINAL" | "$JQ_BIN" -r '.[] | [.workspace, .title] | @tsv')
149
- echo "Renamed $renamed workspace(s)."