tuiboard 0.13.0 → 0.13.1
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/app.tsx +5 -0
- package/src/ui/AgentsBar.tsx +10 -7
- package/src/ui/agent-cursor.ts +49 -0
- package/src/views/AgentsOnly.tsx +20 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.1] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Agents list stays in order when sessions move** (#52). A new session
|
|
14
|
+
appearing on top could show twice, leave blank rows, or draw rows in the
|
|
15
|
+
wrong order past the panel border; the lists now keep one stable row per
|
|
16
|
+
position. The cursor also stays on the session you selected while the list
|
|
17
|
+
reorders, instead of silently landing on another one (so Enter, `H` and `c`
|
|
18
|
+
act on the session you picked).
|
|
19
|
+
- **`--view=agents` / `--view=timeline` / `--view=board` start on their own
|
|
20
|
+
zone** (#54), so the cursor and keys work right away instead of acting on
|
|
21
|
+
the hidden planner.
|
|
22
|
+
|
|
10
23
|
## [0.13.0] - 2026-09-17
|
|
11
24
|
|
|
12
25
|
tuiboard and [herdr](https://herdr.dev) now work as one: the Agents zone shows
|
|
@@ -474,6 +487,7 @@ First public release on npm. This entry captures the full feature set at launch.
|
|
|
474
487
|
|
|
475
488
|
Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
|
|
476
489
|
|
|
490
|
+
[0.13.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.13.1
|
|
477
491
|
[0.13.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.13.0
|
|
478
492
|
[0.12.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.12.0
|
|
479
493
|
[0.11.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.11.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tuiboard",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live view of your coding-agent sessions (Claude Code, Codex, OpenCode, Pi). Use only the panels you want.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/app.tsx
CHANGED
|
@@ -148,6 +148,11 @@ if (view === "planner") {
|
|
|
148
148
|
store.setActiveZone("planner");
|
|
149
149
|
store.setZoomed(true);
|
|
150
150
|
}
|
|
151
|
+
// The single-zone views show one zone; keys must act on it. Without this the
|
|
152
|
+
// planner (the default landing zone above) stayed active behind them.
|
|
153
|
+
if (view === "agents" || view === "timeline" || view === "board") {
|
|
154
|
+
store.setActiveZone(view);
|
|
155
|
+
}
|
|
151
156
|
|
|
152
157
|
// ─── App shell ──────────────────────────────────────────────────────────────
|
|
153
158
|
|
package/src/ui/AgentsBar.tsx
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* visible. Clicking a row sets activeZone + agent cursor.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { Index, Show, createEffect, createMemo } from "solid-js";
|
|
12
12
|
|
|
13
13
|
import { AgentRow } from "~/ui/AgentRow";
|
|
14
|
+
import { useStickyAgentCursor } from "~/ui/agent-cursor";
|
|
14
15
|
import { T } from "~/ui/glyphs";
|
|
15
16
|
import { HARNESS } from "~/store/agents";
|
|
16
17
|
import type { TuiStore } from "~/store/index";
|
|
@@ -42,6 +43,7 @@ export function AgentsBar(props: AgentsBarProps) {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
let scrollBoxRef: ScrollBoxLike | undefined;
|
|
46
|
+
useStickyAgentCursor(props.store, allShown);
|
|
45
47
|
|
|
46
48
|
// Keep the cursor row visible as j/k moves it (mouse wheel scrolls freely
|
|
47
49
|
// via the scrollbox itself). setTimeout(0) waits for layout to commit.
|
|
@@ -96,22 +98,23 @@ export function AgentsBar(props: AgentsBarProps) {
|
|
|
96
98
|
scrollbarOptions: { visible: false },
|
|
97
99
|
}}
|
|
98
100
|
>
|
|
99
|
-
<
|
|
101
|
+
{/* <Index>: stable rows by position — see AgentsOnly (#52). */}
|
|
102
|
+
<Index each={allShown()}>
|
|
100
103
|
{(session, i) => (
|
|
101
|
-
<box id={agentRowId(i
|
|
104
|
+
<box id={agentRowId(i)}>
|
|
102
105
|
<AgentRow
|
|
103
106
|
indicators={props.store.agentIndicators}
|
|
104
|
-
session={session}
|
|
105
|
-
cursor={isActive() && i
|
|
107
|
+
session={session()}
|
|
108
|
+
cursor={isActive() && i === agentRow()}
|
|
106
109
|
nameMaxChars={48}
|
|
107
110
|
onClick={() => {
|
|
108
111
|
props.store.setActiveZone("agents");
|
|
109
|
-
props.store.setCursor(0, i
|
|
112
|
+
props.store.setCursor(0, i);
|
|
110
113
|
}}
|
|
111
114
|
/>
|
|
112
115
|
</box>
|
|
113
116
|
)}
|
|
114
|
-
</
|
|
117
|
+
</Index>
|
|
115
118
|
</scrollbox>
|
|
116
119
|
</Show>
|
|
117
120
|
</box>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the Agents cursor on the same session while the list reorders.
|
|
3
|
+
*
|
|
4
|
+
* The cursor is a row index (`ui.row`), and sessions move all the time —
|
|
5
|
+
* sorted by recency, a new one lands on top, a working one climbs. Without
|
|
6
|
+
* this, the highlighted row silently became a different session and Enter /
|
|
7
|
+
* H / c acted on it. Call once inside the component that renders the list.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createEffect } from "solid-js";
|
|
11
|
+
|
|
12
|
+
import type { AgentSession } from "~/store/agents";
|
|
13
|
+
import type { TuiStore } from "~/store/index";
|
|
14
|
+
|
|
15
|
+
/** Pure decision, for tests: where the cursor should be after a change. */
|
|
16
|
+
export function followSelection(
|
|
17
|
+
prev: { list: readonly AgentSession[] | undefined; row: number; id: string | undefined },
|
|
18
|
+
list: readonly AgentSession[],
|
|
19
|
+
row: number,
|
|
20
|
+
): number {
|
|
21
|
+
// Only a list change (not a keypress) may move the cursor.
|
|
22
|
+
if (list === prev.list || row !== prev.row || !prev.id) return row;
|
|
23
|
+
if (list[row]?.sessionId === prev.id) return row;
|
|
24
|
+
const idx = list.findIndex((s) => s.sessionId === prev.id);
|
|
25
|
+
return idx >= 0 ? idx : Math.min(row, Math.max(0, list.length - 1));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useStickyAgentCursor(store: TuiStore, list: () => readonly AgentSession[]): void {
|
|
29
|
+
const prev: { list: readonly AgentSession[] | undefined; row: number; id: string | undefined } = {
|
|
30
|
+
list: undefined,
|
|
31
|
+
row: -1,
|
|
32
|
+
id: undefined,
|
|
33
|
+
};
|
|
34
|
+
createEffect(() => {
|
|
35
|
+
const items = list();
|
|
36
|
+
const row = store.state.ui.row;
|
|
37
|
+
// `ui.row` belongs to whichever zone is active.
|
|
38
|
+
if (store.state.ui.activeZone !== "agents") {
|
|
39
|
+
prev.list = undefined;
|
|
40
|
+
prev.id = undefined;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const next = followSelection(prev, items, row);
|
|
44
|
+
prev.list = items;
|
|
45
|
+
prev.row = next;
|
|
46
|
+
prev.id = items[next]?.sessionId;
|
|
47
|
+
if (next !== row) store.setCursor(0, next);
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/views/AgentsOnly.tsx
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* scrollChildIntoView (same trick used in BoardView for active columns).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Index, Show, createEffect, createMemo } from "solid-js";
|
|
9
9
|
|
|
10
10
|
import { AgentRow } from "~/ui/AgentRow";
|
|
11
|
+
import { useStickyAgentCursor } from "~/ui/agent-cursor";
|
|
11
12
|
import { T } from "~/ui/glyphs";
|
|
12
13
|
import { HARNESS } from "~/store/agents";
|
|
13
14
|
import type { TuiStore } from "~/store/index";
|
|
@@ -16,10 +17,9 @@ interface ScrollBoxLike {
|
|
|
16
17
|
scrollChildIntoView(id: string): void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `tuiboard-agent-${sessionId.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
20
|
+
/** Rows are keyed by position (see the <Index> below), and so are their ids. */
|
|
21
|
+
function rowId(index: number): string {
|
|
22
|
+
return `tuiboard-agent-card-${index}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function AgentsOnly(props: { store: TuiStore }) {
|
|
@@ -29,17 +29,17 @@ export function AgentsOnly(props: { store: TuiStore }) {
|
|
|
29
29
|
const filter = () => props.store.state.ui.agentsFilter;
|
|
30
30
|
const filterTag = () => (filter() === "all" ? "" : ` · ${HARNESS[filter() as keyof typeof HARNESS].code}`);
|
|
31
31
|
let scrollBoxRef: ScrollBoxLike | undefined;
|
|
32
|
+
useStickyAgentCursor(props.store, sessions);
|
|
32
33
|
|
|
33
34
|
// Auto-scroll the list so the active row is visible. setTimeout(0) waits
|
|
34
35
|
// for OpenTUI to finish layout before requesting scroll.
|
|
35
36
|
createEffect(() => {
|
|
36
37
|
const row = agentRow();
|
|
37
38
|
if (!isActive() || !scrollBoxRef) return;
|
|
38
|
-
|
|
39
|
-
if (!target) return;
|
|
39
|
+
if (row >= sessions().length) return;
|
|
40
40
|
setTimeout(() => {
|
|
41
41
|
try {
|
|
42
|
-
scrollBoxRef?.scrollChildIntoView(rowId(
|
|
42
|
+
scrollBoxRef?.scrollChildIntoView(rowId(row));
|
|
43
43
|
} catch {
|
|
44
44
|
// Child not mounted yet — harmless.
|
|
45
45
|
}
|
|
@@ -85,23 +85,29 @@ export function AgentsOnly(props: { store: TuiStore }) {
|
|
|
85
85
|
scrollbarOptions: { visible: false },
|
|
86
86
|
}}
|
|
87
87
|
>
|
|
88
|
-
|
|
88
|
+
{/*
|
|
89
|
+
<Index>, not <For>: one stable row per position whose data
|
|
90
|
+
updates in place. Sessions are re-created on every refresh and
|
|
91
|
+
reorder by recency, and moving children inside OpenTUI's
|
|
92
|
+
scrollbox left stale/duplicate rows on screen (#52).
|
|
93
|
+
*/}
|
|
94
|
+
<Index each={sessions()}>
|
|
89
95
|
{(session, i) => (
|
|
90
|
-
<box id={rowId(
|
|
96
|
+
<box id={rowId(i)}>
|
|
91
97
|
<AgentRow
|
|
92
|
-
session={session}
|
|
93
|
-
cursor={isActive() && i
|
|
98
|
+
session={session()}
|
|
99
|
+
cursor={isActive() && i === agentRow()}
|
|
94
100
|
nameMaxChars={120}
|
|
95
101
|
variant="card"
|
|
96
102
|
indicators={props.store.agentIndicators}
|
|
97
103
|
onClick={() => {
|
|
98
104
|
props.store.setActiveZone("agents");
|
|
99
|
-
props.store.setCursor(0, i
|
|
105
|
+
props.store.setCursor(0, i);
|
|
100
106
|
}}
|
|
101
107
|
/>
|
|
102
108
|
</box>
|
|
103
109
|
)}
|
|
104
|
-
</
|
|
110
|
+
</Index>
|
|
105
111
|
</scrollbox>
|
|
106
112
|
</Show>
|
|
107
113
|
</box>
|