@higherdev/cli 0.12.1 → 0.13.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/dist/tui/App.js +21 -3
- package/dist/tui/Dashboard.js +12 -22
- package/dist/tui/Panels.js +67 -22
- package/dist/tui/agent-rows.js +57 -0
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Bubble } from "./Bubble.js";
|
|
|
10
10
|
import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js";
|
|
11
11
|
import { DecisionPanel, decisionRows } from "./Decision.js";
|
|
12
12
|
import { COMMANDS, Help } from "./Help.js";
|
|
13
|
-
import { AgentsPanel, BoardPanel, FeedPanel, InboxPanel, TicketPanel } from "./Panels.js";
|
|
13
|
+
import { AgentsPanel, BoardPanel, FeedPanel, InboxPanel, TicketPanel, inboxEntries } from "./Panels.js";
|
|
14
14
|
import { SettingsPanel } from "./Settings.js";
|
|
15
15
|
import { Splash } from "./Splash.js";
|
|
16
16
|
import TextInput from "./TextInput.js";
|
|
@@ -50,6 +50,7 @@ export function App({ initial }) {
|
|
|
50
50
|
const [started, setStarted] = useState(false);
|
|
51
51
|
const [field, setField] = useState(null);
|
|
52
52
|
const [editing, setEditing] = useState(null);
|
|
53
|
+
const [inboxFocus, setInboxFocus] = useState(0);
|
|
53
54
|
const selectedRef = useRef(null);
|
|
54
55
|
const fieldRef = useRef(null);
|
|
55
56
|
const editingRef = useRef(null);
|
|
@@ -117,6 +118,7 @@ export function App({ initial }) {
|
|
|
117
118
|
const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
|
|
118
119
|
const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
|
|
119
120
|
const configuring = view === "settings" && !editing;
|
|
121
|
+
const inbox = useMemo(() => inboxEntries(board, width), [board, width]);
|
|
120
122
|
const moveCursor = useCallback((delta) => {
|
|
121
123
|
if (!order.length)
|
|
122
124
|
return false;
|
|
@@ -135,6 +137,12 @@ export function App({ initial }) {
|
|
|
135
137
|
setField(next);
|
|
136
138
|
return true;
|
|
137
139
|
}, [settingsOrder]);
|
|
140
|
+
const moveInbox = useCallback((delta) => {
|
|
141
|
+
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current + delta)));
|
|
142
|
+
}, [inbox.length]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current)));
|
|
145
|
+
}, [inbox.length]);
|
|
138
146
|
const refresh = useCallback(async () => {
|
|
139
147
|
await refreshRef.current?.();
|
|
140
148
|
}, []);
|
|
@@ -274,6 +282,8 @@ export function App({ initial }) {
|
|
|
274
282
|
return;
|
|
275
283
|
case "view":
|
|
276
284
|
setView(action.view);
|
|
285
|
+
if (action.view === "inbox")
|
|
286
|
+
setInboxFocus(0);
|
|
277
287
|
if (action.view === "board" && order.length) {
|
|
278
288
|
const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
|
|
279
289
|
selectedRef.current = next;
|
|
@@ -545,9 +555,9 @@ export function App({ initial }) {
|
|
|
545
555
|
if (item.message.panel === "help")
|
|
546
556
|
return _jsx(Help, { width: width }, item.key);
|
|
547
557
|
return _jsx(Bubble, { message: item.message, width: width }, item.key);
|
|
548
|
-
} }), splash ? (_jsx(Splash, { columns: columns, rows: rows, width: width, ready: ready, helpFull: plan.helpFull, animate: plan.fits, onDone: () => setReady(true) })) : null, _jsxs(Box, { flexDirection: "column", width: width, children: [view === "board" && plan.panels > 0 ? _jsx(BoardPanel, { board: board, width: width, rows: plan.panels, cursor: cursor }) : null, view === "agents" && plan.panels > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentsPanel, { board: board, width: width, rows: agentsView.top }), agentsView.bottom > 0 ? _jsxs(_Fragment, { children: [_jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: agentsView.bottom, live: running > 0 })] }) : null] })) : null, view === "feed" && plan.panels > 0 ? _jsx(FeedPanel, { entries: feed, width: width, rows: plan.panels }) : null, view === "settings" && plan.panels > 0 ? _jsx(SettingsPanel, { entries: settings, width: width, rows: plan.panels, title: "Settings", cursor: field, editing: editing }) : null, view === "inbox" && plan.panels > 0 ? _jsx(InboxPanel, { board: board, width: width, rows: plan.panels }) : null, view === "ticket" && plan.panels > 0 ? ticket
|
|
558
|
+
} }), splash ? (_jsx(Splash, { columns: columns, rows: rows, width: width, ready: ready, helpFull: plan.helpFull, animate: plan.fits, onDone: () => setReady(true) })) : null, _jsxs(Box, { flexDirection: "column", width: width, children: [view === "board" && plan.panels > 0 ? _jsx(BoardPanel, { board: board, width: width, rows: plan.panels, cursor: cursor }) : null, view === "agents" && plan.panels > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentsPanel, { board: board, width: width, rows: agentsView.top }), agentsView.bottom > 0 ? _jsxs(_Fragment, { children: [_jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: agentsView.bottom, live: running > 0 })] }) : null] })) : null, view === "feed" && plan.panels > 0 ? _jsx(FeedPanel, { entries: feed, width: width, rows: plan.panels }) : null, view === "settings" && plan.panels > 0 ? _jsx(SettingsPanel, { entries: settings, width: width, rows: plan.panels, title: "Settings", cursor: field, editing: editing }) : null, view === "inbox" && plan.panels > 0 ? _jsx(InboxPanel, { board: board, width: width, rows: plan.panels, focus: inboxFocus }) : null, view === "ticket" && plan.panels > 0 ? ticket
|
|
549
559
|
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels })
|
|
550
|
-
: _jsxs(Text, { color: UI.warn, children: ["No ticket ", ticketKey, " here."] }) : null, view === "home" && plan.cockpit > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Cockpit, { board: board, width: width, rows: plan.cockpit, cursor: cursor }), _jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: plan.stream, live: running > 0 })] })) : null, inFlight.map((message) => _jsx(Bubble, { message: message, width: width }, message.id)), _jsx(DecisionPanel, { decisions: decisions, board: board, width: width, rows: plan.decision }), notice ? _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: UI.warn, children: notice }) }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI.text, bold: true, children: workspace.slug }), _jsxs(Text, { color: UI.dim, children: [" ", workspace.repo, " "] }), _jsx(Text, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }), _jsxs(Text, { color: UI.dim, children: [live === "live" ? "5s poll" : live, " "] }), _jsx(Text, { color: UI.dim, children: running ? `${running} running ` : "" }), board.decisions.length ? _jsxs(Text, { color: UI.warn, children: [board.decisions.length, " decisions "] }) : null, workspace.paused ? _jsx(Text, { color: UI.warn, children: "paused " }) : null, _jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["\u00B7 ", mode, cursor && selected ? ` ${selected.key} ↑↓ move · enter opens · esc leaves` : ""] })] }), _jsx(Box, { children: _jsx(TextInput, { value: draft, onChange: (next) => {
|
|
560
|
+
: _jsxs(Text, { color: UI.warn, children: ["No ticket ", ticketKey, " here."] }) : null, view === "home" && plan.cockpit > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Cockpit, { board: board, width: width, rows: plan.cockpit, cursor: cursor }), _jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: plan.stream, live: running > 0 })] })) : null, inFlight.map((message) => _jsx(Bubble, { message: message, width: width }, message.id)), _jsx(DecisionPanel, { decisions: decisions, board: board, width: width, rows: plan.decision }), notice ? _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: UI.warn, children: notice }) }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI.text, bold: true, children: workspace.slug }), _jsxs(Text, { color: UI.dim, children: [" ", workspace.repo, " "] }), _jsx(Text, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }), _jsxs(Text, { color: UI.dim, children: [live === "live" ? "5s poll" : live, " "] }), _jsx(Text, { color: UI.dim, children: running ? `${running} running ` : "" }), board.decisions.length ? _jsxs(Text, { color: UI.warn, children: [board.decisions.length, " decisions "] }) : null, workspace.paused ? _jsx(Text, { color: UI.warn, children: "paused " }) : null, _jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["\u00B7 ", mode, view === "inbox" ? " ↑↓ scroll" : cursor && selected ? ` ${selected.key} ↑↓ move · enter opens · esc leaves` : ""] })] }), _jsx(Box, { children: _jsx(TextInput, { value: draft, onChange: (next) => {
|
|
551
561
|
setDraft(next);
|
|
552
562
|
if (editingRef.current)
|
|
553
563
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
@@ -566,6 +576,10 @@ export function App({ initial }) {
|
|
|
566
576
|
setCursor(null);
|
|
567
577
|
selectedRef.current = null;
|
|
568
578
|
}, onUp: () => {
|
|
579
|
+
if (view === "inbox" && !draft) {
|
|
580
|
+
moveInbox(-1);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
569
583
|
if (configuring && moveField(-1))
|
|
570
584
|
return;
|
|
571
585
|
if (browsing && moveCursor(-1))
|
|
@@ -575,6 +589,10 @@ export function App({ initial }) {
|
|
|
575
589
|
historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
|
|
576
590
|
setDraft(history.current[historyAt.current] ?? "");
|
|
577
591
|
}, onDown: () => {
|
|
592
|
+
if (view === "inbox" && !draft) {
|
|
593
|
+
moveInbox(1);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
578
596
|
if (configuring && moveField(1))
|
|
579
597
|
return;
|
|
580
598
|
if (browsing && moveCursor(1))
|
package/dist/tui/Dashboard.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { BOARD_COLUMNS,
|
|
3
|
+
import { BOARD_COLUMNS, epicProgressCaption, epicProgress, statusLabel, statusTone, } from "./data.js";
|
|
4
4
|
import { elapsed, truncate } from "../out/format.js";
|
|
5
5
|
import { inkColor } from "../out/theme.js";
|
|
6
6
|
import { UI } from "./theme.js";
|
|
7
7
|
import { BoundedPanel as Panel, Heading, More, contentRows } from "./bounded.js";
|
|
8
|
+
import { agentDisplayRows } from "./agent-rows.js";
|
|
8
9
|
const DOT = "●";
|
|
9
10
|
/**
|
|
10
11
|
* The width at which the board and the agents stop competing for the same
|
|
@@ -106,33 +107,22 @@ export function BoardColumn({ board, width, rows, cursor, }) {
|
|
|
106
107
|
] }));
|
|
107
108
|
}
|
|
108
109
|
export function AgentsColumn({ board, width, rows, }) {
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const busy = (id) => board.runs.some((run) => run.agent_id === id && run.status === "running") ? 0 : 1;
|
|
112
|
-
return busy(left.id) - busy(right.id) || left.display_name.localeCompare(right.display_name);
|
|
113
|
-
});
|
|
114
|
-
const shown = ordered.slice(0, contentRows(rows, ordered.length));
|
|
110
|
+
const displayRows = agentDisplayRows(board);
|
|
111
|
+
const shown = displayRows.slice(0, contentRows(rows, displayRows.length));
|
|
115
112
|
return (_jsx(Panel, { width: width, rows: rows, children: [
|
|
116
|
-
_jsx(Heading, { text: "Agents", note: `${
|
|
117
|
-
...(
|
|
113
|
+
_jsx(Heading, { text: "Agents", note: `${displayRows.length}` }, "h"),
|
|
114
|
+
...(displayRows.length === 0
|
|
118
115
|
? [
|
|
119
116
|
_jsx(Text, { color: UI.dim, children: "No agents yet." }, "empty"),
|
|
120
117
|
]
|
|
121
118
|
: []),
|
|
122
|
-
...shown.map((
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const availability = board.availability.find((row) => row.provider === agent.provider);
|
|
128
|
-
const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
|
|
129
|
-
const tone = !agent.enabled ? "muted" : run ? "blue" : blocked ? "warning" : "muted";
|
|
130
|
-
const name = Math.max(8, Math.min(22, width - 14));
|
|
131
|
-
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Box, { width: name, flexShrink: 0, children: _jsx(Text, { color: UI.text, wrap: "truncate", children: truncate(agent.display_name, name - 1) }) }), _jsx(Text, { color: UI.dim, wrap: "truncate", children: run
|
|
132
|
-
? `${ticket ? `${ticket.key} ` : ""}${elapsed(run.started_at ?? run.created_at)}`
|
|
133
|
-
: blocked || (agent.enabled ? "idle" : "offline") })] }, agent.id));
|
|
119
|
+
...shown.map((row) => {
|
|
120
|
+
const tone = row.run ? "blue" : row.state === "offline" ? "warning" : "muted";
|
|
121
|
+
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, _jsx(Text, { color: UI.dim, children: row.run
|
|
122
|
+
? ` · ${row.ticket?.key ?? row.run.kind} ${elapsed(row.run.started_at ?? row.run.created_at)}`
|
|
123
|
+
: ` · ${row.state}` })] })] }, row.key));
|
|
134
124
|
}),
|
|
135
|
-
_jsx(More, { count:
|
|
125
|
+
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
136
126
|
] }));
|
|
137
127
|
}
|
|
138
128
|
const KIND_COLOR = {
|
package/dist/tui/Panels.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { BOARD_COLUMNS,
|
|
3
|
+
import { BOARD_COLUMNS, statusLabel, statusTone, } from "./data.js";
|
|
4
4
|
import { elapsed, pad, relativeTime, truncate } from "../out/format.js";
|
|
5
5
|
import { inkColor } from "../out/theme.js";
|
|
6
6
|
import { UI } from "./theme.js";
|
|
7
7
|
import { BoundedPanel as Panel, Heading, More, contentRows } from "./bounded.js";
|
|
8
|
+
import { scrollWindow } from "./Dashboard.js";
|
|
9
|
+
import { agentDisplayRows } from "./agent-rows.js";
|
|
8
10
|
const DOT = "●";
|
|
9
11
|
/**
|
|
10
12
|
* The single-purpose views behind /board, /agents, /feed and /inbox. Each is
|
|
@@ -13,25 +15,20 @@ const DOT = "●";
|
|
|
13
15
|
* `bounded.tsx` for why the bound is structural rather than arithmetic.
|
|
14
16
|
*/
|
|
15
17
|
export function AgentsPanel({ board, width = 80, rows = 12, }) {
|
|
16
|
-
const
|
|
18
|
+
const displayRows = agentDisplayRows(board);
|
|
19
|
+
const shown = displayRows.slice(0, contentRows(rows, displayRows.length));
|
|
17
20
|
return (_jsx(Panel, { width: width, rows: rows, children: [
|
|
18
|
-
_jsx(Heading, { text: "Agents", note: `${
|
|
19
|
-
...(
|
|
21
|
+
_jsx(Heading, { text: "Agents", note: `${displayRows.length}` }, "h"),
|
|
22
|
+
...(displayRows.length === 0
|
|
20
23
|
? [
|
|
21
24
|
_jsx(Text, { color: UI.dim, children: "No agents yet." }, "empty"),
|
|
22
25
|
]
|
|
23
26
|
: []),
|
|
24
|
-
...shown.map((
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
? board.tickets.find((item) => item.id === run.ticket_id)
|
|
28
|
-
: undefined;
|
|
29
|
-
const availability = board.availability.find((row) => row.provider === agent.provider);
|
|
30
|
-
const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
|
|
31
|
-
const tone = !agent.enabled ? "muted" : run ? "blue" : blocked ? "warning" : "muted";
|
|
32
|
-
return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Text, { color: UI.text, children: pad(truncate(agent.display_name, 13), 14) }), _jsx(Text, { color: UI.dim, children: pad(agent.role, 13) }), _jsx(Text, { color: UI.dim, children: pad(truncate(agent.model, 23), 24) }), _jsx(Text, { color: UI.text, children: pad(run ? "running" : blocked ? "blocked" : "idle", 9) }), _jsxs(Text, { color: UI.dim, children: [ticket ? `${ticket.key} ` : "", run ? elapsed(run.started_at ?? run.created_at) : blocked] })] }, agent.id));
|
|
27
|
+
...shown.map((row) => {
|
|
28
|
+
const tone = row.run ? "blue" : row.state === "offline" ? "warning" : "muted";
|
|
29
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Text, { color: UI.text, children: pad(truncate(row.name, 13), 14) }), _jsx(Text, { color: UI.dim, children: pad(row.agent?.role ?? row.run?.kind ?? "", 13) }), _jsx(Text, { color: UI.dim, children: pad(truncate(row.agent?.model ?? "", 23), 24) }), _jsx(Text, { color: UI.text, children: pad(row.state, 9) }), _jsxs(Text, { color: UI.dim, children: [row.ticket ? `${row.ticket.key} ` : "", row.run ? elapsed(row.run.started_at ?? row.run.created_at) : ""] })] }, row.key));
|
|
33
30
|
}),
|
|
34
|
-
_jsx(More, { count:
|
|
31
|
+
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
35
32
|
] }));
|
|
36
33
|
}
|
|
37
34
|
export function BoardPanel({ board, width = 80, rows = 12, cursor, }) {
|
|
@@ -86,22 +83,70 @@ export function FeedPanel({ entries, width = 80, rows = 12, }) {
|
|
|
86
83
|
* The whole queue, one row each, numbered the way `/decide` reaches them: the
|
|
87
84
|
* one on screen is 1.
|
|
88
85
|
*/
|
|
89
|
-
export function InboxPanel({ board, width = 80, rows = 12, }) {
|
|
90
|
-
const
|
|
86
|
+
export function InboxPanel({ board, width = 80, rows = 12, focus = 0, }) {
|
|
87
|
+
const entries = inboxEntries(board, width);
|
|
88
|
+
const inner = Math.max(0, rows - 1);
|
|
89
|
+
const window = scrollWindow(entries.length, inner, focus);
|
|
90
|
+
const hiddenAbove = window.start;
|
|
91
|
+
const hiddenBelow = entries.length - window.end;
|
|
92
|
+
const note = `${board.decisions.length}${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
|
|
91
93
|
return (_jsx(Panel, { width: width, rows: rows, children: [
|
|
92
|
-
_jsx(Heading, { text: "Decisions", note:
|
|
94
|
+
_jsx(Heading, { text: "Decisions", note: note }, "h"),
|
|
93
95
|
...(board.decisions.length === 0
|
|
94
96
|
? [
|
|
95
97
|
_jsx(Text, { color: UI.dim, children: "Nothing waiting on you." }, "empty"),
|
|
96
98
|
]
|
|
97
99
|
: []),
|
|
98
|
-
...
|
|
99
|
-
const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
|
|
100
|
-
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: index === 0 ? UI.warn : UI.dim, children: pad(`${index + 1})`, 3) }), _jsx(Text, { color: UI.dim, children: pad(ticket?.key ?? decision.id.slice(0, 8), 9) }), _jsx(Text, { color: UI.text, children: truncate(decision.question_md, Math.max(12, width - 14)) })] }, decision.id));
|
|
101
|
-
}),
|
|
102
|
-
_jsx(More, { count: board.decisions.length - shown.length }, "more"),
|
|
100
|
+
...entries.slice(window.start, window.end).map((entry, index) => (_jsx(Text, { color: entry.kind === "header" ? UI.warn : UI.text, wrap: "truncate", inverse: window.start + index === focus, children: entry.kind === "body" ? ` ${entry.text}` : entry.text }, entry.key))),
|
|
103
101
|
] }));
|
|
104
102
|
}
|
|
103
|
+
/** Every physical line in /inbox, with decision bodies wrapped but never clipped. */
|
|
104
|
+
export function inboxEntries(board, width) {
|
|
105
|
+
const entries = [];
|
|
106
|
+
const bodyWidth = Math.max(12, width - 2);
|
|
107
|
+
board.decisions.forEach((decision, index) => {
|
|
108
|
+
const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
|
|
109
|
+
entries.push({
|
|
110
|
+
key: `${decision.id}:header`,
|
|
111
|
+
kind: "header",
|
|
112
|
+
text: `${index + 1}) ${ticket?.key ?? decision.id.slice(0, 8)}`,
|
|
113
|
+
});
|
|
114
|
+
wrapLines(decision.question_md.trim(), bodyWidth).forEach((text, line) => {
|
|
115
|
+
entries.push({ key: `${decision.id}:body:${line}`, kind: "body", text });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
function wrapLines(text, width) {
|
|
121
|
+
if (!text)
|
|
122
|
+
return [""];
|
|
123
|
+
const lines = [];
|
|
124
|
+
for (const paragraph of text.split("\n")) {
|
|
125
|
+
if (!paragraph) {
|
|
126
|
+
lines.push("");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let line = "";
|
|
130
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
131
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
132
|
+
if (candidate.length <= width) {
|
|
133
|
+
line = candidate;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (line)
|
|
137
|
+
lines.push(line);
|
|
138
|
+
let rest = word;
|
|
139
|
+
while (rest.length > width) {
|
|
140
|
+
lines.push(rest.slice(0, width));
|
|
141
|
+
rest = rest.slice(width);
|
|
142
|
+
}
|
|
143
|
+
line = rest;
|
|
144
|
+
}
|
|
145
|
+
if (line)
|
|
146
|
+
lines.push(line);
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
105
150
|
/**
|
|
106
151
|
* One ticket in full, inside the rows it was given.
|
|
107
152
|
*
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const roleForKind = {
|
|
2
|
+
architect: "architect",
|
|
3
|
+
build: "builder",
|
|
4
|
+
followup: "builder",
|
|
5
|
+
orchestrate: "orchestrator",
|
|
6
|
+
review: "reviewer",
|
|
7
|
+
};
|
|
8
|
+
function agentForRun(board, run) {
|
|
9
|
+
if (run.agent_id)
|
|
10
|
+
return board.agents.find((agent) => agent.id === run.agent_id) ?? null;
|
|
11
|
+
const role = roleForKind[run.kind];
|
|
12
|
+
return board.agents.find((agent) => agent.enabled && agent.role === role && agent.provider === run.provider) ?? null;
|
|
13
|
+
}
|
|
14
|
+
/** Expand live invocations into rows, then append enabled agents with no live run. */
|
|
15
|
+
export function agentDisplayRows(board) {
|
|
16
|
+
const activeAgents = new Set();
|
|
17
|
+
const live = board.runs
|
|
18
|
+
.filter((run) => run.status === "running" || run.status === "queued")
|
|
19
|
+
.slice()
|
|
20
|
+
.sort((left, right) => {
|
|
21
|
+
if (left.status !== right.status)
|
|
22
|
+
return left.status === "running" ? -1 : 1;
|
|
23
|
+
const leftAt = left.started_at ?? left.created_at;
|
|
24
|
+
const rightAt = right.started_at ?? right.created_at;
|
|
25
|
+
return leftAt.localeCompare(rightAt) || left.id.localeCompare(right.id);
|
|
26
|
+
})
|
|
27
|
+
.map((run) => {
|
|
28
|
+
const agent = agentForRun(board, run);
|
|
29
|
+
if (agent)
|
|
30
|
+
activeAgents.add(agent.id);
|
|
31
|
+
const ticket = run.ticket_id
|
|
32
|
+
? board.tickets.find((row) => row.id === run.ticket_id) ?? null
|
|
33
|
+
: null;
|
|
34
|
+
return {
|
|
35
|
+
key: run.id,
|
|
36
|
+
name: agent?.display_name ?? run.kind,
|
|
37
|
+
agent,
|
|
38
|
+
run,
|
|
39
|
+
ticket,
|
|
40
|
+
state: run.status,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const idle = board.agents
|
|
44
|
+
.filter((agent) => agent.enabled && !activeAgents.has(agent.id))
|
|
45
|
+
.sort((left, right) => left.display_name.localeCompare(right.display_name))
|
|
46
|
+
.map((agent) => ({
|
|
47
|
+
key: `idle:${agent.id}`,
|
|
48
|
+
name: agent.display_name,
|
|
49
|
+
agent,
|
|
50
|
+
run: null,
|
|
51
|
+
ticket: null,
|
|
52
|
+
state: board.availability.some((row) => row.provider === agent.provider && !row.available)
|
|
53
|
+
? "offline"
|
|
54
|
+
: "idle",
|
|
55
|
+
}));
|
|
56
|
+
return [...live, ...idle];
|
|
57
|
+
}
|