@higherdev/cli 0.15.4 → 0.17.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 +3 -2
- package/dist/agent-commands.js +80 -0
- package/dist/api.js +14 -3
- package/dist/index.js +90 -28
- package/dist/out.js +2 -1
- package/dist/tui/App.js +213 -56
- package/dist/tui/Dashboard.js +25 -8
- package/dist/tui/Decision.js +18 -31
- package/dist/tui/Help.js +5 -5
- package/dist/tui/Panels.js +80 -42
- package/dist/tui/TextInput.js +5 -1
- package/dist/tui/agent-rows.js +1 -1
- package/dist/tui/data.js +42 -16
- package/dist/tui/decide-nav.js +62 -0
- package/dist/tui/inbox.js +29 -0
- package/dist/tui/narrate.js +97 -0
- package/dist/tui/parse.js +13 -10
- 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
|
@@ -5,6 +5,7 @@ import { loadConfig } from "../config.js";
|
|
|
5
5
|
import { epicProgressRows } from "../epics.js";
|
|
6
6
|
import { promptOnStdin } from "../prompt.js";
|
|
7
7
|
import { ticketNew } from "../ticket-commands.js";
|
|
8
|
+
import { agentAdd } from "../agent-commands.js";
|
|
8
9
|
import { workspaceGrantRunnerAccess, workspaceNew, workspaceRotateKey, workspaceSet } from "../workspace-commands.js";
|
|
9
10
|
import { Banner } from "./Banner.js";
|
|
10
11
|
import { Bubble } from "./Bubble.js";
|
|
@@ -12,6 +13,7 @@ import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js
|
|
|
12
13
|
import { DecisionPanel, decisionRows } from "./Decision.js";
|
|
13
14
|
import { COMMANDS, Help } from "./Help.js";
|
|
14
15
|
import { AgentsPanel, BoardPanel, FeedPanel, InboxPanel, TicketPanel, inboxEntries } from "./Panels.js";
|
|
16
|
+
import { sectionAt, ticketScrollOffset, ticketViewLines, toggleSection } from "./ticket-view.js";
|
|
15
17
|
import { SettingsPanel } from "./Settings.js";
|
|
16
18
|
import { Splash } from "./Splash.js";
|
|
17
19
|
import TextInput from "./TextInput.js";
|
|
@@ -19,8 +21,10 @@ import { alertOnce } from "./alert.js";
|
|
|
19
21
|
import { bubbleRows } from "./height.js";
|
|
20
22
|
import { planLayout, splitPanels } from "./layout.js";
|
|
21
23
|
import { parseLine } from "./parse.js";
|
|
22
|
-
import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket,
|
|
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";
|
|
23
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";
|
|
27
|
+
import { EARLIER_PAGE } from "./inbox.js";
|
|
24
28
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
25
29
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
26
30
|
import { UI } from "./theme.js";
|
|
@@ -46,14 +50,23 @@ export function App({ initial }) {
|
|
|
46
50
|
const [busy, setBusy] = useState(false);
|
|
47
51
|
const [notice, setNotice] = useState(null);
|
|
48
52
|
const [ticketKey, setTicketKey] = useState(null);
|
|
53
|
+
const [ticketOffset, setTicketOffset] = useState(0);
|
|
54
|
+
const [ticketCollapsed, setTicketCollapsed] = useState([]);
|
|
49
55
|
const [ready, setReady] = useState(false);
|
|
50
56
|
const [stream, setStream] = useState([]);
|
|
51
57
|
const [logsFilter, setLogsFilter] = useState(null);
|
|
58
|
+
const [rawLogs, setRawLogs] = useState(false);
|
|
52
59
|
const [cursor, setCursor] = useState(null);
|
|
53
60
|
const [started, setStarted] = useState(false);
|
|
54
61
|
const [field, setField] = useState(null);
|
|
55
62
|
const [editing, setEditing] = useState(null);
|
|
56
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);
|
|
67
|
+
const [earlierLimit, setEarlierLimit] = useState(EARLIER_PAGE);
|
|
68
|
+
const earlierLimitRef = useRef(EARLIER_PAGE);
|
|
69
|
+
earlierLimitRef.current = earlierLimit;
|
|
57
70
|
const selectedRef = useRef(null);
|
|
58
71
|
const fieldRef = useRef(null);
|
|
59
72
|
const editingRef = useRef(null);
|
|
@@ -89,12 +102,14 @@ export function App({ initial }) {
|
|
|
89
102
|
if (!loads.current.isCurrent(token))
|
|
90
103
|
return;
|
|
91
104
|
setWorkspace(snapshot.workspace);
|
|
92
|
-
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
|
+
})) }));
|
|
93
108
|
setFeed(snapshot.feed);
|
|
94
109
|
}, []);
|
|
95
110
|
useEffect(() => {
|
|
96
111
|
setLive("connecting");
|
|
97
|
-
const polling = pollSnapshot(config, applySnapshot, setLive, (error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
112
|
+
const polling = pollSnapshot(config, applySnapshot, setLive, (error) => setNotice(error instanceof Error ? error.message : String(error)), () => earlierLimitRef.current);
|
|
98
113
|
refreshRef.current = polling.refresh;
|
|
99
114
|
return polling.close;
|
|
100
115
|
}, [config, applySnapshot]);
|
|
@@ -114,13 +129,17 @@ export function App({ initial }) {
|
|
|
114
129
|
if (logsFilter)
|
|
115
130
|
for (const event of events)
|
|
116
131
|
if (!eventLabels.has(event.run_id)) {
|
|
117
|
-
|
|
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 });
|
|
118
137
|
}
|
|
119
|
-
setStream((prior) => appendLines(prior, toStreamLines(events, eventLabels)));
|
|
138
|
+
setStream((prior) => appendLines(prior, toStreamLines(events, eventLabels, rawLogs)));
|
|
120
139
|
})
|
|
121
140
|
.catch(() => { });
|
|
122
|
-
}, [liveRunIds, board, config, labels, workspace.id, logsFilter]);
|
|
123
|
-
useEffect(() => setStream([]), [logsFilter]);
|
|
141
|
+
}, [liveRunIds, board, config, labels, workspace.id, logsFilter, rawLogs]);
|
|
142
|
+
useEffect(() => setStream([]), [logsFilter, rawLogs]);
|
|
124
143
|
const say = useCallback((speaker, body, steps) => {
|
|
125
144
|
setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
|
|
126
145
|
}, []);
|
|
@@ -129,7 +148,7 @@ export function App({ initial }) {
|
|
|
129
148
|
const settingsOrder = useMemo(() => editableKeys(settings), [settings]);
|
|
130
149
|
const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
|
|
131
150
|
const configuring = view === "settings" && !editing;
|
|
132
|
-
const inbox = useMemo(() => inboxEntries(board, width), [board, width]);
|
|
151
|
+
const inbox = useMemo(() => inboxEntries(board, width, answering), [board, width, answering]);
|
|
133
152
|
const moveCursor = useCallback((delta) => {
|
|
134
153
|
if (!order.length)
|
|
135
154
|
return false;
|
|
@@ -149,11 +168,32 @@ export function App({ initial }) {
|
|
|
149
168
|
return true;
|
|
150
169
|
}, [settingsOrder]);
|
|
151
170
|
const moveInbox = useCallback((delta) => {
|
|
152
|
-
setInboxFocus((current) =>
|
|
153
|
-
|
|
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]);
|
|
154
179
|
useEffect(() => {
|
|
155
180
|
setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current)));
|
|
156
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]);
|
|
157
197
|
useEffect(() => {
|
|
158
198
|
if (view !== "inbox")
|
|
159
199
|
return;
|
|
@@ -170,6 +210,38 @@ export function App({ initial }) {
|
|
|
170
210
|
const refresh = useCallback(async () => {
|
|
171
211
|
await refreshRef.current?.();
|
|
172
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]);
|
|
173
245
|
const applyEdit = useCallback(async (key, raw) => {
|
|
174
246
|
const row = settings.find((entry) => entry.key === key);
|
|
175
247
|
if (!row)
|
|
@@ -212,6 +284,10 @@ export function App({ initial }) {
|
|
|
212
284
|
setStream([]);
|
|
213
285
|
setLogsFilter(null);
|
|
214
286
|
acknowledgedMessages.current.clear();
|
|
287
|
+
setAnswering(null);
|
|
288
|
+
setSelectedDecisionId(snapshot.board.decisions[0]?.id ?? null);
|
|
289
|
+
setEarlierLimit(EARLIER_PAGE);
|
|
290
|
+
earlierLimitRef.current = EARLIER_PAGE;
|
|
215
291
|
setCursor(null);
|
|
216
292
|
selectedRef.current = null;
|
|
217
293
|
setView("home");
|
|
@@ -247,6 +323,33 @@ export function App({ initial }) {
|
|
|
247
323
|
}
|
|
248
324
|
})();
|
|
249
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]);
|
|
250
353
|
const run = useCallback(async (raw) => {
|
|
251
354
|
const text = raw.trim();
|
|
252
355
|
if (view === "settings") {
|
|
@@ -273,13 +376,39 @@ export function App({ initial }) {
|
|
|
273
376
|
}
|
|
274
377
|
}
|
|
275
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
|
+
}
|
|
276
398
|
const selected = board.tickets.find((ticket) => ticket.id === selectedRef.current);
|
|
277
399
|
if (browsing && selected) {
|
|
278
|
-
|
|
279
|
-
setView("ticket");
|
|
400
|
+
await openTicket(selected.key);
|
|
280
401
|
}
|
|
281
402
|
return;
|
|
282
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
|
+
}
|
|
283
412
|
history.current.push(text);
|
|
284
413
|
historyAt.current = -1;
|
|
285
414
|
setDraft("");
|
|
@@ -304,8 +433,11 @@ export function App({ initial }) {
|
|
|
304
433
|
return;
|
|
305
434
|
case "view":
|
|
306
435
|
setView(action.view);
|
|
307
|
-
if (action.view === "inbox")
|
|
308
|
-
|
|
436
|
+
if (action.view === "inbox") {
|
|
437
|
+
const header = selectedDecisionId ? decisionHeaderIndex(inbox, selectedDecisionId) : 0;
|
|
438
|
+
setInboxFocus(header >= 0 ? header : 0);
|
|
439
|
+
setAnswering(null);
|
|
440
|
+
}
|
|
309
441
|
if (action.view === "board" && order.length) {
|
|
310
442
|
const next = selectedRef.current && order.includes(selectedRef.current) ? selectedRef.current : order[0];
|
|
311
443
|
selectedRef.current = next;
|
|
@@ -391,22 +523,7 @@ export function App({ initial }) {
|
|
|
391
523
|
}
|
|
392
524
|
return;
|
|
393
525
|
case "ticket":
|
|
394
|
-
|
|
395
|
-
setView("ticket");
|
|
396
|
-
setBusy(true);
|
|
397
|
-
try {
|
|
398
|
-
const { ticket: detail } = await loadTicketDetail(config, action.key);
|
|
399
|
-
setBoard((prior) => ({
|
|
400
|
-
...prior,
|
|
401
|
-
tickets: prior.tickets.map((ticket) => ticket.id === detail.id ? { ...ticket, ...detail } : ticket),
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
406
|
-
}
|
|
407
|
-
finally {
|
|
408
|
-
setBusy(false);
|
|
409
|
-
}
|
|
526
|
+
await openTicket(action.key);
|
|
410
527
|
return;
|
|
411
528
|
case "epic-new":
|
|
412
529
|
setBusy(true);
|
|
@@ -502,8 +619,13 @@ export function App({ initial }) {
|
|
|
502
619
|
case "agent-add":
|
|
503
620
|
setBusy(true);
|
|
504
621
|
try {
|
|
505
|
-
|
|
506
|
-
|
|
622
|
+
let created;
|
|
623
|
+
await suspendTerminal(async () => {
|
|
624
|
+
created = await agentAdd(action.args, { config, isTTY: true, prompt: tuiPrompt });
|
|
625
|
+
});
|
|
626
|
+
if (!created)
|
|
627
|
+
throw new Error("Agent wizard did not finish.");
|
|
628
|
+
const { agent } = created;
|
|
507
629
|
say("system", `Added ${agent.display_name} (${agent.id}).`);
|
|
508
630
|
await refresh();
|
|
509
631
|
}
|
|
@@ -562,10 +684,11 @@ export function App({ initial }) {
|
|
|
562
684
|
return;
|
|
563
685
|
case "logs":
|
|
564
686
|
setLogsFilter(action.key);
|
|
565
|
-
|
|
687
|
+
setRawLogs(action.raw);
|
|
688
|
+
say("system", `${action.raw ? "Raw" : "Narrated"} activity${action.key ? ` filtered to ${action.key}` : " filter cleared"}.`);
|
|
566
689
|
return;
|
|
567
690
|
case "decide": {
|
|
568
|
-
const focused = inbox
|
|
691
|
+
const focused = selectedDecisionId ?? decisionIdAt(inbox, inboxFocus);
|
|
569
692
|
const decision = selectDecision(board.decisions, action.target, focused);
|
|
570
693
|
if (!decision) {
|
|
571
694
|
setNotice(action.target ? `No decision matches ${action.target}.` : "Nothing is waiting on a decision.");
|
|
@@ -575,30 +698,32 @@ export function App({ initial }) {
|
|
|
575
698
|
setNotice("Skipping decisions is not available through the HDX API. Answer it instead.");
|
|
576
699
|
return;
|
|
577
700
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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);
|
|
583
708
|
return;
|
|
584
709
|
}
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
await resolveDecision(config, decision.id, answer);
|
|
588
|
-
say("system", `Answered: ${answer}`);
|
|
589
|
-
await refresh();
|
|
590
|
-
}
|
|
591
|
-
catch (error) {
|
|
592
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
593
|
-
}
|
|
594
|
-
finally {
|
|
595
|
-
setBusy(false);
|
|
596
|
-
}
|
|
710
|
+
await submitDecision(decision.id, action.answer);
|
|
597
711
|
return;
|
|
598
712
|
}
|
|
599
713
|
case "help":
|
|
600
714
|
setMessages((prior) => [...prior, { id: nextId(), speaker: "system", body: "", panel: "help", done: true }]);
|
|
601
715
|
return;
|
|
716
|
+
case "inbox-more":
|
|
717
|
+
if (view !== "inbox")
|
|
718
|
+
setView("inbox");
|
|
719
|
+
if (!board.earlierHasMore) {
|
|
720
|
+
say("system", "No more earlier messages.");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
setEarlierLimit((current) => current + EARLIER_PAGE);
|
|
724
|
+
earlierLimitRef.current += EARLIER_PAGE;
|
|
725
|
+
await refresh();
|
|
726
|
+
return;
|
|
602
727
|
case "refresh":
|
|
603
728
|
await refresh();
|
|
604
729
|
return;
|
|
@@ -612,12 +737,16 @@ export function App({ initial }) {
|
|
|
612
737
|
return;
|
|
613
738
|
}
|
|
614
739
|
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, order, settingsOrder, changeWorkspace,
|
|
615
|
-
config, refresh, suspendTerminal, exit, inbox, inboxFocus
|
|
740
|
+
config, refresh, suspendTerminal, exit, inbox, inboxFocus, openTicket, answering, selectedDecisionId,
|
|
741
|
+
submitDecision, ticketKey, ticketCollapsed, ticketOffset, width]);
|
|
616
742
|
useInput((input, key) => {
|
|
617
743
|
if (key.ctrl && input === "c")
|
|
618
744
|
exit();
|
|
619
745
|
});
|
|
620
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;
|
|
621
750
|
const waiting = decisions.length;
|
|
622
751
|
const announced = useRef(0);
|
|
623
752
|
useEffect(() => {
|
|
@@ -648,13 +777,21 @@ export function App({ initial }) {
|
|
|
648
777
|
if (item.message.panel === "help")
|
|
649
778
|
return _jsx(Help, { width: width }, item.key);
|
|
650
779
|
return _jsx(Bubble, { message: item.message, width: width }, item.key);
|
|
651
|
-
} }), 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
|
|
652
|
-
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels })
|
|
653
|
-
: _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) => {
|
|
654
783
|
setDraft(next);
|
|
655
784
|
if (editingRef.current)
|
|
656
785
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
657
|
-
}, 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
|
+
}
|
|
658
795
|
if (editing) {
|
|
659
796
|
setEditing(null);
|
|
660
797
|
setDraft("");
|
|
@@ -669,6 +806,11 @@ export function App({ initial }) {
|
|
|
669
806
|
setCursor(null);
|
|
670
807
|
selectedRef.current = null;
|
|
671
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
|
+
}
|
|
672
814
|
if (view === "inbox" && !draft) {
|
|
673
815
|
moveInbox(-1);
|
|
674
816
|
return;
|
|
@@ -682,6 +824,11 @@ export function App({ initial }) {
|
|
|
682
824
|
historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
|
|
683
825
|
setDraft(history.current[historyAt.current] ?? "");
|
|
684
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
|
+
}
|
|
685
832
|
if (view === "inbox" && !draft) {
|
|
686
833
|
moveInbox(1);
|
|
687
834
|
return;
|
|
@@ -699,6 +846,16 @@ export function App({ initial }) {
|
|
|
699
846
|
return;
|
|
700
847
|
}
|
|
701
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
|
+
}
|
|
702
859
|
} }) })] })] }));
|
|
703
860
|
}
|
|
704
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, }) {
|
|
@@ -127,9 +127,10 @@ export function AgentsColumn({ board, width, rows, }) {
|
|
|
127
127
|
}
|
|
128
128
|
const KIND_COLOR = {
|
|
129
129
|
error: UI.danger,
|
|
130
|
+
agent: UI.accent,
|
|
130
131
|
tool: UI.dim,
|
|
131
132
|
status: UI.dim,
|
|
132
|
-
|
|
133
|
+
raw: UI.dim,
|
|
133
134
|
};
|
|
134
135
|
/**
|
|
135
136
|
* What the agents are doing, as it arrives. The panel is a fixed height on
|
|
@@ -146,7 +147,7 @@ export function StreamPanel({ lines, width, rows, live, }) {
|
|
|
146
147
|
_jsx(Text, { color: UI.dim, children: live ? "Waiting for the first step." : "Nothing running." }, "empty"),
|
|
147
148
|
]
|
|
148
149
|
: []),
|
|
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))),
|
|
150
|
+
...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
151
|
] }));
|
|
151
152
|
}
|
|
152
153
|
/** How many rows the epics strip wants, heading included, or none. */
|
|
@@ -154,6 +155,21 @@ export function epicsRows(board, cap = 3) {
|
|
|
154
155
|
const open = epicProgress(board).filter((row) => row.epic.status !== "done");
|
|
155
156
|
return open.length === 0 ? 0 : Math.min(open.length, cap) + 1;
|
|
156
157
|
}
|
|
158
|
+
export function nowEntries(board) {
|
|
159
|
+
return board.tickets.filter((ticket) => !["backlog", "merged", "cancelled"].includes(ticket.status))
|
|
160
|
+
.map((ticket) => ({ key: ticket.key, headline: ticket.latest_headline ?? `${ticket.key}: ${ticket.title}` }));
|
|
161
|
+
}
|
|
162
|
+
function NowStrip({ board, width, rows }) {
|
|
163
|
+
const entries = nowEntries(board);
|
|
164
|
+
if (!entries.length || rows < 2)
|
|
165
|
+
return null;
|
|
166
|
+
const shown = entries.slice(0, contentRows(rows, entries.length));
|
|
167
|
+
return _jsx(Panel, { width: width, rows: rows, children: [
|
|
168
|
+
_jsx(Heading, { text: "Now", note: `${entries.length}` }, "h"),
|
|
169
|
+
...shown.map((entry) => _jsx(Text, { color: UI.text, wrap: "truncate", children: entry.headline }, entry.key)),
|
|
170
|
+
_jsx(More, { count: entries.length - shown.length }, "more"),
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
157
173
|
/**
|
|
158
174
|
* Epic progress, above the board it explains. Capped hard: this is the summary
|
|
159
175
|
* line, and the tickets underneath are what you are here to read.
|
|
@@ -174,12 +190,13 @@ export function EpicsStrip({ board, width, rows }) {
|
|
|
174
190
|
export function Cockpit({ board, width, rows, cursor, }) {
|
|
175
191
|
// The epics strip is spent out of the same budget, so adding it shortens the
|
|
176
192
|
// board rather than making the frame taller.
|
|
177
|
-
const
|
|
178
|
-
const
|
|
193
|
+
const now = Math.min(nowEntries(board).length + 1, 4, Math.max(0, rows - 4));
|
|
194
|
+
const epics = Math.min(epicsRows(board), Math.max(0, rows - now - (now > 0 ? 1 : 0) - 4));
|
|
195
|
+
const rest = rows - (now > 0 ? now + 1 : 0) - (epics > 0 ? epics + 1 : 0);
|
|
179
196
|
const columns = renderColumns(board, width, rest, cursor);
|
|
180
|
-
if (epics === 0)
|
|
197
|
+
if (epics === 0 && now === 0)
|
|
181
198
|
return columns;
|
|
182
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(EpicsStrip, { board: board, width: width, rows: epics }), _jsx(Box, { height: 1 }), columns] }));
|
|
199
|
+
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
200
|
}
|
|
184
201
|
function renderColumns(board, width, rows, cursor) {
|
|
185
202
|
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
|
}
|