@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 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))
@@ -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, availabilityLabel, epicProgressCaption, epicProgress, statusLabel, statusTone, } from "./data.js";
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
- // Whoever is working comes first. An idle registry is what you scroll past.
110
- const ordered = [...board.agents].sort((left, right) => {
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: `${board.agents.filter((a) => a.enabled).length} on` }, "h"),
117
- ...(board.agents.length === 0
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((agent) => {
123
- const run = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
124
- const ticket = run?.ticket_id
125
- ? board.tickets.find((item) => item.id === run.ticket_id)
126
- : undefined;
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: ordered.length - shown.length }, "more"),
125
+ _jsx(More, { count: displayRows.length - shown.length }, "more"),
136
126
  ] }));
137
127
  }
138
128
  const KIND_COLOR = {
@@ -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, availabilityLabel, statusLabel, statusTone, } from "./data.js";
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 shown = board.agents.slice(0, contentRows(rows, board.agents.length));
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: `${board.agents.length}` }, "h"),
19
- ...(board.agents.length === 0
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((agent) => {
25
- const run = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
26
- const ticket = run?.ticket_id
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: board.agents.length - shown.length }, "more"),
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 shown = board.decisions.slice(0, contentRows(rows, board.decisions.length));
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: `${board.decisions.length}` }, "h"),
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
- ...shown.map((decision, index) => {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"