patchrelay 0.45.1 → 0.46.1
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/dist/build-info.json +3 -3
- package/dist/cli/commands/watch.js +2 -0
- package/dist/cli/watch/App.js +61 -28
- package/dist/cli/watch/HelpBar.js +25 -26
- package/dist/cli/watch/IssueDetailView.js +3 -3
- package/dist/cli/watch/IssueListView.js +14 -32
- package/dist/cli/watch/IssueRow.js +19 -204
- package/dist/cli/watch/LogView.js +74 -0
- package/dist/cli/watch/StatusBar.js +3 -27
- package/dist/cli/watch/codex-log-rows.js +82 -0
- package/dist/cli/watch/detail-rows.js +56 -574
- package/dist/cli/watch/event-log-rows.js +119 -0
- package/dist/cli/watch/issue-token.js +81 -0
- package/dist/cli/watch/watch-actions.js +9 -5
- package/dist/cli/watch/watch-state.js +8 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -21,6 +21,8 @@ export async function handleWatchCommand(params) {
|
|
|
21
21
|
process.stderr.write("Use `patchrelay issue list`, `patchrelay issue show <issueKey>`, or run the dashboard from a terminal.\n");
|
|
22
22
|
return 1;
|
|
23
23
|
}
|
|
24
|
+
if (!process.env.NODE_ENV)
|
|
25
|
+
process.env.NODE_ENV = "production";
|
|
24
26
|
const { render } = await import("ink");
|
|
25
27
|
const { createElement } = await import("react");
|
|
26
28
|
const { App } = await import("../watch/App.js");
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -6,6 +6,7 @@ import { useWatchStream } from "./use-watch-stream.js";
|
|
|
6
6
|
import { useDetailStream } from "./use-detail-stream.js";
|
|
7
7
|
import { IssueListView } from "./IssueListView.js";
|
|
8
8
|
import { IssueDetailView } from "./IssueDetailView.js";
|
|
9
|
+
import { LogView } from "./LogView.js";
|
|
9
10
|
import { buildWatchDetailExportText, exportWatchTextToTempFile, findLastAssistantMessage, findLastCommand, findLastCommandOutput, openTextInPager, writeTextToClipboard, } from "./watch-actions.js";
|
|
10
11
|
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
11
12
|
import { PROMPT_COMPOSER_HINT, measurePromptComposerRows } from "./prompt-layout.js";
|
|
@@ -323,32 +324,18 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
323
324
|
dispatch({ type: "enter-detail", issueKey: issue.issueKey });
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
|
-
else if (key.tab) {
|
|
327
|
+
else if (input === "a" || key.tab) {
|
|
327
328
|
dispatch({ type: "cycle-filter" });
|
|
328
329
|
}
|
|
330
|
+
return;
|
|
329
331
|
}
|
|
330
|
-
|
|
332
|
+
if (state.view === "log") {
|
|
331
333
|
if (key.escape || key.backspace || key.delete) {
|
|
332
|
-
dispatch({ type: "exit-
|
|
334
|
+
dispatch({ type: "exit-log" });
|
|
333
335
|
}
|
|
334
336
|
else if (input === "f") {
|
|
335
337
|
dispatch({ type: "toggle-follow" });
|
|
336
338
|
}
|
|
337
|
-
else if (input === "r") {
|
|
338
|
-
handleRetry();
|
|
339
|
-
}
|
|
340
|
-
else if (input === "p") {
|
|
341
|
-
setPromptMode(true);
|
|
342
|
-
setPromptCursor(promptBuffer.length);
|
|
343
|
-
}
|
|
344
|
-
else if (input === "s") {
|
|
345
|
-
if (state.activeDetailKey) {
|
|
346
|
-
showPersistentStatus("stopping...");
|
|
347
|
-
void postStop(baseUrl, state.activeDetailKey, bearerToken).then((result) => {
|
|
348
|
-
showStatus(result.ok ? "stop sent" : `stop failed: ${result.reason ?? "unknown"}`);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
339
|
else if (input === "y") {
|
|
353
340
|
handleCopyLastAssistant();
|
|
354
341
|
}
|
|
@@ -364,22 +351,16 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
364
351
|
else if (input === "v") {
|
|
365
352
|
handleOpenTranscriptInPager();
|
|
366
353
|
}
|
|
367
|
-
else if (input === "h") {
|
|
368
|
-
dispatch({ type: "switch-detail-tab", tab: "history" });
|
|
369
|
-
}
|
|
370
|
-
else if (input === "t") {
|
|
371
|
-
dispatch({ type: "switch-detail-tab", tab: "timeline" });
|
|
372
|
-
}
|
|
373
354
|
else if (input === "j" || key.downArrow) {
|
|
374
355
|
dispatch({ type: "detail-scroll", delta: 1 });
|
|
375
356
|
}
|
|
376
357
|
else if (input === "k" || key.upArrow) {
|
|
377
358
|
dispatch({ type: "detail-scroll", delta: -1 });
|
|
378
359
|
}
|
|
379
|
-
else if (key.pageDown || (key.ctrl && input === "d")
|
|
360
|
+
else if (key.pageDown || (key.ctrl && input === "d")) {
|
|
380
361
|
dispatch({ type: "detail-page", direction: "down" });
|
|
381
362
|
}
|
|
382
|
-
else if (key.pageUp || (key.ctrl && input === "u")
|
|
363
|
+
else if (key.pageUp || (key.ctrl && input === "u")) {
|
|
383
364
|
dispatch({ type: "detail-page", direction: "up" });
|
|
384
365
|
}
|
|
385
366
|
else if (key.home) {
|
|
@@ -394,6 +375,56 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
394
375
|
else if (input === "]" || key.rightArrow) {
|
|
395
376
|
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
396
377
|
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// detail view
|
|
381
|
+
if (key.escape || key.backspace || key.delete) {
|
|
382
|
+
dispatch({ type: "exit-detail" });
|
|
383
|
+
}
|
|
384
|
+
else if (input === "l") {
|
|
385
|
+
dispatch({ type: "enter-log" });
|
|
386
|
+
}
|
|
387
|
+
else if (input === "f") {
|
|
388
|
+
dispatch({ type: "toggle-follow" });
|
|
389
|
+
}
|
|
390
|
+
else if (input === "r") {
|
|
391
|
+
handleRetry();
|
|
392
|
+
}
|
|
393
|
+
else if (input === "p") {
|
|
394
|
+
setPromptMode(true);
|
|
395
|
+
setPromptCursor(promptBuffer.length);
|
|
396
|
+
}
|
|
397
|
+
else if (input === "s") {
|
|
398
|
+
if (state.activeDetailKey) {
|
|
399
|
+
showPersistentStatus("stopping...");
|
|
400
|
+
void postStop(baseUrl, state.activeDetailKey, bearerToken).then((result) => {
|
|
401
|
+
showStatus(result.ok ? "stop sent" : `stop failed: ${result.reason ?? "unknown"}`);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (input === "j" || key.downArrow) {
|
|
406
|
+
dispatch({ type: "detail-scroll", delta: 1 });
|
|
407
|
+
}
|
|
408
|
+
else if (input === "k" || key.upArrow) {
|
|
409
|
+
dispatch({ type: "detail-scroll", delta: -1 });
|
|
410
|
+
}
|
|
411
|
+
else if (key.pageDown || (key.ctrl && input === "d")) {
|
|
412
|
+
dispatch({ type: "detail-page", direction: "down" });
|
|
413
|
+
}
|
|
414
|
+
else if (key.pageUp || (key.ctrl && input === "u")) {
|
|
415
|
+
dispatch({ type: "detail-page", direction: "up" });
|
|
416
|
+
}
|
|
417
|
+
else if (key.home) {
|
|
418
|
+
dispatch({ type: "detail-jump", target: "start" });
|
|
419
|
+
}
|
|
420
|
+
else if (key.end) {
|
|
421
|
+
dispatch({ type: "detail-jump", target: "end" });
|
|
422
|
+
}
|
|
423
|
+
else if (input === "[" || key.leftArrow) {
|
|
424
|
+
dispatch({ type: "detail-navigate", direction: "prev", filtered });
|
|
425
|
+
}
|
|
426
|
+
else if (input === "]" || key.rightArrow) {
|
|
427
|
+
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
397
428
|
}
|
|
398
429
|
});
|
|
399
430
|
const reservedRows = 1 + (promptMode
|
|
@@ -401,9 +432,11 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
401
432
|
: promptStatus
|
|
402
433
|
? measureRenderedTextRows(promptStatus, width)
|
|
403
434
|
: 0);
|
|
404
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered,
|
|
435
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, frozen: frozen, compact: compact })) : state.view === "log" ? (_jsx(LogView, { issue: activeIssue, timeline: state.timeline, follow: state.follow, scrollOffset: state.detailScrollOffset, activeRunId: state.activeRunId, reservedRows: reservedRows, onLayoutChange: (viewportRows, contentRows) => {
|
|
436
|
+
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
437
|
+
} })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: activeIssue, timeline: state.timeline, follow: state.follow, scrollOffset: state.detailScrollOffset, unreadBelow: state.detailUnreadBelow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, compact: compact, reservedRows: reservedRows, onLayoutChange: (viewportRows, contentRows) => {
|
|
405
438
|
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
406
|
-
} }), promptMode && (_jsx(PromptComposer, { buffer: promptBuffer, cursor: promptCursor })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] }))
|
|
439
|
+
} }), promptMode && (_jsx(PromptComposer, { buffer: promptBuffer, cursor: promptCursor })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) }));
|
|
407
440
|
}
|
|
408
441
|
function PromptComposer({ buffer, cursor }) {
|
|
409
442
|
const withCursor = `${buffer.slice(0, cursor)}|${buffer.slice(cursor)}`;
|
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
export function buildHelpBarText(view, follow
|
|
4
|
-
if (view === "
|
|
5
|
-
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
6
|
-
if (compact) {
|
|
7
|
-
return "j/k: scroll f: live p: prompt y/c/o: copy r: retry s: stop esc: list q: quit";
|
|
8
|
-
}
|
|
3
|
+
export function buildHelpBarText(view, follow) {
|
|
4
|
+
if (view === "log") {
|
|
9
5
|
return [
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"s: stop",
|
|
20
|
-
"r: retry",
|
|
21
|
-
]
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.join(" ");
|
|
6
|
+
"j/k scroll",
|
|
7
|
+
"[ ] turn",
|
|
8
|
+
`f live ${follow ? "on" : "off"}`,
|
|
9
|
+
"y/c/o copy",
|
|
10
|
+
"e export",
|
|
11
|
+
"v pager",
|
|
12
|
+
"esc back",
|
|
13
|
+
"q quit",
|
|
14
|
+
].join(" ");
|
|
24
15
|
}
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
16
|
+
if (view === "detail") {
|
|
17
|
+
return [
|
|
18
|
+
"j/k scroll",
|
|
19
|
+
"[ ] issue",
|
|
20
|
+
"l log",
|
|
21
|
+
"p prompt",
|
|
22
|
+
"r retry",
|
|
23
|
+
"s stop",
|
|
24
|
+
"esc list",
|
|
25
|
+
"q quit",
|
|
26
|
+
].join(" ");
|
|
27
27
|
}
|
|
28
|
-
return "
|
|
28
|
+
return "↑↓ select enter detail a filter x pause q quit";
|
|
29
29
|
}
|
|
30
|
-
export function HelpBar({ view, follow
|
|
31
|
-
|
|
32
|
-
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
30
|
+
export function HelpBar({ view, follow }) {
|
|
31
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: buildHelpBarText(view, follow) }) }));
|
|
33
32
|
}
|
|
@@ -17,9 +17,9 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
17
17
|
connected,
|
|
18
18
|
lastServerMessageAt,
|
|
19
19
|
}), width);
|
|
20
|
-
const helpRows = measureRenderedTextRows(buildHelpBarText("detail", follow
|
|
20
|
+
const helpRows = measureRenderedTextRows(buildHelpBarText("detail", follow), width);
|
|
21
21
|
return statusRows + helpRows;
|
|
22
|
-
}, [activeRunStartedAt, connected,
|
|
22
|
+
}, [activeRunStartedAt, connected, follow, lastServerMessageAt, unreadBelow, width]);
|
|
23
23
|
const viewportRows = Math.max(4, totalRows - reservedRows - footerRows);
|
|
24
24
|
const lines = useMemo(() => {
|
|
25
25
|
if (!issue) {
|
|
@@ -60,7 +60,7 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
60
60
|
const start = Math.min(scrollOffset, maxOffset);
|
|
61
61
|
const visibleLines = lines.slice(start, start + viewportRows);
|
|
62
62
|
const fillerCount = Math.max(0, viewportRows - visibleLines.length);
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column", children: [visibleLines.map((line) => (_jsx(RenderedLine, { line: line }, line.key))), Array.from({ length: fillerCount }, (_, index) => (_jsx(Text, { children: " " }, `detail-fill-${index}`))), _jsx(DetailStatusStrip, { follow: follow, unreadBelow: unreadBelow, activeRunStartedAt: activeRunStartedAt, connected: connected, lastServerMessageAt: lastServerMessageAt }), _jsx(HelpBar, { view: "detail", follow: follow
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visibleLines.map((line) => (_jsx(RenderedLine, { line: line }, line.key))), Array.from({ length: fillerCount }, (_, index) => (_jsx(Text, { children: " " }, `detail-fill-${index}`))), _jsx(DetailStatusStrip, { follow: follow, unreadBelow: unreadBelow, activeRunStartedAt: activeRunStartedAt, connected: connected, lastServerMessageAt: lastServerMessageAt }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
|
|
64
64
|
}
|
|
65
65
|
function RenderedLine({ line }) {
|
|
66
66
|
if (line.segments.length === 0) {
|
|
@@ -1,46 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useReducer } from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
|
-
import { IssueRow
|
|
4
|
+
import { IssueRow } from "./IssueRow.js";
|
|
5
5
|
import { StatusBar } from "./StatusBar.js";
|
|
6
6
|
import { HelpBar } from "./HelpBar.js";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
export function computeVisibleWindow(issues, selectedIndex, maxRows, cols, titleWidth, compact) {
|
|
7
|
+
const CHROME_ROWS = 3;
|
|
8
|
+
export function computeVisibleWindow(issues, selectedIndex, maxRows) {
|
|
10
9
|
if (issues.length === 0)
|
|
11
10
|
return { start: 0, end: 0 };
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
let start =
|
|
15
|
-
let end =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const canAddAbove = start > 0 && usedRows + (heights[start - 1] ?? 1) <= maxRows;
|
|
19
|
-
const canAddBelow = end < issues.length && usedRows + (heights[end] ?? 1) <= maxRows;
|
|
20
|
-
if (!canAddAbove && !canAddBelow)
|
|
21
|
-
break;
|
|
22
|
-
const aboveDistance = clampedSelected - start;
|
|
23
|
-
const belowDistance = end - 1 - clampedSelected;
|
|
24
|
-
const preferAbove = canAddAbove && (!canAddBelow || aboveDistance <= belowDistance);
|
|
25
|
-
if (preferAbove) {
|
|
26
|
-
start -= 1;
|
|
27
|
-
usedRows += heights[start] ?? 1;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (canAddBelow) {
|
|
31
|
-
usedRows += heights[end] ?? 1;
|
|
32
|
-
end += 1;
|
|
33
|
-
}
|
|
11
|
+
const clamped = Math.max(0, Math.min(selectedIndex, issues.length - 1));
|
|
12
|
+
const half = Math.floor(maxRows / 2);
|
|
13
|
+
let start = Math.max(0, clamped - half);
|
|
14
|
+
let end = Math.min(issues.length, start + maxRows);
|
|
15
|
+
if (end - start < maxRows) {
|
|
16
|
+
start = Math.max(0, end - maxRows);
|
|
34
17
|
}
|
|
35
18
|
return { start, end };
|
|
36
19
|
}
|
|
37
|
-
export function IssueListView({ issues,
|
|
20
|
+
export function IssueListView({ issues, selectedIndex, connected, lastServerMessageAt, filter, frozen, compact = false, }) {
|
|
38
21
|
const { stdout } = useStdout();
|
|
39
22
|
const cols = stdout?.columns ?? 80;
|
|
40
23
|
const rows = stdout?.rows ?? 24;
|
|
41
|
-
const titleWidth = Math.max(0, cols -
|
|
42
|
-
const maxVisibleRows = Math.max(1, rows -
|
|
43
|
-
// Periodic refresh for elapsed times
|
|
24
|
+
const titleWidth = Math.max(0, cols - 42);
|
|
25
|
+
const maxVisibleRows = Math.max(1, rows - CHROME_ROWS);
|
|
44
26
|
const [, tick] = useReducer((c) => c + 1, 0);
|
|
45
27
|
useEffect(() => {
|
|
46
28
|
if (frozen)
|
|
@@ -48,9 +30,9 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, las
|
|
|
48
30
|
const id = setInterval(tick, 5000);
|
|
49
31
|
return () => clearInterval(id);
|
|
50
32
|
}, [frozen]);
|
|
51
|
-
const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows
|
|
33
|
+
const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows);
|
|
52
34
|
const visible = issues.slice(startIndex, endIndex);
|
|
53
35
|
const hiddenAbove = startIndex;
|
|
54
36
|
const hiddenBelow = Math.max(0, issues.length - endIndex);
|
|
55
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, {
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, frozen: frozen ?? false }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: " " })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 ? _jsx(Text, { dimColor: true, children: ` ↑${hiddenAbove}` }) : null, visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth, compact: compact }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 ? _jsx(Text, { dimColor: true, children: ` ↓${hiddenBelow}` }) : null] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
|
|
56
38
|
}
|
|
@@ -1,208 +1,23 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// ─── State display ──────────────────────────────────────────────
|
|
9
|
-
const TERMINAL_STATES = new Set(["done", "failed", "escalated"]);
|
|
10
|
-
function needsOperatorIntervention(issue) {
|
|
11
|
-
return issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated";
|
|
12
|
-
}
|
|
13
|
-
function effectiveState(issue) {
|
|
14
|
-
if (issue.sessionState === "done")
|
|
15
|
-
return "done";
|
|
16
|
-
if (issue.sessionState === "failed")
|
|
17
|
-
return "failed";
|
|
18
|
-
if (issue.completionCheckActive)
|
|
19
|
-
return "completion_check";
|
|
20
|
-
if (issue.blockedByCount > 0 && !issue.activeRunType)
|
|
21
|
-
return "blocked";
|
|
22
|
-
if (issue.sessionState === "waiting_input")
|
|
23
|
-
return "awaiting_input";
|
|
24
|
-
if (hasOpenPr(issue.prNumber, issue.prState))
|
|
25
|
-
return issue.factoryState;
|
|
26
|
-
if (issue.readyForExecution && !issue.activeRunType && !hasDisplayPrBlocker(issue))
|
|
27
|
-
return "ready";
|
|
28
|
-
return issue.factoryState;
|
|
29
|
-
}
|
|
30
|
-
function sessionDisplay(issue) {
|
|
31
|
-
if (needsOperatorIntervention(issue)) {
|
|
32
|
-
return { label: "needs help", color: "red" };
|
|
33
|
-
}
|
|
34
|
-
switch (issue.sessionState) {
|
|
35
|
-
case "running":
|
|
36
|
-
return { label: "running", color: "cyan" };
|
|
37
|
-
case "idle":
|
|
38
|
-
return { label: "idle", color: "blueBright" };
|
|
39
|
-
case "waiting_input":
|
|
40
|
-
return { label: "needs input", color: "yellow" };
|
|
41
|
-
case "done":
|
|
42
|
-
return { label: "done", color: "green" };
|
|
43
|
-
case "failed":
|
|
44
|
-
return { label: "failed", color: "red" };
|
|
45
|
-
default:
|
|
46
|
-
return { label: "unknown", color: "white" };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function stageLabel(issue) {
|
|
50
|
-
const state = effectiveState(issue);
|
|
51
|
-
switch (state) {
|
|
52
|
-
case "blocked": return "blocked";
|
|
53
|
-
case "ready": return "ready";
|
|
54
|
-
case "delegated": return "delegated";
|
|
55
|
-
case "implementing": return "implementing";
|
|
56
|
-
case "completion_check": return "completion check";
|
|
57
|
-
case "pr_open": return "PR open";
|
|
58
|
-
case "changes_requested": return "review changes";
|
|
59
|
-
case "repairing_ci": return "repairing CI";
|
|
60
|
-
case "awaiting_queue": return "waiting downstream";
|
|
61
|
-
case "repairing_queue": return "repairing queue";
|
|
62
|
-
case "done": return "merged";
|
|
63
|
-
case "failed": return "failed";
|
|
64
|
-
case "escalated": return "escalated";
|
|
65
|
-
case "awaiting_input": return "needs input";
|
|
66
|
-
default: return state;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// ─── Context facts (what matters right now) ─────────────────────
|
|
70
|
-
function buildFacts(issue, selected, compact = false) {
|
|
71
|
-
const facts = [];
|
|
72
|
-
const rereviewNeeded = isRereviewNeeded(issue);
|
|
73
|
-
// PR number
|
|
74
|
-
if (issue.prNumber !== undefined) {
|
|
75
|
-
facts.push({ text: `PR #${issue.prNumber}` });
|
|
76
|
-
}
|
|
77
|
-
if (!issue.sessionState) {
|
|
78
|
-
facts.push({ text: `stage ${stageLabel(issue)}` });
|
|
79
|
-
}
|
|
80
|
-
else if (selected) {
|
|
81
|
-
facts.push({ text: `internal stage ${stageLabel(issue)}` });
|
|
82
|
-
}
|
|
83
|
-
if (issue.waitingReason && issue.sessionState === "waiting_input") {
|
|
84
|
-
facts.push({ text: issue.waitingReason, color: "yellow" });
|
|
85
|
-
}
|
|
86
|
-
if (needsOperatorIntervention(issue)) {
|
|
87
|
-
facts.push({ text: "operator action needed", color: "red" });
|
|
88
|
-
}
|
|
89
|
-
// Review state — only show when it matters (not yet approved, or changes requested)
|
|
90
|
-
if (isApprovedReviewState(issue.prReviewState)) {
|
|
91
|
-
facts.push({ text: "approved", color: "green" });
|
|
92
|
-
}
|
|
93
|
-
else if (rereviewNeeded) {
|
|
94
|
-
facts.push({ text: "re-review needed", color: "yellow" });
|
|
95
|
-
}
|
|
96
|
-
else if (isChangesRequestedReviewState(issue.prReviewState)) {
|
|
97
|
-
facts.push({ text: "changes requested", color: "yellow" });
|
|
98
|
-
}
|
|
99
|
-
else if (hasOpenPr(issue.prNumber, issue.prState)
|
|
100
|
-
&& (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && !TERMINAL_STATES.has(effectiveState(issue))))) {
|
|
101
|
-
facts.push({ text: "awaiting review", color: "yellow" });
|
|
102
|
-
}
|
|
103
|
-
if (issue.factoryState === "awaiting_queue") {
|
|
104
|
-
facts.push({ text: "downstream ready", color: "cyan" });
|
|
105
|
-
}
|
|
106
|
-
// Check status — compact
|
|
107
|
-
const checksFact = prChecksFact(issue);
|
|
108
|
-
if (checksFact) {
|
|
109
|
-
facts.push(checksFact);
|
|
110
|
-
}
|
|
111
|
-
// Blocker
|
|
112
|
-
if (issue.blockedByCount > 0) {
|
|
113
|
-
facts.push({ text: `waiting on ${issue.blockedByKeys.join(", ")}`, color: "yellow" });
|
|
114
|
-
}
|
|
115
|
-
return compact ? facts.slice(0, 3) : facts;
|
|
116
|
-
}
|
|
117
|
-
// ─── What's blocking progress ───────────────────────────────────
|
|
118
|
-
function blockerText(issue) {
|
|
119
|
-
const rereviewNeeded = isRereviewNeeded(issue);
|
|
120
|
-
if (issue.sessionState === "waiting_input")
|
|
121
|
-
return issue.waitingReason ?? "Waiting for input";
|
|
122
|
-
if (needsOperatorIntervention(issue))
|
|
123
|
-
return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
|
|
124
|
-
if (issue.completionCheckActive)
|
|
125
|
-
return "No PR found; checking next step";
|
|
126
|
-
if (issue.waitingReason && issue.activeRunType && issue.factoryState === "pr_open")
|
|
127
|
-
return issue.waitingReason;
|
|
128
|
-
if (issue.waitingReason && issue.activeRunType && issue.factoryState === "awaiting_queue")
|
|
129
|
-
return issue.waitingReason;
|
|
130
|
-
if (issue.waitingReason && !issue.activeRunType)
|
|
131
|
-
return issue.waitingReason;
|
|
132
|
-
if (issue.blockedByCount > 0)
|
|
133
|
-
return `Waiting on ${issue.blockedByKeys.join(", ")}`;
|
|
134
|
-
if (effectiveState(issue) === "repairing_queue")
|
|
135
|
-
return "Merge queue conflict, repairing branch";
|
|
136
|
-
if (effectiveState(issue) === "repairing_ci") {
|
|
137
|
-
const check = issue.latestFailureCheckName ?? "CI";
|
|
138
|
-
return `Repairing ${check}`;
|
|
139
|
-
}
|
|
140
|
-
const checksFact = prChecksFact(issue);
|
|
141
|
-
if (checksFact?.color === "red") {
|
|
142
|
-
return checksFact.text;
|
|
143
|
-
}
|
|
144
|
-
if (checksFact?.color === "yellow" && checksFact.text.startsWith("checks ")) {
|
|
145
|
-
return `${checksFact.text} still running`;
|
|
146
|
-
}
|
|
147
|
-
if (rereviewNeeded)
|
|
148
|
-
return "Awaiting re-review after requested changes";
|
|
149
|
-
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
150
|
-
return "Review changes requested";
|
|
151
|
-
if (hasOpenPr(issue.prNumber, issue.prState) && isAwaitingReviewState(issue.prReviewState))
|
|
152
|
-
return "Awaiting review";
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
// ─── Render ─────────────────────────────────────────────────────
|
|
3
|
+
import { issueTokenFor, prTokenFor } from "./issue-token.js";
|
|
4
|
+
import { truncate } from "./format-utils.js";
|
|
5
|
+
const KEY_WIDTH = 8;
|
|
6
|
+
const GLYPH_WIDTH = 3;
|
|
7
|
+
const PHRASE_WIDTH = 18;
|
|
156
8
|
export function IssueRow({ issue, selected, titleWidth, compact = false, }) {
|
|
157
9
|
const key = issue.issueKey ?? issue.projectId;
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label }), facts.length > 0 && (_jsx(Text, { dimColor: true, children: ` \u00b7 ` })), facts.map((fact, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: ` \u00b7 ` }) : null, _jsx(Text, { color: fact.color ?? "white", dimColor: !fact.color, children: fact.text })] }, i)))] }), title ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: title }) })) : null, blocker ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: blocker }) })) : null, detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null, selected && issue.factoryState && issue.sessionState ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: `Debug stage: ${stageLabel(issue)}` }) })) : null] }));
|
|
173
|
-
}
|
|
174
|
-
export function estimateIssueRowHeight(issue, selected, cols, titleWidth, compact = false) {
|
|
175
|
-
const width = Math.max(20, cols);
|
|
176
|
-
const key = issue.issueKey ?? issue.projectId;
|
|
177
|
-
const tw = titleWidth ?? 60;
|
|
178
|
-
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
179
|
-
const detail = compact || !selected ? undefined : summarizeIssueStatusNote(issue.statusNote);
|
|
180
|
-
const session = sessionDisplay(issue);
|
|
181
|
-
const facts = buildFacts(issue, selected, compact);
|
|
182
|
-
const blocker = compact || !selected ? null : blockerText(issue);
|
|
183
|
-
const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
|
|
184
|
-
if (isTerminal && !selected) {
|
|
185
|
-
return 1;
|
|
186
|
-
}
|
|
187
|
-
if (compact) {
|
|
188
|
-
return 1;
|
|
189
|
-
}
|
|
190
|
-
const line1Parts = [
|
|
191
|
-
`${selected ? "\u25b8" : " "} ${key}`,
|
|
192
|
-
relativeTime(issue.updatedAt).padStart(4),
|
|
193
|
-
session.label,
|
|
194
|
-
...facts.map((fact) => fact.text),
|
|
195
|
-
];
|
|
196
|
-
let rows = measureRenderedTextRows(line1Parts.join(" · "), width);
|
|
197
|
-
if (title)
|
|
198
|
-
rows += measureRenderedTextRows(title, Math.max(8, width - 2));
|
|
199
|
-
if (blocker)
|
|
200
|
-
rows += measureRenderedTextRows(blocker, Math.max(8, width - 2));
|
|
201
|
-
if (detail)
|
|
202
|
-
rows += measureRenderedTextRows(detail, Math.max(8, width - 4));
|
|
203
|
-
if (selected && issue.factoryState && issue.sessionState)
|
|
204
|
-
rows += 1;
|
|
205
|
-
if (detail)
|
|
206
|
-
rows += 1;
|
|
207
|
-
return Math.max(1, rows);
|
|
10
|
+
const token = issueTokenFor(issue);
|
|
11
|
+
const pr = prTokenFor(issue);
|
|
12
|
+
const cursorChar = selected ? "\u25b8" : " ";
|
|
13
|
+
const paddedKey = key.padEnd(KEY_WIDTH, " ");
|
|
14
|
+
const paddedPhrase = token.phrase.padEnd(PHRASE_WIDTH, " ");
|
|
15
|
+
const availableTitleWidth = Math.max(0, (titleWidth ?? 60) - (pr ? 10 : 0));
|
|
16
|
+
const title = !compact && selected && issue.title
|
|
17
|
+
? ` ${truncate(issue.title, Math.max(0, availableTitleWidth))}`
|
|
18
|
+
: "";
|
|
19
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: cursorChar }), _jsx(Text, { bold: selected, color: token.color, children: ` ${paddedKey}` }), _jsx(Text, { color: token.color, children: ` ${token.glyph.padEnd(GLYPH_WIDTH - 1, " ")}` }), _jsx(Text, { children: ` ${paddedPhrase}` }), pr ? (_jsx(_Fragment, { children: _jsx(Text, { color: pr.color, children: `#${pr.prNumber} ${pr.glyph}` }) })) : null, title ? _jsx(Text, { dimColor: true, children: title }) : null] }));
|
|
20
|
+
}
|
|
21
|
+
export function estimateIssueRowHeight(_issue, _selected, _cols, _titleWidth, _compact = false) {
|
|
22
|
+
return 1;
|
|
208
23
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useStdout } from "ink";
|
|
4
|
+
import { HelpBar } from "./HelpBar.js";
|
|
5
|
+
import { buildCodexLogLines } from "./codex-log-rows.js";
|
|
6
|
+
import { renderTextLines } from "./render-rich-text.js";
|
|
7
|
+
import { issueTokenFor, prTokenFor } from "./issue-token.js";
|
|
8
|
+
export function LogView({ issue, timeline, follow, scrollOffset, activeRunId, reservedRows = 0, onLayoutChange, }) {
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const width = Math.max(20, stdout?.columns ?? 80);
|
|
11
|
+
const totalRows = stdout?.rows ?? 24;
|
|
12
|
+
const viewportRows = Math.max(4, totalRows - reservedRows - 2);
|
|
13
|
+
const lines = useMemo(() => {
|
|
14
|
+
if (!issue) {
|
|
15
|
+
return [{ key: "loading", segments: [{ text: "Loading issue…", dimColor: true }] }];
|
|
16
|
+
}
|
|
17
|
+
const headerLines = buildLogHeader(issue, activeRunId, width);
|
|
18
|
+
const bodyLines = buildCodexLogLines(timeline, width);
|
|
19
|
+
if (bodyLines.length === 0) {
|
|
20
|
+
return [
|
|
21
|
+
...headerLines,
|
|
22
|
+
blankLine("header-gap"),
|
|
23
|
+
...renderTextLines("No app-server output yet.", { key: "empty", width, style: { dimColor: true } }),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
return [...headerLines, blankLine("header-gap"), ...bodyLines];
|
|
27
|
+
}, [activeRunId, issue, timeline, width]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onLayoutChange(viewportRows, lines.length);
|
|
30
|
+
}, [viewportRows, lines.length, onLayoutChange]);
|
|
31
|
+
const startIndex = clamp(scrollOffset, 0, Math.max(0, lines.length - viewportRows));
|
|
32
|
+
const visible = lines.slice(startIndex, startIndex + viewportRows);
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "column", children: visible.map((line) => (_jsx(Box, { children: line.segments.map((segment, index) => {
|
|
34
|
+
const props = {};
|
|
35
|
+
if (segment.dimColor)
|
|
36
|
+
props.dimColor = true;
|
|
37
|
+
if (segment.bold)
|
|
38
|
+
props.bold = true;
|
|
39
|
+
if (segment.color)
|
|
40
|
+
props.color = segment.color;
|
|
41
|
+
return (_jsx(Text, { ...props, children: segment.text }, `${line.key}-${index}`));
|
|
42
|
+
}) }, line.key))) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "log", follow: follow }) })] }));
|
|
43
|
+
}
|
|
44
|
+
function buildLogHeader(issue, activeRunId, width) {
|
|
45
|
+
const token = issueTokenFor(issue);
|
|
46
|
+
const pr = prTokenFor(issue);
|
|
47
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
48
|
+
const segments = [
|
|
49
|
+
{ text: key, color: token.color, bold: true },
|
|
50
|
+
{ text: " " },
|
|
51
|
+
{ text: token.glyph, color: token.color },
|
|
52
|
+
{ text: " " },
|
|
53
|
+
{ text: token.phrase },
|
|
54
|
+
];
|
|
55
|
+
if (pr) {
|
|
56
|
+
segments.push({ text: " " });
|
|
57
|
+
segments.push({ text: `#${pr.prNumber} ${pr.glyph}`, color: pr.color });
|
|
58
|
+
}
|
|
59
|
+
if (activeRunId !== null) {
|
|
60
|
+
segments.push({ text: " " });
|
|
61
|
+
segments.push({ text: `run #${activeRunId}`, dimColor: true });
|
|
62
|
+
}
|
|
63
|
+
const plain = renderTextLines(segments.map((s) => s.text).join(""), { key: "log-header", width });
|
|
64
|
+
if (plain.length > 0) {
|
|
65
|
+
plain[0] = { key: plain[0].key, segments };
|
|
66
|
+
}
|
|
67
|
+
return plain;
|
|
68
|
+
}
|
|
69
|
+
function clamp(value, min, max) {
|
|
70
|
+
return Math.max(min, Math.min(max, value));
|
|
71
|
+
}
|
|
72
|
+
function blankLine(key) {
|
|
73
|
+
return { key, segments: [{ text: "" }] };
|
|
74
|
+
}
|