opencode-sesh 0.9.1 → 0.10.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/CHANGELOG.md +10 -1
- package/README.md +2 -0
- package/package.json +5 -4
- package/tui/sesh-panel.tsx +89 -27
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-09-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The TUI session picker is taller (about three quarters of the screen) and
|
|
15
|
+
stays vertically centred; hovering a row moves the selection.
|
|
16
|
+
- `ctrl+p` opens the transcript preview as a centred overlay on top of the
|
|
17
|
+
picker that follows the hovered session and closes with Esc.
|
|
10
18
|
## [0.9.1] - 2026-09-25
|
|
11
19
|
|
|
12
20
|
### Changed
|
|
@@ -352,7 +360,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
352
360
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
353
361
|
CI.
|
|
354
362
|
|
|
355
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.
|
|
363
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.10.0...HEAD
|
|
364
|
+
[0.10.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.1...v0.10.0
|
|
356
365
|
[0.9.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.9.0...v0.9.1
|
|
357
366
|
[0.9.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.8.0...v0.9.0
|
|
358
367
|
[0.8.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.3...v0.8.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# sesh
|
|
2
2
|
|
|
3
|
+
[](https://github.com/ozandogrultan/opencode-sesh/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
**A session browser for [opencode](https://opencode.ai).** Search every session
|
|
4
6
|
across every project, preview the transcript, and resume in place — from your
|
|
5
7
|
terminal or from inside the TUI.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.10.0",
|
|
5
4
|
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Ozan Dogrultan",
|
|
8
8
|
"repository": {
|
|
@@ -44,12 +44,13 @@
|
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"prepare": "husky",
|
|
47
|
-
"postinstall": "bash install.sh --sync-panel || true",
|
|
48
47
|
"test": "bash tests/sesh.sh && bash tests/changelog.sh",
|
|
48
|
+
"test:unit": "bash tests/sesh.sh",
|
|
49
49
|
"test:changelog": "bash tests/changelog.sh",
|
|
50
50
|
"test:picker": "python3 tests/picker.py",
|
|
51
|
-
"typecheck": "tsc
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
52
|
"lint:sh": "for script in bin/*.sh bin/sesh install.sh tests/*.sh scripts/*.sh; do bash -n \"$script\" || exit; done",
|
|
53
|
+
"postinstall": "bash install.sh --sync-panel || true",
|
|
53
54
|
"prepublishOnly": "bun run test && bun run typecheck"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
package/tui/sesh-panel.tsx
CHANGED
|
@@ -1646,13 +1646,23 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1646
1646
|
})
|
|
1647
1647
|
|
|
1648
1648
|
const termHeight = api.renderer.height
|
|
1649
|
-
|
|
1649
|
+
// The dialog host anchors content a quarter of the way down the screen and
|
|
1650
|
+
// lets it size to content, so a taller picker drifts downward. Pin the
|
|
1651
|
+
// content height and offset it up by the same amount to keep it centred.
|
|
1652
|
+
const dialogWidth = Math.min(116, api.renderer.width - 2)
|
|
1653
|
+
const CHROME_ROWS = 12
|
|
1654
|
+
const listHeight = () => Math.max(6, Math.floor(termHeight * 0.75) - CHROME_ROWS)
|
|
1655
|
+
const dialogHeight = () => listHeight() + CHROME_ROWS
|
|
1656
|
+
const dialogOffset = () => Math.min(0, Math.floor(termHeight / 4 - dialogHeight() / 2 - 1))
|
|
1657
|
+
const previewWidth = () => Math.max(40, Math.min(94, dialogWidth - 6))
|
|
1658
|
+
const previewHeight = () => Math.max(8, Math.floor(termHeight / 2))
|
|
1650
1659
|
|
|
1651
1660
|
const visiblePickerRows = createMemo<DialogRow[]>(() => {
|
|
1652
1661
|
const flat = pickerRows()
|
|
1653
1662
|
// A transcript hit adds a second line to a row; bound by screen height,
|
|
1654
1663
|
// not just the number of session IDs.
|
|
1655
|
-
const
|
|
1664
|
+
const height = listHeight()
|
|
1665
|
+
const windowSize = query().trim() ? Math.max(2, Math.floor(height / 3)) : height
|
|
1656
1666
|
const currentID = selectableEntries()[cursor()]?.id
|
|
1657
1667
|
let pos = currentID
|
|
1658
1668
|
? flat.findIndex((row) => row.kind === "item" && row.entry.id === currentID)
|
|
@@ -1727,6 +1737,22 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1727
1737
|
setPendingDelete(undefined)
|
|
1728
1738
|
}
|
|
1729
1739
|
|
|
1740
|
+
// Escape first closes the transcript overlay, then the picker. One Escape
|
|
1741
|
+
// can reach both the focused search input and the key layer (sometimes in
|
|
1742
|
+
// separate ticks), so ignore the duplicate that trails right behind.
|
|
1743
|
+
let escapeHandledAt = 0
|
|
1744
|
+
const handleEscape = () => {
|
|
1745
|
+
const now = Date.now()
|
|
1746
|
+
if (escapeHandledAt > now) return
|
|
1747
|
+
if (showPreview()) {
|
|
1748
|
+
setShowPreview(false)
|
|
1749
|
+
escapeHandledAt = now + 150
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
cancelDelete()
|
|
1753
|
+
close()
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1730
1756
|
const deleteEntry = async (entry: Entry) => {
|
|
1731
1757
|
cancelDelete()
|
|
1732
1758
|
try {
|
|
@@ -1793,6 +1819,9 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1793
1819
|
}
|
|
1794
1820
|
|
|
1795
1821
|
const disposeNav = api.keymap.registerLayer({
|
|
1822
|
+
// Outrank the built-in dialog layer so Escape reaches this picker before
|
|
1823
|
+
// the dialog host closes the whole thing.
|
|
1824
|
+
priority: 1,
|
|
1796
1825
|
bindings: [
|
|
1797
1826
|
{ key: "up", desc: "Previous session", preventDefault: true, cmd: () => moveCursor(-1) },
|
|
1798
1827
|
{ key: "down", desc: "Next session", preventDefault: true, cmd: () => moveCursor(1) },
|
|
@@ -1852,10 +1881,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1852
1881
|
key: "escape",
|
|
1853
1882
|
desc: "Close",
|
|
1854
1883
|
preventDefault: true,
|
|
1855
|
-
cmd:
|
|
1856
|
-
cancelDelete()
|
|
1857
|
-
close()
|
|
1858
|
-
},
|
|
1884
|
+
cmd: handleEscape,
|
|
1859
1885
|
},
|
|
1860
1886
|
],
|
|
1861
1887
|
})
|
|
@@ -1871,7 +1897,14 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1871
1897
|
|
|
1872
1898
|
api.ui.dialog.replace(
|
|
1873
1899
|
() => (
|
|
1874
|
-
<box
|
|
1900
|
+
<box
|
|
1901
|
+
flexDirection="column"
|
|
1902
|
+
height={dialogHeight()}
|
|
1903
|
+
marginTop={dialogOffset()}
|
|
1904
|
+
paddingBottom={1}
|
|
1905
|
+
gap={1}
|
|
1906
|
+
backgroundColor={api.theme.current.backgroundPanel}
|
|
1907
|
+
>
|
|
1875
1908
|
<box paddingLeft={4} paddingRight={4}>
|
|
1876
1909
|
<box flexDirection="row" justifyContent="space-between">
|
|
1877
1910
|
<text attributes={TextAttributes.BOLD}>
|
|
@@ -1895,7 +1928,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1895
1928
|
onKeyDown={(event) => {
|
|
1896
1929
|
if (event.name === "escape") {
|
|
1897
1930
|
event.preventDefault()
|
|
1898
|
-
|
|
1931
|
+
handleEscape()
|
|
1899
1932
|
}
|
|
1900
1933
|
}}
|
|
1901
1934
|
/>
|
|
@@ -1923,6 +1956,8 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1923
1956
|
</box>
|
|
1924
1957
|
<box
|
|
1925
1958
|
flexDirection="column"
|
|
1959
|
+
flexGrow={1}
|
|
1960
|
+
overflow="hidden"
|
|
1926
1961
|
onMouseScroll={(event: { scroll?: { direction: string } }) => {
|
|
1927
1962
|
const direction = event.scroll?.direction
|
|
1928
1963
|
if (direction === "down") moveCursor(3)
|
|
@@ -1932,7 +1967,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1932
1967
|
<For each={visiblePickerRows()}>
|
|
1933
1968
|
{(row) => {
|
|
1934
1969
|
if (row.kind === "group") return (
|
|
1935
|
-
<box
|
|
1970
|
+
<box paddingLeft={4}>
|
|
1936
1971
|
<text style={{ fg: api.theme.current.accent }} attributes={TextAttributes.BOLD}>
|
|
1937
1972
|
{matchedGroups().find((group) => group.name === row.label)?.list.some((entry) => pins().directories.includes(entry.dir)) ? "★ " : ""}{row.label} <span style={{ fg: api.theme.current.textMuted }}>({row.count})</span>
|
|
1938
1973
|
</text>
|
|
@@ -1949,6 +1984,12 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1949
1984
|
? api.theme.current.primary
|
|
1950
1985
|
: RGBA.fromInts(0, 0, 0, 0)
|
|
1951
1986
|
}
|
|
1987
|
+
onMouseOver={() => {
|
|
1988
|
+
// Hovering moves the cursor so the highlight, Ctrl-S/Ctrl-D
|
|
1989
|
+
// and Enter all resolve to the row under the pointer.
|
|
1990
|
+
const index = selectableEntries().findIndex((entry) => entry.id === row.entry.id)
|
|
1991
|
+
if (index >= 0) setCursor(index)
|
|
1992
|
+
}}
|
|
1952
1993
|
onMouseDown={() => choose(row.entry)}
|
|
1953
1994
|
>
|
|
1954
1995
|
<box flexDirection="row" gap={1}>
|
|
@@ -2038,11 +2079,46 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2038
2079
|
</box>
|
|
2039
2080
|
</Show>
|
|
2040
2081
|
</box>
|
|
2082
|
+
<box paddingLeft={4} paddingRight={4} flexShrink={0} flexDirection="column">
|
|
2083
|
+
<text
|
|
2084
|
+
style={{
|
|
2085
|
+
fg: pendingDelete() ? api.theme.current.warning : api.theme.current.textMuted,
|
|
2086
|
+
}}
|
|
2087
|
+
>
|
|
2088
|
+
{pendingDelete()
|
|
2089
|
+
? `Delete "${truncate(pendingDelete()!.title, 40)}"? y confirm · n cancel`
|
|
2090
|
+
: "↑↓ move · enter open · ctrl+g project · option+w needs · option+p pinned · ctrl+p preview"}
|
|
2091
|
+
</text>
|
|
2092
|
+
<Show when={!pendingDelete()}>
|
|
2093
|
+
<text style={{ fg: api.theme.current.textMuted }}>ctrl+s/d pin · ctrl+x delete · ctrl+f fork · esc close</text>
|
|
2094
|
+
</Show>
|
|
2095
|
+
</box>
|
|
2041
2096
|
<Show when={showPreview()}>
|
|
2042
|
-
<box
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2097
|
+
<box
|
|
2098
|
+
position="absolute"
|
|
2099
|
+
left={Math.floor((dialogWidth - previewWidth()) / 2)}
|
|
2100
|
+
top={Math.max(0, Math.floor((dialogHeight() - previewHeight()) / 2))}
|
|
2101
|
+
width={previewWidth()}
|
|
2102
|
+
height={previewHeight()}
|
|
2103
|
+
flexDirection="column"
|
|
2104
|
+
backgroundColor={api.theme.current.backgroundElement}
|
|
2105
|
+
paddingLeft={2}
|
|
2106
|
+
paddingRight={2}
|
|
2107
|
+
paddingTop={1}
|
|
2108
|
+
paddingBottom={1}
|
|
2109
|
+
gap={1}
|
|
2110
|
+
>
|
|
2111
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
2112
|
+
<text attributes={TextAttributes.BOLD}>
|
|
2113
|
+
{previewID() ? truncate(selectableEntries()[cursor()]!.title, previewWidth() - 8) : "Preview"}
|
|
2114
|
+
</text>
|
|
2115
|
+
<text style={{ fg: api.theme.current.textMuted }}>esc close</text>
|
|
2116
|
+
</box>
|
|
2117
|
+
<Show when={previewID()}>
|
|
2118
|
+
<text style={{ fg: api.theme.current.textMuted }}>
|
|
2119
|
+
{prettyDir(selectableEntries()[cursor()]!.dir, process.env.HOME ?? "")}
|
|
2120
|
+
</text>
|
|
2121
|
+
</Show>
|
|
2046
2122
|
<scrollbox flexGrow={1} scrollbarOptions={{ visible: false }}>
|
|
2047
2123
|
<Show when={previewID()} fallback={<text style={{ fg: api.theme.current.textMuted }}>Select a session to preview</text>}>
|
|
2048
2124
|
<Show when={previewText()} fallback={<text style={{ fg: api.theme.current.textMuted }}>Loading…</text>}>
|
|
@@ -2062,20 +2138,6 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2062
2138
|
</scrollbox>
|
|
2063
2139
|
</box>
|
|
2064
2140
|
</Show>
|
|
2065
|
-
<box paddingLeft={4} paddingRight={4} flexShrink={0} flexDirection="column">
|
|
2066
|
-
<text
|
|
2067
|
-
style={{
|
|
2068
|
-
fg: pendingDelete() ? api.theme.current.warning : api.theme.current.textMuted,
|
|
2069
|
-
}}
|
|
2070
|
-
>
|
|
2071
|
-
{pendingDelete()
|
|
2072
|
-
? `Delete "${truncate(pendingDelete()!.title, 40)}"? y confirm · n cancel`
|
|
2073
|
-
: "↑↓ move · enter open · ctrl+g project · option+w needs · option+p pinned · ctrl+p preview"}
|
|
2074
|
-
</text>
|
|
2075
|
-
<Show when={!pendingDelete()}>
|
|
2076
|
-
<text style={{ fg: api.theme.current.textMuted }}>ctrl+s/d pin · ctrl+x delete · ctrl+f fork · esc close</text>
|
|
2077
|
-
</Show>
|
|
2078
|
-
</box>
|
|
2079
2141
|
</box>
|
|
2080
2142
|
),
|
|
2081
2143
|
() => {
|