opencode-sesh 0.2.0 → 0.3.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 +27 -10
- package/CHANGELOG.md +64 -1
- package/README.md +26 -6
- package/bin/sesh +4 -0
- package/bin/sesh-list.sh +12 -9
- package/bin/sesh-pins.sh +55 -0
- package/bin/sesh-prune.sh +175 -0
- package/bin/sesh-shortcuts.sh +3 -1
- package/bin/sesh-waiting.sh +127 -0
- package/bin/sesh.sh +17 -1
- package/install.sh +2 -0
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +404 -96
package/AGENTS.md
CHANGED
|
@@ -35,7 +35,12 @@ HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.co
|
|
|
35
35
|
## Layout
|
|
36
36
|
|
|
37
37
|
- `bin/` — flat; no subdirs. Scripts resolve siblings from their own location.
|
|
38
|
-
Never hardcode install paths.
|
|
38
|
+
Never hardcode install paths. `sesh-waiting.sh` owns the needs-input query
|
|
39
|
+
(shared heuristic with the TUI sidebar copy, marked `NEEDS_INPUT_SQL` in
|
|
40
|
+
both); `sesh-prune.sh` archives stale sessions. Reads go through
|
|
41
|
+
`sqlite3 -readonly` (or `opencode db`); the single archive `UPDATE` in
|
|
42
|
+
`sesh-prune.sh` is the only sanctioned direct write (no archive endpoint
|
|
43
|
+
exists — deletes stay on `opencode session delete`).
|
|
39
44
|
- `tui/sesh-panel.tsx` — SolidJS panel, `/** @jsxImportSource @opentui/solid */`.
|
|
40
45
|
- `opencode/tools/sesh-list.ts` — the agent-facing `sesh-list` tool.
|
|
41
46
|
- `scripts/changelog.sh` — release tooling: drafts `[Unreleased]` from
|
|
@@ -72,6 +77,11 @@ the schema below. Keystrokes never touch the database:
|
|
|
72
77
|
selection leaves a one-shot `STATE_DIR/action-notice` the next render shows
|
|
73
78
|
and consumes. Snapshot records carry `archived` as 0/1 (the DB column is an
|
|
74
79
|
epoch, not a boolean).
|
|
80
|
+
- Session and directory pins live in `${XDG_DATA_HOME:-$HOME/.local/share}/sesh/pins.json`
|
|
81
|
+
(`SESH_PINS_FILE` overrides it for tests), shared by fzf and the TUI. Writes
|
|
82
|
+
use a sibling lock directory and atomic rename. Option-S toggles a selected
|
|
83
|
+
session; Option-D toggles its directory. Pinned directories sort before groups
|
|
84
|
+
containing pinned sessions, with pinned sessions first inside each group.
|
|
75
85
|
|
|
76
86
|
**Output contract** (TSV, 6 fields):
|
|
77
87
|
`agentId sessionId liveState display cwd trackingId`. `display` is the only
|
|
@@ -125,10 +135,14 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
125
135
|
## TUI panel (`tui/sesh-panel.tsx`)
|
|
126
136
|
|
|
127
137
|
- **Sidebar:** `api.slots.register` on `sidebar_content` (append mode — native
|
|
128
|
-
sidebar content stays). Renders the newest
|
|
129
|
-
with the current one highlighted, polled every 15 s;
|
|
130
|
-
ctrl+x deletes one (armed, then confirmed).
|
|
131
|
-
|
|
138
|
+
sidebar content stays). Renders pinned sessions before the newest unarchived
|
|
139
|
+
sessions, with the current one highlighted, polled every 15 s; Ctrl-P previews
|
|
140
|
+
a row and ctrl+x deletes one (armed, then confirmed). The section lists every
|
|
141
|
+
session without a row cap and flex-grows to fill the sidebar, with overflow
|
|
142
|
+
scrolling inside; the picker remains the place for transcript search.
|
|
143
|
+
Sessions waiting on the user (unanswered questions, stuck runs) pin a
|
|
144
|
+
**Needs input** group above the directories, with the count in the heading —
|
|
145
|
+
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`.
|
|
132
146
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
133
147
|
project/directory, recency-ordered, resume via
|
|
134
148
|
`api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
|
|
@@ -168,12 +182,15 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
168
182
|
opt-in: clicking the *Sessions* heading (or its search box) activates a
|
|
169
183
|
priority-20 layer, because a global `↑`/`↓` would hijack the main prompt.
|
|
170
184
|
Cursor state is derived from `itemRows()`, so hover and cursor resolve to the
|
|
171
|
-
same row
|
|
172
|
-
|
|
185
|
+
same row. The section lists every session with no row cap
|
|
186
|
+
(`tests/tui.mjs` asserts the cap stays out); overflow scrolls inside the
|
|
187
|
+
stretched section.
|
|
173
188
|
- **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
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
the second (or `y`, or Enter in the picker footer) commits the armed row
|
|
190
|
+
wherever the pointer or cursor is, and `n`/Esc (or a five-second timeout)
|
|
191
|
+
cancels. Moving the pointer must never disarm: edge-hover flicker ate the
|
|
192
|
+
confirm before `confirmArmed` landed. The `y`/`n` layer is registered *only
|
|
193
|
+
while armed*, so it can never shadow the search box's typing.
|
|
177
194
|
- **Picker `ctrl+g` is project scope**, not close (parity with the fzf picker's
|
|
178
195
|
`Ctrl-G`); Esc is the only close key. `ctrl+f` forks through
|
|
179
196
|
`api.client.session.fork` and navigates to the returned session.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Needs-input triage: sessions with unanswered agent questions or runs stuck
|
|
15
|
+
mid-tool pin a **Needs input** group above the sidebar directories (count in
|
|
16
|
+
the heading), and `sesh --needs-input [--json]` lists the same set in the
|
|
17
|
+
terminal. Detection reads the opencode store directly, so it works for every
|
|
18
|
+
session, not just ones owned by the current server.
|
|
19
|
+
- `sesh prune [--older-than 30d] [--dry-run] [--yes] [--delete]` archives stale
|
|
20
|
+
sessions so the list stays triageable. Pinned sessions, sessions waiting on
|
|
21
|
+
you, fork children and already-archived sessions are never touched;
|
|
22
|
+
archiving is reversible, `--delete` hard-deletes through the opencode CLI,
|
|
23
|
+
and non-TTY runs need `--yes`.
|
|
24
|
+
## [0.2.1] - 2026-09-24
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The TUI sidebar's Sessions section now lists every session and fills the
|
|
29
|
+
available sidebar height: the fixed 15-row cap is gone, with overflow
|
|
30
|
+
scrolling inside the section.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Pin sessions with Option-S or directories with Option-D in both pickers. Pins live
|
|
35
|
+
in a shared XDG data file, appear with a star, and sort ahead of recent
|
|
36
|
+
sessions in the terminal picker, TUI picker, sidebar and home list.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- The sidebar delete confirm no longer disarms when the pointer leaves the
|
|
41
|
+
armed row: the second Ctrl+X (or `y`) commits the visibly armed row wherever
|
|
42
|
+
the pointer is, so deleting works while hovering the sidebar edges.
|
|
43
|
+
|
|
44
|
+
- Session previews now use Ctrl-P rather than Space in the terminal picker,
|
|
45
|
+
sidebar and home list, so typing cannot accidentally open a preview.
|
|
46
|
+
- Hovering a sidebar session while keyboard navigation is active now moves the
|
|
47
|
+
selection to that row, so Option-S/Option-D pin the row under the pointer rather
|
|
48
|
+
than the previously selected (often currently open) session.
|
|
49
|
+
- The TUI sidebar's Sessions section now has a working show/hide control,
|
|
50
|
+
separate from clicking the heading to activate keyboard navigation.
|
|
51
|
+
- Transcript previews pass OpenTUI's tree-sitter client to the markdown
|
|
52
|
+
renderer, restoring themed Markdown and fenced-code highlighting.
|
|
53
|
+
- Preview Markdown now uses the active theme's semantic accents for headings,
|
|
54
|
+
emphasis, code and list markers when the default Markdown colors read like
|
|
55
|
+
body text.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- Pin shortcuts are labeled Option-S/Option-D (the macOS key) in both pickers
|
|
60
|
+
and the docs. The terminal picker still binds fzf's `alt-s`/`alt-d`, which is
|
|
61
|
+
fzf's name for Option and unchanged.
|
|
62
|
+
- The TUI picker now shows transcript search matches as indexing progresses,
|
|
63
|
+
identifies transcript-only hits, and distinguishes an in-progress search from
|
|
64
|
+
a completed search with no results.
|
|
65
|
+
- Session previews identify the selected session and show loading feedback
|
|
66
|
+
while it changes, instead of displaying another session's transcript.
|
|
67
|
+
- The TUI picker uses `Ctrl-P` for transcript preview; Space always enters a
|
|
68
|
+
space in the search field, including at the start of a query.
|
|
69
|
+
- Sidebar and home searches direct users to the full picker for transcript
|
|
70
|
+
search when no titles or directories match.
|
|
10
71
|
## [0.2.0] - 2026-09-21
|
|
11
72
|
|
|
12
73
|
### Added
|
|
@@ -178,7 +239,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
178
239
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
179
240
|
CI.
|
|
180
241
|
|
|
181
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.
|
|
242
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.3.0...HEAD
|
|
243
|
+
[0.3.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.1...v0.3.0
|
|
244
|
+
[0.2.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.2.0...v0.2.1
|
|
182
245
|
[0.2.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.7...v0.2.0
|
|
183
246
|
[0.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.6...v0.1.7
|
|
184
247
|
[0.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.5...v0.1.6
|
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.
|
|
@@ -112,7 +115,8 @@ bash install.sh --uninstall # from source
|
|
|
112
115
|
| Enter | Resume the selected session in this terminal |
|
|
113
116
|
| Ctrl-F | Resume as a fork (the original is untouched) |
|
|
114
117
|
| Ctrl-G | Toggle current-directory scope / all sessions |
|
|
115
|
-
|
|
|
118
|
+
| Option-S / Option-D | Pin or unpin the selected session / directory |
|
|
119
|
+
| Ctrl-P | Toggle the transcript preview |
|
|
116
120
|
| `?` (empty query) | Toggle shortcut help |
|
|
117
121
|
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
118
122
|
| Escape | Exit |
|
|
@@ -123,7 +127,15 @@ matches nothing says so instead of showing an empty screen.
|
|
|
123
127
|
|
|
124
128
|
Flags: `--cwd` (current directory only), `--limit N` (default: all),
|
|
125
129
|
`--archived`, `--print` (print `id<TAB>cwd` instead of resuming), `--fork`,
|
|
126
|
-
`--json` (with `--print`, emit JSON), `--query TEXT`, `--check
|
|
130
|
+
`--json` (with `--print`, emit JSON), `--query TEXT`, `--check`,
|
|
131
|
+
`--needs-input` (list sessions waiting on you instead of opening the picker).
|
|
132
|
+
|
|
133
|
+
`sesh prune [--older-than 30d] [--dry-run] [--yes] [--delete]` archives stale
|
|
134
|
+
sessions (not updated within the threshold) so the list stays triageable.
|
|
135
|
+
Pinned sessions, sessions waiting on you, fork children and already-archived
|
|
136
|
+
sessions are never touched; archiving is reversible, while `--delete`
|
|
137
|
+
hard-deletes through `opencode session delete`. Without `--dry-run`, a TTY run
|
|
138
|
+
confirms first and a non-TTY run needs `--yes`.
|
|
127
139
|
|
|
128
140
|
Because `--print` just emits the id and directory, `sesh` doubles as a scriptable
|
|
129
141
|
session lookup:
|
|
@@ -144,18 +156,25 @@ non-interactive `sesh-delete.sh` refuses unless given `--yes`.
|
|
|
144
156
|
| `ctrl+o` | Open the full picker (also in the command palette) |
|
|
145
157
|
| Type | Search titles, directories and transcript text |
|
|
146
158
|
| `↑`/`↓`, `PgUp`/`PgDn`, `Home`/`End` | Move the selection |
|
|
147
|
-
|
|
|
159
|
+
| Ctrl-P | Toggle the transcript preview |
|
|
148
160
|
| Enter | Open the selected session |
|
|
149
161
|
| Ctrl-X | Delete the selected session (asks to confirm) |
|
|
150
162
|
| Ctrl-F | Fork the selected session |
|
|
151
163
|
| Ctrl-G | Toggle scope: the selected session's project, or every project |
|
|
164
|
+
| Option-S / Option-D | Pin or unpin the selected session / directory |
|
|
152
165
|
| Esc | Close |
|
|
153
166
|
|
|
154
167
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
|
155
168
|
elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
|
|
156
169
|
the list from the keyboard instead of the mouse (`↑`/`↓` to move, Enter to open,
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
Ctrl-P to preview, Option-S/Option-D to pin, Ctrl-X to delete, Esc to leave). Pinned
|
|
171
|
+
sessions appear ahead of the rest; the section lists every session and scrolls
|
|
172
|
+
to fill the sidebar, so click **▾ hide** to collapse it when you need the room.
|
|
173
|
+
Sessions waiting on you (unanswered agent questions, runs stuck mid-tool) pin
|
|
174
|
+
themselves to a **Needs input** group above the directories, with the count in
|
|
175
|
+
the heading; `sesh --needs-input` lists the same set in the terminal.
|
|
176
|
+
Hovering a row while keyboard navigation is active selects it for the next
|
|
177
|
+
shortcut. The full picker is one keystroke away.
|
|
159
178
|
|
|
160
179
|
The full picker pages through your whole global session list (no fixed window)
|
|
161
180
|
and indexes transcript text for every session in the background, showing
|
|
@@ -174,6 +193,7 @@ All variables are optional.
|
|
|
174
193
|
| `SESH_GLOW` | first `glow` on `PATH` | optional Markdown preview renderer |
|
|
175
194
|
| `SESH_OPENCODE` | `opencode` | opencode executable (resume / delete) |
|
|
176
195
|
| `SESH_CACHE_DIR` | `~/.cache/sesh` | persistent search-index cache |
|
|
196
|
+
| `SESH_PINS_FILE` | `${XDG_DATA_HOME:-~/.local/share}/sesh/pins.json` | shared persistent session/directory pins |
|
|
177
197
|
|
|
178
198
|
## How it works
|
|
179
199
|
|
package/bin/sesh
CHANGED
package/bin/sesh-list.sh
CHANGED
|
@@ -157,7 +157,7 @@ render() {
|
|
|
157
157
|
{
|
|
158
158
|
[ -n "$notice" ] && printf '\t\tunknown\t%s%s%s\t\tnotice:action\n' "$YELLOW" "$notice" "$RESET"
|
|
159
159
|
[ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
|
|
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" '
|
|
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" '
|
|
161
161
|
def ago($epoch):
|
|
162
162
|
((now - ($epoch | tonumber)) | floor) as $s
|
|
163
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;
|
|
@@ -180,17 +180,20 @@ render() {
|
|
|
180
180
|
then notice_row(if $q == "" then "No sessions in this store" else "No sessions match \"" + $query + "\"" end; "notice:no-match")
|
|
181
181
|
else empty end),
|
|
182
182
|
($matched
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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),
|
|
188
191
|
($g.items | to_entries[] | .key as $i | .value as $s
|
|
189
192
|
# Archived rows are opt-in and need to look like it; nothing else about
|
|
190
193
|
# a row is knowable here (live agent state is not exposed by opencode).
|
|
191
194
|
| (if $s.archived == 1 then $yellow else $gray end) as $c
|
|
192
195
|
| ($gray + (if $i == (($g.items|length)-1) then "└" else "├" end) + $reset) as $branch
|
|
193
|
-
|
|
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)))
|
|
194
197
|
' "$SNAPSHOT"
|
|
195
198
|
} || printf '\t\tunknown\t%sSnapshot rendering failed; retry refresh%s\t\tnotice:render-error\n' "$YELLOW" "$RESET"
|
|
196
199
|
}
|
|
@@ -372,7 +375,7 @@ refresh() {
|
|
|
372
375
|
done < "$records"
|
|
373
376
|
fi
|
|
374
377
|
|
|
375
|
-
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" '
|
|
376
379
|
.meta as $meta
|
|
377
380
|
| ($hist | map({key: .sessionId, value: .}) | from_entries) as $histById
|
|
378
381
|
| [$meta[]
|
|
@@ -387,7 +390,7 @@ refresh() {
|
|
|
387
390
|
updatedEpoch: ($h.updatedEpoch // (($m.time_updated / 1000 | floor))),
|
|
388
391
|
title: (if ($h.title // "") != "" then $h.title else ($m.title // "(untitled)") end)}
|
|
389
392
|
| .searchText = ((.title | ascii_downcase) + " " + ($histById[.sessionId].fulltextLower // ""))]
|
|
390
|
-
|
|
393
|
+
| sort_by([(.cwd as $dir | $pins.directories | index($dir) != null), (.sessionId as $id | $pins.sessions | index($id) != null), .updatedEpoch]) | reverse
|
|
391
394
|
| (if $limit == 0 then . else .[:$limit] end)[]
|
|
392
395
|
' "$cache_state" > "$out"; then
|
|
393
396
|
set_status 'stale: snapshot construction failed; showing last good snapshot'
|
package/bin/sesh-pins.sh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Persistent pins shared by the fzf picker and the opencode TUI plugin.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
umask 077
|
|
5
|
+
|
|
6
|
+
PINS_FILE=${SESH_PINS_FILE:-"${XDG_DATA_HOME:-$HOME/.local/share}/sesh/pins.json"}
|
|
7
|
+
JQ_BIN=${SESH_JQ:-jq}
|
|
8
|
+
empty='{"sessions":[],"directories":[]}'
|
|
9
|
+
action=${1:-}
|
|
10
|
+
value=${2:-}
|
|
11
|
+
|
|
12
|
+
read_pins() {
|
|
13
|
+
if [ -f "$PINS_FILE" ]; then
|
|
14
|
+
local pins=''
|
|
15
|
+
pins=$("$JQ_BIN" -c 'select(type == "object") | {sessions: ((.sessions // []) | map(select(type == "string"))), directories: ((.directories // []) | map(select(type == "string")))}' "$PINS_FILE" 2>/dev/null) || pins=''
|
|
16
|
+
printf '%s\n' "${pins:-$empty}"
|
|
17
|
+
else
|
|
18
|
+
printf '%s\n' "$empty"
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
case "$action" in
|
|
23
|
+
read) read_pins; exit 0 ;;
|
|
24
|
+
toggle-session)
|
|
25
|
+
[[ "$value" =~ ^ses_[A-Za-z0-9]+$ ]] || exit 2
|
|
26
|
+
field=sessions ;;
|
|
27
|
+
toggle-directory)
|
|
28
|
+
[[ "$value" == /* && "$value" != *$'\n'* ]] || exit 2
|
|
29
|
+
field=directories ;;
|
|
30
|
+
*) echo 'usage: sesh-pins.sh read|toggle-session ID|toggle-directory PATH' >&2; exit 2 ;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
mkdir -p "$(dirname "$PINS_FILE")"
|
|
34
|
+
lock="$PINS_FILE.lock"
|
|
35
|
+
for ((attempt = 0; attempt < 50; attempt++)); do
|
|
36
|
+
if mkdir "$lock" 2>/dev/null; then printf '%s\n' "$$" > "$lock/pid"; break; fi
|
|
37
|
+
owner=''
|
|
38
|
+
[ ! -f "$lock/pid" ] || owner=$(cat "$lock/pid" 2>/dev/null || true)
|
|
39
|
+
if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then
|
|
40
|
+
rm -f "$lock/pid" 2>/dev/null || true
|
|
41
|
+
rmdir "$lock" 2>/dev/null || true
|
|
42
|
+
fi
|
|
43
|
+
[ "$attempt" -lt 49 ] || { echo 'sesh: pins are busy; try again' >&2; exit 1; }
|
|
44
|
+
sleep 0.1
|
|
45
|
+
done
|
|
46
|
+
tmp=''
|
|
47
|
+
cleanup() { [ -z "$tmp" ] || rm -f "$tmp"; rm -f "$lock/pid"; rmdir "$lock" 2>/dev/null || true; }
|
|
48
|
+
trap cleanup EXIT
|
|
49
|
+
tmp=$(mktemp "${PINS_FILE}.XXXXXX")
|
|
50
|
+
read_pins | "$JQ_BIN" -c --arg field "$field" --arg value "$value" '
|
|
51
|
+
.[$field] |= (if index($value) then map(select(. != $value)) else . + [$value] end)
|
|
52
|
+
' > "$tmp"
|
|
53
|
+
mv -f "$tmp" "$PINS_FILE"
|
|
54
|
+
tmp=''
|
|
55
|
+
read_pins
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Archive (or, with --delete, hard-delete) stale sessions: not updated within
|
|
3
|
+
# the threshold, never pinned, never waiting on the user, never already
|
|
4
|
+
# archived, never a fork child.
|
|
5
|
+
#
|
|
6
|
+
# Archive is the default because it is reversible; hard deletes go through
|
|
7
|
+
# `opencode session delete` so its own safeguards apply. The archive itself
|
|
8
|
+
# is a single atomic UPDATE through the same sqlite3-or-`opencode db` read
|
|
9
|
+
# path the list commands use — there is no archive endpoint in the opencode
|
|
10
|
+
# API or CLI, so a direct write is the only route (deletes stay on the CLI).
|
|
11
|
+
#
|
|
12
|
+
# Usage: sesh-prune.sh [--older-than 30d] [--dry-run] [--yes] [--delete]
|
|
13
|
+
# --older-than Nd|Nh|Nw (bare N means days; default 30d)
|
|
14
|
+
# --dry-run lists candidates and changes nothing
|
|
15
|
+
# without --dry-run, a TTY run confirms first; a non-TTY run needs --yes
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
20
|
+
|
|
21
|
+
older_than=30d
|
|
22
|
+
dry_run=0
|
|
23
|
+
assume_yes=0
|
|
24
|
+
hard_delete=0
|
|
25
|
+
while [ "$#" -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--older-than) older_than=${2:-}; shift ;;
|
|
28
|
+
--older-than=*) older_than=${1#--older-than=} ;;
|
|
29
|
+
--dry-run) dry_run=1 ;;
|
|
30
|
+
--yes|-y) assume_yes=1 ;;
|
|
31
|
+
--delete) hard_delete=1 ;;
|
|
32
|
+
--help) echo "usage: ${0##*/} [--older-than 30d] [--dry-run] [--yes] [--delete]"; exit 0 ;;
|
|
33
|
+
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
34
|
+
esac
|
|
35
|
+
shift
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if [[ "$older_than" =~ ^([0-9]+)([dhw])?$ ]]; then
|
|
39
|
+
amount=$((10#${BASH_REMATCH[1]}))
|
|
40
|
+
unit=${BASH_REMATCH[2]:-d}
|
|
41
|
+
[ "$amount" -gt 0 ] || { echo "--older-than must be positive" >&2; exit 2; }
|
|
42
|
+
else
|
|
43
|
+
echo "--older-than takes Nd, Nh or Nw (bare N means days)" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
case "$unit" in
|
|
47
|
+
h) threshold_ms=$((amount * 3600000)) ;;
|
|
48
|
+
d) threshold_ms=$((amount * 86400000)) ;;
|
|
49
|
+
w) threshold_ms=$((amount * 604800000)) ;;
|
|
50
|
+
esac
|
|
51
|
+
NOW_MS=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || echo "$(( $(date +%s) * 1000 ))")
|
|
52
|
+
CUTOFF=$((NOW_MS - threshold_ms))
|
|
53
|
+
|
|
54
|
+
resolve_db() {
|
|
55
|
+
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
56
|
+
local from_cli=''
|
|
57
|
+
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
58
|
+
from_cli=$("$OPENCODE_BIN" db path 2>/dev/null || true)
|
|
59
|
+
fi
|
|
60
|
+
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
61
|
+
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
SQLITE_BIN=''
|
|
66
|
+
if [ -n "${SESH_SQLITE:-}" ]; then
|
|
67
|
+
SQLITE_BIN=$SESH_SQLITE
|
|
68
|
+
elif command -v sqlite3 >/dev/null 2>&1; then
|
|
69
|
+
SQLITE_BIN=sqlite3
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
DB_PATH=$(resolve_db) || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
73
|
+
[ -f "$DB_PATH" ] || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
74
|
+
|
|
75
|
+
if [ -n "$SQLITE_BIN" ]; then
|
|
76
|
+
query() { "$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"; }
|
|
77
|
+
write_db() { "$SQLITE_BIN" "$DB_PATH" "$1"; }
|
|
78
|
+
else
|
|
79
|
+
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
80
|
+
query() { "$OPENCODE_BIN" db "$1" --format json; }
|
|
81
|
+
write_db() { "$OPENCODE_BIN" db "$1" --format json > /dev/null; }
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
85
|
+
[ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
|
|
86
|
+
|
|
87
|
+
CANDIDATES=$(query "SELECT id, directory, title, time_updated AS updated FROM session WHERE time_updated < $CUTOFF AND COALESCE(time_archived, 0) = 0 AND parent_id IS NULL ORDER BY time_updated ASC;")
|
|
88
|
+
# sqlite3 -json prints nothing (not []) when no rows match.
|
|
89
|
+
[ -n "$CANDIDATES" ] || CANDIDATES='[]'
|
|
90
|
+
PINS=$("$SCRIPT_DIR/sesh-pins.sh" read 2>/dev/null || printf '{"sessions":[],"directories":[]}')
|
|
91
|
+
WAITING_IDS=$(bash "$SCRIPT_DIR/sesh-waiting.sh" --json 2>/dev/null | "$JQ_BIN" -r '.[].id' || true)
|
|
92
|
+
[ -n "$WAITING_IDS" ] || WAITING_IDS='__none__'
|
|
93
|
+
|
|
94
|
+
ELIGIBLE=$(printf '%s' "$CANDIDATES" | "$JQ_BIN" -c --slurpfile _pins <(printf '%s' "$PINS") --arg waiting "$WAITING_IDS" '
|
|
95
|
+
($waiting | split("\n") | map(select(. != "" and . != "__none__"))) as $waiting_ids
|
|
96
|
+
| [ .[]
|
|
97
|
+
| .id as $id
|
|
98
|
+
| .directory as $dir
|
|
99
|
+
| select($id | test("^ses_[A-Za-z0-9]+$"))
|
|
100
|
+
| select(([$_pins[0].sessions[]] | index($id) | not))
|
|
101
|
+
| select(([$_pins[0].directories[]] | index($dir) | not))
|
|
102
|
+
| select(($waiting_ids | index($id) | not))
|
|
103
|
+
]
|
|
104
|
+
')
|
|
105
|
+
COUNT=$(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r 'length')
|
|
106
|
+
VERB=Archive
|
|
107
|
+
[ "$hard_delete" = 0 ] || VERB=Delete
|
|
108
|
+
|
|
109
|
+
if [ "$COUNT" = 0 ]; then
|
|
110
|
+
echo "Nothing to prune older than $older_than."
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r --argjson now "$NOW_MS" '
|
|
115
|
+
def ago(ms): ((($now - ms) / 1000) | floor) as $s
|
|
116
|
+
| if $s < 3600 then "\($s / 60 | floor)m"
|
|
117
|
+
elif $s < 86400 then "\($s / 3600 | floor)h"
|
|
118
|
+
else "\($s / 86400 | floor)d" end;
|
|
119
|
+
.[] | "\(.id)\t\(.title // "untitled")\t\(ago(.updated)) ago"
|
|
120
|
+
' | while IFS=$'\t' read -r id title age; do
|
|
121
|
+
printf '%s %s \033[90m%s\033[0m\n' "$id" "$title" "$age"
|
|
122
|
+
done
|
|
123
|
+
|
|
124
|
+
if [ "$dry_run" = 1 ]; then
|
|
125
|
+
printf '%s %s session(s) older than %s (--dry-run: nothing changed).\n' "$VERB" "$COUNT" "$older_than" >&2
|
|
126
|
+
exit 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if [ "$assume_yes" = 0 ]; then
|
|
130
|
+
if [ -t 0 ]; then
|
|
131
|
+
printf '%s' "$VERB $COUNT session(s) older than $older_than? [y/N] "
|
|
132
|
+
reply=''
|
|
133
|
+
IFS= read -r -n 1 reply || reply=''
|
|
134
|
+
printf '\n'
|
|
135
|
+
case "$reply" in
|
|
136
|
+
y|Y) ;;
|
|
137
|
+
*) echo 'Cancelled; nothing was changed.'; exit 0 ;;
|
|
138
|
+
esac
|
|
139
|
+
else
|
|
140
|
+
echo "Refusing to prune $COUNT session(s): pass --yes when stdin is not a terminal." >&2
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [ "$hard_delete" = 0 ]; then
|
|
146
|
+
IDS=$(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id')
|
|
147
|
+
IN_LIST=''
|
|
148
|
+
for id in $IDS; do
|
|
149
|
+
case "$id" in
|
|
150
|
+
ses_[A-Za-z0-9]*) IN_LIST="$IN_LIST'$id'," ;;
|
|
151
|
+
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
152
|
+
esac
|
|
153
|
+
done
|
|
154
|
+
IN_LIST=${IN_LIST%,}
|
|
155
|
+
write_db "UPDATE session SET time_archived = $NOW_MS WHERE id IN ($IN_LIST);"
|
|
156
|
+
echo "Archived $COUNT session(s) older than $older_than."
|
|
157
|
+
else
|
|
158
|
+
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: opencode executable is unavailable for deletes.' >&2; exit 1; }
|
|
159
|
+
failed=0
|
|
160
|
+
done_count=0
|
|
161
|
+
for id in $(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id'); do
|
|
162
|
+
case "$id" in
|
|
163
|
+
ses_[A-Za-z0-9]*) ;;
|
|
164
|
+
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
165
|
+
esac
|
|
166
|
+
if "$OPENCODE_BIN" session delete "$id" >/dev/null 2>&1; then
|
|
167
|
+
done_count=$((done_count + 1))
|
|
168
|
+
else
|
|
169
|
+
echo "Could not delete $id." >&2
|
|
170
|
+
failed=$((failed + 1))
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
echo "Deleted $done_count session(s) older than $older_than."
|
|
174
|
+
[ "$failed" = 0 ] || exit 1
|
|
175
|
+
fi
|
package/bin/sesh-shortcuts.sh
CHANGED
|
@@ -7,7 +7,9 @@ sesh shortcuts
|
|
|
7
7
|
Enter Resume the selected session here
|
|
8
8
|
Ctrl-F Resume the selected session as a fork
|
|
9
9
|
Ctrl-G Toggle current-directory scope / all sessions
|
|
10
|
-
|
|
10
|
+
Ctrl-P Toggle transcript preview
|
|
11
|
+
Option-S Pin / unpin selected session
|
|
12
|
+
Option-D Pin / unpin selected directory
|
|
11
13
|
? Toggle this help
|
|
12
14
|
Ctrl-X Delete the selected session (asks to confirm)
|
|
13
15
|
Escape Exit the picker
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# List sessions waiting on the user: unanswered agent questions and runs
|
|
3
|
+
# stuck with a tool still marked running. Server-independent: it reads the
|
|
4
|
+
# opencode SQLite store directly, so it sees every session — not just ones
|
|
5
|
+
# owned by a running TUI server, whose in-memory permission/question state
|
|
6
|
+
# is invisible cross-project.
|
|
7
|
+
#
|
|
8
|
+
# A session waits when it is not archived, is not a fork child, and either
|
|
9
|
+
# - has a `question` tool part that never completed with no later user
|
|
10
|
+
# message in the same session (awaiting an answer, errored included), or
|
|
11
|
+
# - has a `tool` part still marked `running` older than STUCK_AFTER_MS
|
|
12
|
+
# (awaiting approval, or orphaned by a dead server).
|
|
13
|
+
#
|
|
14
|
+
# Usage: sesh-waiting.sh [--json]
|
|
15
|
+
# default prints a human table; --json prints [{id,reason,title,directory,updated}]
|
|
16
|
+
# Keep NEEDS_INPUT_SQL in sync with the TUI sidebar copy in tui/sesh-panel.tsx.
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
21
|
+
JSON=0
|
|
22
|
+
while [ "$#" -gt 0 ]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
--json) JSON=1 ;;
|
|
25
|
+
--help) echo "usage: ${0##*/} [--json]"; exit 0 ;;
|
|
26
|
+
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
27
|
+
esac
|
|
28
|
+
shift
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
resolve_db() {
|
|
32
|
+
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
33
|
+
local from_cli=''
|
|
34
|
+
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
35
|
+
from_cli=$("$OPENCODE_BIN" db path 2>/dev/null || true)
|
|
36
|
+
fi
|
|
37
|
+
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
38
|
+
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
SQLITE_BIN=''
|
|
43
|
+
if [ -n "${SESH_SQLITE:-}" ]; then
|
|
44
|
+
SQLITE_BIN=$SESH_SQLITE
|
|
45
|
+
elif command -v sqlite3 >/dev/null 2>&1; then
|
|
46
|
+
SQLITE_BIN=sqlite3
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
DB_PATH=$(resolve_db) || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
50
|
+
[ -f "$DB_PATH" ] || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
51
|
+
|
|
52
|
+
NOW_MS=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
|
|
53
|
+
STUCK_BEFORE=$((NOW_MS - 600000))
|
|
54
|
+
|
|
55
|
+
# NEEDS_INPUT_SQL (shared heuristic — keep in sync with tui/sesh-panel.tsx).
|
|
56
|
+
read -r -d '' QUERY <<SQL || true
|
|
57
|
+
SELECT s.id AS id, s.directory AS directory, s.title AS title, s.time_updated AS updated,
|
|
58
|
+
CASE WHEN EXISTS (
|
|
59
|
+
SELECT 1 FROM part p
|
|
60
|
+
WHERE p.session_id = s.id
|
|
61
|
+
AND json_extract(p.data, '\$.type') = 'tool'
|
|
62
|
+
AND json_extract(p.data, '\$.tool') = 'question'
|
|
63
|
+
AND COALESCE(json_extract(p.data, '\$.state.status'), 'pending') != 'completed'
|
|
64
|
+
AND NOT EXISTS (
|
|
65
|
+
SELECT 1 FROM message m
|
|
66
|
+
WHERE m.session_id = s.id
|
|
67
|
+
AND json_extract(m.data, '\$.role') = 'user'
|
|
68
|
+
AND m.time_created > p.time_created)
|
|
69
|
+
) THEN 'question' ELSE 'stuck' END AS reason
|
|
70
|
+
FROM session s
|
|
71
|
+
WHERE COALESCE(s.time_archived, 0) = 0
|
|
72
|
+
AND s.parent_id IS NULL
|
|
73
|
+
AND (EXISTS (
|
|
74
|
+
SELECT 1 FROM part p
|
|
75
|
+
WHERE p.session_id = s.id
|
|
76
|
+
AND json_extract(p.data, '\$.type') = 'tool'
|
|
77
|
+
AND json_extract(p.data, '\$.tool') = 'question'
|
|
78
|
+
AND COALESCE(json_extract(p.data, '\$.state.status'), 'pending') != 'completed'
|
|
79
|
+
AND NOT EXISTS (
|
|
80
|
+
SELECT 1 FROM message m
|
|
81
|
+
WHERE m.session_id = s.id
|
|
82
|
+
AND json_extract(m.data, '\$.role') = 'user'
|
|
83
|
+
AND m.time_created > p.time_created)
|
|
84
|
+
) OR EXISTS (
|
|
85
|
+
SELECT 1 FROM part p2
|
|
86
|
+
WHERE p2.session_id = s.id
|
|
87
|
+
AND json_extract(p2.data, '\$.type') = 'tool'
|
|
88
|
+
AND json_extract(p2.data, '\$.state.status') = 'running'
|
|
89
|
+
AND p2.time_created < $STUCK_BEFORE))
|
|
90
|
+
ORDER BY s.time_updated DESC;
|
|
91
|
+
SQL
|
|
92
|
+
|
|
93
|
+
if [ -n "$SQLITE_BIN" ]; then
|
|
94
|
+
# Read-only: sesh never writes the store on this path (archive writes go
|
|
95
|
+
# through `opencode db`, hard deletes through `opencode session delete`).
|
|
96
|
+
ROWS=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$QUERY")
|
|
97
|
+
else
|
|
98
|
+
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
99
|
+
ROWS=$("$OPENCODE_BIN" db "$QUERY" --format json)
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# sqlite3 -json prints nothing (not []) when no rows match.
|
|
103
|
+
[ -n "$ROWS" ] || ROWS='[]'
|
|
104
|
+
|
|
105
|
+
if [ "$JSON" = 1 ]; then
|
|
106
|
+
printf '%s\n' "$ROWS"
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
111
|
+
[ -n "$JQ_BIN" ] || { echo 'sesh: jq is required for the table view (or use --json)' >&2; exit 1; }
|
|
112
|
+
COUNT=$(printf '%s' "$ROWS" | "$JQ_BIN" -r 'length')
|
|
113
|
+
if [ "$COUNT" = 0 ]; then
|
|
114
|
+
echo 'No sessions are waiting on you.'
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
printf '%s' "$ROWS" | "$JQ_BIN" -r --argjson now "$NOW_MS" '
|
|
118
|
+
def ago(ms): ((($now - ms) / 1000) | floor) as $s
|
|
119
|
+
| if $s < 60 then "now"
|
|
120
|
+
elif $s < 3600 then "\($s / 60 | floor)m"
|
|
121
|
+
elif $s < 86400 then "\($s / 3600 | floor)h"
|
|
122
|
+
else "\($s / 86400 | floor)d" end;
|
|
123
|
+
def reason(t): if t == "question" then "awaiting answer" else "run stuck" end;
|
|
124
|
+
.[] | "\(.id)\t\(reason(.reason))\t\(.title // "untitled")\t\(.directory)\t\(ago(.updated)) ago"
|
|
125
|
+
' | while IFS=$'\t' read -r id reason title dir age; do
|
|
126
|
+
printf '%s \033[33m%s\033[0m %s \033[90m%s · %s\033[0m\n' "$id" "$reason" "$title" "$dir" "$age"
|
|
127
|
+
done
|