opencode-sesh 0.7.3 → 0.8.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/AGENTS.md CHANGED
@@ -139,8 +139,10 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
139
139
  ## TUI panel (`tui/sesh-panel.tsx`)
140
140
 
141
141
  - **Sidebar:** `api.slots.register` on `sidebar_content` (append mode — native
142
- sidebar content stays). Renders pinned sessions before the newest unarchived
143
- sessions, with the current one highlighted, polled every 15 s; Ctrl-P previews
142
+ sidebar content stays). Pinned directories sort first, then directory groups
143
+ by recency; sessions within each group sort by last update regardless of pins.
144
+ Waiting, running, and current-idle sessions have distinct title colors and
145
+ text labels (in addition to row markers), polled every 15 s; Ctrl-P previews
144
146
  a row and ctrl+x deletes one (armed, then confirmed). The section lists every
145
147
  session without a row cap and flex-grows to fill the sidebar, with overflow
146
148
  scrolling inside; the picker remains the place for transcript search.
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.8.0] - 2026-09-24
11
+
12
+ ### Changed
13
+
14
+ - Sidebar and picker sessions within each directory now sort by last update,
15
+ newest first, even when a session is pinned. Sidebar titles and labels make
16
+ waiting, running, and currently open idle sessions distinguishable at a glance.
10
17
  ## [0.7.3] - 2026-09-24
11
18
 
12
19
  ### Removed
@@ -326,7 +333,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
326
333
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
327
334
  CI.
328
335
 
