@higherdev/cli 0.16.0 → 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 +1 -1
- package/dist/api.js +8 -3
- package/dist/index.js +30 -20
- package/dist/out.js +1 -1
- package/dist/tui/App.js +186 -52
- package/dist/tui/Dashboard.js +25 -8
- package/dist/tui/Decision.js +18 -31
- package/dist/tui/Help.js +4 -4
- package/dist/tui/Panels.js +44 -28
- package/dist/tui/TextInput.js +5 -1
- package/dist/tui/data.js +19 -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/README.md
CHANGED
|
@@ -25,7 +25,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
25
25
|
| `hd --help` | Show the HigherDEV banner and usage |
|
|
26
26
|
| `hd status` | Show workspace, ticket, run, and decision status |
|
|
27
27
|
| `hd ticket list` | List tickets |
|
|
28
|
-
| `hd ticket show KEY` | Show one ticket |
|
|
28
|
+
| `hd ticket show KEY [--json]` | Show one ticket (body, acceptance, timeline, messages, PR, runs, decisions) |
|
|
29
29
|
| `hd ticket new [PATH.md]` | Create a ticket from the guided form or a Markdown spec |
|
|
30
30
|
| `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket non-interactively |
|
|
31
31
|
| `hd ticket queue KEY` | Queue a complete ticket now |
|
package/dist/api.js
CHANGED
|
@@ -58,14 +58,19 @@ export async function queueTicket(key, config = loadConfig()) {
|
|
|
58
58
|
export async function cancelTicket(key, config = loadConfig()) {
|
|
59
59
|
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/cancel`);
|
|
60
60
|
}
|
|
61
|
-
export async function
|
|
61
|
+
export async function listTicketRunEvents(key, afterAt, afterId, config = loadConfig()) {
|
|
62
62
|
const query = new URLSearchParams();
|
|
63
63
|
if (afterAt)
|
|
64
64
|
query.set("after_at", afterAt);
|
|
65
65
|
if (afterId)
|
|
66
66
|
query.set("after_id", afterId);
|
|
67
|
-
const suffix = query.size ?
|
|
68
|
-
|
|
67
|
+
const suffix = query.size ? `&${query.toString()}` : "";
|
|
68
|
+
const result = await request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events?stream=1${suffix}`);
|
|
69
|
+
const runs = new Map((result.runs ?? []).map((run) => [run.id, run]));
|
|
70
|
+
return { ...result, events: result.events.map((event) => ({ ...event, run: runs.get(event.run_id) })) };
|
|
71
|
+
}
|
|
72
|
+
export async function listTicketStory(key, config = loadConfig()) {
|
|
73
|
+
return request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events`);
|
|
69
74
|
}
|
|
70
75
|
export async function postMessage(fields, config = loadConfig()) {
|
|
71
76
|
return request(config, "POST", `/api/w/${config.slug}/messages`, fields);
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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, createEpic, deleteAgent, deleteEpic, getStatus, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv,
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, createEpic, deleteAgent, deleteEpic, getStatus, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, 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";
|
|
8
8
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
9
9
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
10
10
|
import { ticketNew } from "./ticket-commands.js";
|
|
11
|
+
import { ticketViewLines } from "./tui/ticket-view.js";
|
|
11
12
|
import { agentAdd, AGENT_ADD_USAGE } from "./agent-commands.js";
|
|
12
13
|
import { WORKSPACE_USAGE, workspaceGrantRunnerAccess, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
|
|
13
14
|
import { defaultGh, hasWriteRepoPermission, HDX_RUNNER_GH_USER, requireAdminRepo, runnerRepoPermission } from "./workspace-preflight.js";
|
|
@@ -54,7 +55,8 @@ function printTickets(tickets) {
|
|
|
54
55
|
}
|
|
55
56
|
console.log(table(["KEY", "STATUS", "PROVIDER", "TITLE", "WHY"], tickets.map((ticket) => [
|
|
56
57
|
c.bold(ticket.key), statusChip(ticket.status), ticket.provider ?? c.dim("-"),
|
|
57
|
-
truncate(ticket.title, 48), ticket.
|
|
58
|
+
truncate(ticket.title, 48), ticket.latest_headline ? c.yellow(truncate(ticket.latest_headline, 40))
|
|
59
|
+
: ticket.stuck_reason ? c.yellow(truncate(ticket.stuck_reason, 40)) : "",
|
|
58
60
|
])));
|
|
59
61
|
}
|
|
60
62
|
async function cmdStatus() {
|
|
@@ -85,23 +87,31 @@ async function cmdTicket(argv, deps = {}) {
|
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
89
|
if (action === "show") {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
console.log(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
const parsed = flags(rest);
|
|
91
|
+
const key = parsed.rest[0];
|
|
92
|
+
if (!key || parsed.rest.length !== 1)
|
|
93
|
+
fail("usage: hd ticket show KEY [--json]");
|
|
94
|
+
const data = await showTicket(key.toUpperCase());
|
|
95
|
+
if (parsed.bools.has("json")) {
|
|
96
|
+
console.log(JSON.stringify(data));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { ticket, pr, events, runs, messages, decisions } = data;
|
|
100
|
+
const width = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
101
|
+
for (const line of ticketViewLines({
|
|
102
|
+
...ticket,
|
|
103
|
+
stuck: ticket.stuck_reason,
|
|
104
|
+
pr,
|
|
105
|
+
timeline: events,
|
|
106
|
+
messages,
|
|
107
|
+
runs: runs.map((run) => ({
|
|
108
|
+
id: run.id, kind: run.kind, provider: run.provider, status: run.status,
|
|
109
|
+
summary: run.summary, elapsed_ms: run.elapsed_ms,
|
|
110
|
+
})),
|
|
111
|
+
decisions,
|
|
112
|
+
}, width)) {
|
|
113
|
+
console.log(line.text);
|
|
114
|
+
}
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
107
117
|
if (action === "new") {
|
|
@@ -182,7 +192,7 @@ async function cmdLogs(argv) {
|
|
|
182
192
|
let afterId;
|
|
183
193
|
const follow = bools.has("follow");
|
|
184
194
|
async function tick() {
|
|
185
|
-
const { events } = await
|
|
195
|
+
const { events } = await listTicketRunEvents(key, afterAt, afterId);
|
|
186
196
|
let printed = false;
|
|
187
197
|
for (const event of events) {
|
|
188
198
|
if (seen.has(event.id))
|
package/dist/out.js
CHANGED
|
@@ -64,7 +64,7 @@ export function usage() {
|
|
|
64
64
|
return [
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
|
-
` ${c.blue("hd ticket list | show | new [PATH] | queue | cancel")} ticket operations`,
|
|
67
|
+
` ${c.blue("hd ticket list | show KEY [--json] | new [PATH] | queue | cancel")} ticket operations`,
|
|
68
68
|
` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
|
|
69
69
|
` ${c.blue("hd plan")} use /architect in the TUI`,
|
|
70
70
|
` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
|
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, }) {
|
|
@@ -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);
|