opencode-sesh 2.1.1 → 2.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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",
@@ -50,14 +50,15 @@
50
50
  "packageManager": "bun@1.4.2",
51
51
  "engines": {
52
52
  "node": ">=20",
53
- "opencode": ">=2.0.0"
53
+ "opencode": ">=2.2.0"
54
54
  },
55
55
  "scripts": {
56
56
  "prepare": "husky",
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",
@@ -66,9 +67,9 @@
66
67
  "prepublishOnly": "bun run test && bun run typecheck"
67
68
  },
68
69
  "peerDependencies": {
69
- "@opencode/plugin": ">=2.0.0",
70
- "@opentui/core": ">=0.5.8",
71
- "@opentui/solid": ">=0.5.8",
70
+ "@opencode/plugin": ">=2.2.0",
71
+ "@opentui/core": ">=0.7.0",
72
+ "@opentui/solid": ">=0.7.0",
72
73
  "solid-js": ">=1.9.0"
73
74
  },
74
75
  "devDependencies": {
@@ -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.0.29",
80
- "@opencode/plugin": "2.0.29",
81
- "@opencode/theme": "2.0.29",
82
- "@opentui/core": "^0.6.5",
83
- "@opentui/keymap": "^0.6.5",
84
- "@opentui/solid": "^0.6.5",
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",
@@ -1819,10 +1819,25 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1819
1819
  }
1820
1820
 
1821
1821
  const HOME_SESSION_LIMIT = 6
1822
+ // Layout baseline: OpenCode 2.2.4. Re-check these assumptions and the matching
1823
+ // tests/tui-home.mjs fixture when upgrading OpenCode: the fixture mirrors the
1824
+ // host layout, so it cannot detect upstream layout changes on its own.
1825
+ // Sources pinned to that release:
1826
+ // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/routes/home.tsx
1827
+ // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/prompt/index.tsx#L1676
1828
+ // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/logo.tsx
1829
+ // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/feature-plugins/home/footer.tsx
1830
+ // The native home needs room for its logo, composer chrome and status footer.
1831
+ // Reserve the editor's *maximum* height (one third of the terminal, at least
1832
+ // six lines), so typing/wrapping a prompt cannot push it into our footer.
1833
+ // Chrome: logo (4) + gap (1) + composer (6) + status (3). Below 44 columns,
1834
+ // the logo stacks to seven lines but the three-line status footer disappears.
1835
+ const HOME_CHROME_ROWS = 14
1822
1836
 
1823
1837
  function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1824
1838
  const theme = () => props.api.theme.current
1825
1839
  const home = process.env.HOME ?? ""
1840
+ const [dimensions, setDimensions] = createSignal({ width: props.api.renderer.width, height: props.api.renderer.height })
1826
1841
  const [entries, setEntries] = createSignal<Entry[]>([])
1827
1842
  const [query, setQuery] = createSignal("")
1828
1843
  const [hovered, setHovered] = createSignal<string>()
@@ -1930,7 +1945,12 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1930
1945
  onMount(() => {
1931
1946
  ensureRootMouse(props.api.renderer)
1932
1947
  rootMouseHandlers.add(onRootMouseDown)
1933
- onCleanup(() => rootMouseHandlers.delete(onRootMouseDown))
1948
+ const resize = () => setDimensions({ width: props.api.renderer.width, height: props.api.renderer.height })
1949
+ props.api.renderer.on("resize", resize)
1950
+ onCleanup(() => {
1951
+ rootMouseHandlers.delete(onRootMouseDown)
1952
+ props.api.renderer.off("resize", resize)
1953
+ })
1934
1954
  })
1935
1955
 
1936
1956
  const appendQuery = (input: string) => {
@@ -1991,37 +2011,57 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
1991
2011
  setLoadFailed(snapshot.failed)
1992
2012
  })))
1993
2013
 
