opencode-sesh 0.11.0 → 0.11.1
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/CHANGELOG.md +9 -1
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +207 -13
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.1] - 2026-09-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Confirming a session delete in the TUI picker no longer leaks `y` or `n` into
|
|
15
|
+
the search input; `Enter` and a second `Ctrl-X` commit the armed delete, and
|
|
16
|
+
session deletion is supported on the home screen.
|
|
10
17
|
## [0.11.0] - 2026-09-25
|
|
11
18
|
|
|
12
19
|
### Changed
|
|
@@ -371,7 +378,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
371
378
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
372
379
|
CI.
|
|
373
380
|
|
|
374
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.
|
|
381
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.1...HEAD
|
|
382
|
+
[0.11.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.0...v0.11.1
|
|
375
383
|
[0.11.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.10.0...v0.11.0
|
|
376
384
|
[0.10.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.1...v0.10.0
|
|
377
385
|
[0.9.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.0...v0.9.1
|
package/package.json
CHANGED
package/tui/sesh-panel.tsx
CHANGED
|
@@ -1013,8 +1013,47 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1013
1013
|
let disposeHoverSpace: (() => void) | undefined
|
|
1014
1014
|
let disposeSearch: (() => void) | undefined
|
|
1015
1015
|
let disposeNav: (() => void) | undefined
|
|
1016
|
+
let disposeConfirm: (() => void) | undefined
|
|
1016
1017
|
let insideSearchBox = false
|
|
1017
1018
|
|
|
1019
|
+
createEffect(() => {
|
|
1020
|
+
disposeConfirm?.()
|
|
1021
|
+
disposeConfirm = undefined
|
|
1022
|
+
if (!pendingDelete()) return
|
|
1023
|
+
disposeConfirm = props.api.keymap.registerLayer({
|
|
1024
|
+
mode: "base",
|
|
1025
|
+
priority: 25,
|
|
1026
|
+
bindings: [
|
|
1027
|
+
{
|
|
1028
|
+
key: "y",
|
|
1029
|
+
desc: "Confirm delete",
|
|
1030
|
+
preventDefault: true,
|
|
1031
|
+
cmd: () => {
|
|
1032
|
+
confirmArmed()
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
key: "ctrl+x",
|
|
1037
|
+
desc: "Confirm delete",
|
|
1038
|
+
preventDefault: true,
|
|
1039
|
+
cmd: () => {
|
|
1040
|
+
confirmArmed()
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
key: "enter",
|
|
1045
|
+
desc: "Confirm delete",
|
|
1046
|
+
preventDefault: true,
|
|
1047
|
+
cmd: () => {
|
|
1048
|
+
confirmArmed()
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
{ key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1052
|
+
{ key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1053
|
+
],
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1018
1057
|
const onRootMouseDown = () => {
|
|
1019
1058
|
if (insideSearchBox) {
|
|
1020
1059
|
insideSearchBox = false
|
|
@@ -1144,6 +1183,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1144
1183
|
desc: "Open session",
|
|
1145
1184
|
preventDefault: true,
|
|
1146
1185
|
cmd: () => {
|
|
1186
|
+
if (confirmArmed()) return
|
|
1147
1187
|
const entry = itemRows()[cursor()]
|
|
1148
1188
|
if (entry) void openSession(entry)
|
|
1149
1189
|
},
|
|
@@ -1178,6 +1218,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1178
1218
|
disposeHoverSpace?.()
|
|
1179
1219
|
disposeSearch?.()
|
|
1180
1220
|
disposeNav?.()
|
|
1221
|
+
disposeConfirm?.()
|
|
1181
1222
|
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1182
1223
|
})
|
|
1183
1224
|
|
|
@@ -1337,11 +1378,89 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1337
1378
|
const [hovered, setHovered] = createSignal<string>()
|
|
1338
1379
|
const [searching, setSearching] = createSignal(false)
|
|
1339
1380
|
const [loadFailed, setLoadFailed] = createSignal(false)
|
|
1381
|
+
const [pendingDelete, setPendingDelete] = createSignal<string>()
|
|
1382
|
+
const [deleting, setDeleting] = createSignal<string>()
|
|
1383
|
+
let confirmTimer: ReturnType<typeof setTimeout> | undefined
|
|
1384
|
+
let disposeConfirm: (() => void) | undefined
|
|
1340
1385
|
const preview = createTranscriptPreview(props.api)
|
|
1341
1386
|
let disposeHoverSpace: (() => void) | undefined
|
|
1342
1387
|
let disposeSearch: (() => void) | undefined
|
|
1343
1388
|
let insideSearchBox = false
|
|
1344
1389
|
|
|
1390
|
+
const cancelDelete = () => {
|
|
1391
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1392
|
+
confirmTimer = undefined
|
|
1393
|
+
disposeConfirm?.()
|
|
1394
|
+
disposeConfirm = undefined
|
|
1395
|
+
setPendingDelete(undefined)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const confirmArmed = (): boolean => {
|
|
1399
|
+
const id = pendingDelete()
|
|
1400
|
+
if (!id) return false
|
|
1401
|
+
const entry = entries().find((e) => e.id === id)
|
|
1402
|
+
if (!entry) cancelDelete()
|
|
1403
|
+
else void deleteEntry(entry)
|
|
1404
|
+
return true
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const requestDelete = (entry: Entry) => {
|
|
1408
|
+
if (deleting()) return
|
|
1409
|
+
if (pendingDelete() === entry.id) {
|
|
1410
|
+
void deleteEntry(entry)
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
setPendingDelete(entry.id)
|
|
1414
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1415
|
+
confirmTimer = setTimeout(() => setPendingDelete(undefined), 5000)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const deleteEntry = async (entry: Entry) => {
|
|
1419
|
+
cancelDelete()
|
|
1420
|
+
if (deleting()) return
|
|
1421
|
+
setDeleting(entry.id)
|
|
1422
|
+
try {
|
|
1423
|
+
const result = await props.api.client.session.delete({
|
|
1424
|
+
sessionID: entry.id,
|
|
1425
|
+
directory: entry.dir || undefined,
|
|
1426
|
+
})
|
|
1427
|
+
if (result && "error" in result && result.error) throw result.error
|
|
1428
|
+
const { entries: remaining } = await fetchEntries(props.api)
|
|
1429
|
+
if (remaining.some((item) => item.id === entry.id)) {
|
|
1430
|
+
setEntries(remaining)
|
|
1431
|
+
props.api.ui.toast({ message: "Session was not deleted", variant: "error" })
|
|
1432
|
+
return
|
|
1433
|
+
}
|
|
1434
|
+
setEntries(remaining)
|
|
1435
|
+
setHovered(undefined)
|
|
1436
|
+
props.api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
|
|
1437
|
+
if ("params" in props.api.route.current && (props.api.route.current.params as any)?.sessionID === entry.id) {
|
|
1438
|
+
props.api.route.navigate("home")
|
|
1439
|
+
}
|
|
1440
|
+
} catch {
|
|
1441
|
+
props.api.ui.toast({ message: "Could not delete session", variant: "error" })
|
|
1442
|
+
} finally {
|
|
1443
|
+
setDeleting(undefined)
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
createEffect(() => {
|
|
1448
|
+
disposeConfirm?.()
|
|
1449
|
+
disposeConfirm = undefined
|
|
1450
|
+
if (!pendingDelete()) return
|
|
1451
|
+
disposeConfirm = props.api.keymap.registerLayer({
|
|
1452
|
+
mode: "base",
|
|
1453
|
+
priority: 25,
|
|
1454
|
+
bindings: [
|
|
1455
|
+
{ key: "y", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1456
|
+
{ key: "ctrl+x", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1457
|
+
{ key: "enter", desc: "Confirm delete", preventDefault: true, cmd: () => { confirmArmed() } },
|
|
1458
|
+
{ key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1459
|
+
{ key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1460
|
+
],
|
|
1461
|
+
})
|
|
1462
|
+
})
|
|
1463
|
+
|
|
1345
1464
|
const onRootMouseDown = () => {
|
|
1346
1465
|
if (insideSearchBox) {
|
|
1347
1466
|
insideSearchBox = false
|
|
@@ -1389,14 +1508,26 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1389
1508
|
preventDefault: true,
|
|
1390
1509
|
cmd: () => preview.open(entry),
|
|
1391
1510
|
},
|
|
1511
|
+
{
|
|
1512
|
+
key: "ctrl+x",
|
|
1513
|
+
desc: "Delete session",
|
|
1514
|
+
preventDefault: true,
|
|
1515
|
+
cmd: () => {
|
|
1516
|
+
if (!confirmArmed()) requestDelete(entry)
|
|
1517
|
+
},
|
|
1518
|
+
},
|
|
1392
1519
|
{ key: "ctrl+s", desc: "Pin session", preventDefault: true, cmd: () => props.onTogglePin("sessions", entry.id) },
|
|
1393
1520
|
{ key: "ctrl+d", desc: "Pin directory", preventDefault: true, cmd: () => props.onTogglePin("directories", entry.dir) },
|
|
1394
1521
|
],
|
|
1395
1522
|
})
|
|
1396
1523
|
})
|
|
1397
1524
|
|
|
1398
|
-
onCleanup(() =>
|
|
1399
|
-
|
|
1525
|
+
onCleanup(() => {
|
|
1526
|
+
disposeSearch?.()
|
|
1527
|
+
disposeHoverSpace?.()
|
|
1528
|
+
disposeConfirm?.()
|
|
1529
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1530
|
+
})
|
|
1400
1531
|
|
|
1401
1532
|
onMount(() => {
|
|
1402
1533
|
let alive = true
|
|
@@ -1497,8 +1628,10 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1497
1628
|
color={theme().text}
|
|
1498
1629
|
matchColor={theme().warning}
|
|
1499
1630
|
/>
|
|
1500
|
-
<text flexShrink={0} style={{ fg: theme().textMuted }}>
|
|
1501
|
-
{
|
|
1631
|
+
<text flexShrink={0} style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}>
|
|
1632
|
+
{pendingDelete() === entry.id
|
|
1633
|
+
? "ctrl+x again"
|
|
1634
|
+
: `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
|
|
1502
1635
|
</text>
|
|
1503
1636
|
</box>
|
|
1504
1637
|
)}
|
|
@@ -1789,12 +1922,22 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1789
1922
|
// Delete confirmation lives in its own layer, registered only while a row
|
|
1790
1923
|
// is armed, so y/n never shadow the search box's typing.
|
|
1791
1924
|
let disposeConfirm: (() => void) | undefined
|
|
1925
|
+
let confirmTimer: ReturnType<typeof setTimeout> | undefined
|
|
1792
1926
|
const cancelDelete = () => {
|
|
1927
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1928
|
+
confirmTimer = undefined
|
|
1793
1929
|
disposeConfirm?.()
|
|
1794
1930
|
disposeConfirm = undefined
|
|
1795
1931
|
setPendingDelete(undefined)
|
|
1796
1932
|
}
|
|
1797
1933
|
|
|
1934
|
+
const confirmArmed = (): boolean => {
|
|
1935
|
+
const armed = pendingDelete()
|
|
1936
|
+
if (!armed) return false
|
|
1937
|
+
void deleteEntry(armed)
|
|
1938
|
+
return true
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1798
1941
|
// Escape first closes the transcript overlay, then the picker. One Escape
|
|
1799
1942
|
// can reach both the focused search input and the key layer (sometimes in
|
|
1800
1943
|
// separate ticks), so ignore the duplicate that trails right behind.
|
|
@@ -1807,6 +1950,11 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1807
1950
|
escapeHandledAt = now + 150
|
|
1808
1951
|
return
|
|
1809
1952
|
}
|
|
1953
|
+
if (pendingDelete()) {
|
|
1954
|
+
cancelDelete()
|
|
1955
|
+
escapeHandledAt = now + 150
|
|
1956
|
+
return
|
|
1957
|
+
}
|
|
1810
1958
|
cancelDelete()
|
|
1811
1959
|
close()
|
|
1812
1960
|
}
|
|
@@ -1827,6 +1975,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1827
1975
|
}
|
|
1828
1976
|
setAllEntries(remaining)
|
|
1829
1977
|
api.ui.toast({ message: `Deleted "${truncate(entry.title, 40)}"`, variant: "info" })
|
|
1978
|
+
if (currentSessionID() === entry.id) api.route.navigate("home")
|
|
1830
1979
|
} catch {
|
|
1831
1980
|
api.ui.toast({ message: "Could not delete session", variant: "error" })
|
|
1832
1981
|
}
|
|
@@ -1834,19 +1983,38 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1834
1983
|
|
|
1835
1984
|
const armDelete = (entry: Entry) => {
|
|
1836
1985
|
setPendingDelete(entry)
|
|
1986
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1987
|
+
confirmTimer = setTimeout(cancelDelete, 5000)
|
|
1837
1988
|
disposeConfirm?.()
|
|
1838
1989
|
disposeConfirm = api.keymap.registerLayer({
|
|
1990
|
+
priority: 20,
|
|
1839
1991
|
bindings: [
|
|
1840
1992
|
{
|
|
1841
1993
|
key: "y",
|
|
1842
1994
|
desc: "Confirm delete",
|
|
1843
1995
|
preventDefault: true,
|
|
1844
1996
|
cmd: () => {
|
|
1845
|
-
|
|
1846
|
-
|
|
1997
|
+
confirmArmed()
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
key: "ctrl+x",
|
|
2002
|
+
desc: "Confirm delete",
|
|
2003
|
+
preventDefault: true,
|
|
2004
|
+
cmd: () => {
|
|
2005
|
+
confirmArmed()
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
key: "enter",
|
|
2010
|
+
desc: "Confirm delete",
|
|
2011
|
+
preventDefault: true,
|
|
2012
|
+
cmd: () => {
|
|
2013
|
+
confirmArmed()
|
|
1847
2014
|
},
|
|
1848
2015
|
},
|
|
1849
2016
|
{ key: "n", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
2017
|
+
{ key: "escape", desc: "Cancel delete", preventDefault: true, cmd: cancelDelete },
|
|
1850
2018
|
],
|
|
1851
2019
|
})
|
|
1852
2020
|
}
|
|
@@ -1892,7 +2060,15 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1892
2060
|
preventDefault: true,
|
|
1893
2061
|
cmd: () => selectCursor(selectableEntries().length - 1),
|
|
1894
2062
|
},
|
|
1895
|
-
{
|
|
2063
|
+
{
|
|
2064
|
+
key: "enter",
|
|
2065
|
+
desc: "Open session",
|
|
2066
|
+
preventDefault: true,
|
|
2067
|
+
cmd: () => {
|
|
2068
|
+
if (confirmArmed()) return
|
|
2069
|
+
choose()
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
1896
2072
|
{ key: "option+p", desc: "Toggle transcript preview", preventDefault: true, cmd: togglePreview },
|
|
1897
2073
|
{ key: "ctrl+s", desc: "Pin session", preventDefault: true, cmd: () => {
|
|
1898
2074
|
const entry = selectableEntries()[cursor()]
|
|
@@ -1907,11 +2083,9 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1907
2083
|
desc: "Delete session",
|
|
1908
2084
|
preventDefault: true,
|
|
1909
2085
|
cmd: () => {
|
|
2086
|
+
if (confirmArmed()) return
|
|
1910
2087
|
const target = selectableEntries()[cursor()]
|
|
1911
|
-
if (
|
|
1912
|
-
const armed = pendingDelete()
|
|
1913
|
-
if (armed && armed.id === target.id) void deleteEntry(target)
|
|
1914
|
-
else armDelete(target)
|
|
2088
|
+
if (target) armDelete(target)
|
|
1915
2089
|
},
|
|
1916
2090
|
},
|
|
1917
2091
|
{
|
|
@@ -1946,6 +2120,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1946
2120
|
|
|
1947
2121
|
const cleanup = () => {
|
|
1948
2122
|
alive = false
|
|
2123
|
+
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1949
2124
|
pickerState = { query: query(), scope: scope(), waitingOnly: waitingOnly(), pinnedOnly: pinnedOnly() }
|
|
1950
2125
|
disposeNav()
|
|
1951
2126
|
disposeConfirm?.()
|
|
@@ -1981,12 +2156,31 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1981
2156
|
focusedBackgroundColor={api.theme.current.backgroundPanel}
|
|
1982
2157
|
focusedTextColor={api.theme.current.text}
|
|
1983
2158
|
cursorColor={api.theme.current.primary}
|
|
1984
|
-
onInput={
|
|
1985
|
-
|
|
2159
|
+
onInput={(value) => {
|
|
2160
|
+
if (pendingDelete()) return
|
|
2161
|
+
setQuery(value)
|
|
2162
|
+
}}
|
|
2163
|
+
onSubmit={() => {
|
|
2164
|
+
if (confirmArmed()) return
|
|
2165
|
+
choose()
|
|
2166
|
+
}}
|
|
1986
2167
|
onKeyDown={(event) => {
|
|
1987
2168
|
if (event.name === "escape") {
|
|
1988
2169
|
event.preventDefault()
|
|
1989
2170
|
handleEscape()
|
|
2171
|
+
return
|
|
2172
|
+
}
|
|
2173
|
+
if (pendingDelete()) {
|
|
2174
|
+
if (event.name === "y" || (event.ctrl && event.name === "x") || event.name === "enter" || event.name === "return") {
|
|
2175
|
+
event.preventDefault()
|
|
2176
|
+
confirmArmed()
|
|
2177
|
+
return
|
|
2178
|
+
}
|
|
2179
|
+
if (event.name === "n") {
|
|
2180
|
+
event.preventDefault()
|
|
2181
|
+
cancelDelete()
|
|
2182
|
+
return
|
|
2183
|
+
}
|
|
1990
2184
|
}
|
|
1991
2185
|
}}
|
|
1992
2186
|
/>
|