patchrelay 0.36.19 → 0.37.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/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +226 -27
- package/dist/cli/watch/HelpBar.js +18 -9
- package/dist/cli/watch/IssueDetailView.js +32 -14
- package/dist/cli/watch/detail-rows.js +1 -22
- package/dist/cli/watch/detail-status.js +38 -0
- package/dist/cli/watch/layout-measure.js +7 -0
- package/dist/cli/watch/prompt-layout.js +14 -0
- package/dist/cli/watch/timeline-builder.js +169 -18
- package/dist/cli/watch/timeline-presentation.js +21 -1
- package/dist/cli/watch/transient-status.js +28 -0
- package/dist/cli/watch/watch-actions.js +76 -0
- package/dist/cli/watch/watch-state.js +2 -12
- package/package.json +1 -1
- package/dist/cli/watch/ItemLine.js +0 -80
- package/dist/cli/watch/Timeline.js +0 -22
- package/dist/cli/watch/TimelineRow.js +0 -77
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useReducer, useMemo, useCallback, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import { useReducer, useMemo, useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { watchReducer, initialWatchState, filterIssues } from "./watch-state.js";
|
|
5
5
|
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 { buildWatchDetailExportText, exportWatchTextToTempFile, findLastAssistantMessage, findLastCommand, findLastCommandOutput, openTextInPager, writeTextToClipboard, } from "./watch-actions.js";
|
|
10
|
+
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
11
|
+
import { PROMPT_COMPOSER_HINT, measurePromptComposerRows } from "./prompt-layout.js";
|
|
12
|
+
import { clearTransientStatus, defaultTimerApi, setPersistentStatus, showTransientStatus } from "./transient-status.js";
|
|
9
13
|
async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
10
14
|
const headers = { "content-type": "application/json" };
|
|
11
15
|
if (bearerToken)
|
|
@@ -61,31 +65,53 @@ async function postRetry(baseUrl, issueKey, bearerToken) {
|
|
|
61
65
|
}
|
|
62
66
|
export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
63
67
|
const { exit } = useApp();
|
|
68
|
+
const { stdout } = useStdout();
|
|
64
69
|
const [state, dispatch] = useReducer(watchReducer, {
|
|
65
70
|
...initialWatchState,
|
|
66
71
|
...(initialIssueKey ? { view: "detail", activeDetailKey: initialIssueKey } : {}),
|
|
67
72
|
});
|
|
68
73
|
const filtered = useMemo(() => filterIssues(state.issues, state.filter), [state.issues, state.filter]);
|
|
69
74
|
const [frozen, setFrozen] = useState(false);
|
|
75
|
+
const width = Math.max(20, stdout?.columns ?? 80);
|
|
70
76
|
useWatchStream({ baseUrl, bearerToken, dispatch, active: !frozen });
|
|
71
77
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch, active: !frozen });
|
|
72
78
|
const [promptMode, setPromptMode] = useState(false);
|
|
73
79
|
const [promptBuffer, setPromptBuffer] = useState("");
|
|
80
|
+
const [promptCursor, setPromptCursor] = useState(0);
|
|
81
|
+
const [promptHistory, setPromptHistory] = useState([]);
|
|
82
|
+
const [promptHistoryIndex, setPromptHistoryIndex] = useState(null);
|
|
83
|
+
const [promptDraftBeforeHistory, setPromptDraftBeforeHistory] = useState("");
|
|
84
|
+
const [promptStatus, setPromptStatus] = useState(null);
|
|
85
|
+
const promptStatusController = useRef({ timer: null });
|
|
86
|
+
const activeIssue = state.issues.find((issue) => issue.issueKey === state.activeDetailKey);
|
|
87
|
+
const showStatus = useCallback((message) => {
|
|
88
|
+
showTransientStatus(promptStatusController.current, message, setPromptStatus, defaultTimerApi);
|
|
89
|
+
}, []);
|
|
90
|
+
const showPersistentStatus = useCallback((message) => {
|
|
91
|
+
setPersistentStatus(promptStatusController.current, message, setPromptStatus, defaultTimerApi);
|
|
92
|
+
}, []);
|
|
93
|
+
useEffect(() => () => {
|
|
94
|
+
clearTransientStatus(promptStatusController.current, defaultTimerApi);
|
|
95
|
+
}, []);
|
|
96
|
+
const resetPromptComposer = useCallback(() => {
|
|
97
|
+
setPromptMode(false);
|
|
98
|
+
setPromptBuffer("");
|
|
99
|
+
setPromptCursor(0);
|
|
100
|
+
setPromptHistoryIndex(null);
|
|
101
|
+
setPromptDraftBeforeHistory("");
|
|
102
|
+
}, []);
|
|
74
103
|
const handleRetry = useCallback(() => {
|
|
75
104
|
if (!state.activeDetailKey)
|
|
76
105
|
return;
|
|
77
|
-
|
|
106
|
+
showPersistentStatus("retrying...");
|
|
78
107
|
void postRetry(baseUrl, state.activeDetailKey, bearerToken).then((result) => {
|
|
79
|
-
|
|
80
|
-
setTimeout(() => setPromptStatus(null), 3000);
|
|
108
|
+
showStatus(result.ok ? "retry queued" : `retry failed: ${result.reason ?? "unknown"}`);
|
|
81
109
|
});
|
|
82
|
-
}, [baseUrl, bearerToken, state.activeDetailKey]);
|
|
83
|
-
const [promptStatus, setPromptStatus] = useState(null);
|
|
110
|
+
}, [baseUrl, bearerToken, showPersistentStatus, showStatus, state.activeDetailKey]);
|
|
84
111
|
const handlePromptSubmit = useCallback(() => {
|
|
85
112
|
const text = promptBuffer.trim();
|
|
86
113
|
if (!state.activeDetailKey || !text) {
|
|
87
|
-
|
|
88
|
-
setPromptBuffer("");
|
|
114
|
+
resetPromptComposer();
|
|
89
115
|
return;
|
|
90
116
|
}
|
|
91
117
|
// Add synthetic userMessage to timeline immediately
|
|
@@ -94,36 +120,184 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
94
120
|
method: "item/started",
|
|
95
121
|
params: { item: { id: `prompt-${Date.now()}`, type: "userMessage", status: "completed", text } },
|
|
96
122
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
setPromptHistory((history) => {
|
|
124
|
+
const next = history.at(-1) === text ? history : [...history, text];
|
|
125
|
+
return next.slice(-20);
|
|
126
|
+
});
|
|
127
|
+
resetPromptComposer();
|
|
128
|
+
showPersistentStatus("sending...");
|
|
100
129
|
void postPrompt(baseUrl, state.activeDetailKey, text, bearerToken).then((result) => {
|
|
101
130
|
if (result.delivered) {
|
|
102
|
-
|
|
131
|
+
showStatus("delivered");
|
|
103
132
|
}
|
|
104
133
|
else if (result.queued) {
|
|
105
|
-
|
|
134
|
+
showStatus("queued for next run");
|
|
106
135
|
}
|
|
107
136
|
else if (result.reason) {
|
|
108
|
-
|
|
137
|
+
showStatus(`failed: ${result.reason}`);
|
|
109
138
|
}
|
|
110
|
-
setTimeout(() => setPromptStatus(null), 3000);
|
|
111
139
|
});
|
|
112
|
-
}, [baseUrl, bearerToken, state.activeDetailKey
|
|
140
|
+
}, [baseUrl, bearerToken, dispatch, promptBuffer, resetPromptComposer, showPersistentStatus, showStatus, state.activeDetailKey]);
|
|
141
|
+
const withActiveIssueExport = useCallback(() => {
|
|
142
|
+
if (!activeIssue)
|
|
143
|
+
return null;
|
|
144
|
+
return {
|
|
145
|
+
issue: activeIssue,
|
|
146
|
+
timeline: state.timeline,
|
|
147
|
+
activeRunStartedAt: state.activeRunStartedAt,
|
|
148
|
+
activeRunId: state.activeRunId,
|
|
149
|
+
tokenUsage: state.tokenUsage,
|
|
150
|
+
diffSummary: state.diffSummary,
|
|
151
|
+
plan: state.plan,
|
|
152
|
+
issueContext: state.issueContext,
|
|
153
|
+
detailTab: state.detailTab,
|
|
154
|
+
rawRuns: state.rawRuns,
|
|
155
|
+
rawFeedEvents: state.rawFeedEvents,
|
|
156
|
+
};
|
|
157
|
+
}, [
|
|
158
|
+
activeIssue,
|
|
159
|
+
state.activeRunId,
|
|
160
|
+
state.activeRunStartedAt,
|
|
161
|
+
state.detailTab,
|
|
162
|
+
state.diffSummary,
|
|
163
|
+
state.issueContext,
|
|
164
|
+
state.plan,
|
|
165
|
+
state.rawFeedEvents,
|
|
166
|
+
state.rawRuns,
|
|
167
|
+
state.timeline,
|
|
168
|
+
state.tokenUsage,
|
|
169
|
+
]);
|
|
170
|
+
const handleCopyLastAssistant = useCallback(() => {
|
|
171
|
+
const text = findLastAssistantMessage(state.timeline);
|
|
172
|
+
if (!text) {
|
|
173
|
+
showStatus("no assistant message to copy");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
showStatus(writeTextToClipboard(text) ? "copied assistant message" : "clipboard unavailable");
|
|
177
|
+
}, [showStatus, state.timeline]);
|
|
178
|
+
const handleCopyLastCommand = useCallback(() => {
|
|
179
|
+
const text = findLastCommand(state.timeline);
|
|
180
|
+
if (!text) {
|
|
181
|
+
showStatus("no command to copy");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
showStatus(writeTextToClipboard(text) ? "copied last command" : "clipboard unavailable");
|
|
185
|
+
}, [showStatus, state.timeline]);
|
|
186
|
+
const handleCopyLastCommandOutput = useCallback(() => {
|
|
187
|
+
const text = findLastCommandOutput(state.timeline);
|
|
188
|
+
if (!text) {
|
|
189
|
+
showStatus("no command output to copy");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
showStatus(writeTextToClipboard(text) ? "copied command output" : "clipboard unavailable");
|
|
193
|
+
}, [showStatus, state.timeline]);
|
|
194
|
+
const handleExportTranscript = useCallback(() => {
|
|
195
|
+
const exportInput = withActiveIssueExport();
|
|
196
|
+
if (!exportInput)
|
|
197
|
+
return;
|
|
198
|
+
const text = buildWatchDetailExportText(exportInput);
|
|
199
|
+
const filePath = exportWatchTextToTempFile(text, exportInput.issue.issueKey ?? exportInput.issue.projectId);
|
|
200
|
+
showStatus(`exported transcript: ${filePath}`);
|
|
201
|
+
}, [showStatus, withActiveIssueExport]);
|
|
202
|
+
const handleOpenTranscriptInPager = useCallback(() => {
|
|
203
|
+
const exportInput = withActiveIssueExport();
|
|
204
|
+
if (!exportInput)
|
|
205
|
+
return;
|
|
206
|
+
const text = buildWatchDetailExportText(exportInput);
|
|
207
|
+
const result = openTextInPager(text);
|
|
208
|
+
if (result.ok) {
|
|
209
|
+
showStatus("opened transcript in pager");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const filePath = exportWatchTextToTempFile(text, exportInput.issue.issueKey ?? exportInput.issue.projectId);
|
|
213
|
+
showStatus(`pager failed, exported transcript: ${filePath}`);
|
|
214
|
+
}, [showStatus, withActiveIssueExport]);
|
|
215
|
+
const insertPromptText = useCallback((text) => {
|
|
216
|
+
setPromptBuffer((buffer) => `${buffer.slice(0, promptCursor)}${text}${buffer.slice(promptCursor)}`);
|
|
217
|
+
setPromptCursor((cursor) => cursor + text.length);
|
|
218
|
+
setPromptHistoryIndex(null);
|
|
219
|
+
}, [promptCursor]);
|
|
220
|
+
const movePromptCursor = useCallback((delta) => {
|
|
221
|
+
setPromptCursor((cursor) => Math.max(0, Math.min(promptBuffer.length, cursor + delta)));
|
|
222
|
+
}, [promptBuffer.length]);
|
|
223
|
+
const recallPromptHistory = useCallback((direction) => {
|
|
224
|
+
if (promptHistory.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
if (direction === "older") {
|
|
227
|
+
if (promptHistoryIndex === null) {
|
|
228
|
+
setPromptDraftBeforeHistory(promptBuffer);
|
|
229
|
+
const nextIndex = promptHistory.length - 1;
|
|
230
|
+
const next = promptHistory[nextIndex] ?? "";
|
|
231
|
+
setPromptHistoryIndex(nextIndex);
|
|
232
|
+
setPromptBuffer(next);
|
|
233
|
+
setPromptCursor(next.length);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const nextIndex = Math.max(0, promptHistoryIndex - 1);
|
|
237
|
+
const next = promptHistory[nextIndex] ?? "";
|
|
238
|
+
setPromptHistoryIndex(nextIndex);
|
|
239
|
+
setPromptBuffer(next);
|
|
240
|
+
setPromptCursor(next.length);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (promptHistoryIndex === null)
|
|
244
|
+
return;
|
|
245
|
+
if (promptHistoryIndex >= promptHistory.length - 1) {
|
|
246
|
+
setPromptHistoryIndex(null);
|
|
247
|
+
setPromptBuffer(promptDraftBeforeHistory);
|
|
248
|
+
setPromptCursor(promptDraftBeforeHistory.length);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const nextIndex = promptHistoryIndex + 1;
|
|
252
|
+
const next = promptHistory[nextIndex] ?? "";
|
|
253
|
+
setPromptHistoryIndex(nextIndex);
|
|
254
|
+
setPromptBuffer(next);
|
|
255
|
+
setPromptCursor(next.length);
|
|
256
|
+
}, [promptBuffer, promptDraftBeforeHistory, promptHistory, promptHistoryIndex]);
|
|
113
257
|
useInput((input, key) => {
|
|
114
258
|
if (promptMode) {
|
|
115
259
|
if (key.escape) {
|
|
116
|
-
|
|
117
|
-
|
|
260
|
+
resetPromptComposer();
|
|
261
|
+
}
|
|
262
|
+
else if (key.ctrl && input === "n") {
|
|
263
|
+
insertPromptText("\n");
|
|
118
264
|
}
|
|
119
265
|
else if (key.return) {
|
|
120
266
|
handlePromptSubmit();
|
|
121
267
|
}
|
|
122
|
-
else if (key.
|
|
123
|
-
|
|
268
|
+
else if (key.leftArrow) {
|
|
269
|
+
movePromptCursor(-1);
|
|
270
|
+
}
|
|
271
|
+
else if (key.rightArrow) {
|
|
272
|
+
movePromptCursor(1);
|
|
273
|
+
}
|
|
274
|
+
else if (key.home) {
|
|
275
|
+
setPromptCursor(0);
|
|
276
|
+
}
|
|
277
|
+
else if (key.end) {
|
|
278
|
+
setPromptCursor(promptBuffer.length);
|
|
279
|
+
}
|
|
280
|
+
else if (key.upArrow) {
|
|
281
|
+
recallPromptHistory("older");
|
|
282
|
+
}
|
|
283
|
+
else if (key.downArrow) {
|
|
284
|
+
recallPromptHistory("newer");
|
|
285
|
+
}
|
|
286
|
+
else if (key.backspace) {
|
|
287
|
+
if (promptCursor > 0) {
|
|
288
|
+
setPromptBuffer((buffer) => `${buffer.slice(0, promptCursor - 1)}${buffer.slice(promptCursor)}`);
|
|
289
|
+
setPromptCursor((cursor) => Math.max(0, cursor - 1));
|
|
290
|
+
setPromptHistoryIndex(null);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (key.delete) {
|
|
294
|
+
if (promptCursor < promptBuffer.length) {
|
|
295
|
+
setPromptBuffer((buffer) => `${buffer.slice(0, promptCursor)}${buffer.slice(promptCursor + 1)}`);
|
|
296
|
+
setPromptHistoryIndex(null);
|
|
297
|
+
}
|
|
124
298
|
}
|
|
125
299
|
else if (input && !key.ctrl && !key.meta) {
|
|
126
|
-
|
|
300
|
+
insertPromptText(input);
|
|
127
301
|
}
|
|
128
302
|
return;
|
|
129
303
|
}
|
|
@@ -164,16 +338,31 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
164
338
|
}
|
|
165
339
|
else if (input === "p") {
|
|
166
340
|
setPromptMode(true);
|
|
341
|
+
setPromptCursor(promptBuffer.length);
|
|
167
342
|
}
|
|
168
343
|
else if (input === "s") {
|
|
169
344
|
if (state.activeDetailKey) {
|
|
170
|
-
|
|
345
|
+
showPersistentStatus("stopping...");
|
|
171
346
|
void postStop(baseUrl, state.activeDetailKey, bearerToken).then((result) => {
|
|
172
|
-
|
|
173
|
-
setTimeout(() => setPromptStatus(null), 3000);
|
|
347
|
+
showStatus(result.ok ? "stop sent" : `stop failed: ${result.reason ?? "unknown"}`);
|
|
174
348
|
});
|
|
175
349
|
}
|
|
176
350
|
}
|
|
351
|
+
else if (input === "y") {
|
|
352
|
+
handleCopyLastAssistant();
|
|
353
|
+
}
|
|
354
|
+
else if (input === "c") {
|
|
355
|
+
handleCopyLastCommand();
|
|
356
|
+
}
|
|
357
|
+
else if (input === "o") {
|
|
358
|
+
handleCopyLastCommandOutput();
|
|
359
|
+
}
|
|
360
|
+
else if (input === "e") {
|
|
361
|
+
handleExportTranscript();
|
|
362
|
+
}
|
|
363
|
+
else if (input === "v") {
|
|
364
|
+
handleOpenTranscriptInPager();
|
|
365
|
+
}
|
|
177
366
|
else if (input === "h") {
|
|
178
367
|
dispatch({ type: "switch-detail-tab", tab: "history" });
|
|
179
368
|
}
|
|
@@ -206,7 +395,17 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
206
395
|
}
|
|
207
396
|
}
|
|
208
397
|
});
|
|
209
|
-
|
|
398
|
+
const reservedRows = 1 + (promptMode
|
|
399
|
+
? measurePromptComposerRows(promptBuffer, promptCursor, width)
|
|
400
|
+
: promptStatus
|
|
401
|
+
? measureRenderedTextRows(promptStatus, width)
|
|
402
|
+
: 0);
|
|
403
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), 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, reservedRows: reservedRows, onLayoutChange: (viewportRows, contentRows) => {
|
|
210
404
|
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
211
|
-
} }), promptMode && (
|
|
405
|
+
} }), promptMode && (_jsx(PromptComposer, { buffer: promptBuffer, cursor: promptCursor })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : null }));
|
|
406
|
+
}
|
|
407
|
+
function PromptComposer({ buffer, cursor }) {
|
|
408
|
+
const withCursor = `${buffer.slice(0, cursor)}|${buffer.slice(cursor)}`;
|
|
409
|
+
const lines = withCursor.split("\n");
|
|
410
|
+
return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: index === 0 ? "prompt> " : " " }), _jsx(Text, { children: line })] }, `prompt-line-${index}`))), _jsx(Text, { dimColor: true, children: PROMPT_COMPOSER_HINT })] }));
|
|
212
411
|
}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
export function
|
|
4
|
-
let text;
|
|
3
|
+
export function buildHelpBarText(view, follow, detailTab) {
|
|
5
4
|
if (view === "detail") {
|
|
6
5
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
7
|
-
|
|
6
|
+
return [
|
|
7
|
+
tabHint,
|
|
8
|
+
"j/k: scroll",
|
|
9
|
+
"Ctrl-U/Ctrl-D: page",
|
|
10
|
+
"[ ]: issue",
|
|
11
|
+
"Home/End: jump",
|
|
12
|
+
`f: live ${follow ? "on" : "off"}`,
|
|
13
|
+
"p: prompt",
|
|
14
|
+
"y/c/o: copy",
|
|
15
|
+
"v/e: transcript",
|
|
16
|
+
"s: stop",
|
|
17
|
+
"r: retry",
|
|
18
|
+
]
|
|
8
19
|
.filter(Boolean)
|
|
9
20
|
.join(" ");
|
|
10
21
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
text = "Enter: detail Tab: filter";
|
|
16
|
-
}
|
|
22
|
+
return "Enter: detail Tab: filter";
|
|
23
|
+
}
|
|
24
|
+
export function HelpBar({ view, follow, detailTab }) {
|
|
25
|
+
const text = buildHelpBarText(view, follow, detailTab);
|
|
17
26
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
18
27
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useReducer } from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
|
-
import { HelpBar } from "./HelpBar.js";
|
|
4
|
+
import { HelpBar, buildHelpBarText } from "./HelpBar.js";
|
|
5
5
|
import { buildDetailLines } from "./detail-rows.js";
|
|
6
|
+
import { buildDetailStatusSegments, buildDetailStatusText } from "./detail-status.js";
|
|
7
|
+
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
6
8
|
export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0, onLayoutChange, }) {
|
|
7
|
-
const [, tick] = useReducer((value) => value + 1, 0);
|
|
8
9
|
const { stdout } = useStdout();
|
|
9
10
|
const width = Math.max(20, stdout?.columns ?? 80);
|
|
10
11
|
const totalRows = stdout?.rows ?? 24;
|
|
11
|
-
const footerRows =
|
|
12
|
+
const footerRows = useMemo(() => {
|
|
13
|
+
const statusRows = measureRenderedTextRows(buildDetailStatusText({
|
|
14
|
+
follow,
|
|
15
|
+
unreadBelow,
|
|
16
|
+
activeRunStartedAt,
|
|
17
|
+
connected,
|
|
18
|
+
lastServerMessageAt,
|
|
19
|
+
}), width);
|
|
20
|
+
const helpRows = measureRenderedTextRows(buildHelpBarText("detail", follow, detailTab), width);
|
|
21
|
+
return statusRows + helpRows;
|
|
22
|
+
}, [activeRunStartedAt, connected, detailTab, follow, lastServerMessageAt, unreadBelow, width]);
|
|
12
23
|
const viewportRows = Math.max(4, totalRows - reservedRows - footerRows);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const id = setInterval(tick, 1_000);
|
|
15
|
-
return () => clearInterval(id);
|
|
16
|
-
}, []);
|
|
17
24
|
const lines = useMemo(() => {
|
|
18
25
|
if (!issue) {
|
|
19
26
|
return [{ key: "loading", segments: [{ text: "Loading issue…", dimColor: true }] }];
|
|
@@ -30,9 +37,6 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
30
37
|
detailTab,
|
|
31
38
|
rawRuns,
|
|
32
39
|
rawFeedEvents,
|
|
33
|
-
follow,
|
|
34
|
-
connected,
|
|
35
|
-
lastServerMessageAt,
|
|
36
40
|
width,
|
|
37
41
|
});
|
|
38
42
|
}, [
|
|
@@ -47,9 +51,6 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
47
51
|
detailTab,
|
|
48
52
|
rawRuns,
|
|
49
53
|
rawFeedEvents,
|
|
50
|
-
follow,
|
|
51
|
-
connected,
|
|
52
|
-
lastServerMessageAt,
|
|
53
54
|
width,
|
|
54
55
|
]);
|
|
55
56
|
useEffect(() => {
|
|
@@ -59,7 +60,7 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
59
60
|
const start = Math.min(scrollOffset, maxOffset);
|
|
60
61
|
const visibleLines = lines.slice(start, start + viewportRows);
|
|
61
62
|
const fillerCount = Math.max(0, viewportRows - visibleLines.length);
|
|
62
|
-
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}`))),
|
|
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, detailTab: detailTab })] }));
|
|
63
64
|
}
|
|
64
65
|
function RenderedLine({ line }) {
|
|
65
66
|
if (line.segments.length === 0) {
|
|
@@ -69,3 +70,20 @@ function RenderedLine({ line }) {
|
|
|
69
70
|
// eslint-disable-next-line react/no-array-index-key
|
|
70
71
|
, { ...(segment.color ? { color: segment.color } : {}), ...(segment.dimColor ? { dimColor: true } : {}), ...(segment.bold ? { bold: true } : {}), children: segment.text }, `${line.key}-${index}`))) }));
|
|
71
72
|
}
|
|
73
|
+
function DetailStatusStrip({ follow, unreadBelow, activeRunStartedAt, connected, lastServerMessageAt, }) {
|
|
74
|
+
const [tick, bumpTick] = useReducer((value) => value + 1, 0);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const id = setInterval(bumpTick, 1_000);
|
|
77
|
+
return () => clearInterval(id);
|
|
78
|
+
}, []);
|
|
79
|
+
const segments = useMemo(() => buildDetailStatusSegments({
|
|
80
|
+
follow,
|
|
81
|
+
unreadBelow,
|
|
82
|
+
activeRunStartedAt,
|
|
83
|
+
connected,
|
|
84
|
+
lastServerMessageAt,
|
|
85
|
+
}), [activeRunStartedAt, connected, follow, lastServerMessageAt, tick, unreadBelow]);
|
|
86
|
+
return (_jsx(Text, { children: segments.map((segment, index) => (_jsx(Text
|
|
87
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
88
|
+
, { ...(segment.color ? { color: segment.color } : {}), ...(segment.dimColor ? { dimColor: true } : {}), ...(segment.bold ? { bold: true } : {}), children: segment.text }, `detail-status-${index}`))) }));
|
|
89
|
+
}
|
|
@@ -2,7 +2,6 @@ import { buildStateHistory } from "./history-builder.js";
|
|
|
2
2
|
import { buildTimelineRows } from "./timeline-presentation.js";
|
|
3
3
|
import { planStepColor, planStepSymbol } from "./plan-helpers.js";
|
|
4
4
|
import { progressBar } from "./format-utils.js";
|
|
5
|
-
import { describePatchRelayFreshness } from "./freshness.js";
|
|
6
5
|
import { hasDisplayPrBlocker, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, isRereviewNeeded, prChecksFact, } from "./pr-status.js";
|
|
7
6
|
import { renderRichTextLines, renderTextLines } from "./render-rich-text.js";
|
|
8
7
|
const SESSION_DISPLAY = {
|
|
@@ -85,20 +84,10 @@ function buildHeaderLines(input, width) {
|
|
|
85
84
|
headerSegments.push({ text: " ", dimColor: true });
|
|
86
85
|
headerSegments.push(...joinFactSegments(facts));
|
|
87
86
|
}
|
|
88
|
-
if (input.activeRunStartedAt) {
|
|
89
|
-
headerSegments.push({ text: " ", dimColor: true });
|
|
90
|
-
headerSegments.push({ text: elapsedLabel(input.activeRunStartedAt), dimColor: true });
|
|
91
|
-
}
|
|
92
87
|
if (meta.length > 0) {
|
|
93
88
|
headerSegments.push({ text: " ", dimColor: true });
|
|
94
89
|
headerSegments.push({ text: meta.join(" "), dimColor: true });
|
|
95
90
|
}
|
|
96
|
-
headerSegments.push({ text: " ", dimColor: true });
|
|
97
|
-
headerSegments.push(...freshnessSegments(input.connected, input.lastServerMessageAt));
|
|
98
|
-
if (input.follow) {
|
|
99
|
-
headerSegments.push({ text: " " });
|
|
100
|
-
headerSegments.push({ text: "live", color: "yellow", bold: true });
|
|
101
|
-
}
|
|
102
91
|
const lines = renderTextLines(segmentsToText(headerSegments), {
|
|
103
92
|
key: "detail-header",
|
|
104
93
|
width,
|
|
@@ -255,7 +244,7 @@ function renderTimelineItemLines(key, item, width, indent) {
|
|
|
255
244
|
? { color: "white" }
|
|
256
245
|
: { dimColor: item.type !== "commandExecution" },
|
|
257
246
|
});
|
|
258
|
-
if (item.output && item.
|
|
247
|
+
if (item.output && item.type === "commandExecution") {
|
|
259
248
|
lines.push(...renderTextLines(lastNonEmptyLine(item.output), {
|
|
260
249
|
key: `${key}-output`,
|
|
261
250
|
width,
|
|
@@ -462,12 +451,6 @@ function formatDuration(startedAt, endedAt) {
|
|
|
462
451
|
function formatTime(iso) {
|
|
463
452
|
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
464
453
|
}
|
|
465
|
-
function elapsedLabel(startedAt) {
|
|
466
|
-
const elapsed = Math.max(0, Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000));
|
|
467
|
-
const minutes = Math.floor(elapsed / 60);
|
|
468
|
-
const seconds = elapsed % 60;
|
|
469
|
-
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
470
|
-
}
|
|
471
454
|
function itemSummary(item) {
|
|
472
455
|
switch (item.type) {
|
|
473
456
|
case "commandExecution": {
|
|
@@ -598,10 +581,6 @@ function feedGlyph(status) {
|
|
|
598
581
|
return "✓";
|
|
599
582
|
return "●";
|
|
600
583
|
}
|
|
601
|
-
function freshnessSegments(connected, lastServerMessageAt) {
|
|
602
|
-
const freshness = describePatchRelayFreshness(connected, lastServerMessageAt);
|
|
603
|
-
return [{ text: freshness.label, color: freshness.color, bold: true }];
|
|
604
|
-
}
|
|
605
584
|
function blankLine(key) {
|
|
606
585
|
return { key, segments: [] };
|
|
607
586
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describePatchRelayFreshness } from "./freshness.js";
|
|
2
|
+
export function buildDetailStatusSegments(input, now = Date.now()) {
|
|
3
|
+
const groups = [];
|
|
4
|
+
groups.push(input.follow
|
|
5
|
+
? [{ text: "live edge", color: "green", bold: true }]
|
|
6
|
+
: [{ text: "anchored review", color: "yellow", bold: true }]);
|
|
7
|
+
if (input.unreadBelow > 0) {
|
|
8
|
+
groups.push([{ text: `${input.unreadBelow} new below`, color: "yellow", bold: true }]);
|
|
9
|
+
}
|
|
10
|
+
if (input.activeRunStartedAt) {
|
|
11
|
+
groups.push([{ text: `run ${formatElapsed(input.activeRunStartedAt, now)}`, dimColor: true }]);
|
|
12
|
+
}
|
|
13
|
+
const freshness = describePatchRelayFreshness(input.connected, input.lastServerMessageAt, now);
|
|
14
|
+
groups.push([{ text: freshness.label, color: freshness.color, bold: true }]);
|
|
15
|
+
return joinGroups(groups);
|
|
16
|
+
}
|
|
17
|
+
export function buildDetailStatusText(input, now = Date.now()) {
|
|
18
|
+
return buildDetailStatusSegments(input, now).map((segment) => segment.text).join("");
|
|
19
|
+
}
|
|
20
|
+
function formatElapsed(startedAt, now) {
|
|
21
|
+
const startedMs = Date.parse(startedAt);
|
|
22
|
+
if (!Number.isFinite(startedMs))
|
|
23
|
+
return "0m 00s";
|
|
24
|
+
const elapsed = Math.max(0, Math.floor((now - startedMs) / 1000));
|
|
25
|
+
const minutes = Math.floor(elapsed / 60);
|
|
26
|
+
const seconds = elapsed % 60;
|
|
27
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
28
|
+
}
|
|
29
|
+
function joinGroups(groups) {
|
|
30
|
+
const segments = [];
|
|
31
|
+
for (const [index, group] of groups.entries()) {
|
|
32
|
+
if (index > 0) {
|
|
33
|
+
segments.push({ text: " ", dimColor: true });
|
|
34
|
+
}
|
|
35
|
+
segments.push(...group);
|
|
36
|
+
}
|
|
37
|
+
return segments;
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
2
|
+
export const PROMPT_COMPOSER_HINT = "Enter: send Ctrl-N: newline Up/Down: history Esc: cancel";
|
|
3
|
+
export function buildPromptComposerDisplayLines(buffer, cursor) {
|
|
4
|
+
const withCursor = `${buffer.slice(0, cursor)}|${buffer.slice(cursor)}`;
|
|
5
|
+
const contentLines = withCursor.split("\n");
|
|
6
|
+
return [
|
|
7
|
+
...contentLines.map((line, index) => `${index === 0 ? "prompt> " : " "}${line}`),
|
|
8
|
+
PROMPT_COMPOSER_HINT,
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
export function measurePromptComposerRows(buffer, cursor, width) {
|
|
12
|
+
return buildPromptComposerDisplayLines(buffer, cursor)
|
|
13
|
+
.reduce((count, line) => count + measureRenderedTextRows(line, width), 0);
|
|
14
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getThreadTurns } from "../../codex-thread-utils.js";
|
|
|
2
2
|
// ─── Build Timeline from Rehydration Data ─────────────────────────
|
|
3
3
|
export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activeRunId) {
|
|
4
4
|
const entries = [];
|
|
5
|
+
const activeRun = activeRunId ? runs.find((run) => run.id === activeRunId) : undefined;
|
|
5
6
|
// 1. Add run boundaries and items from reports
|
|
6
7
|
for (const run of runs) {
|
|
7
8
|
entries.push({
|
|
@@ -32,22 +33,28 @@ export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activ
|
|
|
32
33
|
}
|
|
33
34
|
// 2. Items from live thread (active run)
|
|
34
35
|
if (liveThread && activeRunId) {
|
|
35
|
-
entries.push(...itemsFromThread(activeRunId, liveThread));
|
|
36
|
+
entries.push(...itemsFromThread(activeRunId, liveThread, activeRun?.startedAt));
|
|
36
37
|
}
|
|
37
38
|
// 3. Feed events → feed entries + CI check aggregation
|
|
38
39
|
entries.push(...feedEventsToEntries(feedEvents));
|
|
39
40
|
// 4. Sort by timestamp, then by entry order for stability
|
|
40
|
-
entries
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
return sortTimelineEntries(entries);
|
|
42
|
+
}
|
|
43
|
+
export function reconcileTimelineFromRehydration(previousTimeline, runs, feedEvents, liveThread, activeRunId) {
|
|
44
|
+
const rehydrated = buildTimelineFromRehydration(runs, feedEvents, liveThread, activeRunId);
|
|
45
|
+
if (previousTimeline.length === 0) {
|
|
46
|
+
return rehydrated;
|
|
47
|
+
}
|
|
48
|
+
const previousById = new Map(previousTimeline.map((entry) => [entry.id, entry]));
|
|
49
|
+
const rehydratedIds = new Set(rehydrated.map((entry) => entry.id));
|
|
50
|
+
const liveUserMessages = collectUserMessageCounts(rehydrated, activeRunId);
|
|
51
|
+
const merged = rehydrated.map((entry) => mergeTimelineEntry(previousById.get(entry.id), entry));
|
|
52
|
+
const carriedForward = previousTimeline.filter((entry) => {
|
|
53
|
+
if (rehydratedIds.has(entry.id))
|
|
54
|
+
return false;
|
|
55
|
+
return shouldCarryForwardEntry(entry, activeRunId, liveUserMessages);
|
|
49
56
|
});
|
|
50
|
-
return
|
|
57
|
+
return sortTimelineEntries([...merged, ...carriedForward]);
|
|
51
58
|
}
|
|
52
59
|
function kindOrder(kind) {
|
|
53
60
|
switch (kind) {
|
|
@@ -131,27 +138,37 @@ function syntheticTimestamp(startMs, endMs, index, total) {
|
|
|
131
138
|
return new Date(startMs + fraction * (endMs - startMs)).toISOString();
|
|
132
139
|
}
|
|
133
140
|
// ─── Items from Live Thread ───────────────────────────────────────
|
|
134
|
-
function itemsFromThread(runId, thread) {
|
|
141
|
+
function itemsFromThread(runId, thread, runStartedAt) {
|
|
135
142
|
const entries = [];
|
|
143
|
+
let itemIndex = 0;
|
|
136
144
|
for (const turn of getThreadTurns(thread)) {
|
|
137
145
|
for (const item of turn.items) {
|
|
138
146
|
entries.push({
|
|
139
147
|
id: `live-${item.id}`,
|
|
140
|
-
at:
|
|
148
|
+
at: liveItemTimestamp(runStartedAt, itemIndex),
|
|
141
149
|
kind: "item",
|
|
142
150
|
runId,
|
|
143
151
|
item: materializeItem(item),
|
|
144
152
|
});
|
|
153
|
+
itemIndex += 1;
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
return entries;
|
|
148
157
|
}
|
|
158
|
+
const LIVE_ITEM_FALLBACK_START_MS = Date.UTC(9999, 0, 1, 0, 0, 0, 0);
|
|
159
|
+
function liveItemTimestamp(runStartedAt, itemIndex) {
|
|
160
|
+
const baseMs = runStartedAt ? Date.parse(runStartedAt) : LIVE_ITEM_FALLBACK_START_MS;
|
|
161
|
+
const stableBaseMs = Number.isFinite(baseMs) ? baseMs : LIVE_ITEM_FALLBACK_START_MS;
|
|
162
|
+
return new Date(stableBaseMs + itemIndex).toISOString();
|
|
163
|
+
}
|
|
149
164
|
function materializeItem(item) {
|
|
150
165
|
const r = item;
|
|
151
166
|
const id = String(r.id ?? "unknown");
|
|
152
167
|
const type = String(r.type ?? "unknown");
|
|
153
168
|
const base = { id, type, status: "completed" };
|
|
154
169
|
switch (type) {
|
|
170
|
+
case "userMessage":
|
|
171
|
+
return { ...base, text: extractUserMessageText(r.content) };
|
|
155
172
|
case "agentMessage":
|
|
156
173
|
return { ...base, text: String(r.text ?? "") };
|
|
157
174
|
case "commandExecution":
|
|
@@ -340,16 +357,150 @@ function mergeDefinedItemFields(base, patch) {
|
|
|
340
357
|
...base,
|
|
341
358
|
id: patch.id,
|
|
342
359
|
type: patch.type,
|
|
343
|
-
status: patch.status,
|
|
344
|
-
...(patch.text !== undefined ? { text: patch.text } : {}),
|
|
360
|
+
status: preferredItemStatus(base.status, patch.status),
|
|
361
|
+
...(mergePreferredString(base.text, patch.text) !== undefined ? { text: mergePreferredString(base.text, patch.text) } : {}),
|
|
345
362
|
...(patch.command !== undefined ? { command: patch.command } : {}),
|
|
346
|
-
...(patch.output !== undefined ? { output: patch.output } : {}),
|
|
363
|
+
...(mergePreferredString(base.output, patch.output) !== undefined ? { output: mergePreferredString(base.output, patch.output) } : {}),
|
|
347
364
|
...(patch.exitCode !== undefined ? { exitCode: patch.exitCode } : {}),
|
|
348
|
-
...(patch.durationMs !== undefined
|
|
349
|
-
|
|
365
|
+
...(patch.durationMs !== undefined || base.durationMs !== undefined
|
|
366
|
+
? { durationMs: preferredNumber(base.durationMs, patch.durationMs) }
|
|
367
|
+
: {}),
|
|
368
|
+
...(patch.changes !== undefined || base.changes !== undefined
|
|
369
|
+
? { changes: preferredChanges(base.changes, patch.changes) }
|
|
370
|
+
: {}),
|
|
350
371
|
...(patch.toolName !== undefined ? { toolName: patch.toolName } : {}),
|
|
351
372
|
};
|
|
352
373
|
}
|
|
374
|
+
function mergeTimelineEntry(existing, incoming) {
|
|
375
|
+
if (!existing || existing.kind !== incoming.kind) {
|
|
376
|
+
return incoming;
|
|
377
|
+
}
|
|
378
|
+
switch (incoming.kind) {
|
|
379
|
+
case "item":
|
|
380
|
+
return {
|
|
381
|
+
...incoming,
|
|
382
|
+
at: existing.at,
|
|
383
|
+
...(existing.runId !== undefined && incoming.runId === undefined ? { runId: existing.runId } : {}),
|
|
384
|
+
item: incoming.item && existing.item ? mergeDefinedItemFields(existing.item, incoming.item) : incoming.item,
|
|
385
|
+
};
|
|
386
|
+
case "run-start":
|
|
387
|
+
case "run-end":
|
|
388
|
+
return {
|
|
389
|
+
...incoming,
|
|
390
|
+
at: existing.at,
|
|
391
|
+
run: existing.run && incoming.run ? { ...existing.run, ...incoming.run } : incoming.run,
|
|
392
|
+
};
|
|
393
|
+
case "feed":
|
|
394
|
+
return {
|
|
395
|
+
...incoming,
|
|
396
|
+
at: existing.at,
|
|
397
|
+
feed: existing.feed && incoming.feed ? { ...existing.feed, ...incoming.feed } : incoming.feed,
|
|
398
|
+
};
|
|
399
|
+
case "ci-checks":
|
|
400
|
+
return {
|
|
401
|
+
...incoming,
|
|
402
|
+
at: existing.at,
|
|
403
|
+
ciChecks: incoming.ciChecks ?? existing.ciChecks,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function shouldCarryForwardEntry(entry, activeRunId, liveUserMessages) {
|
|
408
|
+
if (entry.kind !== "item" || entry.runId !== activeRunId) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
if (entry.item?.id.startsWith("prompt-") === true) {
|
|
412
|
+
const text = normalizePromptText(entry.item.text);
|
|
413
|
+
return !text || !consumeUserMessageMatch(liveUserMessages, text);
|
|
414
|
+
}
|
|
415
|
+
return entry.item?.status === "inProgress";
|
|
416
|
+
}
|
|
417
|
+
function sortTimelineEntries(entries) {
|
|
418
|
+
return [...entries].sort((a, b) => {
|
|
419
|
+
const cmp = a.at.localeCompare(b.at);
|
|
420
|
+
if (cmp !== 0)
|
|
421
|
+
return cmp;
|
|
422
|
+
// Within same timestamp: run-start before items, items before run-end
|
|
423
|
+
const kindCmp = kindOrder(a.kind) - kindOrder(b.kind);
|
|
424
|
+
if (kindCmp !== 0)
|
|
425
|
+
return kindCmp;
|
|
426
|
+
return a.id.localeCompare(b.id);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function preferredItemStatus(existing, incoming) {
|
|
430
|
+
return itemStatusRank(incoming) >= itemStatusRank(existing) ? incoming : existing;
|
|
431
|
+
}
|
|
432
|
+
function itemStatusRank(status) {
|
|
433
|
+
switch (status) {
|
|
434
|
+
case "failed":
|
|
435
|
+
case "completed":
|
|
436
|
+
case "declined":
|
|
437
|
+
return 2;
|
|
438
|
+
case "inProgress":
|
|
439
|
+
return 1;
|
|
440
|
+
default:
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function mergePreferredString(existing, incoming) {
|
|
445
|
+
if (incoming === undefined)
|
|
446
|
+
return existing;
|
|
447
|
+
if (existing === undefined)
|
|
448
|
+
return incoming;
|
|
449
|
+
return incoming.length >= existing.length ? incoming : existing;
|
|
450
|
+
}
|
|
451
|
+
function preferredNumber(existing, incoming) {
|
|
452
|
+
return incoming ?? existing;
|
|
453
|
+
}
|
|
454
|
+
function preferredChanges(existing, incoming) {
|
|
455
|
+
if (incoming === undefined)
|
|
456
|
+
return existing;
|
|
457
|
+
if (existing === undefined)
|
|
458
|
+
return incoming;
|
|
459
|
+
return incoming.length >= existing.length ? incoming : existing;
|
|
460
|
+
}
|
|
461
|
+
function collectUserMessageCounts(entries, activeRunId) {
|
|
462
|
+
const texts = new Map();
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
if (entry.kind !== "item" || entry.runId !== activeRunId || entry.item?.type !== "userMessage") {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const text = normalizePromptText(entry.item.text);
|
|
468
|
+
if (text) {
|
|
469
|
+
texts.set(text, (texts.get(text) ?? 0) + 1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return texts;
|
|
473
|
+
}
|
|
474
|
+
function consumeUserMessageMatch(messages, text) {
|
|
475
|
+
const count = messages.get(text) ?? 0;
|
|
476
|
+
if (count <= 0) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
if (count === 1) {
|
|
480
|
+
messages.delete(text);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
messages.set(text, count - 1);
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
function normalizePromptText(text) {
|
|
488
|
+
const normalized = text?.trim();
|
|
489
|
+
return normalized && normalized.length > 0 ? normalized : null;
|
|
490
|
+
}
|
|
491
|
+
function extractUserMessageText(content) {
|
|
492
|
+
if (!Array.isArray(content))
|
|
493
|
+
return "";
|
|
494
|
+
return content
|
|
495
|
+
.map((entry) => {
|
|
496
|
+
if (!entry || typeof entry !== "object")
|
|
497
|
+
return undefined;
|
|
498
|
+
const value = entry.text;
|
|
499
|
+
return typeof value === "string" ? value : undefined;
|
|
500
|
+
})
|
|
501
|
+
.filter((value) => Boolean(value))
|
|
502
|
+
.join("\n\n");
|
|
503
|
+
}
|
|
353
504
|
// ─── Feed Events to Timeline Entries ──────────────────────────────
|
|
354
505
|
function feedEventsToEntries(feedEvents) {
|
|
355
506
|
const entries = [];
|
|
@@ -76,6 +76,7 @@ function buildCompactTimelineRows(entries) {
|
|
|
76
76
|
}
|
|
77
77
|
for (const run of runs.values()) {
|
|
78
78
|
const status = resolveCompactRunStatus(run.run, run.items);
|
|
79
|
+
const verboseItems = status === "running" ? selectVerboseItems(run.items) : run.items;
|
|
79
80
|
rows.push({
|
|
80
81
|
id: run.id,
|
|
81
82
|
kind: "run",
|
|
@@ -83,7 +84,7 @@ function buildCompactTimelineRows(entries) {
|
|
|
83
84
|
finalized: status !== "running",
|
|
84
85
|
run: { ...run.run, status, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
|
|
85
86
|
details: summarizeRunDetails(run.items),
|
|
86
|
-
items:
|
|
87
|
+
items: verboseItems.map((item) => ({ at: run.at, item })),
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
rows.sort((left, right) => {
|
|
@@ -167,6 +168,25 @@ function summarizeRunDetails(items) {
|
|
|
167
168
|
}
|
|
168
169
|
return dedupeDetails(details).slice(0, 3);
|
|
169
170
|
}
|
|
171
|
+
function selectVerboseItems(items) {
|
|
172
|
+
const latestAssistantMessage = findLatest(items, (item) => item.type === "agentMessage" && Boolean(item.text?.trim()));
|
|
173
|
+
const latestUserMessage = !latestAssistantMessage
|
|
174
|
+
? findLatest(items, (item) => item.type === "userMessage" && Boolean(item.text?.trim()))
|
|
175
|
+
: undefined;
|
|
176
|
+
const activeCommand = findLatest(items, (item) => item.type === "commandExecution" && item.status === "inProgress");
|
|
177
|
+
const latestCommandWithOutput = findLatest(items, (item) => item.type === "commandExecution" && Boolean(item.output?.trim()));
|
|
178
|
+
const latestCommand = activeCommand
|
|
179
|
+
?? latestCommandWithOutput
|
|
180
|
+
?? findLatest(items, (item) => item.type === "commandExecution" && Boolean(item.command?.trim()));
|
|
181
|
+
const latestFileChange = findLatest(items, (item) => item.type === "fileChange" && Array.isArray(item.changes) && item.changes.length > 0);
|
|
182
|
+
const latestToolCall = !latestFileChange
|
|
183
|
+
? findLatest(items, (item) => item.type === "mcpToolCall" || item.type === "dynamicToolCall")
|
|
184
|
+
: undefined;
|
|
185
|
+
const selectedIds = new Set([latestUserMessage, latestAssistantMessage, latestCommand, latestFileChange, latestToolCall]
|
|
186
|
+
.filter((item) => Boolean(item))
|
|
187
|
+
.map((item) => item.id));
|
|
188
|
+
return items.filter((item) => selectedIds.has(item.id));
|
|
189
|
+
}
|
|
170
190
|
function summarizeNarrative(input) {
|
|
171
191
|
const normalized = input
|
|
172
192
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const defaultTimerApi = {
|
|
2
|
+
setTimeout(callback, delayMs) {
|
|
3
|
+
return setTimeout(callback, delayMs);
|
|
4
|
+
},
|
|
5
|
+
clearTimeout(timer) {
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
export function showTransientStatus(controller, message, setStatus, timers, delayMs = 3_000) {
|
|
10
|
+
setStatus(message);
|
|
11
|
+
if (controller.timer !== null) {
|
|
12
|
+
timers.clearTimeout(controller.timer);
|
|
13
|
+
}
|
|
14
|
+
controller.timer = timers.setTimeout(() => {
|
|
15
|
+
controller.timer = null;
|
|
16
|
+
setStatus(null);
|
|
17
|
+
}, delayMs);
|
|
18
|
+
}
|
|
19
|
+
export function setPersistentStatus(controller, message, setStatus, timers) {
|
|
20
|
+
clearTransientStatus(controller, timers);
|
|
21
|
+
setStatus(message);
|
|
22
|
+
}
|
|
23
|
+
export function clearTransientStatus(controller, timers) {
|
|
24
|
+
if (controller.timer !== null) {
|
|
25
|
+
timers.clearTimeout(controller.timer);
|
|
26
|
+
controller.timer = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { buildDetailLines } from "./detail-rows.js";
|
|
6
|
+
import { lineToPlainText } from "./render-rich-text.js";
|
|
7
|
+
export function findLastAssistantMessage(timeline) {
|
|
8
|
+
return findLastItemField(timeline, (entry) => entry.item?.type === "agentMessage", "text");
|
|
9
|
+
}
|
|
10
|
+
export function findLastCommand(timeline) {
|
|
11
|
+
return findLastItemField(timeline, (entry) => entry.item?.type === "commandExecution", "command");
|
|
12
|
+
}
|
|
13
|
+
export function findLastCommandOutput(timeline) {
|
|
14
|
+
return findLastItemField(timeline, (entry) => entry.item?.type === "commandExecution" && Boolean(entry.item?.output?.trim()), "output");
|
|
15
|
+
}
|
|
16
|
+
export function buildWatchDetailExportText(input) {
|
|
17
|
+
const lines = buildDetailLines({
|
|
18
|
+
...input,
|
|
19
|
+
width: input.width ?? 100,
|
|
20
|
+
});
|
|
21
|
+
return `${lines.map(lineToPlainText).join("\n").trimEnd()}\n`;
|
|
22
|
+
}
|
|
23
|
+
export function writeTextToClipboard(text, stream = process.stderr) {
|
|
24
|
+
if (!stream.isTTY || text.length === 0) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const encoded = Buffer.from(text, "utf8").toString("base64");
|
|
28
|
+
stream.write(`\u001b]52;c;${encoded}\u0007`);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
export function exportWatchTextToTempFile(text, issueKey) {
|
|
32
|
+
const directory = mkdtempSync(join(tmpdir(), "patchrelay-watch-"));
|
|
33
|
+
const safeIssueKey = issueKey.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
34
|
+
const filePath = join(directory, `${safeIssueKey || "issue"}-transcript.txt`);
|
|
35
|
+
writeFileSync(filePath, text, "utf8");
|
|
36
|
+
return filePath;
|
|
37
|
+
}
|
|
38
|
+
export function openTextInPager(text, stream = process.stderr) {
|
|
39
|
+
if (!stream.isTTY) {
|
|
40
|
+
return { ok: false, reason: "interactive TTY required" };
|
|
41
|
+
}
|
|
42
|
+
const streamWithFd = stream;
|
|
43
|
+
if (typeof streamWithFd.fd !== "number") {
|
|
44
|
+
return { ok: false, reason: "TTY stream fd unavailable" };
|
|
45
|
+
}
|
|
46
|
+
const pagerCommand = process.env.PAGER?.trim() || "less -R";
|
|
47
|
+
stream.write("\u001b[?1049l");
|
|
48
|
+
try {
|
|
49
|
+
const result = spawnSync("/bin/sh", ["-lc", pagerCommand], {
|
|
50
|
+
input: text,
|
|
51
|
+
stdio: ["pipe", streamWithFd.fd, streamWithFd.fd],
|
|
52
|
+
});
|
|
53
|
+
if (result.error) {
|
|
54
|
+
return { ok: false, reason: result.error.message };
|
|
55
|
+
}
|
|
56
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
57
|
+
return { ok: false, reason: `${pagerCommand} exited with status ${result.status}` };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
stream.write("\u001b[?1049h\u001b[2J\u001b[H");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function findLastItemField(timeline, predicate, field) {
|
|
66
|
+
for (let index = timeline.length - 1; index >= 0; index -= 1) {
|
|
67
|
+
const entry = timeline[index];
|
|
68
|
+
if (!predicate(entry))
|
|
69
|
+
continue;
|
|
70
|
+
const value = entry.item?.[field];
|
|
71
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reconcileTimelineFromRehydration, appendFeedToTimeline, appendCodexItemToTimeline, completeCodexItemInTimeline, appendDeltaToTimelineItem, } from "./timeline-builder.js";
|
|
2
2
|
// ─── Array size caps (prevent OOM) ───────────────────────────────
|
|
3
3
|
const MAX_TIMELINE_ENTRIES = 2000;
|
|
4
4
|
const MAX_RAW_FEED_EVENTS = 2000;
|
|
5
|
-
const MAX_FEED_EVENTS = 1000;
|
|
6
5
|
function capArray(arr, max) {
|
|
7
6
|
return arr.length > max ? arr.slice(arr.length - max) : arr;
|
|
8
7
|
}
|
|
@@ -32,7 +31,6 @@ export const initialWatchState = {
|
|
|
32
31
|
filter: "non-done",
|
|
33
32
|
follow: true,
|
|
34
33
|
...DETAIL_INITIAL,
|
|
35
|
-
feedEvents: [],
|
|
36
34
|
};
|
|
37
35
|
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
38
36
|
function effectiveSessionState(issue) {
|
|
@@ -216,7 +214,7 @@ export function watchReducer(state, action) {
|
|
|
216
214
|
};
|
|
217
215
|
}
|
|
218
216
|
case "timeline-rehydrate": {
|
|
219
|
-
const timeline =
|
|
217
|
+
const timeline = reconcileTimelineFromRehydration(state.timeline, action.runs, action.feedEvents, action.liveThread, action.activeRunId);
|
|
220
218
|
const activeRun = action.runs.find((r) => r.id === action.activeRunId);
|
|
221
219
|
return {
|
|
222
220
|
...state,
|
|
@@ -243,14 +241,6 @@ export function watchReducer(state, action) {
|
|
|
243
241
|
...state,
|
|
244
242
|
...detailStateForPosition(state, maxDetailScrollOffset(state.detailContentRows, state.detailViewportRows), true),
|
|
245
243
|
};
|
|
246
|
-
case "enter-feed":
|
|
247
|
-
return { ...state, view: "feed", activeDetailKey: null, ...DETAIL_INITIAL };
|
|
248
|
-
case "exit-feed":
|
|
249
|
-
return { ...state, view: "list" };
|
|
250
|
-
case "feed-snapshot":
|
|
251
|
-
return { ...state, feedEvents: action.events };
|
|
252
|
-
case "feed-new-event":
|
|
253
|
-
return { ...state, feedEvents: capArray([...state.feedEvents, action.event], MAX_FEED_EVENTS) };
|
|
254
244
|
case "switch-detail-tab":
|
|
255
245
|
return { ...state, follow: true, ...DETAIL_INITIAL, detailTab: action.tab };
|
|
256
246
|
default:
|
package/package.json
CHANGED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
function cleanCommand(raw) {
|
|
4
|
-
const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
|
|
5
|
-
if (bashMatch?.[1])
|
|
6
|
-
return bashMatch[1];
|
|
7
|
-
const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
|
|
8
|
-
if (bashMatch2?.[1])
|
|
9
|
-
return bashMatch2[1];
|
|
10
|
-
return raw;
|
|
11
|
-
}
|
|
12
|
-
function summarizeFileChange(item) {
|
|
13
|
-
const count = item.changes?.length ?? 0;
|
|
14
|
-
return `updated ${count} file${count === 1 ? "" : "s"}`;
|
|
15
|
-
}
|
|
16
|
-
function summarizeToolCall(item) {
|
|
17
|
-
return `used ${item.toolName ?? item.type}`;
|
|
18
|
-
}
|
|
19
|
-
function summarizeText(item) {
|
|
20
|
-
return (item.text ?? "").replace(/\s+/g, " ").trim();
|
|
21
|
-
}
|
|
22
|
-
function itemPrefix(item) {
|
|
23
|
-
if (item.type === "commandExecution")
|
|
24
|
-
return "$ ";
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
function formatItemDuration(ms) {
|
|
28
|
-
if (ms === undefined || ms === null)
|
|
29
|
-
return "";
|
|
30
|
-
const seconds = Math.floor(ms / 1000);
|
|
31
|
-
if (seconds < 1)
|
|
32
|
-
return "";
|
|
33
|
-
if (seconds < 60)
|
|
34
|
-
return ` ${seconds}s`;
|
|
35
|
-
const minutes = Math.floor(seconds / 60);
|
|
36
|
-
return ` ${minutes}m`;
|
|
37
|
-
}
|
|
38
|
-
function itemText(item) {
|
|
39
|
-
switch (item.type) {
|
|
40
|
-
case "agentMessage":
|
|
41
|
-
case "plan":
|
|
42
|
-
case "reasoning":
|
|
43
|
-
return summarizeText(item);
|
|
44
|
-
case "commandExecution": {
|
|
45
|
-
const cmd = cleanCommand(item.command ?? "?");
|
|
46
|
-
const exit = item.exitCode !== undefined && item.exitCode !== null && item.exitCode !== 0
|
|
47
|
-
? ` exit ${item.exitCode}` : "";
|
|
48
|
-
const dur = formatItemDuration(item.durationMs);
|
|
49
|
-
return `${cmd}${exit}${dur}`;
|
|
50
|
-
}
|
|
51
|
-
case "fileChange":
|
|
52
|
-
return summarizeFileChange(item);
|
|
53
|
-
case "mcpToolCall":
|
|
54
|
-
case "dynamicToolCall": {
|
|
55
|
-
const dur = formatItemDuration(item.durationMs);
|
|
56
|
-
return `${summarizeToolCall(item)}${dur}`;
|
|
57
|
-
}
|
|
58
|
-
case "userMessage":
|
|
59
|
-
return `you: ${summarizeText(item)}`;
|
|
60
|
-
default:
|
|
61
|
-
return item.text ? summarizeText(item) : item.type;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function itemColor(item) {
|
|
65
|
-
if (item.status === "failed" || item.status === "declined")
|
|
66
|
-
return "red";
|
|
67
|
-
if (item.status === "inProgress")
|
|
68
|
-
return "yellow";
|
|
69
|
-
if (item.type === "userMessage")
|
|
70
|
-
return "yellow";
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
export function ItemLine({ item }) {
|
|
74
|
-
const text = itemText(item);
|
|
75
|
-
if (!text) {
|
|
76
|
-
return _jsx(_Fragment, {});
|
|
77
|
-
}
|
|
78
|
-
const color = itemColor(item);
|
|
79
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { wrap: "wrap", bold: item.type === "agentMessage", ...(color ? { color } : {}), children: [itemPrefix(item), text] }), item.output && item.status === "inProgress" && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: item.output.split("\n").filter(Boolean).at(-1) ?? "" }) }))] }));
|
|
80
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
import { Box, Static, Text, useStdout } from "ink";
|
|
4
|
-
import { buildTimelineRows } from "./timeline-presentation.js";
|
|
5
|
-
import { TimelineRow } from "./TimelineRow.js";
|
|
6
|
-
const ACTIVE_TAIL = 8;
|
|
7
|
-
export function Timeline({ entries, follow }) {
|
|
8
|
-
const { stdout } = useStdout();
|
|
9
|
-
const rows = stdout?.rows ?? 24;
|
|
10
|
-
const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
|
|
11
|
-
const displayRows = useMemo(() => buildTimelineRows(entries), [entries]);
|
|
12
|
-
// Always cap the rendered entries to prevent OOM/WASM crashes.
|
|
13
|
-
// In follow mode: older entries go to Static (terminal scrollback).
|
|
14
|
-
// Without follow: show last maxActive entries only.
|
|
15
|
-
const splitIndex = Math.max(0, displayRows.length - maxActive);
|
|
16
|
-
const finalized = follow ? displayRows.slice(0, splitIndex) : [];
|
|
17
|
-
const active = displayRows.slice(splitIndex);
|
|
18
|
-
if (displayRows.length === 0) {
|
|
19
|
-
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
|
20
|
-
}
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
|
|
22
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { ItemLine } from "./ItemLine.js";
|
|
4
|
-
function formatDuration(startedAt, endedAt) {
|
|
5
|
-
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
6
|
-
const seconds = Math.floor(ms / 1000);
|
|
7
|
-
if (seconds < 60)
|
|
8
|
-
return `${seconds}s`;
|
|
9
|
-
const minutes = Math.floor(seconds / 60);
|
|
10
|
-
const s = seconds % 60;
|
|
11
|
-
return `${minutes}m ${String(s).padStart(2, "0")}s`;
|
|
12
|
-
}
|
|
13
|
-
const CHECK_SYMBOLS = { passed: "\u2713", failed: "\u2717", pending: "\u25cf" };
|
|
14
|
-
const CHECK_COLORS = { passed: "green", failed: "red", pending: "yellow" };
|
|
15
|
-
const RUN_LABELS = {
|
|
16
|
-
implementation: "implement",
|
|
17
|
-
ci_repair: "ci fix",
|
|
18
|
-
review_fix: "review fix",
|
|
19
|
-
branch_upkeep: "branch upkeep",
|
|
20
|
-
queue_repair: "merge fix",
|
|
21
|
-
};
|
|
22
|
-
function runDotColor(status) {
|
|
23
|
-
if (status === "completed")
|
|
24
|
-
return "green";
|
|
25
|
-
if (status === "failed")
|
|
26
|
-
return "red";
|
|
27
|
-
if (status === "released")
|
|
28
|
-
return "magenta";
|
|
29
|
-
if (status === "running")
|
|
30
|
-
return "yellow";
|
|
31
|
-
return "white";
|
|
32
|
-
}
|
|
33
|
-
function detailColor(detail) {
|
|
34
|
-
if (detail.tone === "command")
|
|
35
|
-
return "white";
|
|
36
|
-
if (detail.tone === "user")
|
|
37
|
-
return "yellow";
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
function detailPrefix(detail) {
|
|
41
|
-
if (detail.tone === "command")
|
|
42
|
-
return "$ ";
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
function FeedRow({ entry }) {
|
|
46
|
-
const label = entry.feed.status ?? entry.feed.feedKind;
|
|
47
|
-
const repeatSuffix = entry.repeatCount && entry.repeatCount > 1 ? ` \u00d7${entry.repeatCount}` : "";
|
|
48
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u25cf" }), _jsx(Text, { color: "cyan", children: ` ${label}` }), _jsx(Text, { dimColor: true, children: ` ${entry.feed.summary}${repeatSuffix}` })] }));
|
|
49
|
-
}
|
|
50
|
-
function RunRow({ entry, }) {
|
|
51
|
-
const run = entry.run;
|
|
52
|
-
const dotColor = runDotColor(run.status);
|
|
53
|
-
const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
|
|
54
|
-
const showItems = entry.items.length > 0;
|
|
55
|
-
const showDetails = !showItems && entry.details.length > 0;
|
|
56
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { bold: true, color: "yellow", children: ` ${RUN_LABELS[run.runType] ?? run.runType}` }), _jsx(Text, { bold: true, color: dotColor, children: ` ${run.status}` }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), showItems && entry.items.map((itemEntry, index) => (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: itemEntry.item }) }, `${entry.id}-item-${index}`))), showDetails && entry.details.map((detail, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : { dimColor: true }), bold: detail.tone === "message", children: [detailPrefix(detail), detail.text] }) }, `${entry.id}-detail-${index}`)))] }));
|
|
57
|
-
}
|
|
58
|
-
function ItemRow({ entry, }) {
|
|
59
|
-
return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: entry.item }) }));
|
|
60
|
-
}
|
|
61
|
-
function CIChecksRow({ entry }) {
|
|
62
|
-
const ci = entry.ciChecks;
|
|
63
|
-
const dotColor = CHECK_COLORS[ci.overall] ?? "white";
|
|
64
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { color: dotColor, bold: true, children: ` checks` }), _jsx(Text, { children: ` ` }), ci.checks.map((check, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { children: ` ` }) : null, _jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsx(Text, { dimColor: true, children: ` ${check.name}` })] }, `c-${i}`)))] }));
|
|
65
|
-
}
|
|
66
|
-
export function TimelineRow({ entry }) {
|
|
67
|
-
switch (entry.kind) {
|
|
68
|
-
case "feed":
|
|
69
|
-
return _jsx(FeedRow, { entry: entry });
|
|
70
|
-
case "run":
|
|
71
|
-
return _jsx(RunRow, { entry: entry });
|
|
72
|
-
case "item":
|
|
73
|
-
return _jsx(ItemRow, { entry: entry });
|
|
74
|
-
case "ci-checks":
|
|
75
|
-
return _jsx(CIChecksRow, { entry: entry });
|
|
76
|
-
}
|
|
77
|
-
}
|