opencode-sesh 0.1.7 → 0.2.1
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 +78 -16
- package/CHANGELOG.md +87 -1
- package/README.md +51 -24
- package/bin/sesh-delete.sh +38 -4
- package/bin/sesh-list.sh +68 -17
- package/bin/sesh-pins.sh +55 -0
- package/bin/sesh-shortcuts.sh +6 -3
- package/bin/sesh.sh +52 -16
- package/package.json +6 -4
- package/tui/sesh-panel.tsx +661 -121
package/AGENTS.md
CHANGED
|
@@ -23,10 +23,12 @@ slot section plus an xlarge modal picker; the terminal UI is fullscreen fzf.
|
|
|
23
23
|
## Start here
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
bun install # dev deps (TypeScript, opencode/OpenTUI types)
|
|
27
|
+
bun run test # fixture-DB regression suite — must stay green
|
|
28
|
+
bun run test:changelog # release-tooling regressions (scripts/changelog.sh)
|
|
29
|
+
bun run test:picker # PTY picker suite (needs fzf >= 0.73)
|
|
30
|
+
bun run typecheck # tsc over tui/ and opencode/
|
|
31
|
+
bun run lint:sh # bash -n on every script
|
|
30
32
|
HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.config)
|
|
31
33
|
```
|
|
32
34
|
|
|
@@ -36,6 +38,9 @@ HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.co
|
|
|
36
38
|
Never hardcode install paths.
|
|
37
39
|
- `tui/sesh-panel.tsx` — SolidJS panel, `/** @jsxImportSource @opentui/solid */`.
|
|
38
40
|
- `opencode/tools/sesh-list.ts` — the agent-facing `sesh-list` tool.
|
|
41
|
+
- `scripts/changelog.sh` — release tooling: drafts `[Unreleased]` from
|
|
42
|
+
conventional commits, promotes it to a dated version, prints release notes and
|
|
43
|
+
checks the compare links. `tests/changelog.sh` covers it in a throwaway repo.
|
|
39
44
|
- `themes/`, `tests/`, `install.sh`, `README.md`.
|
|
40
45
|
|
|
41
46
|
The **filename becomes the tool name** in opencode, so `sesh-list.ts` is the
|
|
@@ -60,7 +65,18 @@ the schema below. Keystrokes never touch the database:
|
|
|
60
65
|
the whole DB and pruning is always safe.
|
|
61
66
|
- render (cheap, pure `jq`): reads the snapshot only. Filters on `searchText`
|
|
62
67
|
(title + `fulltextLower`, both lowercased at extraction), groups by `cwd`,
|
|
63
|
-
sorts groups/items by recency, renders the TSV.
|
|
68
|
+
sorts groups/items by recency, renders the TSV. Notice rows (headers, a
|
|
69
|
+
vanished selection, an empty store, a query matching nothing) carry an empty
|
|
70
|
+
`sessionId` and a unique `trackingId`. `--header` prints the one-line
|
|
71
|
+
scope/count/status that the picker uses as fzf's header, and a non-actionable
|
|
72
|
+
selection leaves a one-shot `STATE_DIR/action-notice` the next render shows
|
|
73
|
+
and consumes. Snapshot records carry `archived` as 0/1 (the DB column is an
|
|
74
|
+
epoch, not a boolean).
|
|
75
|
+
- Session and directory pins live in `${XDG_DATA_HOME:-$HOME/.local/share}/sesh/pins.json`
|
|
76
|
+
(`SESH_PINS_FILE` overrides it for tests), shared by fzf and the TUI. Writes
|
|
77
|
+
use a sibling lock directory and atomic rename. Option-S toggles a selected
|
|
78
|
+
session; Option-D toggles its directory. Pinned directories sort before groups
|
|
79
|
+
containing pinned sessions, with pinned sessions first inside each group.
|
|
64
80
|
|
|
65
81
|
**Output contract** (TSV, 6 fields):
|
|
66
82
|
`agentId sessionId liveState display cwd trackingId`. `display` is the only
|
|
@@ -69,8 +85,11 @@ empty `sessionId` and are a selection no-op — keep that guard.
|
|
|
69
85
|
|
|
70
86
|
One worker (`sesh-refresh-worker.sh`, per picker, TERMed on exit) serializes
|
|
71
87
|
scans; fzf's `start`/`every(3)`/`change` bindings render only. Selection requests
|
|
72
|
-
a fresh poll (`request`/`wait`, ≤12 s) before dispatch
|
|
73
|
-
|
|
88
|
+
a fresh poll (`request`/`wait`, ≤12 s) before dispatch — skipped for `--print`,
|
|
89
|
+
which is a pure snapshot lookup — and prints a progress line to stderr while it
|
|
90
|
+
waits, because fzf has already exited and the terminal is idle. opencode has no
|
|
91
|
+
live-agent API, so `liveState` is always `unknown` and rows render gray unless
|
|
92
|
+
they are archived (opt-in, tagged `· archived`);
|
|
74
93
|
dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F or --fork).
|
|
75
94
|
Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
76
95
|
|
|
@@ -88,7 +107,12 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
88
107
|
raw string.
|
|
89
108
|
- fzf: stock only, `>= 0.73` (`transform:`, `every():`). `{q}` in binds is
|
|
90
109
|
shell-quoted by fzf — don't add quoting. Ctrl-G is bind-only (stays open);
|
|
91
|
-
Ctrl-F is `--expect` (accepts with fork).
|
|
110
|
+
Ctrl-F is `--expect` (accepts with fork). A `transform-header` inside a chained
|
|
111
|
+
action must come **before** `reload`/`reload-sync`: placed after, fzf silently
|
|
112
|
+
publishes an empty list (verified on 0.74.3).
|
|
113
|
+
- Destructive actions confirm. `sesh-delete.sh` prompts on a TTY and refuses
|
|
114
|
+
without `--yes` when stdin is not one; the TUI arms on the first Ctrl-X and
|
|
115
|
+
commits on the second (or `y`).
|
|
92
116
|
- The installer prefers the local opencode version for `@opencode-ai/*` and
|
|
93
117
|
merges (never clobbers) the config `package.json`; opencode runs `bun install`
|
|
94
118
|
at startup.
|
|
@@ -96,23 +120,38 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
96
120
|
(`feat`, `fix`, `docs`, `ci`, …). See
|
|
97
121
|
[CONTRIBUTING.md](CONTRIBUTING.md#commit-messages) for the types and the
|
|
98
122
|
version mapping.
|
|
123
|
+
- `CHANGELOG.md` follows Keep a Changelog: notable changes are written under
|
|
124
|
+
`[Unreleased]` as the work lands, and the Release workflow promotes that
|
|
125
|
+
section (dated, relinked) into the published version, using it verbatim as the
|
|
126
|
+
GitHub release body. `scripts/changelog.sh draft` can seed entries from
|
|
127
|
+
conventional commits, but hand-written prose is the expected form — never let
|
|
128
|
+
a draft overwrite curated entries.
|
|
99
129
|
|
|
100
130
|
## TUI panel (`tui/sesh-panel.tsx`)
|
|
101
131
|
|
|
102
132
|
- **Sidebar:** `api.slots.register` on `sidebar_content` (append mode — native
|
|
103
|
-
sidebar content stays). Renders
|
|
104
|
-
current one highlighted, polled every 15 s
|
|
105
|
-
|
|
133
|
+
sidebar content stays). Renders pinned sessions before the newest unarchived
|
|
134
|
+
sessions, with the current one highlighted, polled every 15 s; Ctrl-P previews
|
|
135
|
+
a row and ctrl+x deletes one (armed, then confirmed). The section lists every
|
|
136
|
+
session without a row cap and flex-grows to fill the sidebar, with overflow
|
|
137
|
+
scrolling inside; the picker remains the place for transcript search.
|
|
106
138
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
107
139
|
project/directory, recency-ordered, resume via
|
|
108
140
|
`api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
|
|
109
|
-
Ctrl-X
|
|
110
|
-
|
|
141
|
+
Ctrl-X arms and then confirms a delete through `api.client.session.delete` and
|
|
142
|
+
removes the row only after the server confirms; Ctrl-F forks through
|
|
143
|
+
`api.client.session.fork`; Ctrl-G scopes the list to the selected session's
|
|
144
|
+
project.
|
|
111
145
|
- **Picker search:** pages the global session list (`SESSION_PAGE_LIMIT`, capped
|
|
112
146
|
at `SESSION_MAX` and reported when truncated) instead of a fixed 500-row
|
|
113
147
|
window, then indexes transcript text for **every** session in batches
|
|
114
148
|
(`TRANSCRIPT_BATCH`) with bounded remote concurrency (`REMOTE_CONCURRENCY`).
|
|
115
149
|
The header shows indexing progress/coverage. `tests/tui.mjs` locks this.
|
|
150
|
+
- **Durable install:** opencode imports plugins once at startup and never hot
|
|
151
|
+
reloads, so panel changes need a full quit/reopen. `install.sh --sync-panel`
|
|
152
|
+
(run by the `postinstall`) refreshes an already-installed panel and is a
|
|
153
|
+
no-op when absent, and `sesh --check` reports when the installed copy differs
|
|
154
|
+
from the bundled one.
|
|
116
155
|
|
|
117
156
|
### Hard-won rules — do not regress
|
|
118
157
|
|
|
@@ -131,12 +170,35 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
131
170
|
removes any leftover `commands/sesh.md`.
|
|
132
171
|
- Do not bind `<leader>l` (native `session_list`) or shadow the native
|
|
133
172
|
`/sessions` command.
|
|
173
|
+
- **The sidebar never binds arrow keys globally.** Keyboard navigation is
|
|
174
|
+
opt-in: clicking the *Sessions* heading (or its search box) activates a
|
|
175
|
+
priority-20 layer, because a global `↑`/`↓` would hijack the main prompt.
|
|
176
|
+
Cursor state is derived from `itemRows()`, so hover and cursor resolve to the
|
|
177
|
+
same row. The section lists every session with no row cap
|
|
178
|
+
(`tests/tui.mjs` asserts the cap stays out); overflow scrolls inside the
|
|
179
|
+
stretched section.
|
|
180
|
+
- **Delete in the TUI is two-step.** The first `ctrl+x` arms `pendingDelete`,
|
|
181
|
+
the second (or `y`, or Enter in the picker footer) commits the armed row
|
|
182
|
+
wherever the pointer or cursor is, and `n`/Esc (or a five-second timeout)
|
|
183
|
+
cancels. Moving the pointer must never disarm: edge-hover flicker ate the
|
|
184
|
+
confirm before `confirmArmed` landed. The `y`/`n` layer is registered *only
|
|
185
|
+
while armed*, so it can never shadow the search box's typing.
|
|
186
|
+
- **Picker `ctrl+g` is project scope**, not close (parity with the fzf picker's
|
|
187
|
+
`Ctrl-G`); Esc is the only close key. `ctrl+f` forks through
|
|
188
|
+
`api.client.session.fork` and navigates to the returned session.
|
|
189
|
+
- Row titles render through `<Highlighted>`, and JSX must stay out of the
|
|
190
|
+
`function shortDir` … `const TRANSCRIPT_PREVIEW_TURNS` window that
|
|
191
|
+
`tests/tui.mjs` extracts and evaluates as the data layer.
|
|
134
192
|
|
|
135
193
|
## Verifying
|
|
136
194
|
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
139
|
-
|
|
195
|
+
- `bun run test` — fixture-DB regression suite, hermetic via `SESH_*` overrides.
|
|
196
|
+
- `bun run test:changelog` — release-tooling regressions; builds its own git
|
|
197
|
+
history in a temp dir (included in `bun run test`).
|
|
198
|
+
- `bun run test:picker` — PTY suite driving the real fzf picker (needs fzf
|
|
199
|
+
`>= 0.73` and Python 3).
|
|
200
|
+
- `bun run typecheck` — `tsc` over `tui/` and `opencode/`.
|
|
201
|
+
- `bun run lint:sh` — `bash -n` on every script.
|
|
140
202
|
- `HOME=/tmp/fakehome bash install.sh` — installer smoke test.
|
|
141
203
|
- Keep `tui/sesh-panel.tsx` and any installed copy byte-identical when testing
|
|
142
204
|
the panel locally.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The TUI sidebar's Sessions section now lists every session and fills the
|
|
15
|
+
available sidebar height: the fixed 15-row cap is gone, with overflow
|
|
16
|
+
scrolling inside the section.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Pin sessions with Option-S or directories with Option-D in both pickers. Pins live
|
|
21
|
+
in a shared XDG data file, appear with a star, and sort ahead of recent
|
|
22
|
+
sessions in the terminal picker, TUI picker, sidebar and home list.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The sidebar delete confirm no longer disarms when the pointer leaves the
|
|
27
|
+
armed row: the second Ctrl+X (or `y`) commits the visibly armed row wherever
|
|
28
|
+
the pointer is, so deleting works while hovering the sidebar edges.
|
|
29
|
+
|
|
30
|
+
- Session previews now use Ctrl-P rather than Space in the terminal picker,
|
|
31
|
+
sidebar and home list, so typing cannot accidentally open a preview.
|
|
32
|
+
- Hovering a sidebar session while keyboard navigation is active now moves the
|
|
33
|
+
selection to that row, so Option-S/Option-D pin the row under the pointer rather
|
|
34
|
+
than the previously selected (often currently open) session.
|
|
35
|
+
- The TUI sidebar's Sessions section now has a working show/hide control,
|
|
36
|
+
separate from clicking the heading to activate keyboard navigation.
|
|
37
|
+
- Transcript previews pass OpenTUI's tree-sitter client to the markdown
|
|
38
|
+
renderer, restoring themed Markdown and fenced-code highlighting.
|
|
39
|
+
- Preview Markdown now uses the active theme's semantic accents for headings,
|
|
40
|
+
emphasis, code and list markers when the default Markdown colors read like
|
|
41
|
+
body text.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Pin shortcuts are labeled Option-S/Option-D (the macOS key) in both pickers
|
|
46
|
+
and the docs. The terminal picker still binds fzf's `alt-s`/`alt-d`, which is
|
|
47
|
+
fzf's name for Option and unchanged.
|
|
48
|
+
- The TUI picker now shows transcript search matches as indexing progresses,
|
|
49
|
+
identifies transcript-only hits, and distinguishes an in-progress search from
|
|
50
|
+
a completed search with no results.
|
|
51
|
+
- Session previews identify the selected session and show loading feedback
|
|
52
|
+
while it changes, instead of displaying another session's transcript.
|
|
53
|
+
- The TUI picker uses `Ctrl-P` for transcript preview; Space always enters a
|
|
54
|
+
space in the search field, including at the start of a query.
|
|
55
|
+
- Sidebar and home searches direct users to the full picker for transcript
|
|
56
|
+
search when no titles or directories match.
|
|
57
|
+
## [0.2.0] - 2026-09-21
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- The picker header always names the effective scope, the session count and
|
|
62
|
+
whether the index is fresh or stale (`sesh-list.sh --header`), so a `Ctrl-G`
|
|
63
|
+
scope toggle or a degraded snapshot is no longer invisible.
|
|
64
|
+
- `sesh --print --json` emits `{"sessionId","cwd","fork"}` for scripting.
|
|
65
|
+
- Archived sessions are tagged `· archived` when included with `--archived`,
|
|
66
|
+
replacing the agent-state colours that could never fire (opencode exposes no
|
|
67
|
+
live agent state, so every row rendered grey either way).
|
|
68
|
+
- The TUI picker takes `Ctrl-X` (delete), `Ctrl-F` (fork) and `Ctrl-G` (scope to
|
|
69
|
+
the selected session's project), so both UIs now agree on the same keys.
|
|
70
|
+
- The TUI sidebar is capped at the 15 most recent sessions with
|
|
71
|
+
`… N more · ctrl+o for all`, and clicking the **Sessions** heading drives it
|
|
72
|
+
from the keyboard (`↑`/`↓`, Enter, Space, `Ctrl-X`, Esc).
|
|
73
|
+
- Empty stores, queries that match nothing, vanished selections and an
|
|
74
|
+
unavailable session list now say so instead of rendering a blank screen.
|
|
75
|
+
- Session titles highlight the part of the row the search matched, and the
|
|
76
|
+
sidebar search accepts uppercase and punctuation.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- Deleting a session asks first. The picker prompts `[y/N]` before dispatching,
|
|
81
|
+
the TUI arms on the first `Ctrl-X` and commits on a second `Ctrl-X` (or `y`;
|
|
82
|
+
`n`, Esc or moving cancels), and `sesh-delete.sh` refuses a non-terminal stdin
|
|
83
|
+
unless given `--yes` — a piped newline can no longer authorize an irreversible
|
|
84
|
+
delete.
|
|
85
|
+
- Resuming no longer stalls silently while the picker waits for a fresh scan: it
|
|
86
|
+
prints a progress line first, and `--print` lookups skip that poll entirely.
|
|
87
|
+
- Selecting a directory header or a notice row explains itself with a one-shot
|
|
88
|
+
notice row instead of repainting the same list as if the key had been ignored.
|
|
89
|
+
- Reconciled README, AGENTS.md and SECURITY.md with current behavior: the
|
|
90
|
+
terminal UI is described as the full set of scripts (including the refresh
|
|
91
|
+
worker and shortcut help), the extraction cache key documents the latest part
|
|
92
|
+
timestamp, the PTY picker suite (`npm run test:picker`) is listed, and the
|
|
93
|
+
npm distribution and supported-version policy are stated.
|
|
10
94
|
## [0.1.7] - 2026-09-13
|
|
11
95
|
|
|
12
96
|
### Fixed
|
|
@@ -141,7 +225,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
141
225
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
142
226
|
CI.
|
|
143
227
|
|
|
144
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1
|
|
228
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.1...HEAD
|
|
229
|
+
[0.2.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.0...v0.2.1
|
|
230
|
+
[0.2.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.7...v0.2.0
|
|
145
231
|
[0.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.6...v0.1.7
|
|
146
232
|
[0.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.5...v0.1.6
|
|
147
233
|
[0.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.4...v0.1.5
|
package/README.md
CHANGED
|
@@ -27,7 +27,10 @@ grouped by directory, full-text searchable, and previewable from anywhere.
|
|
|
27
27
|
## Why sesh
|
|
28
28
|
|
|
29
29
|
- **Every session, everywhere.** Sessions are grouped by project directory and
|
|
30
|
-
sorted by recency — across *all* your projects, not just the current one.
|
|
30
|
+
sorted by pins, then recency — across *all* your projects, not just the current one.
|
|
31
|
+
- **Pin what matters.** Option-S pins a session and Option-D pins its directory in
|
|
32
|
+
either picker. Pins are shared across the terminal and opencode TUI and
|
|
33
|
+
persist across restarts; pinned directories and sessions sort first.
|
|
31
34
|
- **Full-text search.** Type to match against session titles *and* the text of
|
|
32
35
|
the conversation itself. Reasoning and tool output are excluded, so the index
|
|
33
36
|
stays clean.
|
|
@@ -58,7 +61,8 @@ cd opencode-sesh
|
|
|
58
61
|
bash install.sh
|
|
59
62
|
```
|
|
60
63
|
|
|
61
|
-
`sesh install` (or `bash install.sh`) links `sesh` into
|
|
64
|
+
`sesh install` (or `bash install.sh`) links `sesh` into `$XDG_BIN_HOME`
|
|
65
|
+
(default `~/.local/bin`), copies
|
|
62
66
|
the `sesh-list` tool and the TUI panel into
|
|
63
67
|
`${XDG_CONFIG_HOME:-~/.config}/opencode`, registers the panel in `tui.json`, and
|
|
64
68
|
declares the plugin dependencies (opencode installs them on next start). The
|
|
@@ -111,22 +115,31 @@ bash install.sh --uninstall # from source
|
|
|
111
115
|
| Enter | Resume the selected session in this terminal |
|
|
112
116
|
| Ctrl-F | Resume as a fork (the original is untouched) |
|
|
113
117
|
| Ctrl-G | Toggle current-directory scope / all sessions |
|
|
114
|
-
|
|
|
118
|
+
| Option-S / Option-D | Pin or unpin the selected session / directory |
|
|
119
|
+
| Ctrl-P | Toggle the transcript preview |
|
|
115
120
|
| `?` (empty query) | Toggle shortcut help |
|
|
116
|
-
| Ctrl-X | Delete the selected session |
|
|
121
|
+
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
117
122
|
| Escape | Exit |
|
|
118
123
|
|
|
124
|
+
The header line always shows the effective scope, the session count and whether
|
|
125
|
+
the index is fresh or stale, so a `Ctrl-G` toggle is never silent. A query that
|
|
126
|
+
matches nothing says so instead of showing an empty screen.
|
|
127
|
+
|
|
119
128
|
Flags: `--cwd` (current directory only), `--limit N` (default: all),
|
|
120
129
|
`--archived`, `--print` (print `id<TAB>cwd` instead of resuming), `--fork`,
|
|
121
|
-
`--query TEXT`, `--check`.
|
|
130
|
+
`--json` (with `--print`, emit JSON), `--query TEXT`, `--check`.
|
|
122
131
|
|
|
123
132
|
Because `--print` just emits the id and directory, `sesh` doubles as a scriptable
|
|
124
133
|
session lookup:
|
|
125
134
|
|
|
126
135
|
```bash
|
|
127
136
|
read -r id cwd < <(sesh --print --query "auth")
|
|
137
|
+
sesh --print --json --query "auth" | jq -r .cwd
|
|
128
138
|
```
|
|
129
139
|
|
|
140
|
+
Deleting is irreversible, so the picker asks before dispatching it, and a
|
|
141
|
+
non-interactive `sesh-delete.sh` refuses unless given `--yes`.
|
|
142
|
+
|
|
130
143
|
### TUI panel
|
|
131
144
|
|
|
132
145
|
| Key | Action |
|
|
@@ -135,12 +148,22 @@ read -r id cwd < <(sesh --print --query "auth")
|
|
|
135
148
|
| `ctrl+o` | Open the full picker (also in the command palette) |
|
|
136
149
|
| Type | Search titles, directories and transcript text |
|
|
137
150
|
| `↑`/`↓`, `PgUp`/`PgDn`, `Home`/`End` | Move the selection |
|
|
138
|
-
|
|
|
151
|
+
| Ctrl-P | Toggle the transcript preview |
|
|
139
152
|
| Enter | Open the selected session |
|
|
153
|
+
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
154
|
+
| Ctrl-F | Fork the selected session |
|
|
155
|
+
| Ctrl-G | Toggle scope: the selected session's project, or every project |
|
|
156
|
+
| Option-S / Option-D | Pin or unpin the selected session / directory |
|
|
140
157
|
| Esc | Close |
|
|
141
158
|
|
|
142
159
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
|
143
|
-
elsewhere or press Esc to leave search.
|
|
160
|
+
elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
|
|
161
|
+
the list from the keyboard instead of the mouse (`↑`/`↓` to move, Enter to open,
|
|
162
|
+
Ctrl-P to preview, Option-S/Option-D to pin, Ctrl-X to delete, Esc to leave). Pinned
|
|
163
|
+
sessions appear ahead of the rest; the section lists every session and scrolls
|
|
164
|
+
to fill the sidebar, so click **▾ hide** to collapse it when you need the room.
|
|
165
|
+
Hovering a row while keyboard navigation is active selects it for the next
|
|
166
|
+
shortcut. The full picker is one keystroke away.
|
|
144
167
|
|
|
145
168
|
The full picker pages through your whole global session list (no fixed window)
|
|
146
169
|
and indexes transcript text for every session in the background, showing
|
|
@@ -159,22 +182,25 @@ All variables are optional.
|
|
|
159
182
|
| `SESH_GLOW` | first `glow` on `PATH` | optional Markdown preview renderer |
|
|
160
183
|
| `SESH_OPENCODE` | `opencode` | opencode executable (resume / delete) |
|
|
161
184
|
| `SESH_CACHE_DIR` | `~/.cache/sesh` | persistent search-index cache |
|
|
185
|
+
| `SESH_PINS_FILE` | `${XDG_DATA_HOME:-~/.local/share}/sesh/pins.json` | shared persistent session/directory pins |
|
|
162
186
|
|
|
163
187
|
## How it works
|
|
164
188
|
|
|
165
|
-
`bin/sesh-list.sh` is the only
|
|
166
|
-
(SQLite: `session` / `message` / `part`). Each refresh extracts session
|
|
167
|
-
and text-only parts into a persistent per-session cache keyed on
|
|
168
|
-
|
|
169
|
-
re-renders that snapshot with `jq`
|
|
170
|
-
database scans. Reasoning and tool
|
|
171
|
-
session ids are validated before
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
(`sesh-
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
`bin/sesh-list.sh` is the only `bin/` script that reads the opencode session
|
|
190
|
+
store (SQLite: `session` / `message` / `part`). Each refresh extracts session
|
|
191
|
+
metadata and text-only parts into a persistent per-session cache keyed on
|
|
192
|
+
`time_updated`, part count and the latest part timestamp, then atomically
|
|
193
|
+
publishes a `snapshot.jsonl`. Every keystroke re-renders that snapshot with `jq`
|
|
194
|
+
alone, so typing never starts competing database scans. Reasoning and tool
|
|
195
|
+
payloads are never indexed or previewed, and session ids are validated before
|
|
196
|
+
any SQL interpolation.
|
|
197
|
+
|
|
198
|
+
Flat scripts back the terminal UI — the picker (`sesh.sh`), the refresh engine
|
|
199
|
+
(`sesh-list.sh`, driven by a per-picker `sesh-refresh-worker.sh` that serializes
|
|
200
|
+
scans), preview renderer (`sesh-preview.sh`), deleter (`sesh-delete.sh`) and
|
|
201
|
+
shortcut help (`sesh-shortcuts.sh`). The TUI panel (`tui/sesh-panel.tsx`) is a
|
|
202
|
+
SolidJS OpenTUI plugin that talks to the opencode SDK over the same store; the
|
|
203
|
+
`sesh-list` agent tool reads it through `opencode db`.
|
|
178
204
|
|
|
179
205
|
## FAQ
|
|
180
206
|
|
|
@@ -202,10 +228,11 @@ guide; this project follows the [Code of Conduct](CODE_OF_CONDUCT.md). Run the
|
|
|
202
228
|
checks before opening a PR:
|
|
203
229
|
|
|
204
230
|
```bash
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
231
|
+
bun install
|
|
232
|
+
bun run test # fixture-database regression suite
|
|
233
|
+
bun run test:picker # PTY picker suite (needs fzf >= 0.73)
|
|
234
|
+
bun run typecheck # tsc over tui/ and opencode/
|
|
235
|
+
bun run lint:sh # bash -n on every script
|
|
209
236
|
```
|
|
210
237
|
|
|
211
238
|
`HOME=/tmp/fakehome bash install.sh` smoke-tests the installer without touching
|
package/bin/sesh-delete.sh
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Usage: sesh-delete.sh STATE_DIR SESSION_ID
|
|
2
|
+
# Usage: sesh-delete.sh [--yes] STATE_DIR SESSION_ID
|
|
3
3
|
# Ctrl-X deletes through the opencode CLI so its own safeguards apply.
|
|
4
|
-
#
|
|
4
|
+
# Deletion is irreversible, so this confirms first: an interactive run asks for
|
|
5
|
+
# a y, and a non-interactive run must pass --yes rather than inherit whatever
|
|
6
|
+
# stdin happens to be. Rows with no session id (headers, notices) stay silent
|
|
7
|
+
# no-ops.
|
|
5
8
|
set -euo pipefail
|
|
6
9
|
|
|
7
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
assume_yes=0
|
|
12
|
+
positional=()
|
|
13
|
+
while [ "$#" -gt 0 ]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--yes|-y) assume_yes=1 ;;
|
|
16
|
+
--help) echo "usage: ${0##*/} [--yes] STATE_DIR SESSION_ID"; exit 0 ;;
|
|
17
|
+
*) positional+=("$1") ;;
|
|
18
|
+
esac
|
|
19
|
+
shift
|
|
20
|
+
done
|
|
21
|
+
state_dir=${positional[0]:-}
|
|
22
|
+
session_id=${positional[1]:-}
|
|
10
23
|
snapshot="$state_dir/snapshot.jsonl"
|
|
11
24
|
lock="$state_dir/refresh.lock"
|
|
12
25
|
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
@@ -40,6 +53,27 @@ acquire_lock() {
|
|
|
40
53
|
[[ "$session_id" =~ ^ses_[A-Za-z0-9]+$ ]] || { echo "Invalid session identity."; pause; exit 0; }
|
|
41
54
|
[ -n "$state_dir" ] && [ -f "$snapshot" ] || { echo "No session snapshot available."; pause; exit 0; }
|
|
42
55
|
|
|
56
|
+
# Confirm before taking the lock: a human can sit on the prompt for as long as
|
|
57
|
+
# they like without blocking the snapshot publisher. The row is re-checked
|
|
58
|
+
# against the snapshot below, so a session that vanishes meanwhile still loses.
|
|
59
|
+
if [ "$assume_yes" = 0 ]; then
|
|
60
|
+
if [ -t 0 ]; then
|
|
61
|
+
title=$("$JQ_BIN" -r --arg sid "$session_id" 'select(.sessionId == $sid) | .title // ""' "$snapshot" 2>/dev/null || true)
|
|
62
|
+
[ -n "$title" ] || title='untitled'
|
|
63
|
+
printf '%s' "Delete \"$title\" ($session_id)? [y/N] "
|
|
64
|
+
reply=''
|
|
65
|
+
IFS= read -r -n 1 reply || reply=''
|
|
66
|
+
printf '\n'
|
|
67
|
+
case "$reply" in
|
|
68
|
+
y|Y) ;;
|
|
69
|
+
*) echo "Cancelled; nothing was deleted."; pause; exit 0 ;;
|
|
70
|
+
esac
|
|
71
|
+
else
|
|
72
|
+
echo "Refusing to delete $session_id: pass --yes when stdin is not a terminal." >&2
|
|
73
|
+
exit 2
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
43
77
|
# Serialize this action with the sole snapshot publisher. A short collision
|
|
44
78
|
# is safer than deleting against a moving snapshot.
|
|
45
79
|
if ! acquire_lock; then
|
package/bin/sesh-list.sh
CHANGED
|
@@ -12,6 +12,7 @@ LIMIT=0
|
|
|
12
12
|
LIMIT_SET=0
|
|
13
13
|
SCOPE_SET=0
|
|
14
14
|
REFRESH=0
|
|
15
|
+
HEADER=0
|
|
15
16
|
WAIT_LOCK=0
|
|
16
17
|
TOGGLE_SCOPE=0
|
|
17
18
|
INCLUDE_ARCHIVED=0
|
|
@@ -20,10 +21,11 @@ STATE_DIR=''
|
|
|
20
21
|
QUERY=''
|
|
21
22
|
SELECTED_ID=''
|
|
22
23
|
SCOPE_CWD=''
|
|
23
|
-
usage() { echo "usage: ${0##*/} [--refresh] [--wait-lock SECONDS] --state-dir DIR [--cwd] [--limit N] [--archived] [--toggle-scope] [--query TEXT]" >&2; }
|
|
24
|
+
usage() { echo "usage: ${0##*/} [--refresh] [--header] [--wait-lock SECONDS] --state-dir DIR [--cwd] [--limit N] [--archived] [--toggle-scope] [--query TEXT]" >&2; }
|
|
24
25
|
while [ "$#" -gt 0 ]; do
|
|
25
26
|
case "$1" in
|
|
26
27
|
--refresh) REFRESH=1 ;;
|
|
28
|
+
--header) HEADER=1 ;;
|
|
27
29
|
--wait-lock) WAIT_LOCK=${2:-}; shift ;;
|
|
28
30
|
--state-dir) STATE_DIR=${2:-}; shift ;;
|
|
29
31
|
--cwd) SCOPE_CWD=$PWD; SCOPE_SET=1 ;;
|
|
@@ -113,38 +115,85 @@ atomic_text() {
|
|
|
113
115
|
}
|
|
114
116
|
set_status() { atomic_text "$STATUS" "$1"; }
|
|
115
117
|
|
|
118
|
+
# One-line fzf --header: the effective scope, how many sessions the snapshot
|
|
119
|
+
# holds and whether it is fresh. Reads published state only, so it stays cheap
|
|
120
|
+
# enough to run on fzf's 3s poll without touching the database.
|
|
121
|
+
header() {
|
|
122
|
+
local state='' count=0 scope='all dirs' line=''
|
|
123
|
+
[ -f "$STATUS" ] && state=$(cat "$STATUS" 2>/dev/null || true)
|
|
124
|
+
if [ -f "$SNAPSHOT" ]; then
|
|
125
|
+
count=$(wc -l < "$SNAPSHOT" 2>/dev/null || printf '0')
|
|
126
|
+
count=${count//[!0-9]/}
|
|
127
|
+
[ -n "$count" ] || count=0
|
|
128
|
+
fi
|
|
129
|
+
if [ -n "$SCOPE_CWD" ]; then
|
|
130
|
+
case "$SCOPE_CWD" in
|
|
131
|
+
"$HOME") scope='~' ;;
|
|
132
|
+
"$HOME"/*) scope="~${SCOPE_CWD#"$HOME"}" ;;
|
|
133
|
+
*) scope=$SCOPE_CWD ;;
|
|
134
|
+
esac
|
|
135
|
+
fi
|
|
136
|
+
line="${CYAN}${scope}${RESET} ${GRAY}·${RESET} ${GRAY}${count} sessions${RESET}"
|
|
137
|
+
[ "$INCLUDE_ARCHIVED" = 1 ] && line="${line} ${GRAY}·${RESET} ${YELLOW}archived shown${RESET}"
|
|
138
|
+
[ -n "$state" ] && [ "$state" != fresh ] && line="${line} ${GRAY}·${RESET} ${YELLOW}${state}${RESET}"
|
|
139
|
+
printf '%s' "$line"
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
render() {
|
|
117
|
-
local state=''
|
|
143
|
+
local state='' notice=''
|
|
118
144
|
[ -f "$STATUS" ] && state=$(cat "$STATUS" 2>/dev/null || true)
|
|
145
|
+
# A non-actionable selection (a directory header, a stale notice) leaves a
|
|
146
|
+
# one-shot message here so the reopened picker explains itself instead of
|
|
147
|
+
# repainting the same list as if the key had been ignored.
|
|
148
|
+
if [ -f "$STATE_DIR/action-notice" ]; then
|
|
149
|
+
notice=$(cat "$STATE_DIR/action-notice" 2>/dev/null || true)
|
|
150
|
+
rm -f "$STATE_DIR/action-notice" 2>/dev/null || true
|
|
151
|
+
fi
|
|
119
152
|
if [ ! -f "$SNAPSHOT" ]; then
|
|
153
|
+
[ -n "$notice" ] && printf '\t\tunknown\t%s%s%s\t\tnotice:action\n' "$YELLOW" "$notice" "$RESET"
|
|
120
154
|
[ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
|
|
121
155
|
return 0
|
|
122
156
|
fi
|
|
123
157
|
{
|
|
158
|
+
[ -n "$notice" ] && printf '\t\tunknown\t%s%s%s\t\tnotice:action\n' "$YELLOW" "$notice" "$RESET"
|
|
124
159
|
[ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
|
|
125
|
-
"$JQ_BIN" -sr --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg yellow "$YELLOW" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" --arg selected "$SELECTED_ID" '
|
|
160
|
+
"$JQ_BIN" -sr --argjson pins "$("${BASH_SOURCE[0]%/*}/sesh-pins.sh" read)" --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg yellow "$YELLOW" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" --arg selected "$SELECTED_ID" '
|
|
126
161
|
def ago($epoch):
|
|
127
162
|
((now - ($epoch | tonumber)) | floor) as $s
|
|
128
163
|
| if $s < 60 then "now" elif $s < 3600 then "\($s / 60 | floor)m" elif $s < 86400 then "\($s / 3600 | floor)h" else "\($s / 86400 | floor)d" end;
|
|
129
164
|
def homepath:
|
|
130
165
|
. as $p | if $p == "" then "?" elif $p == $home then "~" elif ($home != "" and startswith($home + "/")) then "~" + .[($home|length):] else . end;
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
|
|
166
|
+
# Non-actionable rows: an empty field 2 makes Enter/Ctrl-F/Ctrl-X no-ops,
|
|
167
|
+
# and the unique id keeps fzf from falling back to a neighbouring session.
|
|
168
|
+
def notice_row($msg; $id): (["", "", "unknown", ($yellow + $msg + $reset), "", $id] | @tsv);
|
|
169
|
+
# Preserve a vanished selection so the picker keeps its identity instead
|
|
170
|
+
# of jumping to a different session; keep it until the user moves away.
|
|
134
171
|
(if ($selected | test("^ses_[A-Za-z0-9]+$")) and (any(.sessionId == $selected) | not)
|
|
135
|
-
then (
|
|
172
|
+
then notice_row("Selected session is no longer available; choose another session"; $selected)
|
|
136
173
|
else empty end),
|
|
174
|
+
# An empty result set must say so: with the info line hidden and an empty
|
|
175
|
+
# preview, a filtered-to-nothing list is otherwise an indistinguishable
|
|
176
|
+
# blank screen.
|
|
137
177
|
(($query | ascii_downcase) as $q
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
| [.[] | select($q == "" or (.searchText | contains($q)))] as $matched
|
|
179
|
+
| (if ($matched | length) == 0
|
|
180
|
+
then notice_row(if $q == "" then "No sessions in this store" else "No sessions match \"" + $query + "\"" end; "notice:no-match")
|
|
181
|
+
else empty end),
|
|
182
|
+
($matched
|
|
183
|
+
| group_by(.cwd)
|
|
184
|
+
| map({cwd: .[0].cwd, updated: (map(.updatedEpoch) | max),
|
|
185
|
+
pinDirectory: (.[0].cwd as $dir | $pins.directories | index($dir) != null),
|
|
186
|
+
pinSession: (any(.[]; .sessionId as $id | $pins.sessions | index($id) != null)),
|
|
187
|
+
items: (sort_by([(.sessionId as $id | $pins.sessions | index($id) != null), .updatedEpoch]) | reverse)})
|
|
188
|
+
| sort_by([.pinDirectory, .pinSession, .updated]) | reverse
|
|
189
|
+
| .[] as $g
|
|
190
|
+
| (["", "", "unknown", ($magenta + (if $g.pinDirectory then "★ " else "" end) + ($g.cwd | homepath | gsub("[\u0000-\u001f\u007f-\u009f]";" ")) + $reset), $g.cwd, ("hdr:" + $g.cwd)] | @tsv),
|
|
144
191
|
($g.items | to_entries[] | .key as $i | .value as $s
|
|
145
|
-
|
|
192
|
+
# Archived rows are opt-in and need to look like it; nothing else about
|
|
193
|
+
# a row is knowable here (live agent state is not exposed by opencode).
|
|
194
|
+
| (if $s.archived == 1 then $yellow else $gray end) as $c
|
|
146
195
|
| ($gray + (if $i == (($g.items|length)-1) then "└" else "├" end) + $reset) as $branch
|
|
147
|
-
|
|
196
|
+
| [$s.agentId, $s.sessionId, $s.liveState, ($branch + " " + $c + (if ($s.sessionId as $id | $pins.sessions | index($id)) != null then "★ " else "" end) + ($s.title | gsub("[\u0000-\u001f\u007f-\u009f]";" ")) + (if $s.archived == 1 then " · archived" else "" end) + $reset + " " + $gray + ago($s.updatedEpoch) + $reset), $s.cwd, $s.sessionId] | @tsv)))
|
|
148
197
|
' "$SNAPSHOT"
|
|
149
198
|
} || printf '\t\tunknown\t%sSnapshot rendering failed; retry refresh%s\t\tnotice:render-error\n' "$YELLOW" "$RESET"
|
|
150
199
|
}
|
|
@@ -326,7 +375,7 @@ refresh() {
|
|
|
326
375
|
done < "$records"
|
|
327
376
|
fi
|
|
328
377
|
|
|
329
|
-
if ! "$JQ_BIN" -c --slurpfile hist "$historical" --arg scope "$SCOPE_CWD" --argjson limit "$LIMIT" --argjson archived "$INCLUDE_ARCHIVED" '
|
|
378
|
+
if ! "$JQ_BIN" -c --slurpfile hist "$historical" --argjson pins "$("${BASH_SOURCE[0]%/*}/sesh-pins.sh" read)" --arg scope "$SCOPE_CWD" --argjson limit "$LIMIT" --argjson archived "$INCLUDE_ARCHIVED" '
|
|
330
379
|
.meta as $meta
|
|
331
380
|
| ($hist | map({key: .sessionId, value: .}) | from_entries) as $histById
|
|
332
381
|
| [$meta[]
|
|
@@ -336,11 +385,12 @@ refresh() {
|
|
|
336
385
|
| . as $m | ($histById[$m.id] // {}) as $h
|
|
337
386
|
| {sessionId: $m.id, malformedRecords: 0, agentId: "", agentState: "",
|
|
338
387
|
liveState: "unknown", transcriptPath: "", source: "opencode",
|
|
388
|
+
archived: (if (($m.archived // 0) | tonumber) == 0 then 0 else 1 end),
|
|
339
389
|
cwd: (if ($h.cwd // "") != "" then $h.cwd else $m.directory end),
|
|
340
390
|
updatedEpoch: ($h.updatedEpoch // (($m.time_updated / 1000 | floor))),
|
|
341
391
|
title: (if ($h.title // "") != "" then $h.title else ($m.title // "(untitled)") end)}
|
|
342
392
|
| .searchText = ((.title | ascii_downcase) + " " + ($histById[.sessionId].fulltextLower // ""))]
|
|
343
|
-
|
|
393
|
+
| sort_by([(.cwd as $dir | $pins.directories | index($dir) != null), (.sessionId as $id | $pins.sessions | index($id) != null), .updatedEpoch]) | reverse
|
|
344
394
|
| (if $limit == 0 then . else .[:$limit] end)[]
|
|
345
395
|
' "$cache_state" > "$out"; then
|
|
346
396
|
set_status 'stale: snapshot construction failed; showing last good snapshot'
|
|
@@ -356,5 +406,6 @@ refresh() {
|
|
|
356
406
|
fi
|
|
357
407
|
}
|
|
358
408
|
|
|
409
|
+
if [ "$HEADER" = 1 ]; then header; exit 0; fi
|
|
359
410
|
if [ "$REFRESH" = 1 ] || [ "$TOGGLE_SCOPE" = 1 ]; then refresh; fi
|
|
360
411
|
render
|