329
- [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.3...HEAD
336
+ [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.8.0...HEAD
337
+ [0.8.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.3...v0.8.0
330
338
  [0.7.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.2...v0.7.3
331
339
  [0.7.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.1...v0.7.2
332
340
  [0.7.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.7.0...v0.7.1
package/README.md CHANGED
@@ -182,11 +182,14 @@ Click the sidebar's `search…` box to filter recent sessions in place; click
182
182
  elsewhere or press Esc to leave search. Click the **Sessions** heading to drive
183
183
  the list from the keyboard instead of the mouse (`↑`/`↓` to move, Enter to open,
184
184
  Ctrl-P to preview, Option-S/Option-D to pin, Ctrl-X to delete, Esc to leave). Pinned
185
- sessions appear ahead of the rest; the section lists every session and scrolls
186
- to fill the sidebar, so click **▾ hide** to collapse it when you need the room.
185
+ directories rise first, while sessions within each directory are ordered by
186
+ last update, newest first. The section lists every session and scrolls to fill
187
+ the sidebar, so click **▾ hide** to collapse it when you need the room.
187
188
  Sessions waiting on you (unanswered agent questions, runs stuck mid-tool) pin
188
189
  themselves to a **Needs input** group above the directories, with the count in
189
- the heading; `sesh --needs-input` lists the same set in the terminal.
190
+ the heading; `sesh --needs-input` lists the same set in the terminal. Row titles
191
+ and labels mark waiting (warning), running (success), and the current idle
192
+ session (accent); other sessions stay neutral.
190
193
  Hovering a row while keyboard navigation is active selects it for the next
191
194
  shortcut. Clicking (or Enter on) a session opens it here. The full picker is one
192
195
  keystroke away.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
6
6
  "license": "MIT",
@@ -248,6 +248,20 @@ function ensureRootMouse(renderer: TuiPluginApi["renderer"]) {
248
248
 
249
249
  type Entry = { id: string; title: string; dir: string; group: string; updated: number }
250
250
  type EntryResult = { entries: Entry[]; truncated: boolean }
251
+ type SidebarActivity = "waiting" | "running" | "active" | "idle"
252
+
253
+ function newestFirst(entries: Entry[]): Entry[] {
254
+ return [...entries].sort((a, b) => b.updated - a.updated || a.id.localeCompare(b.id))
255
+ }
256
+
257
+ // A question needs an answer even if the agent is busy. A stale running-tool
258
+ // record, on the other hand, must not hide a currently busy agent.
259
+ function sidebarActivity(current: boolean, live?: string, pending = false, reason?: string): SidebarActivity {
260
+ if (pending || reason === "question") return "waiting"
261
+ if (live === "busy" || live === "retry") return "running"
262
+ if (reason === "stuck") return "waiting"
263
+ return current ? "active" : "idle"
264
+ }
251
265
 
252
266
  const SESSION_PAGE_LIMIT = 200
253
267
  const SESSION_MAX = 5000
@@ -707,7 +721,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
707
721
 
708
722
  const filteredEntries = createMemo(() => {
709
723
  const q = query().trim().toLowerCase()
710
- const ordered = pinnedFirst(entries(), props.pins())
724
+ const ordered = entries()
711
725
  if (!q) return ordered
712
726
  return ordered.filter(
713
727
  (entry) => entry.title.toLowerCase().includes(q) || entry.dir.toLowerCase().includes(q),
@@ -720,9 +734,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
720
734
  const NEEDS_INPUT_DIR = "__needs_input__"
721
735
  const waitingEntries = createMemo(() => {
722
736
  const ids = waiting()
723
- return shownEntries().filter((entry) => ids.has(entry.id))
737
+ return newestFirst(shownEntries().filter((entry) => ids.has(entry.id)))
724
738
  })
725
- const waitingReason = (id: string) => (waiting().get(id) === "question" ? "awaiting answer" : "run stuck")
739
+ const waitingReason = (id: string) =>
740
+ waiting().get(id) === "question" ? "awaiting answer" : waiting().get(id) === "stuck" ? "run stuck" : "needs input"
726
741
 
727
742
  const tree = createMemo<TreeRow[]>(() => {
728
743
  const rows: TreeRow[] = []
@@ -751,10 +766,11 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
751
766
  return rank(b) - rank(a) || b.updated - a.updated
752
767
  })
753
768
  for (const group of ordered) {
769
+ const sessions = newestFirst(group.list)
754
770
  rows.push({ kind: "group", dir: group.dir, label: prettyDir(group.dir, home), count: group.list.length })
755
771
  if (collapsed()[group.dir]) continue
756
- for (let i = 0; i < group.list.length; i++) {
757
- rows.push({ kind: "item", entry: group.list[i], last: i === group.list.length - 1 })
772
+ for (let i = 0; i < sessions.length; i++) {
773
+ rows.push({ kind: "item", entry: sessions[i], last: i === sessions.length - 1 })
758
774
  }
759
775
  }
760
776
  return rows
@@ -796,21 +812,24 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
796
812
  previousRows = rows
797
813
  })
798
814
 
799
- const sessionStatus = (id: string): "running" | "waiting" | "idle" => {
815
+ const sessionStatus = (id: string): SidebarActivity => {
800
816
  try {
801
817
  const permissions = props.api.state.session.permission(id)
802
818
  const questions = props.api.state.session.question(id)
803
- if ((permissions?.length ?? 0) > 0 || (questions?.length ?? 0) > 0) return "waiting"
804
- const status = props.api.state.session.status(id)
805
- if (status?.type === "busy" || status?.type === "retry") return "running"
819
+ return sidebarActivity(
820
+ id === currentID(),
821
+ props.api.state.session.status(id)?.type,
822
+ (permissions?.length ?? 0) > 0 || (questions?.length ?? 0) > 0,
823
+ waiting().get(id),
824
+ )
806
825
  } catch {}
807
- return "idle"
826
+ return sidebarActivity(id === currentID(), undefined, false, waiting().get(id))
808
827
  }
809
828
 
810
- const statusColor = (id: string) => {
811
- const status = sessionStatus(id)
829
+ const statusColor = (status: SidebarActivity) => {
812
830
  if (status === "running") return theme().success
813
831
  if (status === "waiting") return theme().warning
832
+ if (status === "active") return theme().accent
814
833
  return theme().textMuted
815
834
  }
816
835
 
@@ -1107,8 +1126,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1107
1126
  horizontalScrollbarOptions={{ visible: false }}
1108
1127
  >
1109
1128
  <For each={tree()}>
1110
- {(row) =>
1111
- row.kind === "group" ? (
1129
+ {(row) => {
1130
+ if (row.kind === "group") return (
1112
1131
  <box flexDirection="row" paddingTop={1} onMouseDown={() => toggleGroup(row.dir)}>
1113
1132
  <text flexGrow={1} flexShrink={1} wrapMode="none">
1114
1133
  <span style={{ fg: theme().textMuted }}>{collapsed()[row.dir] ? "▸ " : "▾ "}</span>
@@ -1118,7 +1137,9 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1118
1137
  {" "}({row.count})
1119
1138
  </text>
1120
1139
  </box>
1121
- ) : (
1140
+ )
1141
+ const activity = createMemo(() => sessionStatus(row.entry.id))
1142
+ return (
1122
1143
  <box
1123
1144
  flexDirection="row"
1124
1145
  gap={1}
@@ -1150,24 +1171,24 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1150
1171
  ? theme().error
1151
1172
  : row.entry.id === currentID()
1152
1173
  ? theme().accent
1153
- : statusColor(row.entry.id),
1174
+ : statusColor(activity()),
1154
1175
  }}
1155
1176
  >
1156
- {row.entry.id === currentID() ? "●" : isActive(row.entry.id) ? "▸" : "○"}
1177
+ {row.entry.id === currentID() ? "●" : activity() === "waiting" ? "!" : activity() === "running" ? "▶" : isActive(row.entry.id) ? "▸" : "○"}
1157
1178
  </span>
1158
1179
  </text>
1159
1180
  <Highlighted
1160
1181
  text={`${props.pins().sessions.includes(row.entry.id) ? "★ " : ""}${truncate(row.entry.title, SIDEBAR_TITLE_WIDTH - (props.pins().sessions.includes(row.entry.id) ? 2 : 0))}`}
1161
1182
  query={query()}
1162
- color={isPendingDelete(row.entry.id) ? theme().error : theme().text}
1183
+ color={isPendingDelete(row.entry.id) ? theme().error : activity() === "idle" ? theme().text : statusColor(activity())}
1163
1184
  matchColor={theme().warning}
1164
1185
  />
1165
1186
  <text flexShrink={0} style={{ fg: isPendingDelete(row.entry.id) ? theme().error : theme().textMuted }}>
1166
1187
  {isPendingDelete(row.entry.id) ? (
1167
1188
  "ctrl+x again"
1168
- ) : waiting().has(row.entry.id) ? (
1189
+ ) : activity() !== "idle" ? (
1169
1190
  <span>
1170
- {ago(row.entry.updated)} · <span style={{ fg: theme().warning }}>{waitingReason(row.entry.id)}</span>
1191
+ {ago(row.entry.updated)} · <span style={{ fg: statusColor(activity()) }}>{activity() === "waiting" ? waitingReason(row.entry.id) : activity()}</span>
1171
1192
  </span>
1172
1193
  ) : (
1173
1194
  ago(row.entry.updated)
@@ -1175,7 +1196,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
1175
1196
  </text>
1176
1197
  </box>
1177
1198
  )
1178
- }
1199
+ }}
1179
1200
  </For>
1180
1201
  </scrollbox>
1181
1202
  <Show when={remaining() > 0}>
@@ -1468,7 +1489,7 @@ const tui: TuiPlugin = async (api) => {
1468
1489
  list.push(entry)
1469
1490
  byGroup.set(entry.group, list)
1470
1491
  }
1471
- const groups = [...byGroup.entries()].map(([name, list]) => ({ name, list }))
1492
+ const groups = [...byGroup.entries()].map(([name, list]) => ({ name, list: newestFirst(list) }))
1472
1493
  const currentPins = pins()
1473
1494
  const rank = (group: (typeof groups)[number]) =>
1474
1495
  group.list.some((entry) => currentPins.directories.includes(entry.dir)) ? 2