@higherdev/cli 0.16.0 → 0.18.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 +9 -2
- package/dist/api.js +8 -3
- package/dist/host.js +119 -0
- package/dist/index.js +57 -29
- package/dist/out.js +3 -3
- package/dist/tui/App.js +186 -52
- package/dist/tui/Dashboard.js +29 -11
- package/dist/tui/Decision.js +18 -31
- package/dist/tui/Help.js +4 -4
- package/dist/tui/Panels.js +48 -30
- package/dist/tui/TextInput.js +5 -1
- package/dist/tui/agent-rows.js +14 -2
- package/dist/tui/data.js +20 -6
- package/dist/tui/decide-nav.js +62 -0
- package/dist/tui/narrate.js +97 -0
- package/dist/tui/parse.js +5 -2
- package/dist/tui/stream.js +51 -34
- package/dist/tui/ticket-view.js +226 -0
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -13,6 +13,7 @@ import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js
|
|
|
13
13
|
import { DecisionPanel, decisionRows } from "./Decision.js";
|
|
14
14
|
import { COMMANDS, Help } from "./Help.js";
|
|
15
15
|
import { AgentsPanel, BoardPanel, FeedPanel, InboxPanel, TicketPanel, inboxEntries } from "./Panels.js";
|
|
16
|
+
import { sectionAt, ticketScrollOffset, ticketViewLines, toggleSection } from "./ticket-view.js";
|
|
16
17
|
import { SettingsPanel } from "./Settings.js";
|
|
17
18
|
import { Splash } from "./Splash.js";
|
|
18
19
|
import TextInput from "./TextInput.js";
|
|
@@ -22,6 +23,7 @@ import { planLayout, splitPanels } from "./layout.js";
|
|
|
22
23
|
import { parseLine } from "./parse.js";
|
|
23
24
|
import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, createEpicFromFile, decisionOptions, deleteAgent, deleteEpic, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, pollSnapshot, postAgentMessage, postTicketMessage, queueTicket, resolveDecision, selectDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, waitForReply, } from "./data.js";
|
|
24
25
|
import { inputActive, promptPlaceholder, QUEUED_STEP, REPLY_WAIT_MS, settleChatReply } from "./chat-wait.js";
|
|
26
|
+
import { answeredLine, decisionHeaderIndex, decisionIdAt, moveDecisionFocus, nextUnanswered, resolveDecisionAnswer, } from "./decide-nav.js";
|
|
25
27
|
import { EARLIER_PAGE } from "./inbox.js";
|
|
26
28
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
27
29
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
@@ -48,14 +50,20 @@ export function App({ initial }) {
|
|
|
48
50
|
const [busy, setBusy] = useState(false);
|
|
49
51
|
const [notice, setNotice] = useState(null);
|
|
50
52
|
const [ticketKey, setTicketKey] = useState(null);
|
|
53
|
+
const [ticketOffset, setTicketOffset] = useState(0);
|
|
54
|
+
const [ticketCollapsed, setTicketCollapsed] = useState([]);
|
|
51
55
|
const [ready, setReady] = useState(false);
|
|
52
56
|
const [stream, setStream] = useState([]);
|
|
53
57
|
const [logsFilter, setLogsFilter] = useState(null);
|
|
58
|
+
const [rawLogs, setRawLogs] = useState(false);
|
|
54
59
|
const [cursor, setCursor] = useState(null);
|
|
55
60
|
const [started, setStarted] = useState(false);
|
|
56
61
|
const [field, setField] = useState(null);
|
|
57
62
|
const [editing, setEditing] = useState(null);
|
|
58
63
|
const [inboxFocus, setInboxFocus] = useState(0);
|
|
64
|
+
const [selectedDecisionId, setSelectedDecisionId] = useState(initial.board.decisions[0]?.id ?? null);
|
|
65
|
+
const [answering, setAnswering] = useState(null);
|
|
66
|
+
const focusAfterAnswer = useRef(null);
|
|
59
67
|
const [earlierLimit, setEarlierLimit] = useState(EARLIER_PAGE);
|
|
60
68
|
const earlierLimitRef = useRef(EARLIER_PAGE);
|
|
61
69
|
earlierLimitRef.current = earlierLimit;
|
|
@@ -94,7 +102,9 @@ export function App({ initial }) {
|
|
|
94
102
|
if (!loads.current.isCurrent(token))
|
|
95
103
|
return;
|
|
96
104
|
setWorkspace(snapshot.workspace);
|
|
97
|
-
setBoard(snapshot.board)
|
|
105
|
+
setBoard((current) => ({ ...snapshot.board, tickets: snapshot.board.tickets.map((ticket) => ({
|
|
106
|
+
...ticket, timeline: current.tickets.find((row) => row.id === ticket.id)?.timeline,
|
|
107
|
+
})) }));
|
|
98
108
|
setFeed(snapshot.feed);
|
|
99
109
|
}, []);
|
|
100
110
|
useEffect(() => {
|
|
@@ -119,13 +129,17 @@ export function App({ initial }) {
|
|
|
119
129
|
if (logsFilter)
|
|
120
130
|
for (const event of events)
|
|
121
131
|
if (!eventLabels.has(event.run_id)) {
|
|
122
|
-
|
|
132
|
+
const run = event.run;
|
|
133
|
+
eventLabels.set(event.run_id, { runId: event.run_id, agent: run?.agent_name ?? logsFilter,
|
|
134
|
+
ticket: logsFilter, kind: run?.kind ?? "run", status: run?.status ?? "running",
|
|
135
|
+
attempt: run?.attempt ?? 1, startedAt: run?.started_at ?? event.at,
|
|
136
|
+
endedAt: run?.ended_at ?? null, summary: run?.summary ?? null });
|
|
123
137
|
}
|
|
124
|
-
setStream((prior) => appendLines(prior, toStreamLines(events, eventLabels)));
|
|
138
|
+
setStream((prior) => appendLines(prior, toStreamLines(events, eventLabels, rawLogs)));
|
|
125
139
|
})
|
|
126
140
|
.catch(() => { });
|
|
127
|
-
}, [liveRunIds, board, config, labels, workspace.id, logsFilter]);
|
|
128
|
-
useEffect(() => setStream([]), [logsFilter]);
|
|
141
|
+
}, [liveRunIds, board, config, labels, workspace.id, logsFilter, rawLogs]);
|
|
142
|
+
useEffect(() => setStream([]), [logsFilter, rawLogs]);
|
|
129
143
|
const say = useCallback((speaker, body, steps) => {
|
|
130
144
|
setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
|
|
131
145
|
}, []);
|
|
@@ -134,7 +148,7 @@ export function App({ initial }) {
|
|
|
134
148
|
const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
|
|
135
149
|
const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
|
|
136
150
|
const configuring = view === "settings" && !editing;
|
|
137
|
-
const inbox = useMemo(() => inboxEntries(board, width), [board, width]);
|
|
151
|
+
const inbox = useMemo(() => inboxEntries(board, width, answering), [board, width, answering]);
|
|
138
152
|
const moveCursor = useCallback((delta) => {
|
|
139
153
|
if (!order.length)
|
|
140
154
|
return false;
|
|
@@ -154,11 +168,32 @@ export function App({ initial }) {
|
|
|
154
168
|
return true;
|
|
155
169
|
}, [settingsOrder]);
|
|
156
170
|
const moveInbox = useCallback((delta) => {
|
|
157
|
-
setInboxFocus((current) =>
|
|
158
|
-
|
|
171
|
+
setInboxFocus((current) => {
|
|
172
|
+
const next = moveDecisionFocus(inbox, current, delta);
|
|
173
|
+
const id = decisionIdAt(inbox, next);
|
|
174
|
+
if (id)
|
|
175
|
+
setSelectedDecisionId(id);
|
|
176
|
+
return next;
|
|
177
|
+
});
|
|
178
|
+
}, [inbox]);
|
|
159
179
|
useEffect(() => {
|
|
160
180
|
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current)));
|
|
161
181
|
}, [inbox.length]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (selectedDecisionId && board.decisions.some((decision) => decision.id === selectedDecisionId))
|
|
184
|
+
return;
|
|
185
|
+
setSelectedDecisionId(board.decisions[0]?.id ?? null);
|
|
186
|
+
setAnswering((current) => (current && board.decisions.some((decision) => decision.id === current) ? current : null));
|
|
187
|
+
}, [board.decisions, selectedDecisionId]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const id = focusAfterAnswer.current;
|
|
190
|
+
if (!id)
|
|
191
|
+
return;
|
|
192
|
+
const header = decisionHeaderIndex(inbox, id);
|
|
193
|
+
if (header >= 0)
|
|
194
|
+
setInboxFocus(header);
|
|
195
|
+
focusAfterAnswer.current = null;
|
|
196
|
+
}, [inbox]);
|
|
162
197
|
useEffect(() => {
|
|
163
198
|
if (view !== "inbox")
|
|
164
199
|
return;
|
|
@@ -175,6 +210,38 @@ export function App({ initial }) {
|
|
|
175
210
|
const refresh = useCallback(async () => {
|
|
176
211
|
await refreshRef.current?.();
|
|
177
212
|
}, []);
|
|
213
|
+
const submitDecision = useCallback(async (id, text) => {
|
|
214
|
+
const decision = board.decisions.find((row) => row.id === id);
|
|
215
|
+
if (!decision) {
|
|
216
|
+
setNotice("Nothing is waiting on a decision.");
|
|
217
|
+
setAnswering(null);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const answer = resolveDecisionAnswer(decision, text);
|
|
221
|
+
if (!answer) {
|
|
222
|
+
setNotice("Type a number or your answer.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const next = nextUnanswered(board.decisions, decision.id);
|
|
226
|
+
const line = answeredLine(board.decisions, board.tickets, decision, answer);
|
|
227
|
+
setBusy(true);
|
|
228
|
+
try {
|
|
229
|
+
await resolveDecision(config, decision.id, answer);
|
|
230
|
+
say("system", line);
|
|
231
|
+
setAnswering(null);
|
|
232
|
+
setSelectedDecisionId(next?.id ?? null);
|
|
233
|
+
focusAfterAnswer.current = next?.id ?? null;
|
|
234
|
+
if (next)
|
|
235
|
+
setView("inbox");
|
|
236
|
+
await refresh();
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
setBusy(false);
|
|
243
|
+
}
|
|
244
|
+
}, [board, config, refresh, say]);
|
|
178
245
|
const applyEdit = useCallback(async (key, raw) => {
|
|
179
246
|
const row = settings.find((entry) => entry.key === key);
|
|
180
247
|
if (!row)
|
|
@@ -217,6 +284,8 @@ export function App({ initial }) {
|
|
|
217
284
|
setStream([]);
|
|
218
285
|
setLogsFilter(null);
|
|
219
286
|
acknowledgedMessages.current.clear();
|
|
287
|
+
setAnswering(null);
|
|
288
|
+
setSelectedDecisionId(snapshot.board.decisions[0]?.id ?? null);
|
|
220
289
|
setEarlierLimit(EARLIER_PAGE);
|
|
221
290
|
earlierLimitRef.current = EARLIER_PAGE;
|
|
222
291
|
setCursor(null);
|
|
@@ -254,6 +323,33 @@ export function App({ initial }) {
|
|
|
254
323
|
}
|
|
255
324
|
})();
|
|
256
325
|
}, [config]);
|
|
326
|
+
const openTicket = useCallback(async (key) => {
|
|
327
|
+
setTicketKey(key);
|
|
328
|
+
setView("ticket");
|
|
329
|
+
setTicketOffset(0);
|
|
330
|
+
setTicketCollapsed([]);
|
|
331
|
+
setBusy(true);
|
|
332
|
+
try {
|
|
333
|
+
const { ticket: detail } = await loadTicketDetail(config, key);
|
|
334
|
+
setBoard((prior) => ({ ...prior,
|
|
335
|
+
tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket) }));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
setBusy(false);
|
|
342
|
+
}
|
|
343
|
+
}, [config]);
|
|
344
|
+
const visibleStory = ticketKey ? board.tickets.find((ticket) => ticket.key === ticketKey) : null;
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
if (view !== "ticket" || !ticketKey || !visibleStory?.latest_headline
|
|
347
|
+
|| visibleStory.timeline?.[0]?.headline === visibleStory.latest_headline)
|
|
348
|
+
return;
|
|
349
|
+
void loadTicketDetail(config, ticketKey).then(({ ticket: detail }) => setBoard((prior) => ({ ...prior,
|
|
350
|
+
tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket) })))
|
|
351
|
+
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
352
|
+
}, [view, ticketKey, visibleStory?.latest_headline, visibleStory?.timeline, config]);
|
|
257
353
|
const run = useCallback(async (raw) => {
|
|
258
354
|
const text = raw.trim();
|
|
259
355
|
if (view === "settings") {
|
|
@@ -280,13 +376,39 @@ export function App({ initial }) {
|
|
|
280
376
|
}
|
|
281
377
|
}
|
|
282
378
|
if (!text) {
|
|
379
|
+
if (answering) {
|
|
380
|
+
setNotice("Type a number or your answer.");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (view === "ticket" && ticketKey) {
|
|
384
|
+
const current = board.tickets.find((row) => row.key === ticketKey);
|
|
385
|
+
if (current) {
|
|
386
|
+
const lines = ticketViewLines(current, Math.max(20, width), ticketCollapsed);
|
|
387
|
+
setTicketCollapsed(toggleSection(ticketCollapsed, sectionAt(lines, ticketOffset)));
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const focusedId = selectedDecisionId ?? decisionIdAt(inbox, inboxFocus);
|
|
392
|
+
if (view === "inbox" && focusedId && board.decisions.some((decision) => decision.id === focusedId)) {
|
|
393
|
+
setSelectedDecisionId(focusedId);
|
|
394
|
+
setAnswering(focusedId);
|
|
395
|
+
setView("inbox");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
283
398
|
const selected = board.tickets.find((ticket) => ticket.id === selectedRef.current);
|
|
284
399
|
if (browsing && selected) {
|
|
285
|
-
|
|
286
|
-
setView("ticket");
|
|
400
|
+
await openTicket(selected.key);
|
|
287
401
|
}
|
|
288
402
|
return;
|
|
289
403
|
}
|
|
404
|
+
if (answering) {
|
|
405
|
+
history.current.push(text);
|
|
406
|
+
historyAt.current = -1;
|
|
407
|
+
setDraft("");
|
|
408
|
+
setNotice(null);
|
|
409
|
+
await submitDecision(answering, text);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
290
412
|
history.current.push(text);
|
|
291
413
|
historyAt.current = -1;
|
|
292
414
|
setDraft("");
|
|
@@ -311,8 +433,11 @@ export function App({ initial }) {
|
|
|
311
433
|
return;
|
|
312
434
|
case "view":
|
|
313
435
|
setView(action.view);
|
|
314
|
-
if (action.view === "inbox")
|
|
315
|
-
|
|
436
|
+
if (action.view === "inbox") {
|
|
437
|
+
const header = selectedDecisionId ? decisionHeaderIndex(inbox, selectedDecisionId) : 0;
|
|
438
|
+
setInboxFocus(header >= 0 ? header : 0);
|
|
439
|
+
setAnswering(null);
|
|
440
|
+
}
|
|
316
441
|
if (action.view === "board" && order.length) {
|
|
317
442
|
const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
|
|
318
443
|
selectedRef.current = next;
|
|
@@ -398,22 +523,7 @@ export function App({ initial }) {
|
|
|
398
523
|
}
|
|
399
524
|
return;
|
|
400
525
|
case "ticket":
|
|
401
|
-
|
|
402
|
-
setView("ticket");
|
|
403
|
-
setBusy(true);
|
|
404
|
-
try {
|
|
405
|
-
const { ticket: detail } = await loadTicketDetail(config, action.key);
|
|
406
|
-
setBoard((prior) => ({
|
|
407
|
-
...prior,
|
|
408
|
-
tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket),
|
|
409
|
-
}));
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
413
|
-
}
|
|
414
|
-
finally {
|
|
415
|
-
setBusy(false);
|
|
416
|
-
}
|
|
526
|
+
await openTicket(action.key);
|
|
417
527
|
return;
|
|
418
528
|
case "epic-new":
|
|
419
529
|
setBusy(true);
|
|
@@ -574,10 +684,11 @@ export function App({ initial }) {
|
|
|
574
684
|
return;
|
|
575
685
|
case "logs":
|
|
576
686
|
setLogsFilter(action.key);
|
|
577
|
-
|
|
687
|
+
setRawLogs(action.raw);
|
|
688
|
+
say("system", `${action.raw ? "Raw" : "Narrated"} activity${action.key ? ` filtered to ${action.key}` : " filter cleared"}.`);
|
|
578
689
|
return;
|
|
579
690
|
case "decide": {
|
|
580
|
-
const focused = inbox
|
|
691
|
+
const focused = selectedDecisionId ?? decisionIdAt(inbox, inboxFocus);
|
|
581
692
|
const decision = selectDecision(board.decisions, action.target, focused);
|
|
582
693
|
if (!decision) {
|
|
583
694
|
setNotice(action.target ? `No decision matches ${action.target}.` : "Nothing is waiting on a decision.");
|
|
@@ -587,25 +698,16 @@ export function App({ initial }) {
|
|
|
587
698
|
setNotice("Skipping decisions is not available through the HDX API. Answer it instead.");
|
|
588
699
|
return;
|
|
589
700
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
701
|
+
if (!action.answer) {
|
|
702
|
+
setSelectedDecisionId(decision.id);
|
|
703
|
+
setAnswering(decision.id);
|
|
704
|
+
setView("inbox");
|
|
705
|
+
const header = decisionHeaderIndex(inbox, decision.id);
|
|
706
|
+
if (header >= 0)
|
|
707
|
+
setInboxFocus(header);
|
|
595
708
|
return;
|
|
596
709
|
}
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
await resolveDecision(config, decision.id, answer);
|
|
600
|
-
say("system", `Answered: ${answer}`);
|
|
601
|
-
await refresh();
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
605
|
-
}
|
|
606
|
-
finally {
|
|
607
|
-
setBusy(false);
|
|
608
|
-
}
|
|
710
|
+
await submitDecision(decision.id, action.answer);
|
|
609
711
|
return;
|
|
610
712
|
}
|
|
611
713
|
case "help":
|
|
@@ -635,12 +737,16 @@ export function App({ initial }) {
|
|
|
635
737
|
return;
|
|
636
738
|
}
|
|
637
739
|
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, order, settingsOrder, changeWorkspace,
|
|
638
|
-
config, refresh, suspendTerminal, exit, inbox, inboxFocus
|
|
740
|
+
config, refresh, suspendTerminal, exit, inbox, inboxFocus, openTicket, answering, selectedDecisionId,
|
|
741
|
+
submitDecision, ticketKey, ticketCollapsed, ticketOffset, width]);
|
|
639
742
|
useInput((input, key) => {
|
|
640
743
|
if (key.ctrl && input === "c")
|
|
641
744
|
exit();
|
|
642
745
|
});
|
|
643
746
|
const decisions = board.decisions;
|
|
747
|
+
const answeringDecision = answering ? decisions.find((decision) => decision.id === answering) : null;
|
|
748
|
+
const answeringOptions = answeringDecision ? decisionOptions(answeringDecision) : [];
|
|
749
|
+
const answeringNumber = answeringDecision ? decisions.findIndex((decision) => decision.id === answering) + 1 : 0;
|
|
644
750
|
const waiting = decisions.length;
|
|
645
751
|
const announced = useRef(0);
|
|
646
752
|
useEffect(() => {
|
|
@@ -671,13 +777,21 @@ export function App({ initial }) {
|
|
|
671
777
|
if (item.message.panel === "help")
|
|
672
778
|
return _jsx(Help, { width: width }, item.key);
|
|
673
779
|
return _jsx(Bubble, { message: item.message, width: width }, item.key);
|
|
674
|
-
} }), 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
|
|
675
|
-
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels })
|
|
676
|
-
: _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, logsFilter ? _jsxs(Text, { color: UI.warn, children: ["logs ", logsFilter, " "] }) : null, _jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["\u00B7 ", mode, view === "
|
|
780
|
+
} }), 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, selectedId: selectedDecisionId, answeringId: answering }) : null, view === "ticket" && plan.panels > 0 ? ticket
|
|
781
|
+
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels, offset: ticketOffset, collapsed: ticketCollapsed })
|
|
782
|
+
: _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, selectedId: selectedDecisionId }), 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, logsFilter || rawLogs ? _jsxs(Text, { color: UI.warn, children: ["logs ", rawLogs ? "raw " : "", logsFilter ?? "all", " "] }) : null, _jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["\u00B7 ", mode, answering ? " esc cancels" : view === "ticket" ? " ↑↓ scroll · pgup/pgdn · enter folds" : view === "inbox" ? " ↑↓ decisions · enter answers" : cursor && selected ? ` ${selected.key} ↑↓ move · enter opens · esc leaves` : ""] })] }), _jsx(Box, { children: _jsx(TextInput, { value: draft, onChange: (next) => {
|
|
677
783
|
setDraft(next);
|
|
678
784
|
if (editingRef.current)
|
|
679
785
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
680
|
-
}, onSubmit: (value) => void run(value), isActive: inputActive({ busy, pendingChats: inFlight.length }), placeholder:
|
|
786
|
+
}, onSubmit: (value) => void run(value), isActive: inputActive({ busy, pendingChats: inFlight.length }), placeholder: answering
|
|
787
|
+
? (answeringOptions.length ? `1-${answeringOptions.length} or your answer` : "your answer")
|
|
788
|
+
: promptPlaceholder({ busy, pendingChats: inFlight.length }), prompt: _jsx(Text, { color: answering || mode !== "browse" ? UI.cream : UI.dim, children: answering ? `answer ${answeringNumber}> ` : mode === "browse" ? "> " : `${mode}> ` }), color: UI.text, onCancel: () => {
|
|
789
|
+
if (answering) {
|
|
790
|
+
setAnswering(null);
|
|
791
|
+
setDraft("");
|
|
792
|
+
setNotice(null);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
681
795
|
if (editing) {
|
|
682
796
|
setEditing(null);
|
|
683
797
|
setDraft("");
|
|
@@ -692,6 +806,11 @@ export function App({ initial }) {
|
|
|
692
806
|
setCursor(null);
|
|
693
807
|
selectedRef.current = null;
|
|
694
808
|
}, onUp: () => {
|
|
809
|
+
if (view === "ticket" && !draft) {
|
|
810
|
+
const count = ticket ? ticketViewLines(ticket, Math.max(20, width), ticketCollapsed).length : 0;
|
|
811
|
+
setTicketOffset((current) => ticketScrollOffset(count, Math.max(1, plan.panels), current - 1));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
695
814
|
if (view === "inbox" && !draft) {
|
|
696
815
|
moveInbox(-1);
|
|
697
816
|
return;
|
|
@@ -705,6 +824,11 @@ export function App({ initial }) {
|
|
|
705
824
|
historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
|
|
706
825
|
setDraft(history.current[historyAt.current] ?? "");
|
|
707
826
|
}, onDown: () => {
|
|
827
|
+
if (view === "ticket" && !draft) {
|
|
828
|
+
const count = ticket ? ticketViewLines(ticket, Math.max(20, width), ticketCollapsed).length : 0;
|
|
829
|
+
setTicketOffset((current) => ticketScrollOffset(count, Math.max(1, plan.panels), current + 1));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
708
832
|
if (view === "inbox" && !draft) {
|
|
709
833
|
moveInbox(1);
|
|
710
834
|
return;
|
|
@@ -722,6 +846,16 @@ export function App({ initial }) {
|
|
|
722
846
|
return;
|
|
723
847
|
}
|
|
724
848
|
setDraft(history.current[historyAt.current] ?? "");
|
|
849
|
+
}, onPageUp: () => {
|
|
850
|
+
if (view === "ticket" && !draft) {
|
|
851
|
+
const count = ticket ? ticketViewLines(ticket, Math.max(20, width), ticketCollapsed).length : 0;
|
|
852
|
+
setTicketOffset((current) => ticketScrollOffset(count, Math.max(1, plan.panels), current - Math.max(1, plan.panels)));
|
|
853
|
+
}
|
|
854
|
+
}, onPageDown: () => {
|
|
855
|
+
if (view === "ticket" && !draft) {
|
|
856
|
+
const count = ticket ? ticketViewLines(ticket, Math.max(20, width), ticketCollapsed).length : 0;
|
|
857
|
+
setTicketOffset((current) => ticketScrollOffset(count, Math.max(1, plan.panels), current + Math.max(1, plan.panels)));
|
|
858
|
+
}
|
|
725
859
|
} }) })] })] }));
|
|
726
860
|
}
|
|
727
861
|
export { COMMANDS };
|
package/dist/tui/Dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { BOARD_COLUMNS, epicProgressCaption, epicProgress, statusLabel, statusTone, } from "./data.js";
|
|
4
4
|
import { elapsed, truncate } from "../out/format.js";
|
|
@@ -103,7 +103,7 @@ export function BoardColumn({ board, width, rows, cursor, }) {
|
|
|
103
103
|
? [
|
|
104
104
|
_jsx(Text, { color: UI.dim, children: "No tickets yet." }, "empty"),
|
|
105
105
|
]
|
|
106
|
-
: entries.slice(start, end).map((entry) => entry.kind === "heading" ? (_jsxs(Text, { color: inkColor(statusTone(entry.status)), wrap: "truncate", children: [statusLabel(entry.status), " ", _jsxs(Text, { color: UI.dim, children: ["(", entry.count, ")"] })] }, entry.key)) : (_jsxs(Box, { flexWrap: "nowrap", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: UI.accent, children: entry.key === cursor ? "›" : " " }) }), _jsx(Box, { width: 8, flexShrink: 0, children: _jsx(Text, { color: UI.text, bold: true, wrap: "truncate", children: entry.ticket.key }) }), _jsx(Text, { color: entry.ticket.stuck ? UI.warn : UI.text, inverse: entry.key === cursor, wrap: "truncate", children: truncate(entry.ticket.title, title - 2) })] }, entry.key)))),
|
|
106
|
+
: entries.slice(start, end).map((entry) => entry.kind === "heading" ? (_jsxs(Text, { color: inkColor(statusTone(entry.status)), wrap: "truncate", children: [statusLabel(entry.status), " ", _jsxs(Text, { color: UI.dim, children: ["(", entry.count, ")"] })] }, entry.key)) : (_jsxs(Box, { flexWrap: "nowrap", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: UI.accent, children: entry.key === cursor ? "›" : " " }) }), _jsx(Box, { width: 8, flexShrink: 0, children: _jsx(Text, { color: UI.text, bold: true, wrap: "truncate", children: entry.ticket.key }) }), _jsx(Text, { color: entry.ticket.stuck ? UI.warn : UI.text, inverse: entry.key === cursor, wrap: "truncate", children: truncate(entry.ticket.latest_headline ?? entry.ticket.title, title - 2) })] }, entry.key)))),
|
|
107
107
|
] }));
|
|
108
108
|
}
|
|
109
109
|
export function AgentsColumn({ board, width, rows, }) {
|
|
@@ -117,19 +117,21 @@ export function AgentsColumn({ board, width, rows, }) {
|
|
|
117
117
|
]
|
|
118
118
|
: []),
|
|
119
119
|
...shown.map((row) => {
|
|
120
|
-
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited"
|
|
121
|
-
|
|
120
|
+
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited" || row.state === "draining"
|
|
121
|
+
? "warning" : "muted";
|
|
122
|
+
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, row.state === "draining" ? null : (_jsx(Text, { color: UI.dim, children: row.run
|
|
122
123
|
? ` · ${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));
|
|
124
|
+
: ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` }))] })] }, row.key));
|
|
124
125
|
}),
|
|
125
126
|
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
126
127
|
] }));
|
|
127
128
|
}
|
|
128
129
|
const KIND_COLOR = {
|
|
129
130
|
error: UI.danger,
|
|
131
|
+
agent: UI.accent,
|
|
130
132
|
tool: UI.dim,
|
|
131
133
|
status: UI.dim,
|
|
132
|
-
|
|
134
|
+
raw: UI.dim,
|
|
133
135
|
};
|
|
134
136
|
/**
|
|
135
137
|
* What the agents are doing, as it arrives. The panel is a fixed height on
|
|
@@ -146,7 +148,7 @@ export function StreamPanel({ lines, width, rows, live, }) {
|
|
|
146
148
|
_jsx(Text, { color: UI.dim, children: live ? "Waiting for the first step." : "Nothing running." }, "empty"),
|
|
147
149
|
]
|
|
148
150
|
: []),
|
|
149
|
-
...lines.slice(-budget).map((line) => (_jsxs(Box, { flexWrap: "nowrap", children: [_jsx(Box, { width: name, flexShrink: 0, children: _jsx(Text, { color: UI.dim, wrap: "truncate", children: truncate(line.agent, name - 1) }) }), _jsxs(Text, { color: KIND_COLOR[line.kind], wrap: "truncate", children: [line.kind === "tool" ? "· " : "", truncate(line.title, Math.max(12, width - name - 3))] })] }, line.id))),
|
|
151
|
+
...lines.slice(-budget).map((line) => (_jsxs(Box, { flexWrap: "nowrap", children: [_jsx(Box, { width: name, flexShrink: 0, children: _jsx(Text, { color: UI.dim, wrap: "truncate", children: truncate(line.agent, name - 1) }) }), _jsxs(Text, { color: KIND_COLOR[line.kind], wrap: "truncate", children: [line.kind === "tool" ? "· " : "", truncate(line.title, Math.max(12, width - name - 3)), line.count && line.count > 1 ? ` (x${line.count})` : ""] })] }, line.id))),
|
|
150
152
|
] }));
|
|
151
153
|
}
|
|
152
154
|
/** How many rows the epics strip wants, heading included, or none. */
|
|
@@ -154,6 +156,21 @@ export function epicsRows(board, cap = 3) {
|
|
|
154
156
|
const open = epicProgress(board).filter((row) => row.epic.status !== "done");
|
|
155
157
|
return open.length === 0 ? 0 : Math.min(open.length, cap) + 1;
|
|
156
158
|
}
|
|
159
|
+
export function nowEntries(board) {
|
|
160
|
+
return board.tickets.filter((ticket) => !["backlog", "merged", "cancelled"].includes(ticket.status))
|
|
161
|
+
.map((ticket) => ({ key: ticket.key, headline: ticket.latest_headline ?? `${ticket.key}: ${ticket.title}` }));
|
|
162
|
+
}
|
|
163
|
+
function NowStrip({ board, width, rows }) {
|
|
164
|
+
const entries = nowEntries(board);
|
|
165
|
+
if (!entries.length || rows < 2)
|
|
166
|
+
return null;
|
|
167
|
+
const shown = entries.slice(0, contentRows(rows, entries.length));
|
|
168
|
+
return _jsx(Panel, { width: width, rows: rows, children: [
|
|
169
|
+
_jsx(Heading, { text: "Now", note: `${entries.length}` }, "h"),
|
|
170
|
+
...shown.map((entry) => _jsx(Text, { color: UI.text, wrap: "truncate", children: entry.headline }, entry.key)),
|
|
171
|
+
_jsx(More, { count: entries.length - shown.length }, "more"),
|
|
172
|
+
] });
|
|
173
|
+
}
|
|
157
174
|
/**
|
|
158
175
|
* Epic progress, above the board it explains. Capped hard: this is the summary
|
|
159
176
|
* line, and the tickets underneath are what you are here to read.
|
|
@@ -174,12 +191,13 @@ export function EpicsStrip({ board, width, rows }) {
|
|
|
174
191
|
export function Cockpit({ board, width, rows, cursor, }) {
|
|
175
192
|
// The epics strip is spent out of the same budget, so adding it shortens the
|
|
176
193
|
// board rather than making the frame taller.
|
|
177
|
-
const
|
|
178
|
-
const
|
|
194
|
+
const now = Math.min(nowEntries(board).length + 1, 4, Math.max(0, rows - 4));
|
|
195
|
+
const epics = Math.min(epicsRows(board), Math.max(0, rows - now - (now > 0 ? 1 : 0) - 4));
|
|
196
|
+
const rest = rows - (now > 0 ? now + 1 : 0) - (epics > 0 ? epics + 1 : 0);
|
|
179
197
|
const columns = renderColumns(board, width, rest, cursor);
|
|
180
|
-
if (epics === 0)
|
|
198
|
+
if (epics === 0 && now === 0)
|
|
181
199
|
return columns;
|
|
182
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(EpicsStrip, { board: board, width: width, rows: epics }), _jsx(Box, { height: 1 }), columns] }));
|
|
200
|
+
return (_jsxs(Box, { flexDirection: "column", children: [now > 0 ? _jsxs(_Fragment, { children: [_jsx(NowStrip, { board: board, width: width, rows: now }), _jsx(Box, { height: 1 })] }) : null, epics > 0 ? _jsxs(_Fragment, { children: [_jsx(EpicsStrip, { board: board, width: width, rows: epics }), _jsx(Box, { height: 1 })] }) : null, columns] }));
|
|
183
201
|
}
|
|
184
202
|
function renderColumns(board, width, rows, cursor) {
|
|
185
203
|
const split = splitWidths(width);
|
package/dist/tui/Decision.js
CHANGED
|
@@ -1,48 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { relativeTime, truncate } from "../out/format.js";
|
|
4
|
-
import {
|
|
5
|
-
import { decisionOptions } from "./data.js";
|
|
4
|
+
import { DECISION_CURSOR } from "./decide-nav.js";
|
|
6
5
|
import { UI } from "./theme.js";
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*/
|
|
11
|
-
const DECISION_CHROME = 5;
|
|
12
|
-
/**
|
|
13
|
-
* Rows the flag wants, so the caller can take them out of the panel budget
|
|
14
|
-
* before anything is drawn. Zero when nothing is waiting.
|
|
7
|
+
* Heading, one row per pending decision, and the hint. Cap keeps the flag
|
|
8
|
+
* from eating the cockpit on a busy inbox.
|
|
15
9
|
*/
|
|
16
10
|
export function decisionRows(decisions, cap = 9) {
|
|
17
11
|
if (decisions.length === 0)
|
|
18
12
|
return 0;
|
|
19
|
-
|
|
20
|
-
return Math.min(cap, DECISION_CHROME + 2 + decisionOptions(decisions[0]).length);
|
|
13
|
+
return Math.min(cap, 4 + decisions.length);
|
|
21
14
|
}
|
|
22
15
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* nothing moves on the ticket it blocks until it is answered, so it stays in
|
|
26
|
-
* front of you until it is.
|
|
16
|
+
* Pending decisions on the home view, numbered the same way `/inbox` and
|
|
17
|
+
* `/decide N` use, so the owner can answer any of them.
|
|
27
18
|
*/
|
|
28
|
-
export function DecisionPanel({ decisions, board, width, rows, }) {
|
|
19
|
+
export function DecisionPanel({ decisions, board, width, rows, selectedId = null, }) {
|
|
29
20
|
if (decisions.length === 0)
|
|
30
21
|
return null;
|
|
31
|
-
const decision = decisions[0];
|
|
32
|
-
const ticket = board?.tickets.find((row) => row.id === decision.ticket_id);
|
|
33
|
-
const options = decisionOptions(decision);
|
|
34
22
|
const inner = Math.max(8, width - 4);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (forBody < 1) {
|
|
39
|
-
return (_jsxs(Box, { width: width, flexWrap: "nowrap", children: [_jsx(Text, { color: UI.warn, bold: true, wrap: "truncate", children: decisions.length > 1 ? `${decisions.length} decisions waiting` : "Decision waiting" }), _jsx(Text, { color: UI.dim, wrap: "truncate", children: ` ${truncate(decision.question_md, Math.max(8, width - 26))} /decide` })] }));
|
|
23
|
+
const selected = selectedId ?? decisions[0]?.id ?? null;
|
|
24
|
+
if (rows < 4) {
|
|
25
|
+
return (_jsxs(Box, { width: width, flexWrap: "nowrap", children: [_jsxs(Text, { color: UI.warn, bold: true, wrap: "truncate", children: [decisions.length, " decision", decisions.length === 1 ? "" : "s"] }), _jsx(Text, { color: UI.dim, wrap: "truncate", children: ` /decide N answer or /inbox then Enter` })] }));
|
|
40
26
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
const shown = decisions.slice(0, Math.max(1, rows - 4));
|
|
28
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: UI.warn, flexDirection: "column", paddingX: 1, width: width, marginBottom: 1, children: [_jsxs(Text, { color: UI.warn, bold: true, wrap: "truncate", children: ["Decisions (", decisions.length, ")"] }), shown.map((decision, index) => {
|
|
29
|
+
const ticket = board?.tickets.find((row) => row.id === decision.ticket_id);
|
|
30
|
+
const cursor = decision.id === selected;
|
|
31
|
+
const label = ticket?.key ?? decision.id.slice(0, 8);
|
|
32
|
+
const question = decision.question_md.trim().split("\n")[0] ?? "";
|
|
33
|
+
return (_jsxs(Text, { color: UI.text, wrap: "truncate", inverse: cursor, children: [cursor ? `${DECISION_CURSOR} ` : " ", index + 1, ") ", label, question ? ` ${truncate(question, Math.max(8, inner - label.length - 8))}` : "", ` ${relativeTime(decision.created_at)}`] }, decision.id));
|
|
34
|
+
}), decisions.length > shown.length ? (_jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["+", decisions.length - shown.length, " more in /inbox"] })) : null, _jsx(Text, { color: UI.dim, wrap: "truncate", children: "/decide 3 answer \u00B7 /inbox then Enter" })] }));
|
|
48
35
|
}
|
package/dist/tui/Help.js
CHANGED
|
@@ -6,17 +6,17 @@ export const HELP_FOOTER = "Anything not starting with / goes to whoever you are
|
|
|
6
6
|
/** Everything you can type. The app is driven from here, not from flags. */
|
|
7
7
|
export const COMMANDS = [
|
|
8
8
|
{ name: "/board", help: "the kanban board" },
|
|
9
|
-
{ name: "/inbox", args: "[more]", help: "unread and
|
|
10
|
-
{ name: "/ticket", args: "HD-12 | new [PATH.md]", help: "open or create
|
|
9
|
+
{ name: "/inbox", args: "[more]", help: "unread, earlier, and numbered decisions; Enter answers" },
|
|
10
|
+
{ name: "/ticket", args: "HD-12 | new [PATH.md]", help: "open a full ticket view or create one" },
|
|
11
11
|
{ name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
|
|
12
12
|
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
13
13
|
{ name: "/msg", args: "HD-12 TEXT", help: "message a ticket's builder" },
|
|
14
|
-
{ name: "/logs", args: "[HD-12]", help: "filter
|
|
14
|
+
{ name: "/logs", args: "[raw] [HD-12]", help: "filter activity; raw reveals event JSON" },
|
|
15
15
|
{ name: "/epic", args: "new PATH | approve ID | rm ID", help: "create, approve, or remove a draft epic" },
|
|
16
16
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
17
17
|
{ name: "/architect", help: "talk to the agent that shapes draft epics" },
|
|
18
18
|
{ name: "/plan", help: "alias for /architect" },
|
|
19
|
-
{ name: "/decide", args: "[N|ID] answer", help: "answer
|
|
19
|
+
{ name: "/decide", args: "[N|ID] [answer]", help: "answer the selected or numbered decision" },
|
|
20
20
|
{ name: "/agents", args: "[add [ROLE] [flags] | rm ROLE|ID]", help: "view or manage named agents" },
|
|
21
21
|
{ name: "/env", help: "list workspace environment variable names" },
|
|
22
22
|
{ name: "/settings", help: "change provider caps and agent settings" },
|