@higherdev/cli 0.12.1 → 0.14.2
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 +4 -1
- package/dist/api.js +16 -0
- package/dist/index.js +31 -1
- package/dist/out.js +1 -0
- package/dist/tui/App.js +50 -4
- package/dist/tui/Dashboard.js +12 -22
- package/dist/tui/Help.js +1 -0
- package/dist/tui/Panels.js +81 -23
- package/dist/tui/agent-rows.js +75 -0
- package/dist/tui/data.js +11 -2
- package/dist/tui/parse.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,9 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
43
43
|
| `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
|
|
44
44
|
| `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
|
|
45
45
|
| `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
|
|
46
|
+
| `hd env ls` | List workspace environment variable names |
|
|
47
|
+
| `hd env set NAME=VALUE [NAME=VALUE...]` | Set workspace environment variables |
|
|
48
|
+
| `hd env rm NAME` | Remove a workspace environment variable |
|
|
46
49
|
| `hd logs KEY [-f]` | Show or follow run events |
|
|
47
50
|
| `hd msg KEY "message" [--interrupt]` | Message a builder |
|
|
48
51
|
| `hd decide` | List open decisions |
|
|
@@ -64,7 +67,7 @@ the repository is missing, approve private creation interactively, use `--create
|
|
|
64
67
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
65
68
|
|
|
66
69
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
67
|
-
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
|
|
70
|
+
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
|
|
68
71
|
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
|
69
72
|
`/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
70
73
|
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -105,6 +105,15 @@ export async function updateAgent(id, fields, config = loadConfig()) {
|
|
|
105
105
|
export async function deleteAgent(id, config = loadConfig()) {
|
|
106
106
|
return request(config, "DELETE", `/api/w/${config.slug}/agents`, { id });
|
|
107
107
|
}
|
|
108
|
+
export async function listWorkspaceEnv(config = loadConfig()) {
|
|
109
|
+
return request(config, "GET", `/api/w/${config.slug}/env`);
|
|
110
|
+
}
|
|
111
|
+
export async function setWorkspaceEnv(name, value, config = loadConfig()) {
|
|
112
|
+
return request(config, "PUT", `/api/w/${config.slug}/env`, { name, value });
|
|
113
|
+
}
|
|
114
|
+
export async function removeWorkspaceEnv(name, config = loadConfig()) {
|
|
115
|
+
return request(config, "DELETE", `/api/w/${config.slug}/env`, { name });
|
|
116
|
+
}
|
|
108
117
|
export async function listEpics(config = loadConfig()) {
|
|
109
118
|
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
110
119
|
}
|
|
@@ -125,9 +134,16 @@ export async function listMessages(options = {}, config = loadConfig()) {
|
|
|
125
134
|
query.set("since", options.since);
|
|
126
135
|
if (options.limit)
|
|
127
136
|
query.set("limit", String(options.limit));
|
|
137
|
+
if (options.toRoles?.length)
|
|
138
|
+
query.set("to_role", options.toRoles.join(","));
|
|
139
|
+
if (options.undelivered)
|
|
140
|
+
query.set("undelivered", "true");
|
|
128
141
|
const suffix = query.size ? `?${query.toString()}` : "";
|
|
129
142
|
return request(config, "GET", `/api/w/${config.slug}/messages${suffix}`);
|
|
130
143
|
}
|
|
144
|
+
export async function markMessagesDelivered(ids, config = loadConfig()) {
|
|
145
|
+
return request(config, "PATCH", `/api/w/${config.slug}/messages`, { ids });
|
|
146
|
+
}
|
|
131
147
|
export async function updateCaps(provider_caps, config = loadConfig()) {
|
|
132
148
|
return request(config, "PATCH", `/api/w/${config.slug}/caps`, { provider_caps });
|
|
133
149
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, setPaused, setWorkspaceEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
6
|
import { login, parseLoginFlags } from "./login.js";
|
|
7
7
|
import { loadConfig } from "./config.js";
|
|
@@ -344,6 +344,32 @@ async function cmdCaps(argv) {
|
|
|
344
344
|
const result = await updateCaps({ [provider]: cap });
|
|
345
345
|
console.log(`${provider} ${result.provider_caps[provider]}`);
|
|
346
346
|
}
|
|
347
|
+
async function cmdEnv(argv) {
|
|
348
|
+
const [action, ...args] = argv;
|
|
349
|
+
if (action === "ls") {
|
|
350
|
+
const { env } = await listWorkspaceEnv();
|
|
351
|
+
if (!env.length)
|
|
352
|
+
return console.log(c.dim("No workspace environment variables."));
|
|
353
|
+
console.log(table(["NAME", "UPDATED"], env.map((row) => [row.name, row.updated_at])));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (action === "set" && args.length) {
|
|
357
|
+
for (const assignment of args) {
|
|
358
|
+
const at = assignment.indexOf("=");
|
|
359
|
+
if (at < 1)
|
|
360
|
+
fail("usage: hd env set NAME=VALUE [NAME=VALUE...]");
|
|
361
|
+
await setWorkspaceEnv(assignment.slice(0, at), assignment.slice(at + 1));
|
|
362
|
+
}
|
|
363
|
+
console.log(`set ${args.length} variable${args.length === 1 ? "" : "s"}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (action === "rm" && args.length === 1) {
|
|
367
|
+
await removeWorkspaceEnv(args[0]);
|
|
368
|
+
console.log(`removed ${args[0]}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
fail("usage: hd env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME");
|
|
372
|
+
}
|
|
347
373
|
export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
348
374
|
const [cmd, ...rest] = argv;
|
|
349
375
|
try {
|
|
@@ -405,6 +431,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
405
431
|
await cmdCaps(rest);
|
|
406
432
|
return;
|
|
407
433
|
}
|
|
434
|
+
if (cmd === "env") {
|
|
435
|
+
await cmdEnv(rest);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
408
438
|
if (cmd === "pause" || cmd === "off") {
|
|
409
439
|
await setPaused(true);
|
|
410
440
|
console.log(cmd === "off" ? "off" : "paused");
|
package/dist/out.js
CHANGED
|
@@ -70,6 +70,7 @@ export function usage() {
|
|
|
70
70
|
` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
|
|
71
71
|
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
72
|
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
73
|
+
` ${c.blue("hd env ls | set | rm")} workspace environment`,
|
|
73
74
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
74
75
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
75
76
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
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";
|
|
@@ -18,7 +18,7 @@ import { alertOnce } from "./alert.js";
|
|
|
18
18
|
import { bubbleRows } from "./height.js";
|
|
19
19
|
import { planLayout, splitPanels } from "./layout.js";
|
|
20
20
|
import { parseLine } from "./parse.js";
|
|
21
|
-
import { configuredSlugs, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
21
|
+
import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
22
22
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
23
23
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
24
24
|
import { UI } from "./theme.js";
|
|
@@ -50,12 +50,14 @@ 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);
|
|
56
57
|
const history = useRef([]);
|
|
57
58
|
const historyAt = useRef(-1);
|
|
58
59
|
const refreshRef = useRef(null);
|
|
60
|
+
const acknowledgedMessages = useRef(new Set());
|
|
59
61
|
const loads = useRef(new WorkspaceLoads(initial.workspace.id));
|
|
60
62
|
editingRef.current = editing;
|
|
61
63
|
useEffect(() => {
|
|
@@ -117,6 +119,7 @@ export function App({ initial }) {
|
|
|
117
119
|
const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
|
|
118
120
|
const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
|
|
119
121
|
const configuring = view === "settings" && !editing;
|
|
122
|
+
const inbox = useMemo(() => inboxEntries(board, width), [board, width]);
|
|
120
123
|
const moveCursor = useCallback((delta) => {
|
|
121
124
|
if (!order.length)
|
|
122
125
|
return false;
|
|
@@ -135,6 +138,25 @@ export function App({ initial }) {
|
|
|
135
138
|
setField(next);
|
|
136
139
|
return true;
|
|
137
140
|
}, [settingsOrder]);
|
|
141
|
+
const moveInbox = useCallback((delta) => {
|
|
142
|
+
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current + delta)));
|
|
143
|
+
}, [inbox.length]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current)));
|
|
146
|
+
}, [inbox.length]);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (view !== "inbox")
|
|
149
|
+
return;
|
|
150
|
+
const ids = (board.messages ?? []).map((message) => message.id)
|
|
151
|
+
.filter((id) => !acknowledgedMessages.current.has(id));
|
|
152
|
+
if (!ids.length)
|
|
153
|
+
return;
|
|
154
|
+
ids.forEach((id) => acknowledgedMessages.current.add(id));
|
|
155
|
+
void acknowledgeInbox(config, ids).catch((error) => {
|
|
156
|
+
ids.forEach((id) => acknowledgedMessages.current.delete(id));
|
|
157
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
158
|
+
});
|
|
159
|
+
}, [view, board.messages, config]);
|
|
138
160
|
const refresh = useCallback(async () => {
|
|
139
161
|
await refreshRef.current?.();
|
|
140
162
|
}, []);
|
|
@@ -178,6 +200,7 @@ export function App({ initial }) {
|
|
|
178
200
|
setBoard(snapshot.board);
|
|
179
201
|
setFeed(snapshot.feed);
|
|
180
202
|
setStream([]);
|
|
203
|
+
acknowledgedMessages.current.clear();
|
|
181
204
|
setCursor(null);
|
|
182
205
|
selectedRef.current = null;
|
|
183
206
|
setView("home");
|
|
@@ -274,6 +297,8 @@ export function App({ initial }) {
|
|
|
274
297
|
return;
|
|
275
298
|
case "view":
|
|
276
299
|
setView(action.view);
|
|
300
|
+
if (action.view === "inbox")
|
|
301
|
+
setInboxFocus(0);
|
|
277
302
|
if (action.view === "board" && order.length) {
|
|
278
303
|
const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
|
|
279
304
|
selectedRef.current = next;
|
|
@@ -462,6 +487,19 @@ export function App({ initial }) {
|
|
|
462
487
|
}
|
|
463
488
|
return;
|
|
464
489
|
}
|
|
490
|
+
case "env":
|
|
491
|
+
setBusy(true);
|
|
492
|
+
try {
|
|
493
|
+
const { env } = await listWorkspaceEnv(config);
|
|
494
|
+
say("system", env.length ? env.map((row) => row.name).join("\n") : "No workspace environment variables.");
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
setBusy(false);
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
465
503
|
case "decide": {
|
|
466
504
|
const decision = board.decisions[0];
|
|
467
505
|
if (!decision) {
|
|
@@ -545,9 +583,9 @@ export function App({ initial }) {
|
|
|
545
583
|
if (item.message.panel === "help")
|
|
546
584
|
return _jsx(Help, { width: width }, item.key);
|
|
547
585
|
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
|
|
586
|
+
} }), 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
587
|
? _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) => {
|
|
588
|
+
: _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
589
|
setDraft(next);
|
|
552
590
|
if (editingRef.current)
|
|
553
591
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
@@ -566,6 +604,10 @@ export function App({ initial }) {
|
|
|
566
604
|
setCursor(null);
|
|
567
605
|
selectedRef.current = null;
|
|
568
606
|
}, onUp: () => {
|
|
607
|
+
if (view === "inbox" && !draft) {
|
|
608
|
+
moveInbox(-1);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
569
611
|
if (configuring && moveField(-1))
|
|
570
612
|
return;
|
|
571
613
|
if (browsing && moveCursor(-1))
|
|
@@ -575,6 +617,10 @@ export function App({ initial }) {
|
|
|
575
617
|
historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
|
|
576
618
|
setDraft(history.current[historyAt.current] ?? "");
|
|
577
619
|
}, onDown: () => {
|
|
620
|
+
if (view === "inbox" && !draft) {
|
|
621
|
+
moveInbox(1);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
578
624
|
if (configuring && moveField(1))
|
|
579
625
|
return;
|
|
580
626
|
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" || row.state === "limited" ? "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.limitedUntil ? `limited until ${row.limitedUntil}` : 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/Help.js
CHANGED
|
@@ -16,6 +16,7 @@ export const COMMANDS = [
|
|
|
16
16
|
{ name: "/plan", help: "alias for /architect" },
|
|
17
17
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
18
18
|
{ name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
|
|
19
|
+
{ name: "/env", help: "list workspace environment variable names" },
|
|
19
20
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
|
20
21
|
{ name: "/workspace", args: "[slug | new | set | rotate-key]", help: "list, switch, create, or configure" },
|
|
21
22
|
{ name: "/on", help: "turn on the current workspace" },
|
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,21 @@ 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
|
-
const
|
|
27
|
-
|
|
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" || row.state === "limited" ? "warning" : "muted";
|
|
29
|
+
const state = row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state;
|
|
30
|
+
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(state, row.limitedUntil ? 22 : 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
31
|
}),
|
|
34
|
-
_jsx(More, { count:
|
|
32
|
+
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
35
33
|
] }));
|
|
36
34
|
}
|
|
37
35
|
export function BoardPanel({ board, width = 80, rows = 12, cursor, }) {
|
|
@@ -86,22 +84,82 @@ export function FeedPanel({ entries, width = 80, rows = 12, }) {
|
|
|
86
84
|
* The whole queue, one row each, numbered the way `/decide` reaches them: the
|
|
87
85
|
* one on screen is 1.
|
|
88
86
|
*/
|
|
89
|
-
export function InboxPanel({ board, width = 80, rows = 12, }) {
|
|
90
|
-
const
|
|
87
|
+
export function InboxPanel({ board, width = 80, rows = 12, focus = 0, }) {
|
|
88
|
+
const entries = inboxEntries(board, width);
|
|
89
|
+
const messages = board.messages ?? [];
|
|
90
|
+
const inner = Math.max(0, rows - 1);
|
|
91
|
+
const window = scrollWindow(entries.length, inner, focus);
|
|
92
|
+
const hiddenAbove = window.start;
|
|
93
|
+
const hiddenBelow = entries.length - window.end;
|
|
94
|
+
const note = `${board.decisions.length}d · ${messages.length}m`
|
|
95
|
+
+ `${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
|
|
91
96
|
return (_jsx(Panel, { width: width, rows: rows, children: [
|
|
92
|
-
_jsx(Heading, { text: "
|
|
93
|
-
...(board.decisions.length === 0
|
|
97
|
+
_jsx(Heading, { text: "Inbox", note: note }, "h"),
|
|
98
|
+
...(board.decisions.length === 0 && messages.length === 0
|
|
94
99
|
? [
|
|
95
100
|
_jsx(Text, { color: UI.dim, children: "Nothing waiting on you." }, "empty"),
|
|
96
101
|
]
|
|
97
102
|
: []),
|
|
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"),
|
|
103
|
+
...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
104
|
] }));
|
|
104
105
|
}
|
|
106
|
+
/** Every physical line in /inbox, with decision bodies wrapped but never clipped. */
|
|
107
|
+
export function inboxEntries(board, width) {
|
|
108
|
+
const entries = [];
|
|
109
|
+
const bodyWidth = Math.max(12, width - 2);
|
|
110
|
+
board.decisions.forEach((decision, index) => {
|
|
111
|
+
const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
|
|
112
|
+
entries.push({
|
|
113
|
+
key: `${decision.id}:header`,
|
|
114
|
+
kind: "header",
|
|
115
|
+
text: `${index + 1}) ${ticket?.key ?? decision.id.slice(0, 8)}`,
|
|
116
|
+
});
|
|
117
|
+
wrapLines(decision.question_md.trim(), bodyWidth).forEach((text, line) => {
|
|
118
|
+
entries.push({ key: `${decision.id}:body:${line}`, kind: "body", text });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
(board.messages ?? []).forEach((message) => {
|
|
122
|
+
entries.push({
|
|
123
|
+
key: `${message.id}:header`,
|
|
124
|
+
kind: "header",
|
|
125
|
+
text: `Message from ${message.from_name ?? message.from_role}`,
|
|
126
|
+
});
|
|
127
|
+
wrapLines(message.body_md.trim(), bodyWidth).forEach((text, line) => {
|
|
128
|
+
entries.push({ key: `${message.id}:body:${line}`, kind: "body", text });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
function wrapLines(text, width) {
|
|
134
|
+
if (!text)
|
|
135
|
+
return [""];
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const paragraph of text.split("\n")) {
|
|
138
|
+
if (!paragraph) {
|
|
139
|
+
lines.push("");
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let line = "";
|
|
143
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
144
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
145
|
+
if (candidate.length <= width) {
|
|
146
|
+
line = candidate;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (line)
|
|
150
|
+
lines.push(line);
|
|
151
|
+
let rest = word;
|
|
152
|
+
while (rest.length > width) {
|
|
153
|
+
lines.push(rest.slice(0, width));
|
|
154
|
+
rest = rest.slice(width);
|
|
155
|
+
}
|
|
156
|
+
line = rest;
|
|
157
|
+
}
|
|
158
|
+
if (line)
|
|
159
|
+
lines.push(line);
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
105
163
|
/**
|
|
106
164
|
* One ticket in full, inside the rows it was given.
|
|
107
165
|
*
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const LIMITED_UNTIL = /^(\w+) limited until (\d{1,2}:\d{2})$/;
|
|
2
|
+
export function limitedUntilByProvider(tickets) {
|
|
3
|
+
const limited = new Map();
|
|
4
|
+
for (const ticket of tickets) {
|
|
5
|
+
const match = ticket.stuck_reason?.match(LIMITED_UNTIL);
|
|
6
|
+
if (match)
|
|
7
|
+
limited.set(match[1], match[2]);
|
|
8
|
+
}
|
|
9
|
+
return limited;
|
|
10
|
+
}
|
|
11
|
+
const roleForKind = {
|
|
12
|
+
architect: "architect",
|
|
13
|
+
build: "builder",
|
|
14
|
+
followup: "builder",
|
|
15
|
+
orchestrate: "orchestrator",
|
|
16
|
+
review: "reviewer",
|
|
17
|
+
};
|
|
18
|
+
function agentForRun(board, run) {
|
|
19
|
+
if (run.agent_id)
|
|
20
|
+
return board.agents.find((agent) => agent.id === run.agent_id) ?? null;
|
|
21
|
+
const role = roleForKind[run.kind];
|
|
22
|
+
return board.agents.find((agent) => agent.enabled && agent.role === role && agent.provider === run.provider) ?? null;
|
|
23
|
+
}
|
|
24
|
+
/** Expand live invocations into rows, then append enabled agents with no live run. */
|
|
25
|
+
export function agentDisplayRows(board) {
|
|
26
|
+
const activeAgents = new Set();
|
|
27
|
+
const live = board.runs
|
|
28
|
+
.filter((run) => run.status === "running" || run.status === "queued")
|
|
29
|
+
.slice()
|
|
30
|
+
.sort((left, right) => {
|
|
31
|
+
if (left.status !== right.status)
|
|
32
|
+
return left.status === "running" ? -1 : 1;
|
|
33
|
+
const leftAt = left.started_at ?? left.created_at;
|
|
34
|
+
const rightAt = right.started_at ?? right.created_at;
|
|
35
|
+
return leftAt.localeCompare(rightAt) || left.id.localeCompare(right.id);
|
|
36
|
+
})
|
|
37
|
+
.map((run) => {
|
|
38
|
+
const agent = agentForRun(board, run);
|
|
39
|
+
if (agent)
|
|
40
|
+
activeAgents.add(agent.id);
|
|
41
|
+
const ticket = run.ticket_id
|
|
42
|
+
? board.tickets.find((row) => row.id === run.ticket_id) ?? null
|
|
43
|
+
: null;
|
|
44
|
+
return {
|
|
45
|
+
key: run.id,
|
|
46
|
+
name: agent?.display_name ?? run.kind,
|
|
47
|
+
agent,
|
|
48
|
+
run,
|
|
49
|
+
ticket,
|
|
50
|
+
state: run.status,
|
|
51
|
+
limitedUntil: null,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const limited = limitedUntilByProvider(board.tickets);
|
|
55
|
+
const idle = board.agents
|
|
56
|
+
.filter((agent) => agent.enabled && !activeAgents.has(agent.id))
|
|
57
|
+
.sort((left, right) => left.display_name.localeCompare(right.display_name))
|
|
58
|
+
.map((agent) => {
|
|
59
|
+
const until = limited.get(agent.provider);
|
|
60
|
+
return {
|
|
61
|
+
key: `idle:${agent.id}`,
|
|
62
|
+
name: agent.display_name,
|
|
63
|
+
agent,
|
|
64
|
+
run: null,
|
|
65
|
+
ticket: null,
|
|
66
|
+
state: until
|
|
67
|
+
? "limited"
|
|
68
|
+
: board.availability.some((row) => row.provider === agent.provider && !row.available)
|
|
69
|
+
? "offline"
|
|
70
|
+
: "idle",
|
|
71
|
+
limitedUntil: until ?? null,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
return [...live, ...idle];
|
|
75
|
+
}
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
|
|
1
|
+
import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, markMessagesDelivered, listWorkspaceEnv as getWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
|
|
2
2
|
import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
|
|
3
3
|
import { readEpicSpec } from "../epics.js";
|
|
4
4
|
export const POLL_MS = 5_000;
|
|
@@ -22,13 +22,14 @@ export async function configuredSlugs(config = loadConfig()) {
|
|
|
22
22
|
return (await listWorkspaces(config)).map((workspace) => workspace.slug).sort();
|
|
23
23
|
}
|
|
24
24
|
export async function loadSnapshot(config = loadConfig()) {
|
|
25
|
-
const [status, workspaceData, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
25
|
+
const [status, workspaceData, ticketData, agentData, epicData, feedData, messageData] = await Promise.all([
|
|
26
26
|
getStatus(config),
|
|
27
27
|
getWorkspace(config),
|
|
28
28
|
listTickets(config),
|
|
29
29
|
listAgents(config),
|
|
30
30
|
listEpics(config),
|
|
31
31
|
listFeed(config),
|
|
32
|
+
listMessages({ toRoles: ["human", "all"], undelivered: true, limit: 500 }, config),
|
|
32
33
|
]);
|
|
33
34
|
const byId = new Map(ticketData.tickets.map((ticket) => [ticket.id, ticket]));
|
|
34
35
|
const agents = new Map(agentData.agents.map((agent) => [agent.id, agent]));
|
|
@@ -63,6 +64,7 @@ export async function loadSnapshot(config = loadConfig()) {
|
|
|
63
64
|
agents: agentData.agents,
|
|
64
65
|
epics: epicData.epics,
|
|
65
66
|
decisions: status.decisions,
|
|
67
|
+
messages: messageData.messages,
|
|
66
68
|
availability,
|
|
67
69
|
runs: status.live_runs,
|
|
68
70
|
},
|
|
@@ -127,6 +129,9 @@ export async function cancelTicket(config, key) {
|
|
|
127
129
|
export async function setWorkspacePaused(config, paused) {
|
|
128
130
|
return setPausedNow(paused, config);
|
|
129
131
|
}
|
|
132
|
+
export async function listWorkspaceEnv(config) {
|
|
133
|
+
return getWorkspaceEnv(config);
|
|
134
|
+
}
|
|
130
135
|
export async function createAgent(config, fields) {
|
|
131
136
|
return postAgent(fields, config);
|
|
132
137
|
}
|
|
@@ -148,6 +153,10 @@ export async function waitForReply(config, role, since, timeoutMs) {
|
|
|
148
153
|
export async function resolveDecision(config, id, answer) {
|
|
149
154
|
await answerDecision(id, answer, config);
|
|
150
155
|
}
|
|
156
|
+
export async function acknowledgeInbox(config, ids) {
|
|
157
|
+
if (ids.length)
|
|
158
|
+
await markMessagesDelivered(ids, config);
|
|
159
|
+
}
|
|
151
160
|
export async function updateAgent(config, id, fields) {
|
|
152
161
|
return patchAgent(id, fields, config);
|
|
153
162
|
}
|
package/dist/tui/parse.js
CHANGED
|
@@ -37,6 +37,8 @@ export function parseLine(raw) {
|
|
|
37
37
|
...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { name: opts.name } : {}) }
|
|
38
38
|
: { kind: "unknown", command: "agents needs add ROLE --provider P --model M or rm ROLE|ID" };
|
|
39
39
|
}
|
|
40
|
+
case "env":
|
|
41
|
+
return rest.length ? { kind: "unknown", command: "env takes no arguments" } : { kind: "env" };
|
|
40
42
|
case "help":
|
|
41
43
|
return { kind: "help" };
|
|
42
44
|
case "workspace":
|