@higherdev/cli 0.4.0 → 0.6.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.
@@ -0,0 +1,441 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
4
+ import { Banner } from "./Banner.js";
5
+ import { Bubble } from "./Bubble.js";
6
+ import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js";
7
+ import { DecisionPanel, decisionRows } from "./Decision.js";
8
+ import { COMMANDS, Help } from "./Help.js";
9
+ import { AgentsPanel, BoardPanel, FeedPanel, InboxPanel, TicketPanel } from "./Panels.js";
10
+ import { SettingsPanel } from "./Settings.js";
11
+ import { Splash } from "./Splash.js";
12
+ import TextInput from "./TextInput.js";
13
+ import { alertOnce } from "./alert.js";
14
+ import { bubbleRows } from "./height.js";
15
+ import { planLayout, splitPanels } from "./layout.js";
16
+ import { parseLine } from "./parse.js";
17
+ import { configuredSlugs, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
18
+ import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
19
+ import { appendLines, runLabels, toStreamLines } from "./stream.js";
20
+ import { UI } from "./theme.js";
21
+ import { WorkspaceLoads } from "./workspace-load.js";
22
+ let messageSeq = 0;
23
+ const nextId = () => `m${messageSeq++}`;
24
+ export function App({ initial }) {
25
+ const { exit } = useApp();
26
+ const { stdout } = useStdout();
27
+ const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
28
+ const rows = stdout?.rows && stdout.rows > 0 ? stdout.rows : 24;
29
+ const width = Math.max(48, Math.min(columns - 1, 120));
30
+ const [config, setConfig] = useState(initial.config);
31
+ const [workspace, setWorkspace] = useState(initial.workspace);
32
+ const [board, setBoard] = useState(initial.board);
33
+ const [feed, setFeed] = useState(initial.feed);
34
+ const [mode, setMode] = useState("browse");
35
+ const [view, setView] = useState("home");
36
+ const [live, setLive] = useState("connecting");
37
+ const [messages, setMessages] = useState([]);
38
+ const [draft, setDraft] = useState("");
39
+ const [busy, setBusy] = useState(false);
40
+ const [notice, setNotice] = useState(null);
41
+ const [ticketKey, setTicketKey] = useState(null);
42
+ const [ready, setReady] = useState(false);
43
+ const [stream, setStream] = useState([]);
44
+ const [cursor, setCursor] = useState(null);
45
+ const [started, setStarted] = useState(false);
46
+ const [field, setField] = useState(null);
47
+ const [editing, setEditing] = useState(null);
48
+ const selectedRef = useRef(null);
49
+ const fieldRef = useRef(null);
50
+ const editingRef = useRef(null);
51
+ const history = useRef([]);
52
+ const historyAt = useRef(-1);
53
+ const refreshRef = useRef(null);
54
+ const loads = useRef(new WorkspaceLoads(initial.workspace.id));
55
+ editingRef.current = editing;
56
+ useEffect(() => {
57
+ if (messages.length > 0 || view !== "home")
58
+ setStarted(true);
59
+ }, [messages.length, view]);
60
+ useEffect(() => {
61
+ if (!ready)
62
+ return;
63
+ const live = planLayout({
64
+ rows,
65
+ columns,
66
+ width,
67
+ splash: false,
68
+ ready: true,
69
+ decision: decisionRows(board.decisions),
70
+ inFlight: 0,
71
+ notice: false,
72
+ home: true,
73
+ });
74
+ if (live.cockpit > 0)
75
+ setStarted(true);
76
+ }, [ready, rows, columns, width, board.decisions]);
77
+ const applySnapshot = useCallback((snapshot) => {
78
+ const token = loads.current.start(snapshot.workspace.id);
79
+ if (!loads.current.isCurrent(token))
80
+ return;
81
+ setWorkspace(snapshot.workspace);
82
+ setBoard(snapshot.board);
83
+ setFeed(snapshot.feed);
84
+ }, []);
85
+ useEffect(() => {
86
+ setLive("connecting");
87
+ const polling = pollSnapshot(config, applySnapshot, setLive, (error) => setNotice(error instanceof Error ? error.message : String(error)));
88
+ refreshRef.current = polling.refresh;
89
+ return polling.close;
90
+ }, [config, applySnapshot]);
91
+ const labels = useMemo(() => runLabels(board), [board]);
92
+ const liveRunIds = [...labels.keys()].sort().join(",");
93
+ useEffect(() => {
94
+ if (!liveRunIds) {
95
+ setStream([]);
96
+ return;
97
+ }
98
+ const token = loads.current.start(workspace.id);
99
+ void loadLiveEvents(config, board)
100
+ .then((events) => {
101
+ if (!loads.current.isCurrent(token))
102
+ return;
103
+ setStream((prior) => appendLines(prior, toStreamLines(events, labels)));
104
+ })
105
+ .catch(() => { });
106
+ }, [liveRunIds, board, config, labels, workspace.id]);
107
+ const say = useCallback((speaker, body, steps) => {
108
+ setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
109
+ }, []);
110
+ const order = useMemo(() => boardTicketIds(board), [board]);
111
+ const settings = useMemo(() => settingsRows(workspace, board.agents), [workspace, board.agents]);
112
+ const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
113
+ const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
114
+ const configuring = view === "settings" && !editing;
115
+ const moveCursor = useCallback((delta) => {
116
+ if (!order.length)
117
+ return false;
118
+ const next = nextCursor(order, selectedRef.current, delta);
119
+ if (!next)
120
+ return false;
121
+ selectedRef.current = next;
122
+ setCursor(next);
123
+ return true;
124
+ }, [order]);
125
+ const moveField = useCallback((delta) => {
126
+ const next = nextCursor(settingsOrder, fieldRef.current, delta);
127
+ if (!next)
128
+ return false;
129
+ fieldRef.current = next;
130
+ setField(next);
131
+ return true;
132
+ }, [settingsOrder]);
133
+ const refresh = useCallback(async () => {
134
+ await refreshRef.current?.();
135
+ }, []);
136
+ const applyEdit = useCallback(async (key, raw) => {
137
+ const row = settings.find((entry) => entry.key === key);
138
+ if (!row)
139
+ return;
140
+ const edit = editFor(row, raw);
141
+ if (!edit.ok) {
142
+ setNotice(edit.error);
143
+ return;
144
+ }
145
+ setBusy(true);
146
+ try {
147
+ if (edit.value.target === "cap") {
148
+ await updateProviderCap(config, edit.value.provider, edit.value.cap);
149
+ }
150
+ else {
151
+ await updateAgent(config, edit.value.id, edit.value.fields);
152
+ }
153
+ setNotice(null);
154
+ await refresh();
155
+ }
156
+ catch (error) {
157
+ setNotice(error instanceof Error ? error.message : String(error));
158
+ }
159
+ finally {
160
+ setBusy(false);
161
+ }
162
+ }, [settings, config, refresh]);
163
+ const changeWorkspace = useCallback(async (slug) => {
164
+ setBusy(true);
165
+ try {
166
+ const snapshot = await switchWorkspace(slug, config);
167
+ loads.current.switchTo(snapshot.workspace.id);
168
+ setConfig(snapshot.config);
169
+ setWorkspace(snapshot.workspace);
170
+ setBoard(snapshot.board);
171
+ setFeed(snapshot.feed);
172
+ setStream([]);
173
+ setCursor(null);
174
+ selectedRef.current = null;
175
+ setView("home");
176
+ setTicketKey(null);
177
+ setNotice(null);
178
+ say("system", `Now on ${snapshot.workspace.slug} (${snapshot.workspace.repo}).`);
179
+ }
180
+ catch (error) {
181
+ setNotice(error instanceof Error ? error.message : String(error));
182
+ }
183
+ finally {
184
+ setBusy(false);
185
+ }
186
+ }, [say]);
187
+ const askOrchestrator = useCallback(async (text) => {
188
+ setBusy(true);
189
+ const id = nextId();
190
+ setMessages((prior) => [...prior, { id, speaker: "orchestrator", body: "", pending: true }]);
191
+ const since = new Date().toISOString();
192
+ try {
193
+ await postOrchestrator(text, config);
194
+ setMessages((prior) => prior.map((message) => message.id === id
195
+ ? { ...message, steps: ["· queued, it answers on the next tick"] }
196
+ : message));
197
+ const { waitForReply } = await import("./data.js");
198
+ const reply = await waitForReply(config, since, 180_000);
199
+ setMessages((prior) => prior.map((message) => message.id === id
200
+ ? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
201
+ : message));
202
+ }
203
+ catch (error) {
204
+ const body = error instanceof Error ? error.message : String(error);
205
+ setMessages((prior) => prior.map((message) => message.id === id ? { ...message, body, pending: false } : message));
206
+ }
207
+ finally {
208
+ setMessages((prior) => prior.map((message) => message.id === id ? { ...message, done: true } : message));
209
+ setBusy(false);
210
+ }
211
+ }, [config]);
212
+ const run = useCallback(async (raw) => {
213
+ const text = raw.trim();
214
+ if (view === "settings") {
215
+ const open = editingRef.current;
216
+ if (open) {
217
+ setEditing(null);
218
+ setDraft("");
219
+ await applyEdit(open.key, raw);
220
+ return;
221
+ }
222
+ if (!text) {
223
+ const key = fieldRef.current;
224
+ const row = settings.find((entry) => entry.key === key);
225
+ if (!row || !key)
226
+ return;
227
+ const flipped = nextValue(row);
228
+ if (flipped !== null)
229
+ return applyEdit(key, flipped);
230
+ const seed = seedFor(row);
231
+ setEditing({ key, draft: seed });
232
+ setDraft(seed);
233
+ setNotice(row.hint ?? null);
234
+ return;
235
+ }
236
+ }
237
+ if (!text) {
238
+ const selected = board.tickets.find((ticket) => ticket.id === selectedRef.current);
239
+ if (browsing && selected) {
240
+ setTicketKey(selected.key);
241
+ setView("ticket");
242
+ }
243
+ return;
244
+ }
245
+ history.current.push(text);
246
+ historyAt.current = -1;
247
+ setDraft("");
248
+ setNotice(null);
249
+ const action = parseLine(text);
250
+ if (action.kind === "say") {
251
+ say("you", text);
252
+ if (mode === "orchestrator")
253
+ await askOrchestrator(text);
254
+ else
255
+ setNotice("Use /orchestrator before sending a message.");
256
+ return;
257
+ }
258
+ switch (action.kind) {
259
+ case "mode":
260
+ setMode("orchestrator");
261
+ setCursor(null);
262
+ selectedRef.current = null;
263
+ say("system", "Talking to the orchestrator. It moves work already in flight.");
264
+ return;
265
+ case "view":
266
+ setView(action.view);
267
+ if (action.view === "board" && order.length) {
268
+ const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
269
+ selectedRef.current = next;
270
+ setCursor(next);
271
+ setMode("browse");
272
+ }
273
+ if (action.view === "settings") {
274
+ const first = settingsOrder[0] ?? null;
275
+ fieldRef.current = first;
276
+ setField(first);
277
+ setEditing(null);
278
+ }
279
+ return;
280
+ case "workspace":
281
+ if (action.slug)
282
+ await changeWorkspace(action.slug);
283
+ else {
284
+ setBusy(true);
285
+ try {
286
+ say("system", `Workspaces: ${(await configuredSlugs(config)).join(", ")}.`);
287
+ }
288
+ catch (error) {
289
+ setNotice(error instanceof Error ? error.message : String(error));
290
+ }
291
+ finally {
292
+ setBusy(false);
293
+ }
294
+ }
295
+ return;
296
+ case "ticket":
297
+ setTicketKey(action.key);
298
+ setView("ticket");
299
+ setBusy(true);
300
+ try {
301
+ const { ticket: detail } = await loadTicketDetail(config, action.key);
302
+ setBoard((prior) => ({
303
+ ...prior,
304
+ tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket),
305
+ }));
306
+ }
307
+ catch (error) {
308
+ setNotice(error instanceof Error ? error.message : String(error));
309
+ }
310
+ finally {
311
+ setBusy(false);
312
+ }
313
+ return;
314
+ case "decide": {
315
+ const decision = board.decisions[0];
316
+ if (!decision) {
317
+ setNotice("Nothing is waiting on a decision.");
318
+ return;
319
+ }
320
+ if (action.dismiss) {
321
+ setNotice("Skipping decisions is not available through the HDX API. Answer it instead.");
322
+ return;
323
+ }
324
+ const options = decisionOptions(decision);
325
+ const option = /^\d+$/.test(action.answer) ? options[Number(action.answer) - 1] : undefined;
326
+ const answer = option ?? action.answer;
327
+ if (!answer) {
328
+ setNotice("Answer it with /decide 1 or /decide <your answer>.");
329
+ return;
330
+ }
331
+ setBusy(true);
332
+ try {
333
+ await resolveDecision(config, decision.id, answer);
334
+ say("system", `Answered: ${answer}`);
335
+ await refresh();
336
+ }
337
+ catch (error) {
338
+ setNotice(error instanceof Error ? error.message : String(error));
339
+ }
340
+ finally {
341
+ setBusy(false);
342
+ }
343
+ return;
344
+ }
345
+ case "help":
346
+ setMessages((prior) => [...prior, { id: nextId(), speaker: "system", body: "", panel: "help", done: true }]);
347
+ return;
348
+ case "refresh":
349
+ await refresh();
350
+ return;
351
+ case "exit":
352
+ exit();
353
+ return;
354
+ case "unknown":
355
+ setNotice(`No command /${action.command}. Try /help.`);
356
+ return;
357
+ default:
358
+ return;
359
+ }
360
+ }, [view, settings, applyEdit, board, browsing, mode, say, askOrchestrator, order, settingsOrder, changeWorkspace, config, refresh, exit]);
361
+ useInput((input, key) => {
362
+ if (key.ctrl && input === "c")
363
+ exit();
364
+ });
365
+ const decisions = board.decisions;
366
+ const waiting = decisions.length;
367
+ const announced = useRef(0);
368
+ useEffect(() => {
369
+ if (waiting > announced.current)
370
+ alertOnce();
371
+ announced.current = waiting;
372
+ }, [waiting]);
373
+ const ticket = ticketKey ? board.tickets.find((row) => row.key === ticketKey) : null;
374
+ const settled = messages.filter((message) => message.done);
375
+ const inFlight = messages.filter((message) => !message.done);
376
+ const splash = !started;
377
+ const scrollback = splash ? [] : [
378
+ { key: "banner" },
379
+ { key: "help", message: { id: "help", speaker: "system", body: "", panel: "help" } },
380
+ ...settled.map((message) => ({ key: message.id, message })),
381
+ ];
382
+ const plan = planLayout({
383
+ rows, columns, width, splash, ready, decision: decisionRows(decisions),
384
+ inFlight: inFlight.reduce((total, message) => total + bubbleRows(message, width), 0),
385
+ notice: Boolean(notice), home: view === "home",
386
+ });
387
+ const agentsView = splitPanels(plan.panels);
388
+ const running = board.runs.filter((run) => run.status === "running").length;
389
+ const selected = cursor ? board.tickets.find((row) => row.id === cursor) : null;
390
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: scrollback, children: (item) => {
391
+ if (!item.message)
392
+ return _jsx(Banner, { animate: false }, item.key);
393
+ if (item.message.panel === "help")
394
+ return _jsx(Help, { width: width }, item.key);
395
+ return _jsx(Bubble, { message: item.message, width: width }, item.key);
396
+ } }), 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
397
+ ? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels })
398
+ : _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) => {
399
+ setDraft(next);
400
+ if (editingRef.current)
401
+ setEditing({ key: editingRef.current.key, draft: next });
402
+ }, onSubmit: (value) => void run(value), isActive: !busy, placeholder: busy ? "working…" : "message, or /help", prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "orchestrator" ? "orchestrator> " : "> " }), color: UI.text, onCancel: () => {
403
+ if (editing) {
404
+ setEditing(null);
405
+ setDraft("");
406
+ setNotice(null);
407
+ return;
408
+ }
409
+ if (view !== "home") {
410
+ setView("home");
411
+ setTicketKey(null);
412
+ return;
413
+ }
414
+ setCursor(null);
415
+ selectedRef.current = null;
416
+ }, onUp: () => {
417
+ if (configuring && moveField(-1))
418
+ return;
419
+ if (browsing && moveCursor(-1))
420
+ return;
421
+ if (!history.current.length)
422
+ return;
423
+ historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
424
+ setDraft(history.current[historyAt.current] ?? "");
425
+ }, onDown: () => {
426
+ if (configuring && moveField(1))
427
+ return;
428
+ if (browsing && moveCursor(1))
429
+ return;
430
+ if (historyAt.current < 0)
431
+ return;
432
+ historyAt.current += 1;
433
+ if (historyAt.current >= history.current.length) {
434
+ historyAt.current = -1;
435
+ setDraft("");
436
+ return;
437
+ }
438
+ setDraft(history.current[historyAt.current] ?? "");
439
+ } }) })] })] }));
440
+ }
441
+ export { COMMANDS };