tuiboard 0.10.0 → 0.12.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/.tuiboard/config.example.yaml +29 -9
- package/CHANGELOG.md +105 -0
- package/README.md +63 -30
- package/package.json +3 -2
- package/src/config/loader.ts +32 -7
- package/src/input/handleKey.ts +71 -51
- package/src/input/open-session.ts +403 -0
- package/src/store/agent-adapters/claude-code.ts +348 -0
- package/src/store/agent-adapters/codex.ts +405 -0
- package/src/store/agent-adapters/index.ts +17 -0
- package/src/store/agent-adapters/opencode.ts +195 -0
- package/src/store/agent-adapters/pi.ts +253 -0
- package/src/store/agents.ts +139 -336
- package/src/store/index.ts +63 -7
- package/src/ui/AgentRow.tsx +147 -41
- package/src/ui/AgentsBar.tsx +8 -3
- package/src/ui/BoardView.tsx +35 -4
- package/src/ui/Modal.tsx +263 -68
- package/src/ui/agent-line.ts +95 -0
- package/src/views/AgentsOnly.tsx +11 -5
package/src/store/index.ts
CHANGED
|
@@ -29,7 +29,15 @@ import {
|
|
|
29
29
|
createBoardWatcher,
|
|
30
30
|
type BoardWatcher,
|
|
31
31
|
} from "~/io/watcher";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
HARNESS,
|
|
34
|
+
createAgentsStore,
|
|
35
|
+
filterSessions,
|
|
36
|
+
type AgentSession,
|
|
37
|
+
type AgentsFilter,
|
|
38
|
+
type AgentsStore,
|
|
39
|
+
} from "./agents";
|
|
40
|
+
import { AGENT_ADAPTERS } from "./agent-adapters";
|
|
33
41
|
import {
|
|
34
42
|
createCalendarStore,
|
|
35
43
|
createGoogleEvent,
|
|
@@ -243,6 +251,12 @@ export interface UIState {
|
|
|
243
251
|
* entries and the calendar overlay. Changed with `[` / `]`; `\` resets to 0.
|
|
244
252
|
*/
|
|
245
253
|
agendaOffset: number;
|
|
254
|
+
/**
|
|
255
|
+
* Vertical scroll position of the help modal, as a block index (0 = top).
|
|
256
|
+
* `?` opens the help at 0; `j`/`k` step it. The HelpModal component clamps
|
|
257
|
+
* the upper bound (it knows the block count) and scrolls that block into view.
|
|
258
|
+
*/
|
|
259
|
+
helpScroll: number;
|
|
246
260
|
view: ViewMode;
|
|
247
261
|
/**
|
|
248
262
|
* Tasks marked for bulk ops (`Space`). Key format:
|
|
@@ -253,6 +267,10 @@ export interface UIState {
|
|
|
253
267
|
marked: Record<string, true>;
|
|
254
268
|
/** Active filter. */
|
|
255
269
|
filter: "all" | "today" | "overdue" | "tomorrow" | "followup";
|
|
270
|
+
/** Agents-zone harness filter (`f` while the Agents zone is active). */
|
|
271
|
+
agentsFilter: AgentsFilter;
|
|
272
|
+
/** Outcome of the last Enter (open session), shown in that session's detail. */
|
|
273
|
+
lastLaunch?: { sessionId: string; ok: boolean; text: string; ts: number };
|
|
256
274
|
/** Banner messages (errors, conflicts, undo notifications). */
|
|
257
275
|
banner?: { kind: "info" | "warn" | "error"; text: string; ts: number };
|
|
258
276
|
/** Open modal, if any. Keyboard handler routes input to the modal when set. */
|
|
@@ -339,9 +357,11 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
339
357
|
grabbing: false,
|
|
340
358
|
armMode: false,
|
|
341
359
|
agendaOffset: 0,
|
|
360
|
+
helpScroll: 0,
|
|
342
361
|
view: "kanban",
|
|
343
362
|
marked: {},
|
|
344
363
|
filter: "all",
|
|
364
|
+
agentsFilter: "all",
|
|
345
365
|
},
|
|
346
366
|
undo: [],
|
|
347
367
|
rev: 0,
|
|
@@ -355,12 +375,12 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
355
375
|
initialBoards.map((b) => b.board.filepath),
|
|
356
376
|
);
|
|
357
377
|
|
|
358
|
-
// Agents store has its own lifecycle (chokidar watcher on
|
|
359
|
-
// Shared dispose() boundary below so SIGINT cleans both. When
|
|
360
|
-
// zone is disabled we skip the watcher entirely (no
|
|
361
|
-
// and hand back an inert stub.
|
|
378
|
+
// Agents store has its own lifecycle (chokidar watcher on each agent CLI's
|
|
379
|
+
// session dirs). Shared dispose() boundary below so SIGINT cleans both. When
|
|
380
|
+
// the agents zone is disabled we skip the watcher entirely (no session reads
|
|
381
|
+
// at all) and hand back an inert stub.
|
|
362
382
|
const agentsStore: AgentsStore = enabledZones.agents
|
|
363
|
-
? createAgentsStore()
|
|
383
|
+
? createAgentsStore(AGENT_ADAPTERS)
|
|
364
384
|
: noopAgentsStore();
|
|
365
385
|
// Calendar feeds (read-only) merged into the Agenda zone. Skipped entirely
|
|
366
386
|
// (no network) when the agenda zone is disabled.
|
|
@@ -379,6 +399,8 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
379
399
|
// change — and spuriously flash "Reloaded after external edit". Skip.
|
|
380
400
|
if (lastWrittenContent.get(filepath) === content) return;
|
|
381
401
|
const { board } = parseBoard(content, { filepath });
|
|
402
|
+
const boardConfig = config.boards.find((cb) => cb.path === filepath);
|
|
403
|
+
if (boardConfig?.name) board.name = boardConfig.name;
|
|
382
404
|
const mtimeMs = statMtime(filepath);
|
|
383
405
|
setState(
|
|
384
406
|
"boards",
|
|
@@ -1221,6 +1243,28 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1221
1243
|
setState("ui", "zoomed", (z: boolean) => !z);
|
|
1222
1244
|
}
|
|
1223
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* Sessions the Agents zone lists, after the harness filter. The dashboard
|
|
1248
|
+
* strip additionally hides archived ones — they sort last, so row indices
|
|
1249
|
+
* into this list stay valid for both views.
|
|
1250
|
+
*/
|
|
1251
|
+
function agentSessions(): AgentSession[] {
|
|
1252
|
+
return filterSessions(agentsStore.sessions(), state.ui.agentsFilter);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function setLastLaunch(sessionId: string, ok: boolean, text: string): void {
|
|
1256
|
+
setState("ui", "lastLaunch", { sessionId, ok, text, ts: Date.now() });
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Cycle the Agents-zone filter: all → cc → cx → oc → pi → all. */
|
|
1260
|
+
function cycleAgentsFilter(): AgentsFilter {
|
|
1261
|
+
const cycle: AgentsFilter[] = ["all", ...(Object.keys(HARNESS) as AgentsFilter[])];
|
|
1262
|
+
const next = cycle[(cycle.indexOf(state.ui.agentsFilter) + 1) % cycle.length]!;
|
|
1263
|
+
setState("ui", "agentsFilter", next);
|
|
1264
|
+
setState("ui", "row", 0);
|
|
1265
|
+
return next;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1224
1268
|
function setFilter(f: UIState["filter"]): void {
|
|
1225
1269
|
setState("ui", "filter", f);
|
|
1226
1270
|
// The cursor's row was an index into the unfiltered list — reset to top
|
|
@@ -1299,7 +1343,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1299
1343
|
|
|
1300
1344
|
/**
|
|
1301
1345
|
* Manual full refresh (the `r` key). Re-reads every board from disk, rescans
|
|
1302
|
-
*
|
|
1346
|
+
* agent sessions, and force-refetches the Agenda's calendar (bypassing
|
|
1303
1347
|
* the 30-min cache). Lets the user pull in external changes — a calendar
|
|
1304
1348
|
* event edited in the browser, a board touched elsewhere — without leaving
|
|
1305
1349
|
* tuiboard. Boards normally auto-reload via the file watcher; this also
|
|
@@ -1494,6 +1538,8 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1494
1538
|
}
|
|
1495
1539
|
|
|
1496
1540
|
function openModal(m: ModalKind): void {
|
|
1541
|
+
// Help always opens scrolled to the top.
|
|
1542
|
+
if (m.kind === "help") setState("ui", "helpScroll", 0);
|
|
1497
1543
|
setState("ui", "modal", m);
|
|
1498
1544
|
}
|
|
1499
1545
|
|
|
@@ -1551,6 +1597,12 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1551
1597
|
if (cals.length === 1) void confirmEventPicker();
|
|
1552
1598
|
}
|
|
1553
1599
|
|
|
1600
|
+
/** Scroll the help modal by setting its block index (lower-clamped at 0; the
|
|
1601
|
+
* HelpModal component enforces the upper bound against its block count). */
|
|
1602
|
+
function setHelpScroll(n: number): void {
|
|
1603
|
+
setState("ui", "helpScroll", Math.max(0, n));
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1554
1606
|
/** Move the step-2 calendar selection (wraps). */
|
|
1555
1607
|
function setEventSel(n: number): void {
|
|
1556
1608
|
setState("ui", "eventPicker", produce((p: EventPicker | undefined) => {
|
|
@@ -1679,6 +1731,9 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1679
1731
|
config,
|
|
1680
1732
|
activeBoard,
|
|
1681
1733
|
agents: agentsStore,
|
|
1734
|
+
agentSessions,
|
|
1735
|
+
cycleAgentsFilter,
|
|
1736
|
+
setLastLaunch,
|
|
1682
1737
|
calendar: calendarStore,
|
|
1683
1738
|
// queries
|
|
1684
1739
|
getBoardByPath,
|
|
@@ -1740,6 +1795,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1740
1795
|
closeModal,
|
|
1741
1796
|
openEventModal,
|
|
1742
1797
|
advanceEventToStep2,
|
|
1798
|
+
setHelpScroll,
|
|
1743
1799
|
setEventSel,
|
|
1744
1800
|
confirmEventPicker,
|
|
1745
1801
|
selectCalEvent,
|
package/src/ui/AgentRow.tsx
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* dashboard strip) and AgentsOnly (fullscreen list).
|
|
2
|
+
* Render of an AgentSession, in two densities:
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* line (AgentsBar — the 5-row dashboard strip)
|
|
5
|
+
* cursor · status · harness · name · branch · model ……right-pinned: cwd_short · age
|
|
6
|
+
* Fitted to the measured row width (see agent-line.ts): on a narrow row the
|
|
7
|
+
* model goes first, then the branch, then the cwd — the name survives longest.
|
|
8
|
+
*
|
|
9
|
+
* card (AgentsOnly — fullscreen / zoomed Agents view)
|
|
10
|
+
* cursor · status · harness · name ………………………………… age
|
|
11
|
+
* model · branch · cwd (dimmed)
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
|
-
import { Show, createMemo } from "solid-js";
|
|
14
|
+
import { Show, createMemo, createSignal, onMount } from "solid-js";
|
|
15
|
+
import { homedir } from "node:os";
|
|
9
16
|
|
|
17
|
+
import { layoutCardDetails, layoutCardName, layoutLine } from "~/ui/agent-line";
|
|
10
18
|
import { T } from "~/ui/glyphs";
|
|
11
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
HARNESS,
|
|
21
|
+
cwdShort,
|
|
22
|
+
formatAge,
|
|
23
|
+
shortModel,
|
|
24
|
+
type AgentProvider,
|
|
25
|
+
type AgentSession,
|
|
26
|
+
type AgentStatus,
|
|
27
|
+
} from "~/store/agents";
|
|
12
28
|
|
|
13
29
|
const STATUS_COLOR: Record<AgentStatus, string> = {
|
|
14
30
|
"live-busy": T.today, // bright accent for actively-running
|
|
15
31
|
"live-idle": T.scheduled, // warm but quieter
|
|
16
|
-
"stale
|
|
32
|
+
"stale": T.bannerWarn,
|
|
17
33
|
"dormant": T.textDim,
|
|
18
34
|
"archived": T.textDone,
|
|
19
35
|
};
|
|
@@ -21,16 +37,35 @@ const STATUS_COLOR: Record<AgentStatus, string> = {
|
|
|
21
37
|
const STATUS_GLYPH: Record<AgentStatus, string> = {
|
|
22
38
|
"live-busy": "●",
|
|
23
39
|
"live-idle": "○",
|
|
24
|
-
"stale
|
|
40
|
+
"stale": "△",
|
|
25
41
|
"dormant": "·",
|
|
26
42
|
"archived": "·",
|
|
27
43
|
};
|
|
28
44
|
|
|
45
|
+
/** One hue per harness so the badge reads before the letters do. */
|
|
46
|
+
export const HARNESS_COLOR: Record<AgentProvider, string> = {
|
|
47
|
+
"claude-code": T.warm, // orange
|
|
48
|
+
codex: T.accent, // blue-cyan
|
|
49
|
+
opencode: "#d27ee0", // violet-fuchsia
|
|
50
|
+
pi: "#c3d94e", // acid yellow-green
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Card cwd: `~`-relative when under $HOME, shortened when still long. */
|
|
54
|
+
const HOME = homedir();
|
|
55
|
+
const CARD_CWD_MAX = 60;
|
|
56
|
+
function cardCwd(cwd: string): string {
|
|
57
|
+
const underHome = cwd === HOME || cwd.startsWith(HOME + "/") || cwd.startsWith(HOME + "\\");
|
|
58
|
+
const shown = underHome ? "~" + cwd.slice(HOME.length) : cwd;
|
|
59
|
+
return shown.length > CARD_CWD_MAX ? cwdShort(cwd) : shown;
|
|
60
|
+
}
|
|
61
|
+
|
|
29
62
|
interface AgentRowProps {
|
|
30
63
|
session: AgentSession;
|
|
31
64
|
cursor?: boolean;
|
|
32
65
|
/** Maximum chars for displayName before truncation. Default 40. */
|
|
33
66
|
nameMaxChars?: number;
|
|
67
|
+
/** `line` (default) for the dashboard strip, `card` for the fullscreen view. */
|
|
68
|
+
variant?: "line" | "card";
|
|
34
69
|
onClick?: () => void;
|
|
35
70
|
}
|
|
36
71
|
|
|
@@ -43,42 +78,113 @@ export function AgentRow(props: AgentRowProps) {
|
|
|
43
78
|
const n = props.session.displayName;
|
|
44
79
|
return n.length > nameMax() ? n.slice(0, nameMax() - 1) + "…" : n;
|
|
45
80
|
});
|
|
81
|
+
const model = createMemo(() => shortModel(props.session.model));
|
|
82
|
+
|
|
83
|
+
// Measured row width; undefined until the first layout pass.
|
|
84
|
+
const [width, setWidth] = createSignal<number | undefined>();
|
|
85
|
+
let rowRef: { width: number } | undefined;
|
|
86
|
+
const measure = () => {
|
|
87
|
+
if (rowRef && rowRef.width > 0) setWidth(rowRef.width);
|
|
88
|
+
};
|
|
89
|
+
onMount(() => setTimeout(measure, 0));
|
|
90
|
+
const sizeProps = {
|
|
91
|
+
ref: (r: { width: number }) => (rowRef = r),
|
|
92
|
+
onSizeChange: measure,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const line = createMemo(() =>
|
|
96
|
+
layoutLine(
|
|
97
|
+
{
|
|
98
|
+
name: displayName(),
|
|
99
|
+
branch: props.session.gitBranch,
|
|
100
|
+
model: model(),
|
|
101
|
+
cwd: props.session.cwdShort,
|
|
102
|
+
age: ageStr().padStart(3),
|
|
103
|
+
},
|
|
104
|
+
width(),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
const cardName = createMemo(() => layoutCardName(displayName(), width()));
|
|
108
|
+
const details = createMemo(() =>
|
|
109
|
+
layoutCardDetails([model(), props.session.gitBranch, cardCwd(props.session.cwd)], width()),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// cursor · status glyph · harness badge — shared by both variants.
|
|
113
|
+
const lead = () => [
|
|
114
|
+
<span style={{ fg: props.cursor ? T.accent : T.textDim }}>
|
|
115
|
+
{props.cursor ? "▶ " : " "}
|
|
116
|
+
</span>,
|
|
117
|
+
<span style={{ fg: STATUS_COLOR[props.session.status] }}>
|
|
118
|
+
{STATUS_GLYPH[props.session.status]}{" "}
|
|
119
|
+
</span>,
|
|
120
|
+
<span style={{ fg: HARNESS_COLOR[props.session.provider] }}>
|
|
121
|
+
{HARNESS[props.session.provider].code}{" "}
|
|
122
|
+
</span>,
|
|
123
|
+
];
|
|
46
124
|
|
|
47
125
|
return (
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
126
|
+
<Show
|
|
127
|
+
when={props.variant === "card"}
|
|
128
|
+
fallback={
|
|
129
|
+
<box
|
|
130
|
+
{...sizeProps}
|
|
131
|
+
style={{
|
|
132
|
+
flexDirection: "row",
|
|
133
|
+
paddingLeft: 1,
|
|
134
|
+
paddingRight: 1,
|
|
135
|
+
backgroundColor: props.cursor ? T.cardBgCursor : undefined,
|
|
136
|
+
}}
|
|
137
|
+
onMouseDown={props.onClick ? (() => props.onClick!()) : undefined}
|
|
138
|
+
>
|
|
139
|
+
{/* `truncate` stays on as a safety net only — layoutLine already
|
|
140
|
+
fits the fields to the measured width with a tail cut, while
|
|
141
|
+
OpenTUI's own truncation would elide mid-string. */}
|
|
142
|
+
<text style={{ flexGrow: 1, flexShrink: 1 }} wrapMode="none" truncate>
|
|
143
|
+
{lead()}
|
|
144
|
+
<span style={{ fg: T.text }}>{line().name}</span>
|
|
145
|
+
<Show when={line().branch}>
|
|
146
|
+
<span style={{ fg: T.warmDim }}>{" "}{line().branch}</span>
|
|
147
|
+
</Show>
|
|
148
|
+
<Show when={line().model}>
|
|
149
|
+
<span style={{ fg: T.textDim }}>{" "}{line().model}</span>
|
|
150
|
+
</Show>
|
|
151
|
+
</text>
|
|
152
|
+
{/* cwd + age pinned together on the right. The age is right-aligned in a
|
|
153
|
+
fixed-width field (pad to 3: "59m" / "23h" / "10d") so the END of each
|
|
154
|
+
cwd lands on the same column across rows — a 1- vs 2-digit age no
|
|
155
|
+
longer shoves the directory names out of vertical alignment. */}
|
|
156
|
+
<text style={{ flexShrink: 0 }} wrapMode="none">
|
|
157
|
+
<span style={{ fg: T.textDim }}>{" "}{line().right}</span>
|
|
158
|
+
</text>
|
|
159
|
+
</box>
|
|
160
|
+
}
|
|
56
161
|
>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
162
|
+
<box
|
|
163
|
+
{...sizeProps}
|
|
164
|
+
style={{
|
|
165
|
+
flexDirection: "column",
|
|
166
|
+
paddingLeft: 1,
|
|
167
|
+
paddingRight: 1,
|
|
168
|
+
backgroundColor: props.cursor ? T.cardBgCursor : undefined,
|
|
169
|
+
}}
|
|
170
|
+
onMouseDown={props.onClick ? (() => props.onClick!()) : undefined}
|
|
171
|
+
>
|
|
172
|
+
<box style={{ flexDirection: "row" }}>
|
|
173
|
+
<text style={{ flexGrow: 1, flexShrink: 1 }} wrapMode="none" truncate>
|
|
174
|
+
{lead()}
|
|
175
|
+
<span style={{ fg: T.text }}>{cardName()}</span>
|
|
176
|
+
</text>
|
|
177
|
+
<text style={{ flexShrink: 0 }} wrapMode="none">
|
|
178
|
+
<span style={{ fg: T.textDim }}>{" "}{ageStr().padStart(3)}</span>
|
|
179
|
+
</text>
|
|
180
|
+
</box>
|
|
181
|
+
{/* Indent = width of "▶ ● cc " so the details line up under the name. */}
|
|
182
|
+
<box style={{ flexDirection: "row", paddingLeft: 7 }}>
|
|
183
|
+
<text style={{ flexGrow: 1, flexShrink: 1 }} wrapMode="none" truncate>
|
|
184
|
+
<span style={{ fg: T.textDim }}>{details()}</span>
|
|
185
|
+
</text>
|
|
186
|
+
</box>
|
|
187
|
+
</box>
|
|
188
|
+
</Show>
|
|
83
189
|
);
|
|
84
190
|
}
|
package/src/ui/AgentsBar.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { For, Show, createEffect, createMemo } from "solid-js";
|
|
|
12
12
|
|
|
13
13
|
import { AgentRow } from "~/ui/AgentRow";
|
|
14
14
|
import { T } from "~/ui/glyphs";
|
|
15
|
+
import { HARNESS } from "~/store/agents";
|
|
15
16
|
import type { TuiStore } from "~/store/index";
|
|
16
17
|
|
|
17
18
|
interface ScrollBoxLike {
|
|
@@ -31,10 +32,14 @@ export function AgentsBar(props: AgentsBarProps) {
|
|
|
31
32
|
const isActive = () => props.store.state.ui.activeZone === "agents";
|
|
32
33
|
const agentRow = () => props.store.state.ui.row;
|
|
33
34
|
|
|
34
|
-
/** All visible (non-archived) sessions. */
|
|
35
|
+
/** All visible (non-archived) sessions, after the harness filter. */
|
|
35
36
|
const allShown = createMemo(() =>
|
|
36
|
-
props.store.
|
|
37
|
+
props.store.agentSessions().filter((s) => s.status !== "archived"),
|
|
37
38
|
);
|
|
39
|
+
const filterTag = () => {
|
|
40
|
+
const f = props.store.state.ui.agentsFilter;
|
|
41
|
+
return f === "all" ? "" : ` · ${HARNESS[f].code}`;
|
|
42
|
+
};
|
|
38
43
|
|
|
39
44
|
let scrollBoxRef: ScrollBoxLike | undefined;
|
|
40
45
|
|
|
@@ -68,7 +73,7 @@ export function AgentsBar(props: AgentsBarProps) {
|
|
|
68
73
|
paddingLeft: 1,
|
|
69
74
|
paddingRight: 1,
|
|
70
75
|
}}
|
|
71
|
-
title={`┤ Agents (live) · ${allShown().length} ├`}
|
|
76
|
+
title={`┤ Agents (live)${filterTag()} · ${allShown().length} ├`}
|
|
72
77
|
titleAlignment="left"
|
|
73
78
|
>
|
|
74
79
|
<Show
|
package/src/ui/BoardView.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* board width.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { For, Show, createEffect, createMemo, createSignal, onMount } from "solid-js";
|
|
11
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
12
12
|
|
|
13
13
|
import { isHiddenColumn } from "~/config/loader";
|
|
14
14
|
import { isTask } from "~/parser/markdown";
|
|
@@ -122,6 +122,11 @@ export function BoardView(props: BoardViewProps) {
|
|
|
122
122
|
const colIdx = ui().col;
|
|
123
123
|
if (singlePane() || ui().activeZone === "planner") {
|
|
124
124
|
setScrollX(0);
|
|
125
|
+
// Refresh viewportW too - the zoomed title budget depends on it.
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
const vw = viewportRef?.width ?? 0;
|
|
128
|
+
if (vw > 0) setViewportW(vw);
|
|
129
|
+
}, 0);
|
|
125
130
|
return;
|
|
126
131
|
}
|
|
127
132
|
const cols = props.board.columns;
|
|
@@ -178,6 +183,18 @@ export function BoardView(props: BoardViewProps) {
|
|
|
178
183
|
}, 0);
|
|
179
184
|
});
|
|
180
185
|
|
|
186
|
+
// Re-measure on resize while zoomed (the scroll effect above only
|
|
187
|
+
// reruns on col/zoomed/activeZone changes, not plain resizes).
|
|
188
|
+
const onResize = () => {
|
|
189
|
+
// 50ms, not 0ms: give OpenTUI's layout pass time to catch up.
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
const vw = viewportRef?.width ?? 0;
|
|
192
|
+
if (vw > 0) setViewportW(vw);
|
|
193
|
+
}, 50);
|
|
194
|
+
};
|
|
195
|
+
process.stdout.on("resize", onResize);
|
|
196
|
+
onCleanup(() => process.stdout.off("resize", onResize));
|
|
197
|
+
|
|
181
198
|
return (
|
|
182
199
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
183
200
|
{/* Clipping viewport: fills the board zone, hides overflow. */}
|
|
@@ -216,6 +233,7 @@ export function BoardView(props: BoardViewProps) {
|
|
|
216
233
|
zoomed={singlePane() && isActive()}
|
|
217
234
|
tasksVisible={columnTasksVisible(i())}
|
|
218
235
|
boxId={columnId(props.board.filepath, originalIndex)}
|
|
236
|
+
viewportWidth={viewportW()}
|
|
219
237
|
/>
|
|
220
238
|
);
|
|
221
239
|
}}
|
|
@@ -245,6 +263,9 @@ interface ColumnViewProps {
|
|
|
245
263
|
tasksVisible?: boolean;
|
|
246
264
|
/** Stable DOM-equivalent id used by `scrollChildIntoView`. */
|
|
247
265
|
boxId: string;
|
|
266
|
+
/** Real measured board viewport width in cells, used to size the
|
|
267
|
+
* title budget for a zoomed column instead of a hardcoded guess. */
|
|
268
|
+
viewportWidth?: number;
|
|
248
269
|
}
|
|
249
270
|
|
|
250
271
|
/** Stable id for a task row inside a column, used by scrollChildIntoView. */
|
|
@@ -395,9 +416,19 @@ function ColumnView(props: ColumnViewProps) {
|
|
|
395
416
|
}
|
|
396
417
|
// Column inner cell width for a TaskRow: COL_WIDTH 42 −
|
|
397
418
|
// border 2 − col padding 2 − TaskRow padding 2 = 36 cols
|
|
398
|
-
// (when not zoomed). Zoomed
|
|
399
|
-
//
|
|
400
|
-
|
|
419
|
+
// (when not zoomed). Zoomed: use the real measured
|
|
420
|
+
// viewport width, same 6-cell overhead, plus a 2-cell
|
|
421
|
+
// safety margin for width-measurement drift (fixes a
|
|
422
|
+
// double-truncation glitch, e.g. "walk t...gh backgro...").
|
|
423
|
+
availableWidth={
|
|
424
|
+
props.zoomed
|
|
425
|
+
? Math.max(6, (props.viewportWidth ?? 42) - 8)
|
|
426
|
+
: 36
|
|
427
|
+
}
|
|
428
|
+
// Default hardCap (60 chars) was clipping titles well
|
|
429
|
+
// short of the zoomed column's real width. Let
|
|
430
|
+
// availableWidth alone govern the budget when zoomed.
|
|
431
|
+
titleMaxChars={props.zoomed ? 1000 : undefined}
|
|
401
432
|
onClick={() => {
|
|
402
433
|
props.store.setActiveZone("board");
|
|
403
434
|
props.store.setCursor(props.columnIndex, ri());
|