patchrelay 0.36.18 → 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/dist/linear-agent-session-client.js +109 -0
- package/dist/linear-progress-reporter.js +185 -0
- package/dist/linear-session-sync.js +23 -519
- package/dist/linear-status-comment-sync.js +152 -0
- package/dist/linear-workflow-state-sync.js +103 -0
- package/dist/no-pr-completion-check.js +199 -0
- package/dist/operator-retry-event.js +58 -0
- package/dist/run-finalizer.js +72 -237
- package/dist/service-issue-actions.js +164 -0
- package/dist/service-startup-recovery.js +104 -0
- package/dist/service.js +15 -556
- package/dist/tracked-issue-list-query.js +259 -0
- 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
|
+
}
|