opencode-sesh 0.5.0 → 0.6.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/AGENTS.md +11 -0
- package/CHANGELOG.md +12 -1
- package/README.md +4 -1
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +99 -3
package/AGENTS.md
CHANGED
|
@@ -147,6 +147,17 @@ 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
|
+
- **Presence / click-to-focus (cmux-only).** Each sidebar pane heartbeats
|
|
151
|
+
`{workspace, surface, updated}` to the session file
|
|
152
|
+
`${XDG_DATA_HOME:-~/.local/share}/sesh/presence/<sessionId>.json` every poll,
|
|
153
|
+
but only when `CMUX_WORKSPACE_ID` is set; without it nothing is written and a
|
|
154
|
+
click always navigates locally. Opening a session reads that file and, when a
|
|
155
|
+
*fresh* (≤45 s), *foreign* workspace claims it, runs
|
|
156
|
+
`cmux workspace select <id>` instead of `api.route.navigate`. One file per
|
|
157
|
+
session means concurrent panes never share a file; writes are tmp+rename and
|
|
158
|
+
stale files are swept on each poll. A crashed pane falls through to a local
|
|
159
|
+
open after the TTL. The dispatch lives in the sidebar only — the picker and
|
|
160
|
+
home list always open locally.
|
|
150
161
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
151
162
|
project/directory, recency-ordered, resume via
|
|
152
163
|
`api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Clicking (or Enter on) a sidebar session now switches to the live cmux
|
|
15
|
+
workspace already showing it, instead of opening a duplicate in the current
|
|
16
|
+
pane. Panes advertise their open session (cmux-only, one heartbeat file per
|
|
17
|
+
session, ~45 s TTL); outside cmux or with no live claimant, the click opens
|
|
18
|
+
the session locally as before.
|
|
10
19
|
## [0.5.0] - 2026-09-24
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -16,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
25
|
- `/sesh-needs` opens the sessions waiting on you as an openable list (Enter
|
|
17
26
|
opens, Esc closes). Both new slashes are read-only; prune and retitle stay
|
|
18
27
|
CLI-only.
|
|
28
|
+
|
|
19
29
|
## [0.4.0] - 2026-09-24
|
|
20
30
|
|
|
21
31
|
### Added
|
|
@@ -267,7 +277,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
267
277
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
268
278
|
CI.
|
|
269
279
|
|
|
270
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.
|
|
280
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.6.0...HEAD
|
|
281
|
+
[0.6.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.5.0...v0.6.0
|
|
271
282
|
[0.5.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.4.0...v0.5.0
|
|
272
283
|
[0.4.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.3.0...v0.4.0
|
|
273
284
|
[0.3.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.1...v0.3.0
|
package/README.md
CHANGED
|
@@ -188,7 +188,10 @@ Sessions waiting on you (unanswered agent questions, runs stuck mid-tool) pin
|
|
|
188
188
|
themselves to a **Needs input** group above the directories, with the count in
|
|
189
189
|
the heading; `sesh --needs-input` lists the same set in the terminal.
|
|
190
190
|
Hovering a row while keyboard navigation is active selects it for the next
|
|
191
|
-
shortcut.
|
|
191
|
+
shortcut. Clicking (or Enter on) a session opens it here — unless a live cmux
|
|
192
|
+
workspace is already showing that session, in which case the sidebar focuses
|
|
193
|
+
that workspace instead of opening a duplicate. The full picker is one
|
|
194
|
+
keystroke away.
|
|
192
195
|
|
|
193
196
|
The full picker pages through your whole global session list (no fixed window)
|
|
194
197
|
and indexes transcript text for every session in the background, showing
|
package/package.json
CHANGED
package/tui/sesh-panel.tsx
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import type { Project, Session } from "@opencode-ai/sdk/v2"
|
|
4
4
|
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
|
5
|
-
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises"
|
|
5
|
+
import { mkdir, readFile, readdir, 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"
|
|
7
9
|
import { getTreeSitterClient, RGBA, SyntaxStyle, TextAttributes } from "@opentui/core"
|
|
8
10
|
|
|
9
11
|
type ThemeColors = TuiPluginApi["theme"]["current"]
|
|
@@ -388,6 +390,51 @@ async function queryWaitingIds(db: any): Promise<Map<string, string>> {
|
|
|
388
390
|
return out
|
|
389
391
|
}
|
|
390
392
|
|
|
393
|
+
// Presence registry: which cmux workspace is showing which session, so opening
|
|
394
|
+
// a session that is already visible elsewhere focuses that workspace instead
|
|
395
|
+
// of opening a duplicate here. Each panel heartbeats its own session file
|
|
396
|
+
// (one file per session: concurrent panes never share a file, and renames are
|
|
397
|
+
// atomic, so no locking is needed); files older than PRESENCE_TTL_MS are
|
|
398
|
+
// treated as gone. Outside cmux nothing is written and nothing is read.
|
|
399
|
+
const PRESENCE_TTL_MS = 45000
|
|
400
|
+
function presenceDir(): string {
|
|
401
|
+
const home = process.env.HOME ?? ""
|
|
402
|
+
const dataHome = process.env.XDG_DATA_HOME ?? `${home}/.local/share`
|
|
403
|
+
return join(dataHome, "sesh", "presence")
|
|
404
|
+
}
|
|
405
|
+
function parsePresence(raw: string, now: number): string | undefined {
|
|
406
|
+
let parsed: { workspace?: unknown; updated?: unknown }
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(raw)
|
|
409
|
+
} catch {
|
|
410
|
+
return undefined
|
|
411
|
+
}
|
|
412
|
+
if (typeof parsed?.workspace !== "string" || !parsed.workspace) return undefined
|
|
413
|
+
if (typeof parsed?.updated !== "number" || now - parsed.updated > PRESENCE_TTL_MS) return undefined
|
|
414
|
+
return parsed.workspace
|
|
415
|
+
}
|
|
416
|
+
async function sweepPresence(): Promise<void> {
|
|
417
|
+
try {
|
|
418
|
+
const dir = presenceDir()
|
|
419
|
+
const now = Date.now()
|
|
420
|
+
const names = await readdir(dir)
|
|
421
|
+
await Promise.all(
|
|
422
|
+
names
|
|
423
|
+
.filter((name) => name.endsWith(".json"))
|
|
424
|
+
.map(async (name) => {
|
|
425
|
+
try {
|
|
426
|
+
const raw = await readFile(join(dir, name), "utf8")
|
|
427
|
+
if (!parsePresence(raw, now)) await unlink(join(dir, name))
|
|
428
|
+
} catch {
|
|
429
|
+
// ignore races with other panes
|
|
430
|
+
}
|
|
431
|
+
}),
|
|
432
|
+
)
|
|
433
|
+
} catch {
|
|
434
|
+
// missing directory or concurrent cleanup; presence is best-effort
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
391
438
|
function addTranscript(index: Map<string, string>, sid: string, data: string): void {
|
|
392
439
|
try {
|
|
393
440
|
const part = JSON.parse(data) as { type?: unknown; text?: unknown }
|
|
@@ -696,6 +743,30 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
696
743
|
} catch {
|
|
697
744
|
// leave the previous waiting set in place rather than flashing
|
|
698
745
|
}
|
|
746
|
+
// Heartbeat this pane's session so another pane can focus it instead of
|
|
747
|
+
// opening a duplicate. cmux-only: without CMUX_WORKSPACE_ID nothing is
|
|
748
|
+
// written and opening a session always navigates locally.
|
|
749
|
+
try {
|
|
750
|
+
const workspace = process.env.CMUX_WORKSPACE_ID
|
|
751
|
+
const id = currentID()
|
|
752
|
+
if (workspace && id && SESSION_ID_PATTERN.test(id)) {
|
|
753
|
+
const dir = presenceDir()
|
|
754
|
+
await mkdir(dir, { recursive: true })
|
|
755
|
+
const tmp = join(dir, `${id}.tmp`)
|
|
756
|
+
await writeFile(
|
|
757
|
+
tmp,
|
|
758
|
+
JSON.stringify({
|
|
759
|
+
workspace,
|
|
760
|
+
surface: process.env.CMUX_SURFACE_ID ?? null,
|
|
761
|
+
updated: Date.now(),
|
|
762
|
+
}),
|
|
763
|
+
)
|
|
764
|
+
await rename(tmp, join(dir, `${id}.json`))
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
// presence is best-effort; a failed heartbeat only disables focusing
|
|
768
|
+
}
|
|
769
|
+
if (process.env.CMUX_WORKSPACE_ID) void sweepPresence()
|
|
699
770
|
}
|
|
700
771
|
void load()
|
|
701
772
|
const clock = setInterval(() => void load(), POLL_MS)
|
|
@@ -774,6 +845,31 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
774
845
|
setSectionCollapsed((value) => !value)
|
|
775
846
|
}
|
|
776
847
|
|
|
848
|
+
// Open a session. If a live cmux workspace is already showing it, focus that
|
|
849
|
+
// workspace instead of opening a duplicate here. The heartbeat self-entry is
|
|
850
|
+
// ignored (it is this pane); a stale file (crashed pane) falls through to a
|
|
851
|
+
// local navigate. Only the sidebar goes through here.
|
|
852
|
+
const openSession = async (entry: Entry) => {
|
|
853
|
+
const selfWorkspace = process.env.CMUX_WORKSPACE_ID
|
|
854
|
+
if (selfWorkspace) {
|
|
855
|
+
try {
|
|
856
|
+
const raw = await readFile(join(presenceDir(), `${entry.id}.json`), "utf8")
|
|
857
|
+
const workspace = parsePresence(raw, Date.now())
|
|
858
|
+
if (workspace && workspace !== selfWorkspace) {
|
|
859
|
+
try {
|
|
860
|
+
await promisify(execFile)("cmux", ["workspace", "select", workspace])
|
|
861
|
+
return
|
|
862
|
+
} catch {
|
|
863
|
+
// cmux unavailable; fall through to a local open
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
} catch {
|
|
867
|
+
// no presence file for this session
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
props.api.route.navigate("session", { sessionID: entry.id })
|
|
871
|
+
}
|
|
872
|
+
|
|
777
873
|
// Flat order of the rows a cursor can land on, so keyboard navigation and
|
|
778
874
|
// mouse hover resolve to the same row.
|
|
779
875
|
const itemRows = createMemo(() => tree().flatMap((row) => (row.kind === "item" ? [row.entry] : [])))
|
|
@@ -1007,7 +1103,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1007
1103
|
preventDefault: true,
|
|
1008
1104
|
cmd: () => {
|
|
1009
1105
|
const entry = itemRows()[cursor()]
|
|
1010
|
-
if (entry)
|
|
1106
|
+
if (entry) void openSession(entry)
|
|
1011
1107
|
},
|
|
1012
1108
|
},
|
|
1013
1109
|
{
|
|
@@ -1138,7 +1234,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1138
1234
|
}
|
|
1139
1235
|
}}
|
|
1140
1236
|
onMouseOut={() => setHovered(undefined)}
|
|
1141
|
-
onMouseDown={() =>
|
|
1237
|
+
onMouseDown={() => void openSession(row.entry)}
|
|
1142
1238
|
>
|
|
1143
1239
|
<text flexShrink={0}>
|
|
1144
1240
|
<span style={{ fg: theme().textMuted }}>{row.last ? "└" : "├"}</span>
|