opencode-sesh 2.1.8 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.8",
3
+ "version": "2.3.0",
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",
@@ -217,7 +217,11 @@ function truncate(text: string, width: number): string {
217
217
  // so cap it a bit lower to guarantee separation.
218
218
  const SIDEBAR_TITLE_WIDTH = 27
219
219
  const SIDEBAR_GROUP_WIDTH = 30
220
- const HOME_TITLE_WIDTH = 30
220
+ const HOME_TITLE_MIN = 20
221
+ const HOME_DIR_MIN = 8
222
+ const HOME_HINT_MIN_WIDTH = 32
223
+ const HOME_DIR_MAX = 22
224
+ const HOME_AGE_WIDTH = 4
221
225
 
222
226
  // The sidebar lists every session (newest first, pins ahead) and fills
223
227
  // whatever vertical space it gets: overflow scrolls inside the stretched
@@ -2040,13 +2044,11 @@ const HOME_SESSION_LIMIT = 6
2040
2044
  // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/prompt/index.tsx#L1676
2041
2045
  // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/component/logo.tsx
2042
2046
  // https://github.com/anomalyco/opencode/blob/45898d0f8569225eb1ddb4ee13f8581b34dff311/packages/tui/src/feature-plugins/home/footer.tsx
2043
- // The native home needs room for its logo, composer chrome and status footer.
2044
- // Reserve the editor's *maximum* height (one third of the terminal, at least
2045
- // six lines), so typing/wrapping a prompt cannot push it into our footer.
2046
- // Chrome: logo (4) + gap (1) + composer (6) + status (3). Below 44 columns,
2047
- // the logo stacks to seven lines but the three-line status footer disappears.
2048
- const HOME_CHROME_ROWS = 14
2049
-
2047
+ // The list floats just under the composer instead of taking layout space, so
2048
+ // it budgets from its measured position: whatever lies between it and the
2049
+ // native status footer (three lines, hidden below 44 columns or 14 rows, plus
2050
+ // one line of slack for other footer plugins).
2051
+ const HOME_FOOTER_ROWS = 3
2050
2052
  function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2051
2053
  const theme = () => props.api.theme.current
2052
2054
  const home = process.env.HOME ?? ""
@@ -2233,12 +2235,38 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2233
2235
  : entries()
2234
2236
  return pinnedFirst(list, props.pins())
2235
2237
  })
2238
+ // Unmeasured (before the first frame) reads as no room.
2239
+ const [top, setTop] = createSignal(dimensions().height)
2240
+ const [listWidth, setListWidth] = createSignal(Math.min(75, dimensions().width))
2241
+ let anchor: { y: number; width: number } | undefined
2242
+ // Fixed columns: marker, title (flexes), directory (last segment), age.
2243
+ const columns = () => {
2244
+ const width = Math.max(1, listWidth())
2245
+ const age = HOME_AGE_WIDTH
2246
+ const wanted = Math.max(0, ...visible().map((entry) => rowDir(entry).length))
2247
+ const room = Math.min(HOME_DIR_MAX, wanted, width - 1 - age - 3 - HOME_TITLE_MIN)
2248
+ // A starved title is worse than no directory: drop the column instead.
2249
+ const dir = room >= HOME_DIR_MIN || room >= wanted ? Math.max(0, room) : 0
2250
+ // The armed-delete prompt may not push the age out of a narrow list.
2251
+ const pending = pendingDelete() ? Math.max(0, Math.min(Math.max(dir, 14), width - 1 - age - 3 - 1)) : dir
2252
+ return { age, dir: pending, title: Math.max(1, width - 1 - age - pending - (pending > 0 ? 3 : 2)) }
2253
+ }
2254
+ const rowDir = (entry: Entry) => `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${home && entry.dir === home ? "~" : shortDir(entry.dir, home)}`
2255
+ // Running state comes from the host's live session store: no polling here.
2256
+ const isRunning = (id: string): boolean => {
2257
+ try {
2258
+ return sidebarMarker(false, props.api.state.session.status(id)) === "running"
2259
+ } catch {
2260
+ return false
2261
+ }
2262
+ }
2236
2263
  const layout = createMemo(() => {
2237
2264
  const rows = dimensions().height
2238
- const budget = Math.max(0, rows - Math.max(6, Math.floor(rows / 3)) - HOME_CHROME_ROWS)
2265
+ const footer = dimensions().width >= 44 && rows >= 14 ? HOME_FOOTER_ROWS : 0
2266
+ const budget = Math.max(0, rows - footer - 1 - top())
2239
2267
  // Drop decorative padding/spacing before sessions. Keep the full list in
2240
2268
  // the picker when even a heading and one session cannot fit.
2241
- const padding = budget >= HOME_SESSION_LIMIT + 2 ? 1 : 0
2269
+ const padding = budget >= 4 ? 1 : 0
2242
2270
  const limit = Math.min(HOME_SESSION_LIMIT, Math.max(0, budget - padding - 1))
2243
2271
  const count = Math.max(1, Math.min(filtered().length, limit))
2244
2272
  return { budget, limit, padding, gap: budget >= 1 + padding + count * 2 ? 1 : 0 }
@@ -2252,8 +2280,22 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2252
2280
  })
