opencode-sesh 0.1.6 → 0.2.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 +69 -16
- package/CHANGELOG.md +62 -1
- package/README.md +45 -22
- package/bin/sesh-delete.sh +38 -4
- package/bin/sesh-list.sh +57 -9
- package/bin/sesh-shortcuts.sh +3 -2
- package/bin/sesh.sh +59 -16
- package/install.sh +109 -76
- package/package.json +7 -4
- package/tui/sesh-panel.tsx +414 -70
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,13 @@ 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).
|
|
64
75
|
|
|
65
76
|
**Output contract** (TSV, 6 fields):
|
|
66
77
|
`agentId sessionId liveState display cwd trackingId`. `display` is the only
|
|
@@ -69,8 +80,11 @@ empty `sessionId` and are a selection no-op — keep that guard.
|
|
|
69
80
|
|
|
70
81
|
One worker (`sesh-refresh-worker.sh`, per picker, TERMed on exit) serializes
|
|
71
82
|
scans; fzf's `start`/`every(3)`/`change` bindings render only. Selection requests
|
|
72
|
-
a fresh poll (`request`/`wait`, ≤12 s) before dispatch
|
|
73
|
-
|
|
83
|
+
a fresh poll (`request`/`wait`, ≤12 s) before dispatch — skipped for `--print`,
|
|
84
|
+
which is a pure snapshot lookup — and prints a progress line to stderr while it
|
|
85
|
+
waits, because fzf has already exited and the terminal is idle. opencode has no
|
|
86
|
+
live-agent API, so `liveState` is always `unknown` and rows render gray unless
|
|
87
|
+
they are archived (opt-in, tagged `· archived`);
|
|
74
88
|
dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F or --fork).
|
|
75
89
|
Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
76
90
|
|
|
@@ -88,7 +102,12 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
88
102
|
raw string.
|
|
89
103
|
- fzf: stock only, `>= 0.73` (`transform:`, `every():`). `{q}` in binds is
|
|
90
104
|
shell-quoted by fzf — don't add quoting. Ctrl-G is bind-only (stays open);
|
|
91
|
-
Ctrl-F is `--expect` (accepts with fork).
|
|
105
|
+
Ctrl-F is `--expect` (accepts with fork). A `transform-header` inside a chained
|
|
106
|
+
action must come **before** `reload`/`reload-sync`: placed after, fzf silently
|
|
107
|
+
publishes an empty list (verified on 0.74.3).
|
|
108
|
+
- Destructive actions confirm. `sesh-delete.sh` prompts on a TTY and refuses
|
|
109
|
+
without `--yes` when stdin is not one; the TUI arms on the first Ctrl-X and
|
|
110
|
+
commits on the second (or `y`).
|
|
92
111
|
- The installer prefers the local opencode version for `@opencode-ai/*` and
|
|
93
112
|
merges (never clobbers) the config `package.json`; opencode runs `bun install`
|
|
94
113
|
at startup.
|
|
@@ -96,23 +115,37 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
96
115
|
(`feat`, `fix`, `docs`, `ci`, …). See
|
|
97
116
|
[CONTRIBUTING.md](CONTRIBUTING.md#commit-messages) for the types and the
|
|
98
117
|
version mapping.
|
|
118
|
+
- `CHANGELOG.md` follows Keep a Changelog: notable changes are written under
|
|
119
|
+
`[Unreleased]` as the work lands, and the Release workflow promotes that
|
|
120
|
+
section (dated, relinked) into the published version, using it verbatim as the
|
|
121
|
+
GitHub release body. `scripts/changelog.sh draft` can seed entries from
|
|
122
|
+
conventional commits, but hand-written prose is the expected form — never let
|
|
123
|
+
a draft overwrite curated entries.
|
|
99
124
|
|
|
100
125
|
## TUI panel (`tui/sesh-panel.tsx`)
|
|
101
126
|
|
|
102
127
|
- **Sidebar:** `api.slots.register` on `sidebar_content` (append mode — native
|
|
103
|
-
sidebar content stays). Renders the
|
|
104
|
-
current one highlighted, polled every 15 s
|
|
105
|
-
|
|
128
|
+
sidebar content stays). Renders the newest `SIDEBAR_LIMIT` unarchived sessions
|
|
129
|
+
with the current one highlighted, polled every 15 s; space previews a row and
|
|
130
|
+
ctrl+x deletes one (armed, then confirmed). Anything bulkier belongs to the
|
|
131
|
+
picker.
|
|
106
132
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
107
133
|
project/directory, recency-ordered, resume via
|
|
108
134
|
`api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
|
|
109
|
-
Ctrl-X
|
|
110
|
-
|
|
135
|
+
Ctrl-X arms and then confirms a delete through `api.client.session.delete` and
|
|
136
|
+
removes the row only after the server confirms; Ctrl-F forks through
|
|
137
|
+
`api.client.session.fork`; Ctrl-G scopes the list to the selected session's
|
|
138
|
+
project.
|
|
111
139
|
- **Picker search:** pages the global session list (`SESSION_PAGE_LIMIT`, capped
|
|
112
140
|
at `SESSION_MAX` and reported when truncated) instead of a fixed 500-row
|
|
113
141
|
window, then indexes transcript text for **every** session in batches
|
|
114
142
|
(`TRANSCRIPT_BATCH`) with bounded remote concurrency (`REMOTE_CONCURRENCY`).
|
|
115
143
|
The header shows indexing progress/coverage. `tests/tui.mjs` locks this.
|
|
144
|
+
- **Durable install:** opencode imports plugins once at startup and never hot
|
|
145
|
+
reloads, so panel changes need a full quit/reopen. `install.sh --sync-panel`
|
|
146
|
+
(run by the `postinstall`) refreshes an already-installed panel and is a
|
|
147
|
+
no-op when absent, and `sesh --check` reports when the installed copy differs
|
|
148
|
+
from the bundled one.
|
|
116
149
|
|
|
117
150
|
### Hard-won rules — do not regress
|
|
118
151
|
|
|
@@ -131,12 +164,32 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
131
164
|
removes any leftover `commands/sesh.md`.
|
|
132
165
|
- Do not bind `<leader>l` (native `session_list`) or shadow the native
|
|
133
166
|
`/sessions` command.
|
|
167
|
+
- **The sidebar never binds arrow keys globally.** Keyboard navigation is
|
|
168
|
+
opt-in: clicking the *Sessions* heading (or its search box) activates a
|
|
169
|
+
priority-20 layer, because a global `↑`/`↓` would hijack the main prompt.
|
|
170
|
+
Cursor state is derived from `itemRows()`, so hover and cursor resolve to the
|
|
171
|
+
same row, and the section is capped at `SIDEBAR_LIMIT` (`… N more · ctrl+o for
|
|
172
|
+
all` is the escape hatch) — it must stay a glanceable recent list.
|
|
173
|
+
- **Delete in the TUI is two-step.** The first `ctrl+x` arms `pendingDelete`,
|
|
174
|
+
the second (or `y`, or Enter in the picker footer) commits, and `n`/Esc/moving
|
|
175
|
+
cancels. The `y`/`n` layer is registered *only while armed*, so it can never
|
|
176
|
+
shadow the search box's typing.
|
|
177
|
+
- **Picker `ctrl+g` is project scope**, not close (parity with the fzf picker's
|
|
178
|
+
`Ctrl-G`); Esc is the only close key. `ctrl+f` forks through
|
|
179
|
+
`api.client.session.fork` and navigates to the returned session.
|
|
180
|
+
- Row titles render through `<Highlighted>`, and JSX must stay out of the
|
|
181
|
+
`function shortDir` … `const TRANSCRIPT_PREVIEW_TURNS` window that
|
|
182
|
+
`tests/tui.mjs` extracts and evaluates as the data layer.
|
|
134
183
|
|
|
135
184
|
## Verifying
|
|
136
185
|
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
139
|
-
|
|
186
|
+
- `bun run test` — fixture-DB regression suite, hermetic via `SESH_*` overrides.
|
|
187
|
+
- `bun run test:changelog` — release-tooling regressions; builds its own git
|
|
188
|
+
history in a temp dir (included in `bun run test`).
|
|
189
|
+
- `bun run test:picker` — PTY suite driving the real fzf picker (needs fzf
|
|
190
|
+
`>= 0.73` and Python 3).
|
|
191
|
+
- `bun run typecheck` — `tsc` over `tui/` and `opencode/`.
|
|
192
|
+
- `bun run lint:sh` — `bash -n` on every script.
|
|
140
193
|
- `HOME=/tmp/fakehome bash install.sh` — installer smoke test.
|
|
141
194
|
- Keep `tui/sesh-panel.tsx` and any installed copy byte-identical when testing
|
|
142
195
|
the panel locally.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The picker header always names the effective scope, the session count and
|
|
15
|
+
whether the index is fresh or stale (`sesh-list.sh --header`), so a `Ctrl-G`
|
|
16
|
+
scope toggle or a degraded snapshot is no longer invisible.
|
|
17
|
+
- `sesh --print --json` emits `{"sessionId","cwd","fork"}` for scripting.
|
|
18
|
+
- Archived sessions are tagged `· archived` when included with `--archived`,
|
|
19
|
+
replacing the agent-state colours that could never fire (opencode exposes no
|
|
20
|
+
live agent state, so every row rendered grey either way).
|
|
21
|
+
- The TUI picker takes `Ctrl-X` (delete), `Ctrl-F` (fork) and `Ctrl-G` (scope to
|
|
22
|
+
the selected session's project), so both UIs now agree on the same keys.
|
|
23
|
+
- The TUI sidebar is capped at the 15 most recent sessions with
|
|
24
|
+
`… N more · ctrl+o for all`, and clicking the **Sessions** heading drives it
|
|
25
|
+
from the keyboard (`↑`/`↓`, Enter, Space, `Ctrl-X`, Esc).
|
|
26
|
+
- Empty stores, queries that match nothing, vanished selections and an
|
|
27
|
+
unavailable session list now say so instead of rendering a blank screen.
|
|
28
|
+
- Session titles highlight the part of the row the search matched, and the
|
|
29
|
+
sidebar search accepts uppercase and punctuation.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Deleting a session asks first. The picker prompts `[y/N]` before dispatching,
|
|
34
|
+
the TUI arms on the first `Ctrl-X` and commits on a second `Ctrl-X` (or `y`;
|
|
35
|
+
`n`, Esc or moving cancels), and `sesh-delete.sh` refuses a non-terminal stdin
|
|
36
|
+
unless given `--yes` — a piped newline can no longer authorize an irreversible
|
|
37
|
+
delete.
|
|
38
|
+
- Resuming no longer stalls silently while the picker waits for a fresh scan: it
|
|
39
|
+
prints a progress line first, and `--print` lookups skip that poll entirely.
|
|
40
|
+
- Selecting a directory header or a notice row explains itself with a one-shot
|
|
41
|
+
notice row instead of repainting the same list as if the key had been ignored.
|
|
42
|
+
- Reconciled README, AGENTS.md and SECURITY.md with current behavior: the
|
|
43
|
+
terminal UI is described as the full set of scripts (including the refresh
|
|
44
|
+
worker and shortcut help), the extraction cache key documents the latest part
|
|
45
|
+
timestamp, the PTY picker suite (`npm run test:picker`) is listed, and the
|
|
46
|
+
npm distribution and supported-version policy are stated.
|
|
47
|
+
## [0.1.7] - 2026-09-13
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Sidebar and home session titles are capped to a consistent width so long
|
|
52
|
+
names no longer run into the timestamp. Removed OpenTUI's `truncate` prop,
|
|
53
|
+
which middle-truncated the already-shortened titles.
|
|
54
|
+
- The TUI picker no longer re-selects a session when the list scrolls under the
|
|
55
|
+
cursor, and its preview keeps the current transcript visible while the next
|
|
56
|
+
one loads instead of blanking on every cursor move.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- Sidebar rows use muted `├`/`└` tree glyphs and directory headers get a blank
|
|
61
|
+
line, so sessions are grouped clearly: a gap between directories, tight rows
|
|
62
|
+
within one.
|
|
63
|
+
- `npm install` and upgrades refresh an already-installed TUI panel
|
|
64
|
+
(`install.sh --sync-panel`), and `sesh --check` reports when the installed
|
|
65
|
+
panel is out of date. Because opencode imports plugins once at startup,
|
|
66
|
+
install now states that a full quit/reopen is required (a window reload reuses
|
|
67
|
+
the running process).
|
|
68
|
+
|
|
10
69
|
## [0.1.6] - 2026-09-13
|
|
11
70
|
|
|
12
71
|
### Changed
|
|
@@ -119,7 +178,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
119
178
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
120
179
|
CI.
|
|
121
180
|
|
|
122
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.
|
|
181
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.0...HEAD
|
|
182
|
+
[0.2.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.7...v0.2.0
|
|
183
|
+
[0.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.6...v0.1.7
|
|
123
184
|
[0.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.5...v0.1.6
|
|
124
185
|
[0.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.4...v0.1.5
|
|
125
186
|
[0.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.3...v0.1.4
|
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ cd opencode-sesh
|
|
|
58
58
|
bash install.sh
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
`sesh install` (or `bash install.sh`) links `sesh` into
|
|
61
|
+
`sesh install` (or `bash install.sh`) links `sesh` into `$XDG_BIN_HOME`
|
|
62
|
+
(default `~/.local/bin`), copies
|
|
62
63
|
the `sesh-list` tool and the TUI panel into
|
|
63
64
|
`${XDG_CONFIG_HOME:-~/.config}/opencode`, registers the panel in `tui.json`, and
|
|
64
65
|
declares the plugin dependencies (opencode installs them on next start). The
|
|
@@ -68,7 +69,12 @@ file it would overwrite is backed up first.
|
|
|
68
69
|
The npm package ships the same scripts and installer; the runtime tools below
|
|
69
70
|
are still required.
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
Fully quit opencode and reopen it to load the panel — opencode imports plugins
|
|
73
|
+
once at startup, so reloading a window reuses the running process. Upgrades
|
|
74
|
+
refresh an already-installed panel automatically (the npm `postinstall` syncs
|
|
75
|
+
it), and `sesh --check` reports when the installed panel is out of date.
|
|
76
|
+
|
|
77
|
+
Then:
|
|
72
78
|
|
|
73
79
|
```bash
|
|
74
80
|
sesh # terminal picker
|
|
@@ -108,20 +114,28 @@ bash install.sh --uninstall # from source
|
|
|
108
114
|
| Ctrl-G | Toggle current-directory scope / all sessions |
|
|
109
115
|
| Space (empty query) | Toggle the transcript preview |
|
|
110
116
|
| `?` (empty query) | Toggle shortcut help |
|
|
111
|
-
| Ctrl-X | Delete the selected session |
|
|
117
|
+
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
112
118
|
| Escape | Exit |
|
|
113
119
|
|
|
120
|
+
The header line always shows the effective scope, the session count and whether
|
|
121
|
+
the index is fresh or stale, so a `Ctrl-G` toggle is never silent. A query that
|
|
122
|
+
matches nothing says so instead of showing an empty screen.
|
|
123
|
+
|
|
114
124
|
Flags: `--cwd` (current directory only), `--limit N` (default: all),
|
|
115
125
|
`--archived`, `--print` (print `id<TAB>cwd` instead of resuming), `--fork`,
|
|
116
|
-
`--query TEXT`, `--check`.
|
|
126
|
+
`--json` (with `--print`, emit JSON), `--query TEXT`, `--check`.
|
|
117
127
|
|
|
118
128
|
Because `--print` just emits the id and directory, `sesh` doubles as a scriptable
|
|
119
129
|
session lookup:
|
|
120
130
|
|
|
121
131
|
```bash
|
|
122
132
|
read -r id cwd < <(sesh --print --query "auth")
|
|
133
|
+
sesh --print --json --query "auth" | jq -r .cwd
|
|
123
134
|
```
|
|
124
135
|
|
|
136
|
+
Deleting is irreversible, so the picker asks before dispatching it, and a
|
|
137
|
+
non-interactive `sesh-delete.sh` refuses unless given `--yes`.
|
|
138
|
+
|
|
125
139
|
### TUI panel
|
|
126
140
|
|
|
127
141
|
| Key | Action |
|
|
@@ -132,10 +146,16 @@ read -r id cwd < <(sesh --print --query "auth")
|
|
|
132
146
|
| `↑`/`↓`, `PgUp`/`PgDn`, `Home`/`End` | Move the selection |
|
|
133
147
|
| Space | Toggle the transcript preview |
|
|
134
148
|
| Enter | Open the selected session |
|
|
149
|
+
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
150
|
+
| Ctrl-F | Fork the selected session |
|
|
151
|
+
| Ctrl-G | Toggle scope: the selected session's project, or every project |
|
|
135
152
|
| Esc | Close |
|
|
136
153
|
|
|
137
154
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
|
138
|
-
elsewhere or press Esc to leave search.
|
|
155
|
+
elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
|
|
156
|
+
the list from the keyboard instead of the mouse (`↑`/`↓` to move, Enter to open,
|
|
157
|
+
Space to preview, Ctrl-X to delete, Esc to leave). The sidebar shows the 15 most
|
|
158
|
+
recent sessions — the full picker is one keystroke away.
|
|
139
159
|
|
|
140
160
|
The full picker pages through your whole global session list (no fixed window)
|
|
141
161
|
and indexes transcript text for every session in the background, showing
|
|
@@ -157,19 +177,21 @@ All variables are optional.
|
|
|
157
177
|
|
|
158
178
|
## How it works
|
|
159
179
|
|
|
160
|
-
`bin/sesh-list.sh` is the only
|
|
161
|
-
(SQLite: `session` / `message` / `part`). Each refresh extracts session
|
|
162
|
-
and text-only parts into a persistent per-session cache keyed on
|
|
163
|
-
|
|
164
|
-
re-renders that snapshot with `jq`
|
|
165
|
-
database scans. Reasoning and tool
|
|
166
|
-
session ids are validated before
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
(`sesh-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
`bin/sesh-list.sh` is the only `bin/` script that reads the opencode session
|
|
181
|
+
store (SQLite: `session` / `message` / `part`). Each refresh extracts session
|
|
182
|
+
metadata and text-only parts into a persistent per-session cache keyed on
|
|
183
|
+
`time_updated`, part count and the latest part timestamp, then atomically
|
|
184
|
+
publishes a `snapshot.jsonl`. Every keystroke re-renders that snapshot with `jq`
|
|
185
|
+
alone, so typing never starts competing database scans. Reasoning and tool
|
|
186
|
+
payloads are never indexed or previewed, and session ids are validated before
|
|
187
|
+
any SQL interpolation.
|
|
188
|
+
|
|
189
|
+
Flat scripts back the terminal UI — the picker (`sesh.sh`), the refresh engine
|
|
190
|
+
(`sesh-list.sh`, driven by a per-picker `sesh-refresh-worker.sh` that serializes
|
|
191
|
+
scans), preview renderer (`sesh-preview.sh`), deleter (`sesh-delete.sh`) and
|
|
192
|
+
shortcut help (`sesh-shortcuts.sh`). The TUI panel (`tui/sesh-panel.tsx`) is a
|
|
193
|
+
SolidJS OpenTUI plugin that talks to the opencode SDK over the same store; the
|
|
194
|
+
`sesh-list` agent tool reads it through `opencode db`.
|
|
173
195
|
|
|
174
196
|
## FAQ
|
|
175
197
|
|
|
@@ -197,10 +219,11 @@ guide; this project follows the [Code of Conduct](CODE_OF_CONDUCT.md). Run the
|
|
|
197
219
|
checks before opening a PR:
|
|
198
220
|
|
|
199
221
|
```bash
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
bun install
|
|
223
|
+
bun run test # fixture-database regression suite
|
|
224
|
+
bun run test:picker # PTY picker suite (needs fzf >= 0.73)
|
|
225
|
+
bun run typecheck # tsc over tui/ and opencode/
|
|
226
|
+
bun run lint:sh # bash -n on every script
|
|
204
227
|
```
|
|
205
228
|
|
|
206
229
|
`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,14 +115,47 @@ 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
160
|
"$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" '
|
|
126
161
|
def ago($epoch):
|
|
@@ -128,23 +163,34 @@ render() {
|
|
|
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
|
-
|
|
|
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
|
|
139
183
|
| group_by(.cwd)
|
|
140
184
|
| map({cwd: .[0].cwd, updated: (map(.updatedEpoch) | max), items: (sort_by(.updatedEpoch) | reverse)})
|
|
141
185
|
| sort_by(.updated) | reverse
|
|
142
186
|
| .[] as $g
|
|
143
187
|
| (["", "", "unknown", ($magenta + ($g.cwd | homepath | gsub("[\u0000-\u001f\u007f-\u009f]";" ")) + $reset), $g.cwd, ("hdr:" + $g.cwd)] | @tsv),
|
|
144
188
|
($g.items | to_entries[] | .key as $i | .value as $s
|
|
145
|
-
|
|
189
|
+
# Archived rows are opt-in and need to look like it; nothing else about
|
|
190
|
+
# a row is knowable here (live agent state is not exposed by opencode).
|
|
191
|
+
| (if $s.archived == 1 then $yellow else $gray end) as $c
|
|
146
192
|
| ($gray + (if $i == (($g.items|length)-1) then "└" else "├" end) + $reset) as $branch
|
|
147
|
-
| [$s.agentId, $s.sessionId, $s.liveState, ($branch + " " + $c + ($s.title | gsub("[\u0000-\u001f\u007f-\u009f]";" ")) + $reset + " " + $gray + ago($s.updatedEpoch) + $reset), $s.cwd, $s.sessionId] | @tsv))
|
|
193
|
+
| [$s.agentId, $s.sessionId, $s.liveState, ($branch + " " + $c + ($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
194
|
' "$SNAPSHOT"
|
|
149
195
|
} || printf '\t\tunknown\t%sSnapshot rendering failed; retry refresh%s\t\tnotice:render-error\n' "$YELLOW" "$RESET"
|
|
150
196
|
}
|
|
@@ -336,6 +382,7 @@ refresh() {
|
|
|
336
382
|
| . as $m | ($histById[$m.id] // {}) as $h
|
|
337
383
|
| {sessionId: $m.id, malformedRecords: 0, agentId: "", agentState: "",
|
|
338
384
|
liveState: "unknown", transcriptPath: "", source: "opencode",
|
|
385
|
+
archived: (if (($m.archived // 0) | tonumber) == 0 then 0 else 1 end),
|
|
339
386
|
cwd: (if ($h.cwd // "") != "" then $h.cwd else $m.directory end),
|
|
340
387
|
updatedEpoch: ($h.updatedEpoch // (($m.time_updated / 1000 | floor))),
|
|
341
388
|
title: (if ($h.title // "") != "" then $h.title else ($m.title // "(untitled)") end)}
|
|
@@ -356,5 +403,6 @@ refresh() {
|
|
|
356
403
|
fi
|
|
357
404
|
}
|
|
358
405
|
|
|
406
|
+
if [ "$HEADER" = 1 ]; then header; exit 0; fi
|
|
359
407
|
if [ "$REFRESH" = 1 ] || [ "$TOGGLE_SCOPE" = 1 ]; then refresh; fi
|
|
360
408
|
render
|
package/bin/sesh-shortcuts.sh
CHANGED
|
@@ -9,9 +9,10 @@ sesh shortcuts
|
|
|
9
9
|
Ctrl-G Toggle current-directory scope / all sessions
|
|
10
10
|
Space Toggle transcript preview
|
|
11
11
|
? Toggle this help
|
|
12
|
-
Ctrl-X Delete the selected session
|
|
12
|
+
Ctrl-X Delete the selected session (asks to confirm)
|
|
13
13
|
Escape Exit the picker
|
|
14
14
|
|
|
15
15
|
Flags: --cwd (current directory only), --limit N (default: all),
|
|
16
|
-
--archived (include archived), --print (print id instead of resuming)
|
|
16
|
+
--archived (include archived), --print (print id instead of resuming),
|
|
17
|
+
--json (with --print, emit JSON)
|
|
17
18
|
HELP
|