@higherdev/cli 0.3.0 → 0.5.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/README.md +47 -172
- package/dist/api.js +85 -0
- package/dist/config.js +80 -0
- package/dist/host.js +179 -0
- package/dist/index.js +259 -9257
- package/dist/out/format.js +96 -0
- package/dist/out/theme.js +73 -0
- package/dist/out.js +76 -0
- package/dist/tui/App.js +418 -0
- package/dist/tui/Banner.js +301 -0
- package/dist/tui/Bubble.js +12 -0
- package/dist/tui/Dashboard.js +206 -0
- package/dist/tui/Decision.js +48 -0
- package/dist/tui/Help.js +30 -0
- package/dist/tui/Panels.js +195 -0
- package/dist/tui/Settings.js +45 -0
- package/dist/tui/Splash.js +15 -0
- package/dist/tui/TextInput.js +137 -0
- package/dist/tui/alert.js +19 -0
- package/dist/tui/bounded.js +37 -0
- package/dist/tui/capability.js +4 -0
- package/dist/tui/data.js +165 -0
- package/dist/tui/height.js +58 -0
- package/dist/tui/launch.js +20 -0
- package/dist/tui/layout.js +54 -0
- package/dist/tui/parse.js +48 -0
- package/dist/tui/settings-model.js +76 -0
- package/dist/tui/stream.js +122 -0
- package/dist/tui/theme.js +52 -0
- package/dist/tui/workspace-load.js +18 -0
- package/package.json +8 -34
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { statusTone } from "../tui/data.js";
|
|
2
|
+
import { c, DOT, ELLIPSIS, tone } from "./theme.js";
|
|
3
|
+
export function truncate(text, max) {
|
|
4
|
+
const flat = String(text ?? "")
|
|
5
|
+
.replace(/\s+/g, " ")
|
|
6
|
+
.trim();
|
|
7
|
+
if (flat.length <= max)
|
|
8
|
+
return flat;
|
|
9
|
+
return `${flat.slice(0, Math.max(0, max - 1))}${ELLIPSIS}`;
|
|
10
|
+
}
|
|
11
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
12
|
+
export function displayWidth(text) {
|
|
13
|
+
return text.replace(ANSI, "").length;
|
|
14
|
+
}
|
|
15
|
+
export function pad(text, width) {
|
|
16
|
+
const gap = width - displayWidth(text);
|
|
17
|
+
return gap > 0 ? text + " ".repeat(gap) : text;
|
|
18
|
+
}
|
|
19
|
+
export function padStart(text, width) {
|
|
20
|
+
const gap = width - displayWidth(text);
|
|
21
|
+
return gap > 0 ? " ".repeat(gap) + text : text;
|
|
22
|
+
}
|
|
23
|
+
/** Fixed-width table. Widths measure printable characters, not escape codes. */
|
|
24
|
+
export function table(columns, rows) {
|
|
25
|
+
if (rows.length === 0)
|
|
26
|
+
return c.dim("none");
|
|
27
|
+
const widths = columns.map((column, i) => Math.max(displayWidth(column.header), ...rows.map((row) => displayWidth(row[i] ?? ""))));
|
|
28
|
+
const line = (cells) => cells
|
|
29
|
+
.map((cell, i) => {
|
|
30
|
+
if (i === cells.length - 1 && columns[i]?.align !== "right")
|
|
31
|
+
return cell;
|
|
32
|
+
return columns[i]?.align === "right" ? padStart(cell, widths[i]) : pad(cell, widths[i]);
|
|
33
|
+
})
|
|
34
|
+
.join(" ")
|
|
35
|
+
.trimEnd();
|
|
36
|
+
return [c.dim(line(columns.map((column) => column.header))), ...rows.map(line)].join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function statusChip(status) {
|
|
39
|
+
return tone(statusTone(status), status.replaceAll("_", " "));
|
|
40
|
+
}
|
|
41
|
+
export function stateDot(state) {
|
|
42
|
+
return tone(state, DOT);
|
|
43
|
+
}
|
|
44
|
+
export function relativeTime(iso, now = Date.now()) {
|
|
45
|
+
if (!iso)
|
|
46
|
+
return "never";
|
|
47
|
+
const at = Date.parse(iso);
|
|
48
|
+
if (Number.isNaN(at))
|
|
49
|
+
return "never";
|
|
50
|
+
const seconds = Math.round((now - at) / 1000);
|
|
51
|
+
if (seconds < 0)
|
|
52
|
+
return "just now";
|
|
53
|
+
if (seconds < 60)
|
|
54
|
+
return `${seconds}s ago`;
|
|
55
|
+
const minutes = Math.round(seconds / 60);
|
|
56
|
+
if (minutes < 60)
|
|
57
|
+
return `${minutes}m ago`;
|
|
58
|
+
const hours = Math.round(minutes / 60);
|
|
59
|
+
if (hours < 48)
|
|
60
|
+
return `${hours}h ago`;
|
|
61
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
62
|
+
}
|
|
63
|
+
export function elapsed(from, to, now = Date.now()) {
|
|
64
|
+
if (!from)
|
|
65
|
+
return "";
|
|
66
|
+
const start = Date.parse(from);
|
|
67
|
+
if (Number.isNaN(start))
|
|
68
|
+
return "";
|
|
69
|
+
const parsedEnd = to ? Date.parse(to) : now;
|
|
70
|
+
const end = Number.isNaN(parsedEnd) ? now : parsedEnd;
|
|
71
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
72
|
+
if (seconds < 60)
|
|
73
|
+
return `${seconds}s`;
|
|
74
|
+
const minutes = Math.floor(seconds / 60);
|
|
75
|
+
if (minutes < 60)
|
|
76
|
+
return `${minutes}m${seconds % 60 ? ` ${seconds % 60}s` : ""}`;
|
|
77
|
+
const hours = Math.floor(minutes / 60);
|
|
78
|
+
return `${hours}h${minutes % 60 ? ` ${minutes % 60}m` : ""}`;
|
|
79
|
+
}
|
|
80
|
+
export function heading(text) {
|
|
81
|
+
return c.bold(text);
|
|
82
|
+
}
|
|
83
|
+
export function bullet(text) {
|
|
84
|
+
return `${c.dim("-")} ${text}`;
|
|
85
|
+
}
|
|
86
|
+
export function section(title, body) {
|
|
87
|
+
return `${heading(title)}\n${body}`;
|
|
88
|
+
}
|
|
89
|
+
/** A one-line horizontal bar for spend and progress. */
|
|
90
|
+
export function bar(value, max, width = 20) {
|
|
91
|
+
if (!(max > 0))
|
|
92
|
+
return c.dim("-".repeat(width));
|
|
93
|
+
const filled = Math.max(0, Math.min(width, Math.round((value / max) * width)));
|
|
94
|
+
return c.blue("█".repeat(filled)) + c.dim("░".repeat(width - filled));
|
|
95
|
+
}
|
|
96
|
+
export { DOT };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal palette. The web app is light only by decision; a terminal is the
|
|
3
|
+
* user's own surface, so nothing here paints a background. Foreground and dim
|
|
4
|
+
* only, blue as the single accent, status colour confined to state.
|
|
5
|
+
*/
|
|
6
|
+
const ESC = `${String.fromCharCode(27)}[`;
|
|
7
|
+
export function colorEnabled(stream = process.stdout) {
|
|
8
|
+
if (process.env.HIGHERDEV_FORCE_COLOR === "1")
|
|
9
|
+
return true;
|
|
10
|
+
if (process.env.NO_COLOR)
|
|
11
|
+
return false;
|
|
12
|
+
return Boolean(stream.isTTY);
|
|
13
|
+
}
|
|
14
|
+
const CODES = {
|
|
15
|
+
bold: 1,
|
|
16
|
+
dim: 2,
|
|
17
|
+
red: 31,
|
|
18
|
+
green: 32,
|
|
19
|
+
yellow: 33,
|
|
20
|
+
magenta: 35,
|
|
21
|
+
cyan: 36,
|
|
22
|
+
grey: 90,
|
|
23
|
+
blue: 94,
|
|
24
|
+
};
|
|
25
|
+
function wrap(name, text) {
|
|
26
|
+
if (!colorEnabled())
|
|
27
|
+
return text;
|
|
28
|
+
return `${ESC}${CODES[name]}m${text}${ESC}0m`;
|
|
29
|
+
}
|
|
30
|
+
export const c = {
|
|
31
|
+
bold: (t) => wrap("bold", t),
|
|
32
|
+
dim: (t) => wrap("dim", t),
|
|
33
|
+
blue: (t) => wrap("blue", t),
|
|
34
|
+
green: (t) => wrap("green", t),
|
|
35
|
+
yellow: (t) => wrap("yellow", t),
|
|
36
|
+
red: (t) => wrap("red", t),
|
|
37
|
+
grey: (t) => wrap("grey", t),
|
|
38
|
+
cyan: (t) => wrap("cyan", t),
|
|
39
|
+
magenta: (t) => wrap("magenta", t),
|
|
40
|
+
};
|
|
41
|
+
export function tone(value, text) {
|
|
42
|
+
switch (value) {
|
|
43
|
+
case "blue":
|
|
44
|
+
return c.blue(text);
|
|
45
|
+
case "success":
|
|
46
|
+
return c.green(text);
|
|
47
|
+
case "warning":
|
|
48
|
+
return c.yellow(text);
|
|
49
|
+
case "danger":
|
|
50
|
+
return c.red(text);
|
|
51
|
+
default:
|
|
52
|
+
return c.dim(text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Ink colour name for the same tone, so plain and TUI output agree. */
|
|
56
|
+
export function inkColor(value) {
|
|
57
|
+
switch (value) {
|
|
58
|
+
case "blue":
|
|
59
|
+
return "blueBright";
|
|
60
|
+
case "success":
|
|
61
|
+
return "green";
|
|
62
|
+
case "warning":
|
|
63
|
+
return "yellow";
|
|
64
|
+
case "danger":
|
|
65
|
+
return "red";
|
|
66
|
+
default:
|
|
67
|
+
return "gray";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export const DOT = "●";
|
|
71
|
+
export const ARROW = "→";
|
|
72
|
+
export const BAR = "│";
|
|
73
|
+
export const ELLIPSIS = "…";
|
package/dist/out.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const ESC = `${String.fromCharCode(27)}[`;
|
|
2
|
+
export function colorEnabled(stream = process.stdout) {
|
|
3
|
+
if (process.env.HIGHERDEV_FORCE_COLOR === "1" || process.env.HDX_FORCE_COLOR === "1")
|
|
4
|
+
return true;
|
|
5
|
+
if (process.env.NO_COLOR)
|
|
6
|
+
return false;
|
|
7
|
+
return Boolean(stream.isTTY);
|
|
8
|
+
}
|
|
9
|
+
function wrap(code, text) {
|
|
10
|
+
return colorEnabled() ? `${ESC}${code}m${text}${ESC}0m` : text;
|
|
11
|
+
}
|
|
12
|
+
export const c = {
|
|
13
|
+
bold: (text) => wrap(1, text),
|
|
14
|
+
dim: (text) => wrap(2, text),
|
|
15
|
+
red: (text) => wrap(31, text),
|
|
16
|
+
green: (text) => wrap(32, text),
|
|
17
|
+
yellow: (text) => wrap(33, text),
|
|
18
|
+
blue: (text) => wrap(94, text),
|
|
19
|
+
};
|
|
20
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
21
|
+
const ELLIPSIS = "…";
|
|
22
|
+
export function displayWidth(text) {
|
|
23
|
+
return text.replace(ANSI, "").length;
|
|
24
|
+
}
|
|
25
|
+
export function truncate(text, max) {
|
|
26
|
+
const flat = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
27
|
+
return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 1))}${ELLIPSIS}`;
|
|
28
|
+
}
|
|
29
|
+
export function table(columns, rows) {
|
|
30
|
+
if (!rows.length)
|
|
31
|
+
return c.dim("none");
|
|
32
|
+
const widths = columns.map((header, index) => Math.max(displayWidth(header), ...rows.map((row) => displayWidth(row[index] ?? ""))));
|
|
33
|
+
const line = (cells) => cells.map((cell, index) => {
|
|
34
|
+
const gap = widths[index] - displayWidth(cell);
|
|
35
|
+
return index === cells.length - 1 ? cell : cell + " ".repeat(Math.max(0, gap));
|
|
36
|
+
}).join(" ").trimEnd();
|
|
37
|
+
return [c.dim(line(columns)), ...rows.map(line)].join("\n");
|
|
38
|
+
}
|
|
39
|
+
export function statusChip(status) {
|
|
40
|
+
const label = status.replaceAll("_", " ");
|
|
41
|
+
if (["queued", "running", "in_review"].includes(status))
|
|
42
|
+
return c.blue(label);
|
|
43
|
+
if (["ready", "approved", "merged", "succeeded"].includes(status))
|
|
44
|
+
return c.green(label);
|
|
45
|
+
if (["blocked", "needs_decision", "changes_requested"].includes(status))
|
|
46
|
+
return c.yellow(label);
|
|
47
|
+
if (["failed", "cancelled", "killed"].includes(status))
|
|
48
|
+
return c.red(label);
|
|
49
|
+
return c.dim(label);
|
|
50
|
+
}
|
|
51
|
+
const SMALL_BANNER = [
|
|
52
|
+
"██ ██ ██ ██ ██▀▀█▄ ██████ ██ ██",
|
|
53
|
+
"██ ██ ▄▄ ▄▄▄▄▄▄ ██ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄▄ ██ ██ ██ ██ ██",
|
|
54
|
+
"███████ ██ ██ ██ ██▀ ██ ██ ██ ███▀▀ ██ ██ █████ ▀█▄ ▄█▀",
|
|
55
|
+
"██ ██ ██ ██ ██ ██ ██ ██▀▀▀▀▀ ██ ██ ██ ██ ██ ██ ",
|
|
56
|
+
"██ ██ ██ ▀█▄▄▄██ ██ ██ ▀█▄▄▄█▀ ██ ██▄▄█▀ ██████ ███ ",
|
|
57
|
+
" ▄▄▄▄█▀ ",
|
|
58
|
+
];
|
|
59
|
+
export function banner(columns = process.stdout.columns || 80) {
|
|
60
|
+
const rows = columns >= 66 ? SMALL_BANNER : ["HigherDEV"];
|
|
61
|
+
return rows.map((row) => c.blue(row)).join("\n");
|
|
62
|
+
}
|
|
63
|
+
export function usage() {
|
|
64
|
+
return [
|
|
65
|
+
c.bold("Usage"),
|
|
66
|
+
` ${c.blue("hd status")} workspace overview`,
|
|
67
|
+
` ${c.blue("hd ticket list | show | new")} ticket operations`,
|
|
68
|
+
` ${c.blue("hd workspace new")} create a paused workspace`,
|
|
69
|
+
` ${c.blue("hd agents [set]")} inspect or update agents`,
|
|
70
|
+
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
71
|
+
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
72
|
+
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
|
73
|
+
` ${c.blue("hd on | hd off")} workspace switch`,
|
|
74
|
+
` ${c.blue("hd init | hd upgrade")} host setup`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
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
|
+
const applySnapshot = useCallback((snapshot) => {
|
|
61
|
+
const token = loads.current.start(snapshot.workspace.id);
|
|
62
|
+
if (!loads.current.isCurrent(token))
|
|
63
|
+
return;
|
|
64
|
+
setWorkspace(snapshot.workspace);
|
|
65
|
+
setBoard(snapshot.board);
|
|
66
|
+
setFeed(snapshot.feed);
|
|
67
|
+
}, []);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setLive("connecting");
|
|
70
|
+
const polling = pollSnapshot(config, applySnapshot, setLive, (error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
71
|
+
refreshRef.current = polling.refresh;
|
|
72
|
+
return polling.close;
|
|
73
|
+
}, [config, applySnapshot]);
|
|
74
|
+
const labels = useMemo(() => runLabels(board), [board]);
|
|
75
|
+
const liveRunIds = [...labels.keys()].sort().join(",");
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!liveRunIds) {
|
|
78
|
+
setStream([]);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const token = loads.current.start(workspace.id);
|
|
82
|
+
void loadLiveEvents(config, board)
|
|
83
|
+
.then((events) => {
|
|
84
|
+
if (!loads.current.isCurrent(token))
|
|
85
|
+
return;
|
|
86
|
+
setStream((prior) => appendLines(prior, toStreamLines(events, labels)));
|
|
87
|
+
})
|
|
88
|
+
.catch(() => { });
|
|
89
|
+
}, [liveRunIds, board, config, labels, workspace.id]);
|
|
90
|
+
const say = useCallback((speaker, body, steps) => {
|
|
91
|
+
setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
|
|
92
|
+
}, []);
|
|
93
|
+
const order = useMemo(() => boardTicketIds(board), [board]);
|
|
94
|
+
const settings = useMemo(() => settingsRows(workspace, board.agents), [workspace, board.agents]);
|
|
95
|
+
const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
|
|
96
|
+
const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
|
|
97
|
+
const configuring = view === "settings" && !editing;
|
|
98
|
+
const moveCursor = useCallback((delta) => {
|
|
99
|
+
if (!order.length)
|
|
100
|
+
return false;
|
|
101
|
+
const next = nextCursor(order, selectedRef.current, delta);
|
|
102
|
+
if (!next)
|
|
103
|
+
return false;
|
|
104
|
+
selectedRef.current = next;
|
|
105
|
+
setCursor(next);
|
|
106
|
+
return true;
|
|
107
|
+
}, [order]);
|
|
108
|
+
const moveField = useCallback((delta) => {
|
|
109
|
+
const next = nextCursor(settingsOrder, fieldRef.current, delta);
|
|
110
|
+
if (!next)
|
|
111
|
+
return false;
|
|
112
|
+
fieldRef.current = next;
|
|
113
|
+
setField(next);
|
|
114
|
+
return true;
|
|
115
|
+
}, [settingsOrder]);
|
|
116
|
+
const refresh = useCallback(async () => {
|
|
117
|
+
await refreshRef.current?.();
|
|
118
|
+
}, []);
|
|
119
|
+
const applyEdit = useCallback(async (key, raw) => {
|
|
120
|
+
const row = settings.find((entry) => entry.key === key);
|
|
121
|
+
if (!row)
|
|
122
|
+
return;
|
|
123
|
+
const edit = editFor(row, raw);
|
|
124
|
+
if (!edit.ok) {
|
|
125
|
+
setNotice(edit.error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
setBusy(true);
|
|
129
|
+
try {
|
|
130
|
+
if (edit.value.target === "cap") {
|
|
131
|
+
await updateProviderCap(config, edit.value.provider, edit.value.cap);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
await updateAgent(config, edit.value.id, edit.value.fields);
|
|
135
|
+
}
|
|
136
|
+
setNotice(null);
|
|
137
|
+
await refresh();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
setBusy(false);
|
|
144
|
+
}
|
|
145
|
+
}, [settings, config, refresh]);
|
|
146
|
+
const changeWorkspace = useCallback(async (slug) => {
|
|
147
|
+
if (!configuredSlugs().includes(slug)) {
|
|
148
|
+
setNotice(`No configured workspace ${slug}. You can reach: ${configuredSlugs().join(", ")}.`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
setBusy(true);
|
|
152
|
+
try {
|
|
153
|
+
const snapshot = await switchWorkspace(slug);
|
|
154
|
+
loads.current.switchTo(snapshot.workspace.id);
|
|
155
|
+
setConfig(snapshot.config);
|
|
156
|
+
setWorkspace(snapshot.workspace);
|
|
157
|
+
setBoard(snapshot.board);
|
|
158
|
+
setFeed(snapshot.feed);
|
|
159
|
+
setStream([]);
|
|
160
|
+
setCursor(null);
|
|
161
|
+
selectedRef.current = null;
|
|
162
|
+
setView("home");
|
|
163
|
+
setTicketKey(null);
|
|
164
|
+
setNotice(null);
|
|
165
|
+
say("system", `Now on ${snapshot.workspace.slug} (${snapshot.workspace.repo}).`);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
setBusy(false);
|
|
172
|
+
}
|
|
173
|
+
}, [say]);
|
|
174
|
+
const askOrchestrator = useCallback(async (text) => {
|
|
175
|
+
setBusy(true);
|
|
176
|
+
const id = nextId();
|
|
177
|
+
setMessages((prior) => [...prior, { id, speaker: "orchestrator", body: "", pending: true }]);
|
|
178
|
+
const since = new Date().toISOString();
|
|
179
|
+
try {
|
|
180
|
+
await postOrchestrator(text, config);
|
|
181
|
+
setMessages((prior) => prior.map((message) => message.id === id
|
|
182
|
+
? { ...message, steps: ["· queued, it answers on the next tick"] }
|
|
183
|
+
: message));
|
|
184
|
+
const { waitForReply } = await import("./data.js");
|
|
185
|
+
const reply = await waitForReply(config, since, 180_000);
|
|
186
|
+
setMessages((prior) => prior.map((message) => message.id === id
|
|
187
|
+
? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
|
|
188
|
+
: message));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const body = error instanceof Error ? error.message : String(error);
|
|
192
|
+
setMessages((prior) => prior.map((message) => message.id === id ? { ...message, body, pending: false } : message));
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
setMessages((prior) => prior.map((message) => message.id === id ? { ...message, done: true } : message));
|
|
196
|
+
setBusy(false);
|
|
197
|
+
}
|
|
198
|
+
}, [config]);
|
|
199
|
+
const run = useCallback(async (raw) => {
|
|
200
|
+
const text = raw.trim();
|
|
201
|
+
if (view === "settings") {
|
|
202
|
+
const open = editingRef.current;
|
|
203
|
+
if (open) {
|
|
204
|
+
setEditing(null);
|
|
205
|
+
setDraft("");
|
|
206
|
+
await applyEdit(open.key, raw);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!text) {
|
|
210
|
+
const key = fieldRef.current;
|
|
211
|
+
const row = settings.find((entry) => entry.key === key);
|
|
212
|
+
if (!row || !key)
|
|
213
|
+
return;
|
|
214
|
+
const flipped = nextValue(row);
|
|
215
|
+
if (flipped !== null)
|
|
216
|
+
return applyEdit(key, flipped);
|
|
217
|
+
const seed = seedFor(row);
|
|
218
|
+
setEditing({ key, draft: seed });
|
|
219
|
+
setDraft(seed);
|
|
220
|
+
setNotice(row.hint ?? null);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!text) {
|
|
225
|
+
const selected = board.tickets.find((ticket) => ticket.id === selectedRef.current);
|
|
226
|
+
if (browsing && selected) {
|
|
227
|
+
setTicketKey(selected.key);
|
|
228
|
+
setView("ticket");
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
history.current.push(text);
|
|
233
|
+
historyAt.current = -1;
|
|
234
|
+
setDraft("");
|
|
235
|
+
setNotice(null);
|
|
236
|
+
const action = parseLine(text);
|
|
237
|
+
if (action.kind === "say") {
|
|
238
|
+
say("you", text);
|
|
239
|
+
if (mode === "orchestrator")
|
|
240
|
+
await askOrchestrator(text);
|
|
241
|
+
else
|
|
242
|
+
setNotice("Use /orchestrator before sending a message.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
switch (action.kind) {
|
|
246
|
+
case "mode":
|
|
247
|
+
setMode("orchestrator");
|
|
248
|
+
setCursor(null);
|
|
249
|
+
selectedRef.current = null;
|
|
250
|
+
say("system", "Talking to the orchestrator. It moves work already in flight.");
|
|
251
|
+
return;
|
|
252
|
+
case "view":
|
|
253
|
+
setView(action.view);
|
|
254
|
+
if (action.view === "board" && order.length) {
|
|
255
|
+
const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
|
|
256
|
+
selectedRef.current = next;
|
|
257
|
+
setCursor(next);
|
|
258
|
+
setMode("browse");
|
|
259
|
+
}
|
|
260
|
+
if (action.view === "settings") {
|
|
261
|
+
const first = settingsOrder[0] ?? null;
|
|
262
|
+
fieldRef.current = first;
|
|
263
|
+
setField(first);
|
|
264
|
+
setEditing(null);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
case "workspace":
|
|
268
|
+
if (!action.slug)
|
|
269
|
+
say("system", `Workspaces: ${configuredSlugs().join(", ")}.`);
|
|
270
|
+
else
|
|
271
|
+
await changeWorkspace(action.slug);
|
|
272
|
+
return;
|
|
273
|
+
case "ticket":
|
|
274
|
+
setTicketKey(action.key);
|
|
275
|
+
setView("ticket");
|
|
276
|
+
setBusy(true);
|
|
277
|
+
try {
|
|
278
|
+
const { ticket: detail } = await loadTicketDetail(config, action.key);
|
|
279
|
+
setBoard((prior) => ({
|
|
280
|
+
...prior,
|
|
281
|
+
tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket),
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
setBusy(false);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
case "decide": {
|
|
292
|
+
const decision = board.decisions[0];
|
|
293
|
+
if (!decision) {
|
|
294
|
+
setNotice("Nothing is waiting on a decision.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (action.dismiss) {
|
|
298
|
+
setNotice("Skipping decisions is not available through the HDX API. Answer it instead.");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const options = decisionOptions(decision);
|
|
302
|
+
const option = /^\d+$/.test(action.answer) ? options[Number(action.answer) - 1] : undefined;
|
|
303
|
+
const answer = option ?? action.answer;
|
|
304
|
+
if (!answer) {
|
|
305
|
+
setNotice("Answer it with /decide 1 or /decide <your answer>.");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
setBusy(true);
|
|
309
|
+
try {
|
|
310
|
+
await resolveDecision(config, decision.id, answer);
|
|
311
|
+
say("system", `Answered: ${answer}`);
|
|
312
|
+
await refresh();
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
setBusy(false);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "help":
|
|
323
|
+
setMessages((prior) => [...prior, { id: nextId(), speaker: "system", body: "", panel: "help", done: true }]);
|
|
324
|
+
return;
|
|
325
|
+
case "refresh":
|
|
326
|
+
await refresh();
|
|
327
|
+
return;
|
|
328
|
+
case "exit":
|
|
329
|
+
exit();
|
|
330
|
+
return;
|
|
331
|
+
case "unknown":
|
|
332
|
+
setNotice(`No command /${action.command}. Try /help.`);
|
|
333
|
+
return;
|
|
334
|
+
default:
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}, [view, settings, applyEdit, board, browsing, mode, say, askOrchestrator, order, settingsOrder, changeWorkspace, config, refresh, exit]);
|
|
338
|
+
useInput((input, key) => {
|
|
339
|
+
if (key.ctrl && input === "c")
|
|
340
|
+
exit();
|
|
341
|
+
});
|
|
342
|
+
const decisions = board.decisions;
|
|
343
|
+
const waiting = decisions.length;
|
|
344
|
+
const announced = useRef(0);
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
if (waiting > announced.current)
|
|
347
|
+
alertOnce();
|
|
348
|
+
announced.current = waiting;
|
|
349
|
+
}, [waiting]);
|
|
350
|
+
const ticket = ticketKey ? board.tickets.find((row) => row.key === ticketKey) : null;
|
|
351
|
+
const settled = messages.filter((message) => message.done);
|
|
352
|
+
const inFlight = messages.filter((message) => !message.done);
|
|
353
|
+
const splash = !started;
|
|
354
|
+
const scrollback = splash ? [] : [
|
|
355
|
+
{ key: "banner" },
|
|
356
|
+
{ key: "help", message: { id: "help", speaker: "system", body: "", panel: "help" } },
|
|
357
|
+
...settled.map((message) => ({ key: message.id, message })),
|
|
358
|
+
];
|
|
359
|
+
const plan = planLayout({
|
|
360
|
+
rows, columns, width, splash, ready, decision: decisionRows(decisions),
|
|
361
|
+
inFlight: inFlight.reduce((total, message) => total + bubbleRows(message, width), 0),
|
|
362
|
+
notice: Boolean(notice), home: view === "home",
|
|
363
|
+
});
|
|
364
|
+
const agentsView = splitPanels(plan.panels);
|
|
365
|
+
const running = board.runs.filter((run) => run.status === "running").length;
|
|
366
|
+
const selected = cursor ? board.tickets.find((row) => row.id === cursor) : null;
|
|
367
|
+
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: scrollback, children: (item) => {
|
|
368
|
+
if (!item.message)
|
|
369
|
+
return _jsx(Banner, { animate: false }, item.key);
|
|
370
|
+
if (item.message.panel === "help")
|
|
371
|
+
return _jsx(Help, { width: width }, item.key);
|
|
372
|
+
return _jsx(Bubble, { message: item.message, width: width }, item.key);
|
|
373
|
+
} }), 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
|
|
374
|
+
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels })
|
|
375
|
+
: _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) => {
|
|
376
|
+
setDraft(next);
|
|
377
|
+
if (editingRef.current)
|
|
378
|
+
setEditing({ key: editingRef.current.key, draft: next });
|
|
379
|
+
}, 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: () => {
|
|
380
|
+
if (editing) {
|
|
381
|
+
setEditing(null);
|
|
382
|
+
setDraft("");
|
|
383
|
+
setNotice(null);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (view !== "home") {
|
|
387
|
+
setView("home");
|
|
388
|
+
setTicketKey(null);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
setCursor(null);
|
|
392
|
+
selectedRef.current = null;
|
|
393
|
+
}, onUp: () => {
|
|
394
|
+
if (configuring && moveField(-1))
|
|
395
|
+
return;
|
|
396
|
+
if (browsing && moveCursor(-1))
|
|
397
|
+
return;
|
|
398
|
+
if (!history.current.length)
|
|
399
|
+
return;
|
|
400
|
+
historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
|
|
401
|
+
setDraft(history.current[historyAt.current] ?? "");
|
|
402
|
+
}, onDown: () => {
|
|
403
|
+
if (configuring && moveField(1))
|
|
404
|
+
return;
|
|
405
|
+
if (browsing && moveCursor(1))
|
|
406
|
+
return;
|
|
407
|
+
if (historyAt.current < 0)
|
|
408
|
+
return;
|
|
409
|
+
historyAt.current += 1;
|
|
410
|
+
if (historyAt.current >= history.current.length) {
|
|
411
|
+
historyAt.current = -1;
|
|
412
|
+
setDraft("");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
setDraft(history.current[historyAt.current] ?? "");
|
|
416
|
+
} }) })] })] }));
|
|
417
|
+
}
|
|
418
|
+
export { COMMANDS };
|