opencode-sesh 2.1.2 → 2.1.4
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 +12 -1
- package/CHANGELOG.md +14 -0
- package/README.md +4 -0
- package/dist/tui.js +562 -462
- package/package.json +9 -8
- package/tui-plugins/sesh-panel/tui.tsx +87 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "OpenCode session browser: search, preview, and resume every session across every project from the terminal or TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"build": "node scripts/build-tui.mjs",
|
|
58
58
|
"prepack": "node scripts/build-tui.mjs",
|
|
59
59
|
"test": "bun run test:unit && bash tests/changelog.sh",
|
|
60
|
-
"test:unit": "node scripts/build-tui.mjs && bash tests/sesh.sh && bash tests/install.sh",
|
|
60
|
+
"test:unit": "node scripts/build-tui.mjs && bun run test:tui-home && bash tests/sesh.sh && bash tests/install.sh",
|
|
61
|
+
"test:tui-home": "bun --conditions=browser tests/tui-home.mjs",
|
|
61
62
|
"test:changelog": "bash tests/changelog.sh",
|
|
62
63
|
"test:picker": "python3 tests/picker.py",
|
|
63
64
|
"typecheck": "tsc --noEmit",
|
|
@@ -76,12 +77,12 @@
|
|
|
76
77
|
"@babel/preset-typescript": "7.29.7",
|
|
77
78
|
"@commitlint/cli": "^21.2.2",
|
|
78
79
|
"@commitlint/config-conventional": "^21.2.2",
|
|
79
|
-
"@opencode/client": "2.2.
|
|
80
|
-
"@opencode/plugin": "2.2.
|
|
81
|
-
"@opencode/theme": "2.2.
|
|
82
|
-
"@opentui/core": "^0.
|
|
83
|
-
"@opentui/keymap": "^0.
|
|
84
|
-
"@opentui/solid": "^0.
|
|
80
|
+
"@opencode/client": "2.2.5",
|
|
81
|
+
"@opencode/plugin": "2.2.5",
|
|
82
|
+
"@opencode/theme": "2.2.5",
|
|
83
|
+
"@opentui/core": "^0.8.0",
|
|
84
|
+
"@opentui/keymap": "^0.8.0",
|
|
85
|
+
"@opentui/solid": "^0.8.0",
|
|
85
86
|
"@types/node": "26.6.5",
|
|
86
87
|
"babel-preset-solid": "1.9.16",
|
|
87
88
|
"husky": "^9.1.7",
|
|
@@ -540,11 +540,17 @@ async function openTranscriptDb(): Promise<any | undefined> {
|
|
|
540
540
|
type SessionSnapshot = { entries: Entry[]; waiting: Map<string, string>; failed: boolean }
|
|
541
541
|
|
|
542
542
|
function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
|
|
543
|
-
|
|
543
|
+
// Listener -> whether it reads `snapshot.waiting`. The needs-input query is
|
|
544
|
+
// synchronous on the TUI thread, so it only runs while such a listener exists.
|
|
545
|
+
const listeners = new Map<(snapshot: SessionSnapshot) => void, boolean>()
|
|
544
546
|
let snapshot: SessionSnapshot = { entries: [], waiting: new Map(), failed: false }
|
|
545
547
|
let clock: ReturnType<typeof setInterval> | undefined
|
|
546
548
|
let inFlight: Promise<void> | undefined
|
|
549
|
+
// Whether `snapshot.waiting` came from a poll that ran the query; a poll
|
|
550
|
+
// without waiting listeners clears it so a later subscriber never sees a stale set.
|
|
551
|
+
let waitingLoaded = false
|
|
547
552
|
const active = () => listeners.size > 0
|
|
553
|
+
const wantsWaiting = () => [...listeners.values()].some(Boolean)
|
|
548
554
|
const load = () => {
|
|
549
555
|
if (inFlight || !active()) return
|
|
550
556
|
inFlight = (async () => {
|
|
@@ -563,25 +569,34 @@ function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
|
|
|
563
569
|
snapshot = { ...snapshot, failed: true }
|
|
564
570
|
}
|
|
565
571
|
if (!active()) return
|
|
566
|
-
const db = await openTranscriptDb()
|
|
572
|
+
const db = wantsWaiting() ? await openTranscriptDb() : undefined
|
|
567
573
|
if (db) {
|
|
568
574
|
try {
|
|
569
|
-
if (
|
|
575
|
+
if (wantsWaiting()) {
|
|
576
|
+
snapshot = { ...snapshot, waiting: await queryWaitingIds(db) }
|
|
577
|
+
waitingLoaded = true
|
|
578
|
+
}
|
|
570
579
|
} catch {
|
|
571
580
|
// Keep the previous waiting set when the local store is unavailable.
|
|
572
581
|
} finally {
|
|
573
582
|
db.close()
|
|
574
583
|
}
|
|
584
|
+
} else if (!wantsWaiting()) {
|
|
585
|
+
snapshot = { ...snapshot, waiting: new Map() }
|
|
586
|
+
waitingLoaded = false
|
|
575
587
|
}
|
|
576
|
-
for (const listener of listeners) listener(snapshot)
|
|
588
|
+
for (const listener of [...listeners.keys()]) listener(snapshot)
|
|
577
589
|
})().finally(() => { inFlight = undefined })
|
|
578
590
|
}
|
|
579
|
-
return (listener: (snapshot: SessionSnapshot) => void) => {
|
|
580
|
-
|
|
591
|
+
return (listener: (snapshot: SessionSnapshot) => void, options: { waiting?: boolean } = {}) => {
|
|
592
|
+
const needsWaiting = options.waiting === true
|
|
593
|
+
listeners.set(listener, needsWaiting)
|
|
581
594
|
listener(snapshot)
|
|
582
595
|
if (!clock) {
|
|
583
596
|
load()
|
|
584
597
|
clock = setInterval(load, POLL_MS)
|
|
598
|
+
} else if (needsWaiting && !waitingLoaded) {
|
|
599
|
+
load()
|
|
585
600
|
}
|
|
586
601
|
return () => {
|
|
587
602
|
listeners.delete(listener)
|
|
@@ -1098,7 +1113,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1098
1113
|
setEntries(snapshot.entries)
|
|
1099
1114
|
setLoadFailed(snapshot.failed)
|
|
1100
1115
|
setWaiting(snapshot.waiting)
|
|
1101
|
-
})))
|
|
1116
|
+
}, { waiting: true })))
|
|
1102
1117
|
|
|
1103
1118
|
const filteredEntries = createMemo(() => {
|
|
1104
1119
|
const q = query().trim().toLowerCase()
|
|
@@ -1819,10 +1834,25 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1819
1834
|
}
|
|
1820
1835
|
|
|
1821
1836
|
const HOME_SESSION_LIMIT = 6
|
|
1837
|
+
// Layout baseline: OpenCode 2.2.4. Re-check these assumptions and the matching
|
|
1838
|
+
// tests/tui-home.mjs fixture when upgrading OpenCode: the fixture mirrors the
|
|
1839
|
+
// host layout, so it cannot detect upstream layout changes on its own.
|
|
1840
|
+
// Sources pinned to that release:
|
|
1841
|
+
// https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/routes/home.tsx
|
|
1842
|
+
// https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/prompt/index.tsx#L1676
|
|
1843
|
+
// https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/logo.tsx
|
|
1844
|
+
// https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/feature-plugins/home/footer.tsx
|
|
1845
|
+
// The native home needs room for its logo, composer chrome and status footer.
|
|
1846
|
+
// Reserve the editor's *maximum* height (one third of the terminal, at least
|
|
1847
|
+
// six lines), so typing/wrapping a prompt cannot push it into our footer.
|
|
1848
|
+
// Chrome: logo (4) + gap (1) + composer (6) + status (3). Below 44 columns,
|
|
1849
|
+
// the logo stacks to seven lines but the three-line status footer disappears.
|
|
1850
|
+
const HOME_CHROME_ROWS = 14
|
|
1822
1851
|
|
|
1823
1852
|
function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
1824
1853
|
const theme = () => props.api.theme.current
|
|
1825
1854
|
const home = process.env.HOME ?? ""
|
|
1855
|
+
const [dimensions, setDimensions] = createSignal({ width: props.api.renderer.width, height: props.api.renderer.height })
|
|
1826
1856
|
const [entries, setEntries] = createSignal<Entry[]>([])
|
|
1827
1857
|
const [query, setQuery] = createSignal("")
|
|
1828
1858
|
const [hovered, setHovered] = createSignal<string>()
|
|
@@ -1930,7 +1960,12 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1930
1960
|
onMount(() => {
|
|
1931
1961
|
ensureRootMouse(props.api.renderer)
|
|
1932
1962
|
rootMouseHandlers.add(onRootMouseDown)
|
|
1933
|
-
|
|
1963
|
+
const resize = () => setDimensions({ width: props.api.renderer.width, height: props.api.renderer.height })
|
|
1964
|
+
props.api.renderer.on("resize", resize)
|
|
1965
|
+
onCleanup(() => {
|
|
1966
|
+
rootMouseHandlers.delete(onRootMouseDown)
|
|
1967
|
+
props.api.renderer.off("resize", resize)
|
|
1968
|
+
})
|
|
1934
1969
|
})
|
|
1935
1970
|
|
|
1936
1971
|
const appendQuery = (input: string) => {
|
|
@@ -1991,37 +2026,57 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1991
2026
|
setLoadFailed(snapshot.failed)
|
|
1992
2027
|
})))
|
|
1993
2028
|
|
|
1994
|
-
const
|
|
2029
|
+
const filtered = createMemo(() => {
|
|
1995
2030
|
const q = query().trim().toLowerCase()
|
|
1996
2031
|
const list = q
|
|
1997
2032
|
? entries().filter(
|
|
1998
2033
|
(entry) => entry.title.toLowerCase().includes(q) || entry.dir.toLowerCase().includes(q),
|
|
1999
2034
|
)
|
|
2000
2035
|
: entries()
|
|
2001
|
-
return pinnedFirst(list, props.pins())
|
|
2036
|
+
return pinnedFirst(list, props.pins())
|
|
2037
|
+
})
|
|
2038
|
+
const layout = createMemo(() => {
|
|
2039
|
+
const rows = dimensions().height
|
|
2040
|
+
const budget = Math.max(0, rows - Math.max(6, Math.floor(rows / 3)) - HOME_CHROME_ROWS)
|
|
2041
|
+
// Drop decorative padding/spacing before sessions. Keep the full list in
|
|
2042
|
+
// the picker when even a heading and one session cannot fit.
|
|
2043
|
+
const padding = budget >= HOME_SESSION_LIMIT + 2 ? 1 : 0
|
|
2044
|
+
const limit = Math.min(HOME_SESSION_LIMIT, Math.max(0, budget - padding - 1))
|
|
2045
|
+
const count = Math.max(1, Math.min(filtered().length, limit))
|
|
2046
|
+
return { budget, limit, padding, gap: budget >= 1 + padding + count * 2 ? 1 : 0 }
|
|
2047
|
+
})
|
|
2048
|
+
const visible = createMemo(() => filtered().slice(0, layout().limit))
|
|
2049
|
+
createEffect(() => {
|
|
2050
|
+
const ids = new Set(visible().map((entry) => entry.id))
|
|
2051
|
+
if (hovered() && !ids.has(hovered()!)) setHovered(undefined)
|
|
2052
|
+
if (pendingDelete() && !ids.has(pendingDelete()!)) cancelDelete()
|
|
2053
|
+
if (layout().limit === 0) setSearching(false)
|
|
2002
2054
|
})
|
|
2003
2055
|
|
|
2004
2056
|
return (
|
|
2005
|
-
<box width="100%" alignItems="center">
|
|
2006
|
-
<box flexDirection="column" width="100%" maxWidth={72} paddingLeft={1} paddingRight={1} paddingTop={
|
|
2057
|
+
<box visible={layout().limit > 0} width="100%" flexShrink={0} maxHeight={layout().budget} overflow="hidden" alignItems="center">
|
|
2058
|
+
<box flexDirection="column" width="100%" maxWidth={72} paddingLeft={1} paddingRight={1} paddingTop={layout().padding} gap={layout().gap}>
|
|
2007
2059
|
<Show
|
|
2008
2060
|
when={entries().length > 0}
|
|
2009
2061
|
fallback={
|
|
2010
|
-
<box
|
|
2011
|
-
<text style={{ fg: theme().textMuted }}>
|
|
2012
|
-
|
|
2013
|
-
{loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse"}
|
|
2062
|
+
<box height={1} flexShrink={0}>
|
|
2063
|
+
<text wrapMode="none" overflow="hidden" style={{ fg: theme().textMuted }}>
|
|
2064
|
+
Recent sessions {loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse"}
|
|
2014
2065
|
</text>
|
|
2015
2066
|
</box>
|
|
2016
2067
|
}
|
|
2017
2068
|
>
|
|
2018
|
-
<box flexDirection="row" justifyContent="space-between" gap={2}>
|
|
2019
|
-
<box flexDirection="row" gap={1}>
|
|
2020
|
-
<text style={{ fg: theme().textMuted }}>Recent sessions</text>
|
|
2021
|
-
<
|
|
2069
|
+
<box height={1} flexShrink={0} flexDirection="row" justifyContent="space-between" gap={2} overflow="hidden">
|
|
2070
|
+
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
2071
|
+
<text wrapMode="none" style={{ fg: theme().textMuted }}>Recent sessions</text>
|
|
2072
|
+
<Show when={dimensions().width >= 68}>
|
|
2073
|
+
<text style={{ fg: theme().textMuted }}>· option+o for all</text>
|
|
2074
|
+
</Show>
|
|
2022
2075
|
</box>
|
|
2023
2076
|
<box
|
|
2024
2077
|
flexDirection="row"
|
|
2078
|
+
flexShrink={1}
|
|
2079
|
+
minWidth={0}
|
|
2025
2080
|
gap={1}
|
|
2026
2081
|
paddingLeft={1}
|
|
2027
2082
|
paddingRight={1}
|
|
@@ -2031,7 +2086,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
2031
2086
|
setSearching(true)
|
|
2032
2087
|
}}
|
|
2033
2088
|
>
|
|
2034
|
-
<box width={24}>
|
|
2089
|
+
<box width={24} flexShrink={1} minWidth={0}>
|
|
2035
2090
|
<text
|
|
2036
2091
|
flexGrow={1}
|
|
2037
2092
|
flexShrink={1}
|
|
@@ -2053,6 +2108,9 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
2053
2108
|
<For each={visible()}>
|
|
2054
2109
|
{(entry) => (
|
|
2055
2110
|
<box
|
|
2111
|
+
height={1}
|
|
2112
|
+
flexShrink={0}
|
|
2113
|
+
overflow="hidden"
|
|
2056
2114
|
flexDirection="row"
|
|
2057
2115
|
gap={1}
|
|
2058
2116
|
backgroundColor={entry.id === hovered() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
|
|
@@ -2073,7 +2131,13 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
2073
2131
|
color={theme().text}
|
|
2074
2132
|
matchColor={theme().warning}
|
|
2075
2133
|
/>
|
|
2076
|
-
<text
|
|
2134
|
+
<text
|
|
2135
|
+
flexShrink={1}
|
|
2136
|
+
maxWidth={Math.max(0, Math.floor((Math.min(72, dimensions().width) - 4) / 2))}
|
|
2137
|
+
wrapMode="none"
|
|
2138
|
+
overflow="hidden"
|
|
2139
|
+
style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}
|
|
2140
|
+
>
|
|
2077
2141
|
{pendingDelete() === entry.id
|
|
2078
2142
|
? "option+d again"
|
|
2079
2143
|
: `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
|
|
@@ -2082,7 +2146,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
2082
2146
|
)}
|
|
2083
2147
|
</For>
|
|
2084
2148
|
<Show when={visible().length === 0}>
|
|
2085
|
-
<text style={{ fg: theme().textMuted }}>No title or directory matches · option+o to search transcripts</text>
|
|
2149
|
+
<text height={1} flexShrink={0} wrapMode="none" overflow="hidden" style={{ fg: theme().textMuted }}>No title or directory matches · option+o to search transcripts</text>
|
|
2086
2150
|
</Show>
|
|
2087
2151
|
</Show>
|
|
2088
2152
|
</box>
|