opencode-sesh 0.11.0 → 0.12.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 CHANGED
@@ -17,6 +17,8 @@ territory: read this first, then the specific file you need.
17
17
  db` store (the native `opencode session list` is project-scoped).
18
18
 
19
19
  No iTerm2, no AppleScript, no panes, no window management — **never add any.**
20
+ The one exception is the TUI's right-click action, which shells out to
21
+ `cmux new-workspace` when running inside cmux.
20
22
  opencode has no persistent-sidebar API, so the "sidebar" is a `sidebar_content`
21
23
  slot section plus an xlarge modal picker; the terminal UI is fullscreen fzf.
22
24
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-09-30
11
+
12
+ ### Added
13
+
14
+ - Right-clicking a session in the TUI sidebar, home list or picker opens it in a
15
+ new cmux workspace at its directory; outside cmux it shows a toast.
16
+
17
+ ### Fixed
18
+
19
+ - Avoid creating `.bak` backup files on install
20
+ ## [0.11.1] - 2026-09-25
21
+
22
+ ### Fixed
23
+
24
+ - Confirming a session delete in the TUI picker no longer leaks `y` or `n` into
25
+ the search input; `Enter` and a second `Ctrl-X` commit the armed delete, and
26
+ session deletion is supported on the home screen.
10
27
  ## [0.11.0] - 2026-09-25
11
28
 
12
29
  ### Changed
@@ -371,7 +388,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
371
388
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
372
389
  CI.
373
390
 
374
- [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.0...HEAD
391
+ [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.12.0...HEAD
392
+ [0.12.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.1...v0.12.0
393
+ [0.11.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.0...v0.11.1
375
394
  [0.11.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.10.0...v0.11.0
376
395
  [0.10.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.1...v0.10.0
377
396
  [0.9.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.0...v0.9.1
package/README.md CHANGED
@@ -185,6 +185,8 @@ The picker groups sessions under collapsible directory headings (click ▾/▸).
185
185
  Mouse-wheel scrolling moves through the tree without changing the selection
186
186
  under a stationary pointer. Option-P opens a full-size transcript preview over
187
187
  the picker; use ↑/↓ to preview other sessions, then Esc to return to the list.
188
+ Right-click a session (sidebar, home list or picker) to open it in a new cmux
189
+ workspace at its directory; outside cmux it shows a toast instead.
188
190
 
189
191
  Click the sidebar's `search…` box to filter recent sessions in place; click
190
192
  elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
@@ -228,7 +230,7 @@ All variables are optional.
228
230
 
229
231
  ## Integrations
230
232
 
231
- sesh is terminal-agnostic on purpose: it has no window-management code. The
233
+ sesh is terminal-agnostic apart from the TUI's right-click action. The
232
234
  [`contrib/cmux/`](contrib/cmux/README.md) directory shows how to drive it from
233
235
  cmux — command-palette entries, shortcuts, resuming a session in its own
234
236
  workspace, and a global "resume from anywhere" hotkey — all caller-side.
package/ROADMAP.md CHANGED
@@ -35,10 +35,9 @@ candidate summary; non-TTY runs require `--yes`; `--dry-run` lists only.
35
35
 
36
36
  ## 3. sesh ↔ cmux bridge (done)
37
37
 
38
- **Approach:** kept caller-side. sesh has no window-management code (a hard
39
- repo rule), so [`contrib/cmux/`](contrib/cmux/README.md) ships the cmux
38
+ **Approach:** [`contrib/cmux/`](contrib/cmux/README.md) ships the cmux
40
39
  actions, shortcuts and a `--print --json` recipe that resumes a session in its
41
- own workspace. cmux calls sesh; sesh never calls cmux.
40
+ own workspace; the TUI's right-click action opens one directly.
42
41
 
43
42
  ## 4. Per-project cost digest (done)
44
43
 
@@ -1,8 +1,8 @@
1
1
  # sesh ↔ cmux
2
2
 
3
3
  Optional glue for running sesh inside [cmux](https://cmux.dev). Nothing here is
4
- bundled into the picker: sesh has no window-management code, and these recipes
5
- keep it that way. cmux calls sesh; sesh never calls cmux.
4
+ bundled into the picker: these recipes are caller-side. The only place sesh
5
+ calls cmux is the TUI's right-click action, which runs `cmux new-workspace`.
6
6
 
7
7
  ## Command palette + shortcuts
8
8
 
package/install.sh CHANGED
@@ -87,7 +87,7 @@ remove_tui_entry() {
87
87
  local tmp
88
88
  tmp=$(mktemp "${TMPDIR:-/tmp}/sesh-tui.XXXXXX") || return 0
89
89
  if jq 'if (.plugin | type) == "array" then .plugin = [.plugin[] | select(. != "./plugins/sesh-panel.tsx")] else . end' "$TUI_JSON" > "$tmp" 2>/dev/null; then
90
- cmp -s "$TUI_JSON" "$tmp" || { cp -p "$TUI_JSON" "$TUI_JSON.bak.$(date +%Y%m%d%H%M%S)"; mv "$tmp" "$TUI_JSON"; }
90
+ cmp -s "$TUI_JSON" "$tmp" || mv "$tmp" "$TUI_JSON"
91
91
  rm -f "$tmp"
92
92
  else
93
93
  rm -f "$tmp"
@@ -120,10 +120,6 @@ install_file() {
120
120
  note "up to date $dst"
121
121
  return 0
122
122
  fi
123
- if [ -f "$dst" ]; then
124
- cp -p "$dst" "$dst.bak.$(date +%Y%m%d%H%M%S)"
125
- note "backed up existing $(basename "$dst")"
126
- fi
127
123
  cp -p "$src" "$dst"
128
124
  note "installed $dst"
129
125
  }
@@ -157,7 +153,6 @@ install_tui_panel() {
157
153
  note "tui.json already registers the sesh panel"
158
154
  rm -f "$tui_tmp"
159
155
  else
160
- [ "$tui_created" = 1 ] || cp -p "$TUI_JSON" "$TUI_JSON.bak.$(date +%Y%m%d%H%M%S)"
161
156
  mv "$tui_tmp" "$TUI_JSON"
162
157
  note "registered ./plugins/sesh-panel.tsx in tui.json"
163
158
  fi
@@ -190,7 +185,6 @@ install_tui_panel() {
190
185
  note "plugin dependencies already present in $PACKAGE_JSON"
191
186
  rm -f "$pkg_tmp"
192
187
  else
193
- [ "$pkg_created" = 1 ] || cp -p "$PACKAGE_JSON" "$PACKAGE_JSON.bak.$(date +%Y%m%d%H%M%S)"
194
188
  mv "$pkg_tmp" "$PACKAGE_JSON"
195
189
  note "declared plugin dependencies in $PACKAGE_JSON (opencode installs them on next start)"
196
190
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,6 +4,7 @@ import type { Project, Session } from "@opencode-ai/sdk/v2"
4
4
  import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
5
5
  import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises"
6
6
  import { dirname, join } from "node:path"
7
+ import { spawn } from "node:child_process"
7
8
  import { getTreeSitterClient, RGBA, SyntaxStyle, TextAttributes } from "@opentui/core"
8
9
 
9
10
  type ThemeColors = TuiPluginApi["theme"]["current"]
@@ -250,6 +251,22 @@ type Entry = { id: string; title: string; dir: string; group: string; updated: n
250
251
  type EntryResult = { entries: Entry[]; truncated: boolean }
251
252
  type SidebarMarker = "current" | "running" | "idle"
252
253
 
254
+ const RIGHT_BUTTON = 2
255
+
256
+ function openInCmuxWorkspace(api: TuiPluginApi, entry: Entry) {
257
+ if (!process.env.CMUX_WORKSPACE_ID) {
258
+ api.ui.toast({ message: "Right-click opens a new workspace only inside cmux", variant: "info" })
259
+ return
260
+ }
261
+ const args = ["new-workspace", "--name", entry.title || entry.id, "--command", `opencode --session ${entry.id}`, "--focus", "true"]
262
+ if (entry.dir) args.push("--cwd", entry.dir)
263
+ const child = spawn("cmux", args, { stdio: "ignore" })
264
+ child.on("error", () => api.ui.toast({ message: "Could not open a cmux workspace", variant: "error" }))
265
+ child.on("exit", (code) => {
266
+ if (code) api.ui.toast({ message: "Could not open a cmux workspace", variant: "error" })
267
+ })
268
+ }
269
+
253
270
  // Loading-spinner frames for the marker of a session whose agent is working.
254
271
  const SIDEBAR_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
255
272
 
@@ -1013,8 +1030,47 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1013
1030
  let disposeHoverSpace: (() => void) | undefined
1014
1031
  let disposeSearch: (() => void) | undefined
1015
1032
  let disposeNav: (() => void) | undefined
1033
+ let disposeConfirm: (() => void) | undefined
1016
1034
  let insideSearchBox = false
1017
1035
 
1036
+ createEffect(() => {
1037
+ disposeConfirm?.()
1038
+ disposeConfirm = undefined
1039
+ if (!pendingDelete()) return
1040
+ disposeConfirm = props.api.keymap.registerLayer({
1041
+ mode: "base",
1042
+ priority: 25,
1043
+ bindings: [
1044
+ {
1045
+ key: "y",
1046
+ desc: "Confirm delete",
1047
+ preventDefault: true,
1048
+ cmd: () => {
1049
+ confirmArmed()
1050
+ },
1051
+ },
1052
+ {
1053
+ key: "ctrl+x",
1054
+ desc: "Confirm delete",
1055
+ preventDefault: true,
1056
+ cmd: () => {
1057
+ confirmArmed()
1058
+ },
1059
+ },
1060
+ {
1061
+ key: "enter",
1062
+ desc: "Confirm delete",
1063
+ preventDefault: true,
1064
+ cmd: () => {
1065
+ confirmArmed()
1066
+ },
1067
+ },
1068
+ { key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
1069
+ { key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
1070
+ ],
1071
+ })
1072
+ })
1073
+
1018
1074
  const onRootMouseDown = () => {
1019
1075
  if (insideSearchBox) {
1020
1076
  insideSearchBox = false
@@ -1144,6 +1200,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1144
1200
  desc: "Open session",
1145
1201
  preventDefault: true,
1146
1202
  cmd: () => {
1203
+ if (confirmArmed()) return
1147
1204
  const entry = itemRows()[cursor()]
1148
1205
  if (entry) void openSession(entry)
1149
1206
  },
@@ -1178,6 +1235,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1178
1235
  disposeHoverSpace?.()
1179
1236
  disposeSearch?.()
1180
1237
  disposeNav?.()
1238
+ disposeConfirm?.()
1181
1239
  if (confirmTimer) clearTimeout(confirmTimer)
1182
1240
  })
1183
1241
 
@@ -1278,7 +1336,9 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1278
1336
  }
1279
1337
  }}
1280
1338
  onMouseOut={() => setHovered(undefined)}
1281
- onMouseDown={() => void openSession(row.entry)}
1339
+ onMouseDown={(event: { button?: number }) =>
1340
+ event.button === RIGHT_BUTTON ? openInCmuxWorkspace(props.api, row.entry) : void openSession(row.entry)
1341
+ }
1282
1342
  >
1283
1343
  <text flexShrink={0}>
1284
1344
  <span style={{ fg: theme().textMuted }}>{row.last ? "└" : "├"}</span>
@@ -1337,11 +1397,89 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1337
1397
  const [hovered, setHovered] = createSignal<string>()
1338
1398
  const [searching, setSearching] = createSignal(false)
1339
1399
  const [loadFailed, setLoadFailed] = createSignal(false)
1400
+ const [pendingDelete, setPendingDelete] = createSignal<string>()
1401
+ const [deleting, setDeleting] = createSignal<string>()
1402
+ let confirmTimer: ReturnType<typeof setTimeout> | undefined
1403
+ let disposeConfirm: (() => void) | undefined
1340
1404
  const preview = createTranscriptPreview(props.api)
1341
1405
  let disposeHoverSpace: (() => void) | undefined
1342
1406
  let disposeSearch: (() => void) | undefined
1343
1407
  let insideSearchBox = false
1344
1408
 
1409
+ const cancelDelete = () => {
1410
+ if (confirmTimer) clearTimeout(confirmTimer)
1411
+ confirmTimer = undefined
1412
+ disposeConfirm?.()
1413
+ disposeConfirm = undefined
1414
+ setPendingDelete(undefined)
1415
+ }
1416
+
1417
+ const confirmArmed = (): boolean => {
1418
+ const id = pendingDelete()
1419
+ if (!id) return false
1420
+ const entry = entries().find((e) => e.id === id)
1421
+ if (!entry) cancelDelete()
1422
+ else void deleteEntry(entry)
1423
+ return true
1424
+ }
1425
+
1426
+ const requestDelete = (entry: Entry) => {
1427
+ if (deleting()) return
1428
+ if (pendingDelete() === entry.id) {
1429
+ void deleteEntry(entry)
1430
+ return
1431
+ }
1432
+ setPendingDelete(entry.id)
1433
+ if (confirmTimer) clearTimeout(confirmTimer)
1434
+ confirmTimer = setTimeout(() => setPendingDelete(undefined), 5000)
1435
+ }
1436
+
1437
+ const deleteEntry = async (entry: Entry) => {
1438
+ cancelDelete()
1439
+ if (deleting()) return
1440
+ setDeleting(entry.id)
1441
+ try {
1442
+ const result = await props.api.client.session.delete({
1443
+ sessionID: entry.id,
1444
+ directory: entry.dir || undefined,
1445
+ })
1446
+ if (result && "error" in result && result.error) throw result.error
1447
+ const { entries: remaining } = await fetchEntries(props.api)
1448
+ if (remaining.some((item) => item.id === entry.id)) {
1449
+ setEntries(remaining)
1450
+ props.api.ui.toast({ message: "Session was not deleted", variant: "error" })
1451
+ return
1452
+ }
1453
+ setEntries(remaining)
1454
+ setHovered(undefined)
1455
+ props.api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
1456
+ if ("params" in props.api.route.current && (props.api.route.current.params as any)?.sessionID === entry.id) {
1457
+ props.api.route.navigate("home")
1458
+ }
1459
+ } catch {
1460
+ props.api.ui.toast({ message: "Could not delete session", variant: "error" })
1461
+ } finally {
1462
+ setDeleting(undefined)
1463
+ }
1464
+ }
1465
+
1466
+ createEffect(() => {
1467
+ disposeConfirm?.()
1468
+ disposeConfirm = undefined
1469
+ if (!pendingDelete()) return
1470
+ disposeConfirm = props.api.keymap.registerLayer({
1471
+ mode: "base",
1472
+ priority: 25,
1473
+ bindings: [
1474
+ { key: "y", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
1475
+ { key: "ctrl+x", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
1476
+ { key: "enter", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
1477
+ { key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
1478
+ { key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
1479
+ ],
1480
+ })
1481
+ })
1482
+
1345
1483
  const onRootMouseDown = () => {
1346
1484
  if (insideSearchBox) {
1347
1485
  insideSearchBox = false
@@ -1389,14 +1527,26 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1389
1527
  preventDefault: true,
1390
1528
  cmd: () => preview.open(entry),
1391
1529
  },
1530
+ {
1531
+ key: "ctrl+x",
1532
+ desc: "Delete session",
1533
+ preventDefault: true,
1534
+ cmd: () => {
1535
+ if (!confirmArmed()) requestDelete(entry)
1536
+ },
1537
+ },
1392
1538
  { key: "ctrl+s", desc: "Pin session", preventDefault: true, cmd: () => props.onTogglePin("sessions", entry.id) },
1393
1539
  { key: "ctrl+d", desc: "Pin directory", preventDefault: true, cmd: () => props.onTogglePin("directories", entry.dir) },
1394
1540
  ],
1395
1541
  })
1396
1542
  })
1397
1543
 
1398
- onCleanup(() => disposeSearch?.())
1399
- onCleanup(() => disposeHoverSpace?.())
1544
+ onCleanup(() => {
1545
+ disposeSearch?.()
1546
+ disposeHoverSpace?.()
1547
+ disposeConfirm?.()
1548
+ if (confirmTimer) clearTimeout(confirmTimer)
1549
+ })
1400
1550
 
1401
1551
  onMount(() => {
1402
1552
  let alive = true
@@ -1486,7 +1636,11 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1486
1636
  backgroundColor={entry.id === hovered() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
1487
1637
  onMouseOver={() => setHovered(entry.id)}
1488
1638
  onMouseOut={() => setHovered(undefined)}
1489
- onMouseDown={() => props.api.route.navigate("session", { sessionID: entry.id })}
1639
+ onMouseDown={(event: { button?: number }) =>
1640
+ event.button === RIGHT_BUTTON
1641
+ ? openInCmuxWorkspace(props.api, entry)
1642
+ : props.api.route.navigate("session", { sessionID: entry.id })
1643
+ }
1490
1644
  >
1491
1645
  <text flexShrink={0} style={{ fg: theme().textMuted }}>
1492
1646
  ○
@@ -1497,8 +1651,10 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1497
1651
  color={theme().text}
1498
1652
  matchColor={theme().warning}
1499
1653
  />
1500
- <text flexShrink={0} style={{ fg: theme().textMuted }}>
1501
- {props.pins().directories.includes(entry.dir) ? "★ " : ""}{prettyDir(entry.dir, home)} · {ago(entry.updated)}
1654
+ <text flexShrink={0} style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}>
1655
+ {pendingDelete() === entry.id
1656
+ ? "ctrl+x again"
1657
+ : `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
1502
1658
  </text>