2253
2281
 
2254
2282
  return (
2255
- <box visible={layout().limit > 0} width="100%" flexShrink={0} maxHeight={layout().budget} overflow="hidden" alignItems="center">
2256
- <box flexDirection="column" width="100%" maxWidth={72} paddingLeft={1} paddingRight={1} paddingTop={layout().padding} gap={layout().gap}>
2283
+ <box
2284
+ ref={(box: { y: number; width: number }) => { anchor = box }}
2285
+ renderAfter={() => {
2286
+ const y = anchor?.y
2287
+ if (y !== undefined && y !== top()) queueMicrotask(() => setTop(y))
2288
+ const width = anchor?.width
2289
+ if (width && width !== listWidth()) queueMicrotask(() => setListWidth(width))
2290
+ }}
2291
+ position="absolute"
2292
+ top={1}
2293
+ left={0}
2294
+ right={0}
2295
+ maxHeight={layout().budget}
2296
+ overflow="hidden"
2297
+ >
2298
+ <box visible={layout().limit > 0 && props.api.route.current.name === "home"} flexDirection="column" width="100%" paddingTop={layout().padding} gap={layout().gap}>
2257
2299
  <Show
2258
2300
  when={entries().length > 0}
2259
2301
  fallback={
@@ -2266,33 +2308,31 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2266
2308
  >
2267
2309
  <box height={1} flexShrink={0} flexDirection="row" justifyContent="space-between" gap={2} overflow="hidden">
2268
2310
  <box flexDirection="row" gap={1} flexShrink={0}>
2269
- <text wrapMode="none" style={{ fg: theme().textMuted }}>Recent sessions</text>
2270
- <Show when={dimensions().width >= 68}>
2271
- <text style={{ fg: theme().textMuted }}>· option+o for all</text>
2272
- </Show>
2311
+ <text wrapMode="none" attributes={TextAttributes.DIM} style={{ fg: theme().textMuted }}>Recent sessions</text>
2273
2312
  </box>
2313
+ <Show when={listWidth() >= HOME_HINT_MIN_WIDTH || query() || searching()}>
2274
2314
  <box
2275
2315
  flexDirection="row"
2276
2316
  flexShrink={1}
2277
2317
  minWidth={0}
2278
2318
  gap={1}
2279
2319
  paddingLeft={1}
2280
- paddingRight={1}
2281
2320
  backgroundColor={searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
2282
2321
  onMouseDown={(event: { stopPropagation: () => void }) => {
2283
2322
  event.stopPropagation()
2284
2323
  setSearching(true)
2285
2324
  }}
2286
2325
  >
2287
- <box width={24} flexShrink={1} minWidth={0}>
2326
+ <box width={query() || searching() ? 24 : 12} flexShrink={1} minWidth={0}>
2288
2327
  <text
2289
2328
  flexGrow={1}
2290
2329
  flexShrink={1}
2291
2330
  overflow="hidden"
2292
2331
  wrapMode="none"
2332
+ attributes={query() ? undefined : TextAttributes.DIM}
2293
2333
  style={{ fg: query() ? theme().text : theme().textMuted }}
2294
2334
  >
2295
- {query() || (searching() ? "" : "search…")}
2335
+ {query() || (searching() ? "" : "option+o all")}
2296
2336
  {searching() ? "▏" : ""}
2297
2337
  </text>
2298
2338
  </box>
@@ -2302,6 +2342,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2302
2342
  </text>
2303
2343
  </Show>
2304
2344
  </box>
2345
+ </Show>
2305
2346
  </box>
2306
2347
  <For each={visible()}>
2307
2348
  {(entry) => (
@@ -2320,25 +2361,31 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2320
2361
  : props.api.route.navigate("session", { sessionID: entry.id })
2321
2362
  }
2322
2363
  >
2323
- <text flexShrink={0} style={{ fg: theme().textMuted }}>
2324
- ○
2364
+ <text width={1} flexShrink={0} style={{ fg: isRunning(entry.id) ? theme().success : theme().textMuted }}>
2365
+ {isRunning(entry.id) ? "●" : "○"}
2325
2366
  </text>
2326
- <Highlighted
2327
- text={`${props.pins().sessions.includes(entry.id) ? "★ " : ""}${truncate(entry.title, HOME_TITLE_WIDTH - (props.pins().sessions.includes(entry.id) ? 2 : 0))}`}
2328
- query={query()}
2329
- color={theme().text}
2330
- matchColor={theme().warning}
2331
- />
2332
- <text
2333
- flexShrink={1}
2334
- maxWidth={Math.max(0, Math.floor((Math.min(72, dimensions().width) - 4) / 2))}
2335
- wrapMode="none"
2336
- overflow="hidden"
2337
- style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}
2338
- >
2339
- {pendingDelete() === entry.id
2340
- ? "option+d again"
2341
- : `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`}
2367
+ <box width={columns().title} flexShrink={0} overflow="hidden">
2368
+ <Highlighted
2369
+ text={truncate(`${props.pins().sessions.includes(entry.id) ? "★ " : ""}${entry.title}`, columns().title)}
2370
+ query={query()}
2371
+ color={theme().text}
2372
+ matchColor={theme().warning}
2373
+ />
2374
+ </box>
2375
+ <Show when={columns().dir > 0}>
2376
+ <text
2377
+ width={columns().dir}
2378
+ flexShrink={0}
2379
+ wrapMode="none"
2380
+ overflow="hidden"
2381
+ attributes={pendingDelete() === entry.id ? undefined : TextAttributes.DIM}
2382
+ style={{ fg: pendingDelete() === entry.id ? theme().error : theme().textMuted }}
2383
+ >
2384
+ {truncate(pendingDelete() === entry.id ? "option+d again" : rowDir(entry), columns().dir)}
2385
+ </text>
2386
+ </Show>
2387
+ <text width={columns().age} flexShrink={0} wrapMode="none" attributes={TextAttributes.DIM} style={{ fg: theme().textMuted }}>
2388
+ {ago(entry.updated).padStart(columns().age)}
2342
2389
  </text>
2343
2390
  </box>
2344
2391
  )}
@@ -2352,6 +2399,15 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
2352
2399
  )
