opencode-sesh 1.0.0 → 1.0.2
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 +8 -4
- package/CHANGELOG.md +14 -0
- package/README.md +14 -10
- package/package.json +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +109 -43
package/AGENTS.md
CHANGED
|
@@ -122,7 +122,7 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
122
122
|
action must come **before** `reload`/`reload-sync`: placed after, fzf silently
|
|
123
123
|
publishes an empty list (verified on 0.74.3).
|
|
124
124
|
- Destructive actions confirm. `sesh-delete.sh` prompts on a TTY and refuses
|
|
125
|
-
without `--yes` when stdin is not one; the TUI arms on the first
|
|
125
|
+
without `--yes` when stdin is not one; the TUI arms on the first Option-D and
|
|
126
126
|
commits on the second (or `y`).
|
|
127
127
|
- The installer prefers the local opencode version for `@opencode-ai/*` and
|
|
128
128
|
merges (never clobbers) the config `package.json`; opencode runs `bun install`
|
|
@@ -144,7 +144,7 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
144
144
|
Row markers alone signal session state: a running agent animates a loading
|
|
145
145
|
spinner marker, the open session shows its own ●, and every other idle row
|
|
146
146
|
stays neutral (polled every 15 s); Option-P previews
|
|
147
|
-
a row and
|
|
147
|
+
a row and option+d deletes one (armed, then confirmed). The section lists every
|
|
148
148
|
session without a row cap and flex-grows to fill the sidebar, with overflow
|
|
149
149
|
scrolling inside; the picker remains the place for transcript search.
|
|
150
150
|
Sessions waiting on the user (unanswered questions, stuck runs) pin a
|
|
@@ -153,7 +153,7 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
153
153
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
154
154
|
project/directory, recency-ordered, resume via
|
|
155
155
|
`api.route.navigate("session", …)`. `option+o` and the command palette open it;
|
|
156
|
-
|
|
156
|
+
Option-D arms and then confirms a delete through `api.client.session.delete` and
|
|
157
157
|
removes the row only after the server confirms; Ctrl-F forks through
|
|
158
158
|
`api.client.session.fork`; Ctrl-G scopes the list to the selected session's
|
|
159
159
|
project.
|
|
@@ -203,11 +203,15 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
203
203
|
- **The sidebar never binds arrow keys globally.** Keyboard navigation is
|
|
204
204
|
opt-in: clicking the *Sessions* heading (or its search box) activates a
|
|
205
205
|
priority-20 layer, because a global `↑`/`↓` would hijack the main prompt.
|
|
206
|
+
Clicking outside the sidebar, opening a session, or navigating releases it
|
|
207
|
+
again (with search, hover and any armed delete), so the sidebar cannot
|
|
208
|
+
shadow the composer afterwards. Sidebar-internal clicks stop propagation so
|
|
209
|
+
the outside-click release cannot undo them.
|
|
206
210
|
Cursor state is derived from `itemRows()`, so hover and cursor resolve to the
|
|
207
211
|
same row. The section lists every session with no row cap
|
|
208
212
|
(`tests/tui.mjs` asserts the cap stays out); overflow scrolls inside the
|
|
209
213
|
stretched section.
|
|
210
|
-
- **Delete in the TUI is two-step.** The first `
|
|
214
|
+
- **Delete in the TUI is two-step.** The first `option+d` arms `pendingDelete`,
|
|
211
215
|
the second (or `y`, or Enter in the picker footer) commits the armed row
|
|
212
216
|
wherever the pointer or cursor is, and `n`/Esc (or a five-second timeout)
|
|
213
217
|
cancels. Moving the pointer must never disarm: edge-hover flicker ate the
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.2] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **tui:** Release sidebar captures when focus returns to composer
|
|
13
|
+
|
|
14
|
+
## [1.0.1] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **tui:** Bind delete to option+d, readable selected rows, keep scroll on delete
|
|
19
|
+
|
|
8
20
|
## [1.0.0] - 2026-10-07
|
|
9
21
|
|
|
10
22
|
### Breaking changes
|
|
@@ -418,6 +430,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
418
430
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
419
431
|
CI.
|
|
420
432
|
|
|
433
|
+
[1.0.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.1...v1.0.2
|
|
434
|
+
[1.0.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.0...v1.0.1
|
|
421
435
|
[1.0.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.14.1...v1.0.0
|
|
422
436
|
[0.14.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.14.0...v0.14.1
|
|
423
437
|
[0.14.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.13.0...v0.14.0
|
package/README.md
CHANGED
|
@@ -18,14 +18,18 @@ grouped by directory, full-text searchable, and previewable from anywhere.
|
|
|
18
18
|
[](https://github.com/ozandogrultan/opencode-sesh/stargazers)
|
|
19
19
|
|
|
20
20
|
```text
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
Sessions (8) ▾ hide
|
|
22
|
+
search…
|
|
23
|
+
▾ …/Projects/portal (5)
|
|
24
|
+
├○ Retry the OAuth token refr… 2h
|
|
25
|
+
├○ Login rate limiting notes 3d
|
|
26
|
+
├○ Draft the release notes 7d
|
|
27
|
+
├○ Auth middleware cleanup 11d
|
|
28
|
+
└○ Rotate the signing keys 12d
|
|
29
|
+
▾ ~/Projects/mobile (3)
|
|
30
|
+
├○ Author avatar upload retry 1d
|
|
31
|
+
├○ Fix session cookie on Safari 2d
|
|
32
|
+
└○ Auth redirect loop after log… 6d
|
|
29
33
|
```
|
|
30
34
|
|
|
31
35
|
## Why sesh
|
|
@@ -176,7 +180,7 @@ non-interactive `sesh-delete.sh` refuses unless given `--yes`.
|
|
|
176
180
|
| `↑`/`↓`, `PgUp`/`PgDn`, `Home`/`End` | Move the selection |
|
|
177
181
|
| Option-P | Toggle the transcript preview |
|
|
178
182
|
| Enter | Open the selected session |
|
|
179
|
-
|
|
|
183
|
+
| Option-D | Delete the selected session (asks to confirm) |
|
|
180
184
|
| Ctrl-F | Fork the selected session |
|
|
181
185
|
| Ctrl-G | Toggle scope: the selected session's project, or every project |
|
|
182
186
|
| Option-W / Option-S | Show only sessions needing input / pinned sessions |
|
|
@@ -197,7 +201,7 @@ workspace at its directory; outside cmux it shows a toast instead.
|
|
|
197
201
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
|
198
202
|
elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
|
|
199
203
|
the list from the keyboard instead of the mouse (`↑`/`↓` to move, Enter to open,
|
|
200
|
-
Option-P to preview, Ctrl-S/Ctrl-D to pin,
|
|
204
|
+
Option-P to preview, Ctrl-S/Ctrl-D to pin, Option-D to delete, Esc to leave). Pinned
|
|
201
205
|
directories rise first, while sessions within each directory are ordered by
|
|
202
206
|
last update, newest first. The section lists every session and scrolls to fill
|
|
203
207
|
the sidebar, so click **▾ hide** to collapse it when you need the room.
|
package/package.json
CHANGED
|
@@ -1050,7 +1050,17 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1050
1050
|
setSectionCollapsed((value) => !value)
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
-
const openSession = (entry: Entry) =>
|
|
1053
|
+
const openSession = (entry: Entry) => {
|
|
1054
|
+
// Opening a session hands input back to the main view: release every
|
|
1055
|
+
// sidebar capture first, so its key layers cannot shadow the composer's
|
|
1056
|
+
// once the new session is open.
|
|
1057
|
+
menu.close()
|
|
1058
|
+
cancelDelete()
|
|
1059
|
+
setNavActive(false)
|
|
1060
|
+
setSearching(false)
|
|
1061
|
+
setHovered(undefined)
|
|
1062
|
+
props.api.route.navigate("session", { sessionID: entry.id })
|
|
1063
|
+
}
|
|
1054
1064
|
|
|
1055
1065
|
// Flat order of the rows a cursor can land on, so keyboard navigation and
|
|
1056
1066
|
// mouse hover resolve to the same row.
|
|
@@ -1064,6 +1074,15 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1064
1074
|
return `${route.name}:${id}`
|
|
1065
1075
|
})
|
|
1066
1076
|
createEffect(on(routeKey, () => menu.close(), { defer: true }))
|
|
1077
|
+
// Navigating away also releases the sidebar's keyboard captures: a nav,
|
|
1078
|
+
// search, hover or armed delete left over from the previous session must
|
|
1079
|
+
// not shadow the composer's keys in the next one.
|
|
1080
|
+
createEffect(on(routeKey, () => {
|
|
1081
|
+
cancelDelete()
|
|
1082
|
+
setNavActive(false)
|
|
1083
|
+
setSearching(false)
|
|
1084
|
+
setHovered(undefined)
|
|
1085
|
+
}, { defer: true }))
|
|
1067
1086
|
createEffect(() => {
|
|
1068
1087
|
const state = menuState()
|
|
1069
1088
|
if (state && !itemRows().some((entry) => entry.id === state.entry.id)) menu.close()
|
|
@@ -1149,6 +1168,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1149
1168
|
return true
|
|
1150
1169
|
}
|
|
1151
1170
|
|
|
1171
|
+
let sidebarScroll: { scrollTop: number } | undefined
|
|
1172
|
+
|
|
1152
1173
|
const deleteEntry = async (entry: Entry) => {
|
|
1153
1174
|
cancelDelete()
|
|
1154
1175
|
if (deleting()) return
|
|
@@ -1165,8 +1186,14 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1165
1186
|
props.api.ui.toast({ message: "Session was not deleted", variant: "error" })
|
|
1166
1187
|
return
|
|
1167
1188
|
}
|
|
1189
|
+
const scrollTop = sidebarScroll?.scrollTop ?? 0
|
|
1168
1190
|
setEntries(remaining)
|
|
1169
1191
|
setHovered(undefined)
|
|
1192
|
+
const restore = () => {
|
|
1193
|
+
if (sidebarScroll) sidebarScroll.scrollTop = scrollTop
|
|
1194
|
+
}
|
|
1195
|
+
queueMicrotask(restore)
|
|
1196
|
+
setTimeout(restore, 0)
|
|
1170
1197
|
props.api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
|
|
1171
1198
|
if (currentID() === entry.id) props.api.route.navigate("home")
|
|
1172
1199
|
} catch {
|
|
@@ -1240,7 +1267,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1240
1267
|
},
|
|
1241
1268
|
},
|
|
1242
1269
|
{
|
|
1243
|
-
key: "
|
|
1270
|
+
key: "alt+d",
|
|
1244
1271
|
desc: "Confirm delete",
|
|
1245
1272
|
preventDefault: true,
|
|
1246
1273
|
cmd: () => {
|
|
@@ -1268,6 +1295,12 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1268
1295
|
return
|
|
1269
1296
|
}
|
|
1270
1297
|
if (searching()) setSearching(false)
|
|
1298
|
+
// A click anywhere outside the sidebar means input is back in the main
|
|
1299
|
+
// view: release the nav layer, hover shortcuts and any armed delete so
|
|
1300
|
+
// the sidebar's keys and rows stop shadowing the composer.
|
|
1301
|
+
cancelDelete()
|
|
1302
|
+
if (navActive()) setNavActive(false)
|
|
1303
|
+
setHovered(undefined)
|
|
1271
1304
|
}
|
|
1272
1305
|
|
|
1273
1306
|
onMount(() => {
|
|
@@ -1317,7 +1350,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1317
1350
|
cmd: () => openSidebarPreview(entry),
|
|
1318
1351
|
},
|
|
1319
1352
|
{
|
|
1320
|
-
key: "
|
|
1353
|
+
key: "alt+d",
|
|
1321
1354
|
desc: "Delete session",
|
|
1322
1355
|
preventDefault: true,
|
|
1323
1356
|
cmd: () => {
|
|
@@ -1338,6 +1371,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1338
1371
|
|
|
1339
1372
|
// Keyboard navigation is opt-in (click the "Sessions" heading or the search
|
|
1340
1373
|
// box) so the sidebar never steals the arrow keys from the main prompt.
|
|
1374
|
+
// Clicking anywhere outside the sidebar, opening a session, or navigating
|
|
1375
|
+
// releases it again, so its layers cannot shadow the composer afterwards.
|
|
1341
1376
|
createEffect(() => {
|
|
1342
1377
|
disposeNav?.()
|
|
1343
1378
|
disposeNav = undefined
|
|
@@ -1364,7 +1399,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1364
1399
|
},
|
|
1365
1400
|
},
|
|
1366
1401
|
{
|
|
1367
|
-
key: "
|
|
1402
|
+
key: "alt+d",
|
|
1368
1403
|
desc: "Delete session",
|
|
1369
1404
|
preventDefault: true,
|
|
1370
1405
|
cmd: () => {
|
|
@@ -1442,7 +1477,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1442
1477
|
return (
|
|
1443
1478
|
<box flexDirection="column" paddingRight={1} flexGrow={1} flexShrink={1}>
|
|
1444
1479
|
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
|
1445
|
-
<text flexShrink={1} onMouseDown={() => {
|
|
1480
|
+
<text flexShrink={1} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1481
|
+
// Sidebar-internal: keep this click from reaching the root handler,
|
|
1482
|
+
// which would release the nav layer this toggle just activated.
|
|
1483
|
+
event.stopPropagation()
|
|
1446
1484
|
if (!sectionCollapsed()) setNavActive((value) => !value)
|
|
1447
1485
|
}}>
|
|
1448
1486
|
<b>Sessions</b>
|
|
@@ -1454,7 +1492,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1454
1492
|
<span style={{ fg: theme().warning }}> · {waitingEntries().length} need input</span>
|
|
1455
1493
|
</Show>
|
|
1456
1494
|
</text>
|
|
1457
|
-
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={
|
|
1495
|
+
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1496
|
+
event.stopPropagation()
|
|
1497
|
+
toggleSection()
|
|
1498
|
+
}}>
|
|
1458
1499
|
{sectionCollapsed() ? "▸ show" : "▾ hide"}
|
|
1459
1500
|
</text>
|
|
1460
1501
|
</box>
|
|
@@ -1463,7 +1504,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1463
1504
|
when={entries().length > 0}
|
|
1464
1505
|
fallback={
|
|
1465
1506
|
<text style={{ fg: theme().textMuted }}>
|
|
1466
|
-
{loadFailed() ? "Session list unavailable · will retry" : "No sessions yet ·
|
|
1507
|
+
{loadFailed() ? "Session list unavailable · will retry" : "No sessions yet · option+o to browse"}
|
|
1467
1508
|
</text>
|
|
1468
1509
|
}
|
|
1469
1510
|
>
|
|
@@ -1473,7 +1514,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1473
1514
|
paddingLeft={1}
|
|
1474
1515
|
paddingRight={1}
|
|
1475
1516
|
backgroundColor={searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
|
|
1476
|
-
onMouseDown={() => {
|
|
1517
|
+
onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1518
|
+
// Sidebar-internal: the root handler must not see this click, or
|
|
1519
|
+
// it would clear the search this box just activated.
|
|
1520
|
+
event.stopPropagation()
|
|
1477
1521
|
insideSearchBox = true
|
|
1478
1522
|
setSearching(true)
|
|
1479
1523
|
}}
|
|
@@ -1489,12 +1533,16 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1489
1533
|
{searching() ? "▏" : ""}
|
|
1490
1534
|
</text>
|
|
1491
1535
|
<Show when={query()}>
|
|
1492
|
-
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={() =>
|
|
1536
|
+
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1537
|
+
event.stopPropagation()
|
|
1538
|
+
setQuery("")
|
|
1539
|
+
}}>
|
|
1493
1540
|
✕
|
|
1494
1541
|
</text>
|
|
1495
1542
|
</Show>
|
|
1496
1543
|
</box>
|
|
1497
1544
|
<scrollbox
|
|
1545
|
+
ref={(node: { scrollTop: number }) => (sidebarScroll = node)}
|
|
1498
1546
|
flexGrow={1}
|
|
1499
1547
|
flexShrink={1}
|
|
1500
1548
|
verticalScrollbarOptions={{ visible: false }}
|
|
@@ -1504,7 +1552,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1504
1552
|
<For each={tree()}>
|
|
1505
1553
|
{(row) => {
|
|
1506
1554
|
if (row.kind === "group") return (
|
|
1507
|
-
<box flexDirection="row" paddingTop={1} onMouseDown={() =>
|
|
1555
|
+
<box flexDirection="row" paddingTop={1} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1556
|
+
event.stopPropagation()
|
|
1557
|
+
toggleGroup(row.dir)
|
|
1558
|
+
}}>
|
|
1508
1559
|
<text flexGrow={1} flexShrink={1} wrapMode="none">
|
|
1509
1560
|
<span style={{ fg: theme().textMuted }}>{collapsed()[row.dir] ? "▸ " : "▾ "}</span>
|
|
1510
1561
|
<b>{props.pins().directories.includes(row.dir) ? "★ " : ""}{truncate(row.label, SIDEBAR_GROUP_WIDTH - (props.pins().directories.includes(row.dir) ? 2 : 0))}</b>
|
|
@@ -1541,7 +1592,13 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1541
1592
|
if (event.button === RIGHT_BUTTON) {
|
|
1542
1593
|
event.stopPropagation()
|
|
1543
1594
|
menu.open(row.entry, event.x, event.y)
|
|
1544
|
-
} else
|
|
1595
|
+
} else {
|
|
1596
|
+
// Sidebar-internal: openSession releases the sidebar's own
|
|
1597
|
+
// captures, so this click must not also reach the root
|
|
1598
|
+
// handler as an outside click.
|
|
1599
|
+
event.stopPropagation()
|
|
1600
|
+
void openSession(row.entry)
|
|
1601
|
+
}
|
|
1545
1602
|
}}
|
|
1546
1603
|
>
|
|
1547
1604
|
<text flexShrink={0}>
|
|
@@ -1567,7 +1624,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1567
1624
|
matchColor={theme().warning}
|
|
1568
1625
|
/>
|
|
1569
1626
|
<text flexShrink={0} style={{ fg: isPendingDelete(row.entry.id) ? theme().error : theme().textMuted }}>
|
|
1570
|
-
{isPendingDelete(row.entry.id) ? "
|
|
1627
|
+
{isPendingDelete(row.entry.id) ? "option+d again" : ago(row.entry.updated)}
|
|
1571
1628
|
</text>
|
|
1572
1629
|
</box>
|
|
1573
1630
|
)
|
|
@@ -1575,14 +1632,14 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1575
1632
|
</For>
|
|
1576
1633
|
</scrollbox>
|
|
1577
1634
|
<Show when={remaining() > 0}>
|
|
1578
|
-
<text style={{ fg: theme().textMuted }}>{`… ${remaining()} more ·
|
|
1635
|
+
<text style={{ fg: theme().textMuted }}>{`… ${remaining()} more · option+o for all`}</text>
|
|
1579
1636
|
</Show>
|
|
1580
1637
|
<Show when={query().trim() && filteredEntries().length === 0}>
|
|
1581
|
-
<text style={{ fg: theme().textMuted }}>No title or directory matches ·
|
|
1638
|
+
<text style={{ fg: theme().textMuted }}>No title or directory matches · option+o to search transcripts</text>
|
|
1582
1639
|
</Show>
|
|
1583
1640
|
<Show when={navActive()}>
|
|
1584
1641
|
<box paddingTop={1}>
|
|
1585
|
-
<text style={{ fg: theme().textMuted }}>↑↓ move · enter open ·
|
|
1642
|
+
<text style={{ fg: theme().textMuted }}>↑↓ move · enter open · option+p preview · ctrl+s/d pin · option+d delete · esc done</text>
|
|
1586
1643
|
</box>
|
|
1587
1644
|
</Show>
|
|
1588
1645
|
</Show>
|
|
@@ -1734,7 +1791,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1734
1791
|
priority: 25,
|
|
1735
1792
|
bindings: [
|
|
1736
1793
|
{ key: "y", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1737
|
-
{ key: "
|
|
1794
|
+
{ key: "alt+d", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1738
1795
|
{ key: "enter", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1739
1796
|
{ key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1740
1797
|
{ key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
@@ -1790,7 +1847,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1790
1847
|
cmd: () => preview.open(entry),
|
|
1791
1848
|
},
|
|
1792
1849
|
{
|
|
1793
|
-
key: "
|
|
1850
|
+
key: "alt+d",
|
|
1794
1851
|
desc: "Delete session",
|
|
1795
1852
|
preventDefault: true,
|
|
1796
1853
|
cmd: () => {
|
|
@@ -1851,7 +1908,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1851
1908
|
<box flexDirection="row" gap={1}>
|
|
1852
1909
|
<text style={{ fg: theme().textMuted }}>Recent sessions</text>
|
|
1853
1910
|
<text style={{ fg: theme().textMuted }}>
|
|
1854
|
-
{loadFailed() ? "· list unavailable, will retry" : "· none yet,
|
|
1911
|
+
{loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse"}
|
|
1855
1912
|
</text>
|
|
1856
1913
|
</box>
|
|
1857
1914
|
}
|
|
@@ -1859,7 +1916,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1859
1916
|
<box flexDirection="row" justifyContent="space-between" gap={2}>
|
|
1860
1917
|
<box flexDirection="row" gap={1}>
|
|
1861
1918
|
<text style={{ fg: theme().textMuted }}>Recent sessions</text>
|
|
1862
|
-
<text style={{ fg: theme().textMuted }}>·
|
|
1919
|
+
<text style={{ fg: theme().textMuted }}>· option+o for all</text>
|
|
1863
1920
|
</box>
|
|
1864
1921
|
<box
|
|
1865
1922
|
flexDirection="row"
|
|
@@ -1916,14 +1973,14 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1916
1973
|
/>
|
|
1917
1974
|
<text flexShrink={0} style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}>
|
|
1918
1975
|
{pendingDelete() === entry.id
|
|
1919
|
-
? "
|
|
1976
|
+
? "option+d again"
|
|
1920
1977
|
: `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
|
|
1921
1978
|
</text>
|
|
1922
1979
|
</box>
|
|
1923
1980
|
)}
|
|
1924
1981
|
</For>
|
|
1925
1982
|
<Show when={visible().length === 0}>
|
|
1926
|
-
<text style={{ fg: theme().textMuted }}>No title or directory matches ·
|
|
1983
|
+
<text style={{ fg: theme().textMuted }}>No title or directory matches · option+o to search transcripts</text>
|
|
1927
1984
|
</Show>
|
|
1928
1985
|
</Show>
|
|
1929
1986
|
</box>
|
|
@@ -2104,7 +2161,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2104
2161
|
// lets it size to content, so a taller picker drifts downward. Pin the
|
|
2105
2162
|
// content height and offset it up by the same amount to keep it centred.
|
|
2106
2163
|
const dialogWidth = Math.min(116, api.renderer.width - 2)
|
|
2107
|
-
const CHROME_ROWS =
|
|
2164
|
+
const CHROME_ROWS = 13
|
|
2108
2165
|
const listHeight = () => Math.max(6, Math.floor(termHeight * 0.75) - CHROME_ROWS)
|
|
2109
2166
|
const dialogHeight = () => listHeight() + CHROME_ROWS
|
|
2110
2167
|
const dialogOffset = () => Math.min(0, Math.floor(termHeight / 4 - dialogHeight() / 2 - 1))
|
|
@@ -2115,6 +2172,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2115
2172
|
))
|
|
2116
2173
|
|
|
2117
2174
|
let previousSelection: Entry[] = []
|
|
2175
|
+
let deletedAt: number | undefined
|
|
2118
2176
|
createEffect(() => {
|
|
2119
2177
|
const rows = selectableEntries()
|
|
2120
2178
|
const position = rows.findIndex((entry) => entry.id === previousSelection[cursor()]?.id)
|
|
@@ -2122,12 +2180,17 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2122
2180
|
rows.some((entry, index) => entry.id !== previousSelection[index]?.id))) {
|
|
2123
2181
|
// A search/filter/collapse changed the tree: reveal the retained row or
|
|
2124
2182
|
// start at the first result rather than leaving a stale window open.
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2183
|
+
if (deletedAt !== undefined) {
|
|
2184
|
+
setCursor(Math.max(0, Math.min(deletedAt, rows.length - 1)))
|
|
2185
|
+
deletedAt = undefined
|
|
2186
|
+
} else {
|
|
2187
|
+
const next = position >= 0 ? position : 0
|
|
2188
|
+
setCursor(next)
|
|
2189
|
+
const id = rows[next]?.id
|
|
2190
|
+
setViewportStart(position < 0 || next === 0 ? 0 : Math.max(0, pickerRows().findIndex(
|
|
2191
|
+
(row) => row.kind === "item" && row.entry.id === id,
|
|
2192
|
+
)))
|
|
2193
|
+
}
|
|
2131
2194
|
} else if (cursor() >= rows.length) setCursor(Math.max(0, rows.length - 1))
|
|
2132
2195
|
previousSelection = rows
|
|
2133
2196
|
})
|
|
@@ -2260,6 +2323,8 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2260
2323
|
api.ui.toast({ message: "Session was not deleted", variant: "error" })
|
|
2261
2324
|
return
|
|
2262
2325
|
}
|
|
2326
|
+
deletedAt = selectableEntries().findIndex((item) => item.id === entry.id)
|
|
2327
|
+
if (deletedAt < 0) deletedAt = undefined
|
|
2263
2328
|
setAllEntries(remaining)
|
|
2264
2329
|
api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
|
|
2265
2330
|
if (currentSessionID() === entry.id) api.route.navigate("home")
|
|
@@ -2285,7 +2350,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2285
2350
|
},
|
|
2286
2351
|
},
|
|
2287
2352
|
{
|
|
2288
|
-
key: "
|
|
2353
|
+
key: "alt+d",
|
|
2289
2354
|
desc: "Confirm delete",
|
|
2290
2355
|
preventDefault: true,
|
|
2291
2356
|
cmd: () => {
|
|
@@ -2367,7 +2432,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2367
2432
|
if (entry) onTogglePin("directories", entry.dir)
|
|
2368
2433
|
} },
|
|
2369
2434
|
{
|
|
2370
|
-
key: "
|
|
2435
|
+
key: "alt+d",
|
|
2371
2436
|
desc: "Delete session",
|
|
2372
2437
|
preventDefault: true,
|
|
2373
2438
|
cmd: () => {
|
|
@@ -2422,6 +2487,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2422
2487
|
flexDirection="column"
|
|
2423
2488
|
height={dialogHeight()}
|
|
2424
2489
|
marginTop={dialogOffset()}
|
|
2490
|
+
paddingTop={1}
|
|
2425
2491
|
paddingBottom={1}
|
|
2426
2492
|
gap={1}
|
|
2427
2493
|
backgroundColor={api.theme.current.backgroundPanel}
|
|
@@ -2534,7 +2600,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2534
2600
|
paddingRight={4}
|
|
2535
2601
|
backgroundColor={
|
|
2536
2602
|
row.entry.id === selectableEntries()[cursor()]?.id
|
|
2537
|
-
? api.theme.current.
|
|
2603
|
+
? api.theme.current.backgroundElement
|
|
2538
2604
|
: RGBA.fromInts(0, 0, 0, 0)
|
|
2539
2605
|
}
|
|
2540
2606
|
onMouseMove={() => {
|
|
@@ -2549,7 +2615,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2549
2615
|
>
|
|
2550
2616
|
<box flexDirection="row" gap={1}>
|
|
2551
2617
|
<text flexShrink={0} style={{ fg: row.entry.id === selectableEntries()[cursor()]?.id
|
|
2552
|
-
? api.theme.current.
|
|
2618
|
+
? api.theme.current.text : api.theme.current.textMuted }}>
|
|
2553
2619
|
{row.last ? "└" : "├"}
|
|
2554
2620
|
</text>
|
|
2555
2621
|
<Show when={row.entry.id === currentSessionID()}>
|
|
@@ -2558,7 +2624,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2558
2624
|
style={{
|
|
2559
2625
|
fg:
|
|
2560
2626
|
row.entry.id === selectableEntries()[cursor()]?.id
|
|
2561
|
-
? api.theme.current.
|
|
2627
|
+
? api.theme.current.text
|
|
2562
2628
|
: api.theme.current.primary,
|
|
2563
2629
|
}}
|
|
2564
2630
|
>
|
|
@@ -2571,12 +2637,12 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2571
2637
|
bold={row.entry.id === selectableEntries()[cursor()]?.id}
|
|
2572
2638
|
color={
|
|
2573
2639
|
row.entry.id === selectableEntries()[cursor()]?.id
|
|
2574
|
-
? api.theme.current.
|
|
2640
|
+
? api.theme.current.text
|
|
2575
2641
|
: api.theme.current.text
|
|
2576
2642
|
}
|
|
2577
2643
|
matchColor={
|
|
2578
2644
|
row.entry.id === selectableEntries()[cursor()]?.id
|
|
2579
|
-
? api.theme.current.
|
|
2645
|
+
? api.theme.current.text
|
|
2580
2646
|
: api.theme.current.warning
|
|
2581
2647
|
}
|
|
2582
2648
|
/>
|
|
@@ -2587,7 +2653,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2587
2653
|
pendingDelete()?.id === row.entry.id
|
|
2588
2654
|
? api.theme.current.error
|
|
2589
2655
|
: row.entry.id === selectableEntries()[cursor()]?.id
|
|
2590
|
-
? api.theme.current.
|
|
2656
|
+
? api.theme.current.text
|
|
2591
2657
|
: api.theme.current.textMuted,
|
|
2592
2658
|
}}
|
|
2593
2659
|
>
|
|
@@ -2608,7 +2674,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2608
2674
|
<Highlighted
|
|
2609
2675
|
text={match()}
|
|
2610
2676
|
query={query()}
|
|
2611
|
-
color={row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.
|
|
2677
|
+
color={row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.text : api.theme.current.textMuted}
|
|
2612
2678
|
matchColor={api.theme.current.warning}
|
|
2613
2679
|
/>
|
|
2614
2680
|
</box>
|
|
@@ -2646,10 +2712,10 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2646
2712
|
>
|
|
2647
2713
|
{pendingDelete()
|
|
2648
2714
|
? `Delete "${truncate(pendingDelete()!.title, 40)}"? y confirm · n cancel`
|
|
2649
|
-
: "↑↓ move · enter open · ctrl+g project ·
|
|
2715
|
+
: "↑↓ move · enter open · ctrl+g project · option+w input · option+s pinned · option+p preview"}
|
|
2650
2716
|
</text>
|
|
2651
2717
|
<Show when={!pendingDelete()}>
|
|
2652
|
-
<text style={{ fg: api.theme.current.textMuted }}>ctrl+s/d pin ·
|
|
2718
|
+
<text style={{ fg: api.theme.current.textMuted }}>ctrl+s/d pin · option+d delete · ctrl+f fork · esc close</text>
|
|
2653
2719
|
</Show>
|
|
2654
2720
|
</box>
|
|
2655
2721
|
<Show when={showPreview()}>
|
|
@@ -2903,7 +2969,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2903
2969
|
paddingRight={1}
|
|
2904
2970
|
backgroundColor={
|
|
2905
2971
|
index() === cursor()
|
|
2906
|
-
? api.theme.current.
|
|
2972
|
+
? api.theme.current.backgroundElement
|
|
2907
2973
|
: RGBA.fromInts(0, 0, 0, 0)
|
|
2908
2974
|
}
|
|
2909
2975
|
onMouseDown={() => {
|
|
@@ -2918,7 +2984,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2918
2984
|
style={{
|
|
2919
2985
|
fg:
|
|
2920
2986
|
index() === cursor()
|
|
2921
|
-
? api.theme.current.
|
|
2987
|
+
? api.theme.current.text
|
|
2922
2988
|
: api.theme.current.text,
|
|
2923
2989
|
}}
|
|
2924
2990
|
>
|
|
@@ -2929,7 +2995,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2929
2995
|
style={{
|
|
2930
2996
|
fg:
|
|
2931
2997
|
index() === cursor()
|
|
2932
|
-
? api.theme.current.
|
|
2998
|
+
? api.theme.current.text
|
|
2933
2999
|
: api.theme.current.warning,
|
|
2934
3000
|
}}
|
|
2935
3001
|
>
|
|
@@ -2940,7 +3006,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2940
3006
|
style={{
|
|
2941
3007
|
fg:
|
|
2942
3008
|
index() === cursor()
|
|
2943
|
-
? api.theme.current.
|
|
3009
|
+
? api.theme.current.text
|
|
2944
3010
|
: api.theme.current.textMuted,
|
|
2945
3011
|
}}
|
|
2946
3012
|
>
|