1503
1659
  </box>
1504
1660
  )}
@@ -1789,12 +1945,22 @@ const tui: TuiPlugin = async (api) => {
1789
1945
  // Delete confirmation lives in its own layer, registered only while a row
1790
1946
  // is armed, so y/n never shadow the search box's typing.
1791
1947
  let disposeConfirm: (() => void) | undefined
1948
+ let confirmTimer: ReturnType<typeof setTimeout> | undefined
1792
1949
  const cancelDelete = () => {
1950
+ if (confirmTimer) clearTimeout(confirmTimer)
1951
+ confirmTimer = undefined
1793
1952
  disposeConfirm?.()
1794
1953
  disposeConfirm = undefined
1795
1954
  setPendingDelete(undefined)
1796
1955
  }
1797
1956
 
1957
+ const confirmArmed = (): boolean => {
1958
+ const armed = pendingDelete()
1959
+ if (!armed) return false
1960
+ void deleteEntry(armed)
1961
+ return true
1962
+ }
1963
+
1798
1964
  // Escape first closes the transcript overlay, then the picker. One Escape
1799
1965
  // can reach both the focused search input and the key layer (sometimes in
1800
1966
  // separate ticks), so ignore the duplicate that trails right behind.
@@ -1807,6 +1973,11 @@ const tui: TuiPlugin = async (api) => {
1807
1973
  escapeHandledAt = now + 150
1808
1974
  return
1809
1975
  }
1976
+ if (pendingDelete()) {
1977
+ cancelDelete()
1978
+ escapeHandledAt = now + 150
1979
+ return
1980
+ }
1810
1981
  cancelDelete()
1811
1982
  close()
1812
1983
  }
