opencode-sesh 1.0.1 → 1.0.3
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 -1
- package/CHANGELOG.md +14 -0
- package/README.md +13 -9
- package/package.json +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +80 -14
package/AGENTS.md
CHANGED
|
@@ -85,7 +85,10 @@ the schema below. Keystrokes never touch the database:
|
|
|
85
85
|
- Session and directory pins live in `${XDG_DATA_HOME:-$HOME/.local/share}/sesh/pins.json`
|
|
86
86
|
(`SESH_PINS_FILE` overrides it for tests), shared by fzf and the TUI. Writes
|
|
87
87
|
use a sibling lock directory and atomic rename. Ctrl-S toggles a selected
|
|
88
|
-
session; Ctrl-D toggles its directory
|
|
88
|
+
session; Ctrl-D toggles its directory (terminal picker and option+o picker).
|
|
89
|
+
The sidebar and home lists use Option-S / Option-R instead: opencode binds
|
|
90
|
+
`ctrl+d` itself (composer delete-char, session delete), so ctrl pins would
|
|
91
|
+
shadow the composer. Pinned directories sort before groups
|
|
89
92
|
containing pinned sessions, with pinned sessions first inside each group.
|
|
90
93
|
|
|
91
94
|
**Output contract** (TSV, 6 fields):
|
|
@@ -203,6 +206,10 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
203
206
|
- **The sidebar never binds arrow keys globally.** Keyboard navigation is
|
|
204
207
|
opt-in: clicking the *Sessions* heading (or its search box) activates a
|
|
205
208
|
priority-20 layer, because a global `↑`/`↓` would hijack the main prompt.
|
|
209
|
+
Clicking outside the sidebar, opening a session, or navigating releases it
|
|
210
|
+
again (with search, hover and any armed delete), so the sidebar cannot
|
|
211
|
+
shadow the composer afterwards. Sidebar-internal clicks stop propagation so
|
|
212
|
+
the outside-click release cannot undo them.
|
|
206
213
|
Cursor state is derived from `itemRows()`, so hover and cursor resolve to the
|
|
207
214
|
same row. The section lists every session with no row cap
|
|
208
215
|
(`tests/tui.mjs` asserts the cap stays out); overflow scrolls inside 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.3] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **tui:** Rebind sidebar pins to option keys to stop shadowing composer
|
|
13
|
+
|
|
14
|
+
## [1.0.2] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **tui:** Release sidebar captures when focus returns to composer
|
|
19
|
+
|
|
8
20
|
## [1.0.1] - 2026-10-08
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -424,6 +436,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
424
436
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
425
437
|
CI.
|
|
426
438
|
|
|
439
|
+
[1.0.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.2...v1.0.3
|
|
440
|
+
[1.0.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.1...v1.0.2
|
|
427
441
|
[1.0.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.0...v1.0.1
|
|
428
442
|
[1.0.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.14.1...v1.0.0
|
|
429
443
|
[0.14.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.14.0...v0.14.1
|
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
|
|
@@ -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,
|
|
204
|
+
Option-P to preview, Option-S/Option-R 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,12 +1074,29 @@ 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()
|
|
1070
1089
|
})
|
|
1071
1090
|
createEffect(() => {
|
|
1072
1091
|
if (props.api.ui.dialog.open) menu.close()
|
|
1092
|
+
// A modal dialog owns input while open: yield the sidebar's keyboard
|
|
1093
|
+
// captures too, so its layers cannot double-fire with the dialog's own.
|
|
1094
|
+
if (props.api.ui.dialog.open) {
|
|
1095
|
+
cancelDelete()
|
|
1096
|
+
setNavActive(false)
|
|
1097
|
+
setSearching(false)
|
|
1098
|
+
setHovered(undefined)
|
|
1099
|
+
}
|
|
1073
1100
|
})
|
|
1074
1101
|
|
|
1075
1102
|
const moveCursor = (delta: number) => {
|
|
@@ -1276,6 +1303,12 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1276
1303
|
return
|
|
1277
1304
|
}
|
|
1278
1305
|
if (searching()) setSearching(false)
|
|
1306
|
+
// A click anywhere outside the sidebar means input is back in the main
|
|
1307
|
+
// view: release the nav layer, hover shortcuts and any armed delete so
|
|
1308
|
+
// the sidebar's keys and rows stop shadowing the composer.
|
|
1309
|
+
cancelDelete()
|
|
1310
|
+
if (navActive()) setNavActive(false)
|
|
1311
|
+
setHovered(undefined)
|
|
1279
1312
|
}
|
|
1280
1313
|
|
|
1281
1314
|
onMount(() => {
|
|
@@ -1332,8 +1365,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1332
1365
|
if (!confirmArmed()) requestDelete(entry)
|
|
1333
1366
|
},
|
|
1334
1367
|
},
|
|
1335
|
-
{ key: "
|
|
1336
|
-
{ key: "
|
|
1368
|
+
{ key: "alt+s", desc: "Pin session", preventDefault: true, cmd: () => props.onTogglePin("sessions", entry.id) },
|
|
1369
|
+
{ key: "alt+r", desc: "Pin directory", preventDefault: true, cmd: () => props.onTogglePin("directories", entry.dir) },
|
|
1337
1370
|
{
|
|
1338
1371
|
key: "/",
|
|
1339
1372
|
desc: "Search sessions",
|
|
@@ -1346,6 +1379,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1346
1379
|
|
|
1347
1380
|
// Keyboard navigation is opt-in (click the "Sessions" heading or the search
|
|
1348
1381
|
// box) so the sidebar never steals the arrow keys from the main prompt.
|
|
1382
|
+
// Clicking anywhere outside the sidebar, opening a session, or navigating
|
|
1383
|
+
// releases it again, so its layers cannot shadow the composer afterwards.
|
|
1349
1384
|
createEffect(() => {
|
|
1350
1385
|
disposeNav?.()
|
|
1351
1386
|
disposeNav = undefined
|
|
@@ -1382,7 +1417,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1382
1417
|
},
|
|
1383
1418
|
},
|
|
1384
1419
|
{
|
|
1385
|
-
key: "
|
|
1420
|
+
key: "alt+s",
|
|
1386
1421
|
desc: "Pin session",
|
|
1387
1422
|
preventDefault: true,
|
|
1388
1423
|
cmd: () => {
|
|
@@ -1391,7 +1426,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1391
1426
|
},
|
|
1392
1427
|
},
|
|
1393
1428
|
{
|
|
1394
|
-
key: "
|
|
1429
|
+
key: "alt+r",
|
|
1395
1430
|
desc: "Pin directory",
|
|
1396
1431
|
preventDefault: true,
|
|
1397
1432
|
cmd: () => {
|
|
@@ -1450,7 +1485,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1450
1485
|
return (
|
|
1451
1486
|
<box flexDirection="column" paddingRight={1} flexGrow={1} flexShrink={1}>
|
|
1452
1487
|
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
|
1453
|
-
<text flexShrink={1} onMouseDown={() => {
|
|
1488
|
+
<text flexShrink={1} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1489
|
+
// Sidebar-internal: keep this click from reaching the root handler,
|
|
1490
|
+
// which would release the nav layer this toggle just activated.
|
|
1491
|
+
event.stopPropagation()
|
|
1454
1492
|
if (!sectionCollapsed()) setNavActive((value) => !value)
|
|
1455
1493
|
}}>
|
|
1456
1494
|
<b>Sessions</b>
|
|
@@ -1462,7 +1500,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1462
1500
|
<span style={{ fg: theme().warning }}> · {waitingEntries().length} need input</span>
|
|
1463
1501
|
</Show>
|
|
1464
1502
|
</text>
|
|
1465
|
-
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={
|
|
1503
|
+
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1504
|
+
event.stopPropagation()
|
|
1505
|
+
toggleSection()
|
|
1506
|
+
}}>
|
|
1466
1507
|
{sectionCollapsed() ? "▸ show" : "▾ hide"}
|
|
1467
1508
|
</text>
|
|
1468
1509
|
</box>
|
|
@@ -1481,7 +1522,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1481
1522
|
paddingLeft={1}
|
|
1482
1523
|
paddingRight={1}
|
|
1483
1524
|
backgroundColor={searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
|
|
1484
|
-
onMouseDown={() => {
|
|
1525
|
+
onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1526
|
+
// Sidebar-internal: the root handler must not see this click, or
|
|
1527
|
+
// it would clear the search this box just activated.
|
|
1528
|
+
event.stopPropagation()
|
|
1485
1529
|
insideSearchBox = true
|
|
1486
1530
|
setSearching(true)
|
|
1487
1531
|
}}
|
|
@@ -1497,7 +1541,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1497
1541
|
{searching() ? "▏" : ""}
|
|
1498
1542
|
</text>
|
|
1499
1543
|
<Show when={query()}>
|
|
1500
|
-
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={() =>
|
|
1544
|
+
<text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1545
|
+
event.stopPropagation()
|
|
1546
|
+
setQuery("")
|
|
1547
|
+
}}>
|
|
1501
1548
|
✕
|
|
1502
1549
|
</text>
|
|
1503
1550
|
</Show>
|
|
@@ -1513,7 +1560,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1513
1560
|
<For each={tree()}>
|
|
1514
1561
|
{(row) => {
|
|
1515
1562
|
if (row.kind === "group") return (
|
|
1516
|
-
<box flexDirection="row" paddingTop={1} onMouseDown={() =>
|
|
1563
|
+
<box flexDirection="row" paddingTop={1} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1564
|
+
event.stopPropagation()
|
|
1565
|
+
toggleGroup(row.dir)
|
|
1566
|
+
}}>
|
|
1517
1567
|
<text flexGrow={1} flexShrink={1} wrapMode="none">
|
|
1518
1568
|
<span style={{ fg: theme().textMuted }}>{collapsed()[row.dir] ? "▸ " : "▾ "}</span>
|
|
1519
1569
|
<b>{props.pins().directories.includes(row.dir) ? "★ " : ""}{truncate(row.label, SIDEBAR_GROUP_WIDTH - (props.pins().directories.includes(row.dir) ? 2 : 0))}</b>
|
|
@@ -1550,7 +1600,13 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1550
1600
|
if (event.button === RIGHT_BUTTON) {
|
|
1551
1601
|
event.stopPropagation()
|
|
1552
1602
|
menu.open(row.entry, event.x, event.y)
|
|
1553
|
-
} else
|
|
1603
|
+
} else {
|
|
1604
|
+
// Sidebar-internal: openSession releases the sidebar's own
|
|
1605
|
+
// captures, so this click must not also reach the root
|
|
1606
|
+
// handler as an outside click.
|
|
1607
|
+
event.stopPropagation()
|
|
1608
|
+
void openSession(row.entry)
|
|
1609
|
+
}
|
|
1554
1610
|
}}
|
|
1555
1611
|
>
|
|
1556
1612
|
<text flexShrink={0}>
|
|
@@ -1591,7 +1647,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1591
1647
|
</Show>
|
|
1592
1648
|
<Show when={navActive()}>
|
|
1593
1649
|
<box paddingTop={1}>
|
|
1594
|
-
<text style={{ fg: theme().textMuted }}>↑↓ move · enter open · option+p preview ·
|
|
1650
|
+
<text style={{ fg: theme().textMuted }}>↑↓ move · enter open · option+p preview · option+s/r pin · option+d delete · esc done</text>
|
|
1595
1651
|
</box>
|
|
1596
1652
|
</Show>
|
|
1597
1653
|
</Show>
|
|
@@ -1759,6 +1815,16 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1759
1815
|
if (searching()) setSearching(false)
|
|
1760
1816
|
}
|
|
1761
1817
|
|
|
1818
|
+
// A modal dialog owns input while open: yield this list's keyboard captures
|
|
1819
|
+
// too, so its layers cannot double-fire with the dialog's own.
|
|
1820
|
+
createEffect(() => {
|
|
1821
|
+
if (props.api.ui.dialog.open) {
|
|
1822
|
+
cancelDelete()
|
|
1823
|
+
setSearching(false)
|
|
1824
|
+
setHovered(undefined)
|
|
1825
|
+
}
|
|
1826
|
+
})
|
|
1827
|
+
|
|
1762
1828
|
onMount(() => {
|
|
1763
1829
|
ensureRootMouse(props.api.renderer)
|
|
1764
1830
|
rootMouseHandlers.add(onRootMouseDown)
|
|
@@ -1806,8 +1872,8 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1806
1872
|
if (!confirmArmed()) requestDelete(entry)
|
|
1807
1873
|
},
|
|
1808
1874
|
},
|
|
1809
|
-
{ key: "
|
|
1810
|
-
{ key: "
|
|
1875
|
+
{ key: "alt+s", desc: "Pin session", preventDefault: true, cmd: () => props.onTogglePin("sessions", entry.id) },
|
|
1876
|
+
{ key: "alt+r", desc: "Pin directory", preventDefault: true, cmd: () => props.onTogglePin("directories", entry.dir) },
|
|
1811
1877
|
],
|
|
1812
1878
|
})
|
|
1813
1879
|
})
|