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 +4 -2
- package/CHANGELOG.md +9 -1
- package/README.md +6 -3
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +43 -22
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).
|
|
143
|
-
sessions
|
|
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.
|
|
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
|
-
|
|
186
|
-
|
|
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
package/tui/sesh-panel.tsx
CHANGED
|
@@ -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 =
|
|
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) =>
|
|
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 <
|
|
757
|
-
rows.push({ kind: "item", entry:
|
|
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):
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
|
826
|
+
return sidebarActivity(id === currentID(), undefined, false, waiting().get(id))
|
|
808
827
|
}
|
|
809
828
|
|
|
810
|
-
const statusColor = (
|
|
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(
|
|
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
|
-
) :
|
|
1189
|
+
) : activity() !== "idle" ? (
|
|
1169
1190
|
<span>
|
|
1170
|
-
{ago(row.entry.updated)} · <span style={{ fg:
|
|
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
|