tuiboard 0.12.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/.tuiboard/config.example.yaml +4 -0
- package/CHANGELOG.md +70 -0
- package/README.md +54 -1
- package/bin/tuiboard.ts +9 -0
- package/package.json +1 -1
- package/src/app-exit.ts +37 -0
- package/src/app.tsx +28 -13
- package/src/config/loader.ts +8 -0
- package/src/input/handleKey.ts +52 -3
- package/src/input/herdr-open.ts +132 -0
- package/src/input/open-session.ts +78 -29
- package/src/store/agent-adapters/claude-code.ts +1 -0
- package/src/store/agent-adapters/codex.ts +1 -0
- package/src/store/agent-adapters/opencode.ts +1 -0
- package/src/store/agent-adapters/pi.ts +1 -0
- package/src/store/agents.ts +48 -11
- package/src/store/herdr.ts +272 -0
- package/src/store/index.ts +4 -2
- package/src/ui/AgentRow.tsx +45 -13
- package/src/ui/AgentsBar.tsx +11 -7
- package/src/ui/Modal.tsx +24 -1
- package/src/ui/agent-cursor.ts +49 -0
- package/src/views/AgentsOnly.tsx +21 -14
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,21 +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
|
-
|
|
104
|
-
|
|
106
|
+
indicators={props.store.agentIndicators}
|
|
107
|
+
session={session()}
|
|
108
|
+
cursor={isActive() && i === agentRow()}
|
|
105
109
|
nameMaxChars={48}
|
|
106
110
|
onClick={() => {
|
|
107
111
|
props.store.setActiveZone("agents");
|
|
108
|
-
props.store.setCursor(0, i
|
|
112
|
+
props.store.setCursor(0, i);
|
|
109
113
|
}}
|
|
110
114
|
/>
|
|
111
115
|
</box>
|
|
112
116
|
)}
|
|
113
|
-
</
|
|
117
|
+
</Index>
|
|
114
118
|
</scrollbox>
|
|
115
119
|
</Show>
|
|
116
120
|
</box>
|
package/src/ui/Modal.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { AGENDA_WIDTH } from "~/ui/layout";
|
|
|
24
24
|
import { formatHm } from "~/store/timeline";
|
|
25
25
|
import { HARNESS } from "~/store/agents";
|
|
26
26
|
import { HARNESS_COLOR } from "~/ui/AgentRow";
|
|
27
|
+
import { herdrPlace } from "~/store/herdr";
|
|
27
28
|
import type { TuiStore } from "~/store/index";
|
|
28
29
|
import type { PriorityLevel, TimeBlock } from "~/types";
|
|
29
30
|
|
|
@@ -936,6 +937,27 @@ function AgentDetailModal(props: { store: TuiStore; modal: Extract<NonNullable<T
|
|
|
936
937
|
<span style={{ fg: T.textDim }}>status </span>
|
|
937
938
|
<span style={{ fg: T.text }}>{s().status}</span>
|
|
938
939
|
</text>
|
|
940
|
+
<Show when={s().herdr}>
|
|
941
|
+
{(h: () => NonNullable<ReturnType<typeof s>["herdr"]>) => (
|
|
942
|
+
<>
|
|
943
|
+
<text>
|
|
944
|
+
<span style={{ fg: T.textDim }}>herdr </span>
|
|
945
|
+
<span style={{ fg: T.accent }}>{herdrPlace(h())}</span>
|
|
946
|
+
<span style={{ fg: T.textDim }}>
|
|
947
|
+
{" "}pane {h().paneId} · {h().status} · matched by {h().matchedBy}
|
|
948
|
+
</span>
|
|
949
|
+
</text>
|
|
950
|
+
<Show when={h().matchedBy === "cwd"}>
|
|
951
|
+
<text wrapMode="word">
|
|
952
|
+
<span style={{ fg: T.textDim }}>
|
|
953
|
+
{" "}herdr doesn't report which {h().agent} session this pane is on — run
|
|
954
|
+
`herdr integration install {h().agent}` for an exact match.
|
|
955
|
+
</span>
|
|
956
|
+
</text>
|
|
957
|
+
</Show>
|
|
958
|
+
</>
|
|
959
|
+
)}
|
|
960
|
+
</Show>
|
|
939
961
|
<text>
|
|
940
962
|
<span style={{ fg: T.textDim }}>cwd </span>
|
|
941
963
|
<span style={{ fg: T.text }}>{s().cwd}</span>
|
|
@@ -1126,7 +1148,8 @@ const HELP_SECTIONS: HelpSection[] = [
|
|
|
1126
1148
|
emoji: "🤖",
|
|
1127
1149
|
title: "Agents zone",
|
|
1128
1150
|
rows: [
|
|
1129
|
-
["Enter", "Open (resume) the selected session in a new terminal tab/window"],
|
|
1151
|
+
["Enter", "Open (resume) the selected session in a new terminal tab/window — or go to it if it's open in herdr"],
|
|
1152
|
+
["H", "herdr: go to the session, or resume it in a new tab of its project's workspace"],
|
|
1130
1153
|
["c", "Copy a 'cd + resume' command for the session"],
|
|
1131
1154
|
["o", "Session detail (harness, model, cwd, branch, last prompts, resume cmd)"],
|
|
1132
1155
|
["f", "Filter by harness: all → cc Claude Code → cx Codex → oc OpenCode → pi Pi"],
|
|
@@ -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,22 +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"
|
|
102
|
+
indicators={props.store.agentIndicators}
|
|
96
103
|
onClick={() => {
|
|
97
104
|
props.store.setActiveZone("agents");
|
|
98
|
-
props.store.setCursor(0, i
|
|
105
|
+
props.store.setCursor(0, i);
|
|
99
106
|
}}
|
|
100
107
|
/>
|
|
101
108
|
</box>
|
|
102
109
|
)}
|
|
103
|
-
</
|
|
110
|
+
</Index>
|
|
104
111
|
</scrollbox>
|
|
105
112
|
</Show>
|
|
106
113
|
</box>
|