1994
- const visible = createMemo(() => {
2014
+ const filtered = createMemo(() => {
1995
2015
  const q = query().trim().toLowerCase()
1996
2016
  const list = q
1997
2017
  ? entries().filter(
1998
2018
  (entry) => entry.title.toLowerCase().includes(q) || entry.dir.toLowerCase().includes(q),
1999
2019
  )
2000
2020
  : entries()
2001
- return pinnedFirst(list, props.pins()).slice(0, HOME_SESSION_LIMIT)
2021
+ return pinnedFirst(list, props.pins())
2022
+ })
2023
+ const layout = createMemo(() => {
2024
+ const rows = dimensions().height
2025
+ const budget = Math.max(0, rows - Math.max(6, Math.floor(rows / 3)) - HOME_CHROME_ROWS)
2026
+ // Drop decorative padding/spacing before sessions. Keep the full list in
2027
+ // the picker when even a heading and one session cannot fit.
2028
+ const padding = budget >= HOME_SESSION_LIMIT + 2 ? 1 : 0
2029
+ const limit = Math.min(HOME_SESSION_LIMIT, Math.max(0, budget - padding - 1))
2030
+ const count = Math.max(1, Math.min(filtered().length, limit))
2031
+ return { budget, limit, padding, gap: budget >= 1 + padding + count * 2 ? 1 : 0 }
2032
+ })
2033
+ const visible = createMemo(() => filtered().slice(0, layout().limit))
2034
+ createEffect(() => {
2035
+ const ids = new Set(visible().map((entry) => entry.id))
2036
+ if (hovered() && !ids.has(hovered()!)) setHovered(undefined)
2037
+ if (pendingDelete() && !ids.has(pendingDelete()!)) cancelDelete()
2038
+ if (layout().limit === 0) setSearching(false)
2002
2039
  })
2003
2040
 
2004
2041
  return (
2005
- <box width="100%" alignItems="center">
2006
- <box flexDirection="column" width="100%" maxWidth={72} paddingLeft={1} paddingRight={1} paddingTop={1} gap={1}>
2042
+ <box visible={layout().limit > 0} width="100%" flexShrink={0} maxHeight={layout().budget} overflow="hidden" alignItems="center">
2043
+ <box flexDirection="column" width="100%" maxWidth={72} paddingLeft={1} paddingRight={1} paddingTop={layout().padding} gap={layout().gap}>
2007
2044
  <Show
2008
2045
  when={entries().length > 0}
2009
2046
  fallback={
2010
- <box flexDirection="row" gap={1}>
2011
- <text style={{ fg: theme().textMuted }}>Recent sessions</text>
2012
- <text style={{ fg: theme().textMuted }}>
2013
- {loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse"}
2047
+ <box height={1} flexShrink={0}>
2048
+ <text wrapMode="none" overflow="hidden" style={{ fg: theme().textMuted }}>
2049
+ Recent sessions {loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse"}
2014
2050
  </text>
2015
2051
  </box>
2016
2052
  }
2017
2053
  >
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
- <text style={{ fg: theme().textMuted }}>· option+o for all</text>
2054
+ <box height={1} flexShrink={0} flexDirection="row" justifyContent="space-between" gap={2} overflow="hidden">
2055
+ <box flexDirection="row" gap={1} flexShrink={0}>
2056
+ <text wrapMode="none" style={{ fg: theme().textMuted }}>Recent sessions</text>
2057
+ <Show when={dimensions().width >= 68}>
2058
+ <text style={{ fg: theme().textMuted }}>· option+o for all</text>
2059
+ </Show>
2022
2060
  </box>
2023
2061
  <box
2024
2062
  flexDirection="row"
2063
+ flexShrink={1}
2064
+ minWidth={0}
2025
2065
  gap={1}
2026
2066
  paddingLeft={1}
2027
2067
  paddingRight={1}
@@ -2031,7 +2071,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2031
2071
  setSearching(true)
2032
2072
  }}
2033
2073
  >
2034
- <box width={24}>
2074
+ <box width={24} flexShrink={1} minWidth={0}>
2035
2075
  <text
2036
2076
  flexGrow={1}
2037
2077
  flexShrink={1}
@@ -2053,6 +2093,9 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2053
2093
  <For each={visible()}>
2054
2094
  {(entry) => (
2055
2095
  <box
2096
+ height={1}
2097
+ flexShrink={0}
2098
+ overflow="hidden"
2056
2099
  flexDirection="row"
2057
2100
  gap={1}
2058
2101
  backgroundColor={entry.id === hovered() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
@@ -2073,7 +2116,13 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2073
2116
  color={theme().text}
2074
2117
  matchColor={theme().warning}
2075
2118
  />
2076
- <text flexShrink={0} style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}>
2119
+ <text
2120
+ flexShrink={1}
2121
+ maxWidth={Math.max(0, Math.floor((Math.min(72, dimensions().width) - 4) / 2))}
2122
+ wrapMode="none"
2123
+ overflow="hidden"
2124
+ style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}
2125
+ >
2077
2126
  {pendingDelete() === entry.id
2078
2127
  ? "option+d again"
2079
2128
  : `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
@@ -2082,7 +2131,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2082
2131
  )}
2083
2132
  </For>
2084
2133
  <Show when={visible().length === 0}>
2085
- <text style={{ fg: theme().textMuted }}>No title or directory matches · option+o to search transcripts</text>
2134
+ <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
2135
  </Show>
2087
2136
  </Show>
2088
2137
  </box>