2353
2400
  }
2354
2401
 
2402
+ // prompt.footer also mounts in session composers: only home subscribes to polling.
2403
+ function HomeBottom(props: { api: TuiPluginApi } & PinProps) {
2404
+ return (
2405
+ <Show when={props.api.route.current.name === "home"}>
2406
+ <HomeSessions api={props.api} pins={props.pins} onTogglePin={props.onTogglePin} poll={props.poll} />
2407
+ </Show>
2408
+ )
2409
+ }
2410
+
2355
2411
  const tui: TuiPlugin = async (api) => {
2356
2412
  const [pins, setPins] = createSignal<Pins>(emptyPins())
2357
2413
  // Keep exploratory context while the plugin remains loaded; a full OpenCode
@@ -2386,7 +2442,7 @@ const tui: TuiPlugin = async (api) => {
2386
2442
  return <SidebarSessions api={api} pins={pins} onTogglePin={onTogglePin} poll={poll} />
2387
2443
  },
2388
2444
  home_bottom() {
2389
- return <HomeSessions api={api} pins={pins} onTogglePin={onTogglePin} poll={poll} />
2445
+ return <HomeBottom api={api} pins={pins} onTogglePin={onTogglePin} poll={poll} />
2390
2446
  },
2391
2447
  },
2392
2448
  })
@@ -3597,9 +3653,9 @@ function createApi(ctx: Plugin.Context) {
3597
3653
  render: cfg.slots.sidebar_content as any,
3598
3654
  }))
3599
3655
  }
3600
- if (cfg.slots.home_bottom) {
3656
+ if (cfg.slots.home_bottom && ctx.options?.showHome !== false) {
3601
3657
  slotDisposers.push(ctx.ui.slot({
3602
- prepend: "home.footer",
3658
+ append: "prompt.footer",
3603
3659
  render: cfg.slots.home_bottom as any,
3604
3660
  }))
3605
3661
  }