opencode-sesh 1.0.1 → 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 CHANGED
@@ -203,6 +203,10 @@ 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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ 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
+
8
14
  ## [1.0.1] - 2026-10-08
9
15
 
10
16
  ### Fixed
@@ -424,6 +430,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
424
430
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
425
431
  CI.
426
432
 
433
+ [1.0.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.1...v1.0.2
427
434
  [1.0.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.0...v1.0.1
428
435
  [1.0.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.14.1...v1.0.0
429
436
  [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
  [![GitHub stars](https://img.shields.io/github/stars/ozandogrultan/opencode-sesh?style=social)](https://github.com/ozandogrultan/opencode-sesh/stargazers)
19
19
 
20
20
  ```text
21
- ❯ auth
22
- ~/code/api (8)
23
- ├ Retry the OAuth token refresh 2h
24
- ├ Login rate limiting notes 3d
25
- └ auth middleware cleanup 11d
26
- ~/code/web (3)
27
- ├ Fix session cookie on Safari 1d
28
- └ Redirect loop after logout 6d
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1050,7 +1050,17 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1050
1050
  setSectionCollapsed((value) => !value)
1051
1051
  }
1052
1052
 
1053
- const openSession = (entry: Entry) => props.api.route.navigate("session", { sessionID: entry.id })
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()
@@ -1276,6 +1295,12 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1276
1295
  return
1277
1296
  }
1278
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)
1279
1304
  }
1280
1305
 
1281
1306
  onMount(() => {
@@ -1346,6 +1371,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1346
1371
 
1347
1372
  // Keyboard navigation is opt-in (click the "Sessions" heading or the search
1348
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.
1349
1376
  createEffect(() => {
1350
1377
  disposeNav?.()
1351
1378
  disposeNav = undefined
@@ -1450,7 +1477,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1450
1477
  return (
1451
1478
  <box flexDirection="column" paddingRight={1} flexGrow={1} flexShrink={1}>
1452
1479
  <box flexDirection="row" justifyContent="space-between" gap={1}>
1453
- <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()
1454
1484
  if (!sectionCollapsed()) setNavActive((value) => !value)
1455
1485
  }}>
1456
1486
  <b>Sessions</b>
@@ -1462,7 +1492,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1462
1492
  <span style={{ fg: theme().warning }}> · {waitingEntries().length} need input</span>
1463
1493
  </Show>
1464
1494
  </text>
1465
- <text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={toggleSection}>
1495
+ <text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
1496
+ event.stopPropagation()
1497
+ toggleSection()
1498
+ }}>
1466
1499
  {sectionCollapsed() ? "▸ show" : "▾ hide"}
1467
1500
  </text>
1468
1501
  </box>
@@ -1481,7 +1514,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1481
1514
  paddingLeft={1}
1482
1515
  paddingRight={1}
1483
1516
  backgroundColor={searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
1484
- 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()
1485
1521
  insideSearchBox = true
1486
1522
  setSearching(true)
1487
1523
  }}
@@ -1497,7 +1533,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1497
1533
  {searching() ? "▏" : ""}
1498
1534
  </text>
1499
1535
  <Show when={query()}>
1500
- <text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={() => setQuery("")}>
1536
+ <text flexShrink={0} style={{ fg: theme().textMuted }} onMouseDown={(event: { stopPropagation: () => void }) => {
1537
+ event.stopPropagation()
1538
+ setQuery("")
1539
+ }}>
1501
1540
  ✕
1502
1541
  </text>
1503
1542
  </Show>
@@ -1513,7 +1552,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1513
1552
  <For each={tree()}>
1514
1553
  {(row) => {
1515
1554
  if (row.kind === "group") return (
1516
- <box flexDirection="row" paddingTop={1} onMouseDown={() => toggleGroup(row.dir)}>
1555
+ <box flexDirection="row" paddingTop={1} onMouseDown={(event: { stopPropagation: () => void }) => {
1556
+ event.stopPropagation()
1557
+ toggleGroup(row.dir)
1558
+ }}>
1517
1559
  <text flexGrow={1} flexShrink={1} wrapMode="none">
1518
1560
  <span style={{ fg: theme().textMuted }}>{collapsed()[row.dir] ? "▸ " : "▾ "}</span>
1519
1561
  <b>{props.pins().directories.includes(row.dir) ? "★ " : ""}{truncate(row.label, SIDEBAR_GROUP_WIDTH - (props.pins().directories.includes(row.dir) ? 2 : 0))}</b>
@@ -1550,7 +1592,13 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1550
1592
  if (event.button === RIGHT_BUTTON) {
1551
1593
  event.stopPropagation()
1552
1594
  menu.open(row.entry, event.x, event.y)
1553
- } else void openSession(row.entry)
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
+ }
1554
1602
  }}
1555
1603
  >
1556
1604
  <text flexShrink={0}>