@@ -1827,6 +1998,7 @@ const tui: TuiPlugin = async (api) => {
1827
1998
  }
1828
1999
  setAllEntries(remaining)
1829
2000
  api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
2001
+ if (currentSessionID() === entry.id) api.route.navigate("home")
1830
2002
  } catch {
1831
2003
  api.ui.toast({ message: "Could not delete session", variant: "error" })
1832
2004
  }
@@ -1834,19 +2006,38 @@ const tui: TuiPlugin = async (api) => {
1834
2006
 
1835
2007
  const armDelete = (entry: Entry) => {
1836
2008
  setPendingDelete(entry)
2009
+ if (confirmTimer) clearTimeout(confirmTimer)
2010
+ confirmTimer = setTimeout(cancelDelete, 5000)
1837
2011
  disposeConfirm?.()
1838
2012
  disposeConfirm = api.keymap.registerLayer({
2013
+ priority: 20,
1839
2014
  bindings: [
1840
2015
  {
1841
2016
  key: "y",
1842
2017
  desc: "Confirm delete",
1843
2018
  preventDefault: true,
1844
2019
  cmd: () => {
1845
- const armed = pendingDelete()
1846
- if (armed) void deleteEntry(armed)
2020
+ confirmArmed()
2021
+ },
2022
+ },
2023
+ {
2024
+ key: "ctrl+x",
2025
+ desc: "Confirm delete",
2026
+ preventDefault: true,
2027
+ cmd: () => {
2028
+ confirmArmed()
2029
+ },
2030
+ },
2031
+ {
2032
+ key: "enter",
2033
+ desc: "Confirm delete",
2034
+ preventDefault: true,
2035
+ cmd: () => {
2036
+ confirmArmed()
1847
2037
  },
1848
2038
  },
1849
2039
  { key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
2040
+ { key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
1850
2041
  ],
1851
2042
  })
1852
2043
  }
@@ -1892,7 +2083,15 @@ const tui: TuiPlugin = async (api) => {
1892
2083
  preventDefault: true,
1893
2084
  cmd: () => selectCursor(selectableEntries().length - 1),
1894
2085
  },
1895
- { key: "enter", desc: "Open session", preventDefault: true, cmd: () => choose() },
2086
+ {
2087
+ key: "enter",
2088
+ desc: "Open session",
2089
+ preventDefault: true,
2090
+ cmd: () => {
2091
+ if (confirmArmed()) return
2092
+ choose()
2093
+ },
2094
+ },
1896
2095
  { key: "option+p", desc: "Toggle transcript preview", preventDefault: true, cmd: togglePreview },
1897
2096
  { key: "ctrl+s", desc: "Pin session", preventDefault: true, cmd: () => {
1898
2097
  const entry = selectableEntries()[cursor()]
@@ -1907,11 +2106,9 @@ const tui: TuiPlugin = async (api) => {
1907
2106
  desc: "Delete session",
1908
2107
  preventDefault: true,
1909
2108
  cmd: () => {
2109
+ if (confirmArmed()) return
1910
2110
  const target = selectableEntries()[cursor()]
1911
- if (!target) return
1912
- const armed = pendingDelete()
1913
- if (armed && armed.id === target.id) void deleteEntry(target)
1914
- else armDelete(target)
2111
+ if (target) armDelete(target)
1915
2112
  },
1916
2113
  },
1917
2114
  {
@@ -1946,6 +2143,7 @@ const tui: TuiPlugin = async (api) => {
1946
2143
 
1947
2144
  const cleanup = () => {
1948
2145
  alive = false
2146
+ if (confirmTimer) clearTimeout(confirmTimer)
1949
2147
  pickerState = { query: query(), scope: scope(), waitingOnly: waitingOnly(), pinnedOnly: pinnedOnly() }
1950
2148
  disposeNav()
1951
2149
  disposeConfirm?.()
@@ -1981,12 +2179,31 @@ const tui: TuiPlugin = async (api) => {
1981
2179
  focusedBackgroundColor={api.theme.current.backgroundPanel}
1982
2180
  focusedTextColor={api.theme.current.text}
1983
2181
  cursorColor={api.theme.current.primary}
1984
- onInput={setQuery}
1985
- onSubmit={() => choose()}
2182
+ onInput={(value) => {
2183
+ if (pendingDelete()) return
2184
+ setQuery(value)
2185
+ }}
2186
+ onSubmit={() => {
2187
+ if (confirmArmed()) return
2188
+ choose()
2189
+ }}
1986
2190
  onKeyDown={(event) => {
1987
2191
  if (event.name === "escape") {
1988
2192
  event.preventDefault()
1989
2193
  handleEscape()
2194
+ return
2195
+ }
2196
+ if (pendingDelete()) {
2197
+ if (event.name === "y" || (event.ctrl && event.name === "x") || event.name === "enter" || event.name === "return") {
2198
+ event.preventDefault()
2199
+ confirmArmed()
2200
+ return
2201
+ }
2202
+ if (event.name === "n") {
2203
+ event.preventDefault()
2204
+ cancelDelete()
2205
+ return
2206
+ }
1990
2207
  }
1991
2208
  }}
1992
2209
  />
@@ -2048,7 +2265,9 @@ const tui: TuiPlugin = async (api) => {
2048
2265
  const index = selectableEntries().findIndex((entry) => entry.id === row.entry.id)
2049
2266
  if (index >= 0) setCursor(index)
2050
2267
  }}
2051
- onMouseDown={() => choose(row.entry)}
2268
+ onMouseDown={(event: { button?: number }) =>
2269
+ event.button === RIGHT_BUTTON ? (cleanup(), api.ui.dialog.clear(), openInCmuxWorkspace(api, row.entry)) : choose(row.entry)
2270
+ }
2052
2271
  >
2053
2272
  <box flexDirection="row" gap={1}>
2054
2273
  <text flexShrink={0} style={{ fg: row.entry.id === selectableEntries()[cursor()]?.id