patchrelay 0.36.19 → 0.37.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/cluster-health.js +25 -22
- package/dist/cli/data.js +1 -0
- package/dist/cli/formatters/text.js +5 -1
- 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/IssueRow.js +4 -3
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +5 -25
- package/dist/cli/watch/detail-status.js +38 -0
- package/dist/cli/watch/layout-measure.js +7 -0
- package/dist/cli/watch/pr-status.js +2 -1
- package/dist/cli/watch/prompt-layout.js +14 -0
- package/dist/cli/watch/state-visualization.js +5 -1
- 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/factory-state.js +1 -1
- package/dist/github-webhook-handler.js +26 -4
- package/dist/idle-reconciliation.js +19 -2
- package/dist/implementation-outcome-policy.js +3 -1
- package/dist/issue-overview-query.js +5 -0
- package/dist/linear-session-reporting.js +15 -6
- package/dist/linear-status-comment-sync.js +13 -1
- package/dist/pr-state.js +49 -0
- package/dist/service-issue-actions.js +5 -4
- package/dist/tracked-issue-list-query.js +3 -1
- package/dist/tracked-issue-projector.js +5 -0
- package/dist/waiting-reason.js +3 -2
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deriveGateCheckStatusFromRollup } from "../github-rollup.js";
|
|
2
2
|
import { ACTIVE_RUN_STATES } from "../factory-state.js";
|
|
3
|
+
import { hasOpenPr, resolveClosedPrDisposition } from "../pr-state.js";
|
|
3
4
|
const RECONCILIATION_GRACE_MS = 120_000;
|
|
4
5
|
const DOWNSTREAM_STALE_MS = 900_000;
|
|
5
6
|
export async function collectClusterHealth(config, db, runCommand) {
|
|
@@ -98,7 +99,7 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
98
99
|
}
|
|
99
100
|
checks.push(...await collectActiveOverlapFindings(snapshots, runCommand));
|
|
100
101
|
for (const snapshot of snapshots) {
|
|
101
|
-
if (!snapshot.issue.prNumber) {
|
|
102
|
+
if (!hasOpenPr(snapshot.issue.prNumber, snapshot.issue.prState)) {
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
104
105
|
const githubHealth = await evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQuillProbe, reviewQuillAttemptOwners, mergeStewardProbe);
|
|
@@ -277,19 +278,8 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
277
278
|
const latestBlockingReviewHeadSha = extractLatestBlockingReviewHeadSha(pr.latestReviews);
|
|
278
279
|
const mergeConflictDetected = pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
|
|
279
280
|
const reviewQuillAttempt = issue.issueKey ? reviewQuillAttemptOwners?.get(issue.issueKey) : undefined;
|
|
280
|
-
const ciEntry = buildCiEntry({
|
|
281
|
-
issue,
|
|
282
|
-
gateCheckStatus,
|
|
283
|
-
reviewDecision,
|
|
284
|
-
reviewRequested,
|
|
285
|
-
currentHeadSha: pr.headRefOid,
|
|
286
|
-
latestBlockingReviewHeadSha,
|
|
287
|
-
mergeConflictDetected,
|
|
288
|
-
reviewQuillAttempt,
|
|
289
|
-
});
|
|
290
281
|
if (pr.state === "MERGED" && issue.factoryState !== "done" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
291
282
|
return {
|
|
292
|
-
ciEntry,
|
|
293
283
|
finding: {
|
|
294
284
|
status: "fail",
|
|
295
285
|
scope: "github:reconcile",
|
|
@@ -297,16 +287,29 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
297
287
|
},
|
|
298
288
|
};
|
|
299
289
|
}
|
|
300
|
-
if (pr.state === "CLOSED"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
290
|
+
if (pr.state === "CLOSED") {
|
|
291
|
+
const closedPrDisposition = resolveClosedPrDisposition(issue);
|
|
292
|
+
if (closedPrDisposition === "redelegate" && issue.factoryState !== "delegated" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
293
|
+
return {
|
|
294
|
+
finding: {
|
|
295
|
+
status: "fail",
|
|
296
|
+
scope: "github:reconcile",
|
|
297
|
+
message: "PR is closed but unfinished work has not been re-delegated",
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {};
|
|
309
302
|
}
|
|
303
|
+
const ciEntry = buildCiEntry({
|
|
304
|
+
issue,
|
|
305
|
+
gateCheckStatus,
|
|
306
|
+
reviewDecision,
|
|
307
|
+
reviewRequested,
|
|
308
|
+
currentHeadSha: pr.headRefOid,
|
|
309
|
+
latestBlockingReviewHeadSha,
|
|
310
|
+
mergeConflictDetected,
|
|
311
|
+
reviewQuillAttempt,
|
|
312
|
+
});
|
|
310
313
|
if (gateCheckStatus === "failure" && issue.factoryState !== "repairing_ci" && issue.activeRunId === undefined && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
311
314
|
return {
|
|
312
315
|
ciEntry,
|
|
@@ -521,7 +524,7 @@ function needsReviewAutomation(issue) {
|
|
|
521
524
|
if (issue.factoryState === "awaiting_queue" || issue.factoryState === "done") {
|
|
522
525
|
return false;
|
|
523
526
|
}
|
|
524
|
-
return issue.prNumber
|
|
527
|
+
return hasOpenPr(issue.prNumber, issue.prState);
|
|
525
528
|
}
|
|
526
529
|
async function collectReviewQuillAttemptOwners(snapshots, config, runCommand) {
|
|
527
530
|
const owners = new Map();
|
package/dist/cli/data.js
CHANGED
|
@@ -119,6 +119,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
119
119
|
...(latestReport ? { latestReport } : {}),
|
|
120
120
|
...(latestSummary ? { latestSummary } : {}),
|
|
121
121
|
...(dbIssue.prNumber ? { prNumber: dbIssue.prNumber } : {}),
|
|
122
|
+
...(dbIssue.prState ? { prState: dbIssue.prState } : {}),
|
|
122
123
|
...(dbIssue.prReviewState ? { prReviewState: dbIssue.prReviewState } : {}),
|
|
123
124
|
...((dbIssue.sessionState) ? { sessionState: dbIssue.sessionState } : {}),
|
|
124
125
|
...((dbIssue.waitingReason) ? { waitingReason: dbIssue.waitingReason } : {}),
|
|
@@ -20,7 +20,11 @@ export function formatInspect(result) {
|
|
|
20
20
|
value("Debug stage", result.issue?.factoryState),
|
|
21
21
|
result.activeRun ? value("Active run", `${result.activeRun.runType} (${result.activeRun.status})`) : undefined,
|
|
22
22
|
result.latestRun && !result.activeRun ? value("Latest run", `${result.latestRun.runType} (${result.latestRun.status})`) : undefined,
|
|
23
|
-
result.prNumber
|
|
23
|
+
result.prNumber
|
|
24
|
+
? value("PR", `#${result.prNumber}${result.prState || result.prReviewState
|
|
25
|
+
? ` [${[result.prState, result.prReviewState].filter(Boolean).join(", ")}]`
|
|
26
|
+
: ""}`)
|
|
27
|
+
: undefined,
|
|
24
28
|
result.completionCheckOutcome ? value("Completion check", result.completionCheckOutcome) : undefined,
|
|
25
29
|
result.completionCheckSummary ? value("Completion summary", truncateLine(result.completionCheckSummary)) : undefined,
|
|
26
30
|
result.completionCheckQuestion ? value("Question", truncateLine(result.completionCheckQuestion)) : undefined,
|
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
3
4
|
import { summarizeIssueStatusNote } from "./issue-status-note.js";
|
|
4
5
|
import { relativeTime, truncate } from "./format-utils.js";
|
|
5
6
|
import { hasDisplayPrBlocker, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, isRereviewNeeded, prChecksFact, } from "./pr-status.js";
|
|
@@ -19,7 +20,7 @@ function effectiveState(issue) {
|
|
|
19
20
|
return "blocked";
|
|
20
21
|
if (issue.sessionState === "waiting_input")
|
|
21
22
|
return "awaiting_input";
|
|
22
|
-
if (issue.prNumber
|
|
23
|
+
if (hasOpenPr(issue.prNumber, issue.prState))
|
|
23
24
|
return issue.factoryState;
|
|
24
25
|
if (issue.readyForExecution && !issue.activeRunType && !hasDisplayPrBlocker(issue))
|
|
25
26
|
return "ready";
|
|
@@ -94,7 +95,7 @@ function buildFacts(issue, selected) {
|
|
|
94
95
|
else if (isChangesRequestedReviewState(issue.prReviewState)) {
|
|
95
96
|
facts.push({ text: "changes requested", color: "yellow" });
|
|
96
97
|
}
|
|
97
|
-
else if (issue.prNumber
|
|
98
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)
|
|
98
99
|
&& (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && !TERMINAL_STATES.has(effectiveState(issue))))) {
|
|
99
100
|
facts.push({ text: "awaiting review", color: "yellow" });
|
|
100
101
|
}
|
|
@@ -146,7 +147,7 @@ function blockerText(issue) {
|
|
|
146
147
|
return "Awaiting re-review after requested changes";
|
|
147
148
|
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
148
149
|
return "Review changes requested";
|
|
149
|
-
if (issue.prNumber
|
|
150
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && isAwaitingReviewState(issue.prReviewState))
|
|
150
151
|
return "Awaiting review";
|
|
151
152
|
return null;
|
|
152
153
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
3
4
|
import { computeAggregates } from "./watch-state.js";
|
|
4
5
|
import { FreshnessBadge } from "./FreshnessBadge.js";
|
|
5
6
|
const FILTER_LABELS = {
|
|
@@ -11,7 +12,7 @@ export function StatusBar({ issues, totalCount, filter, connected, lastServerMes
|
|
|
11
12
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
12
13
|
const aggregateSource = filter === "all" ? allIssues : issues;
|
|
13
14
|
const agg = computeAggregates(aggregateSource);
|
|
14
|
-
const withPr = aggregateSource.filter((i) => i.prNumber
|
|
15
|
+
const withPr = aggregateSource.filter((i) => hasOpenPr(i.prNumber, i.prState)).length;
|
|
15
16
|
const waitingInput = aggregateSource.filter((i) => i.sessionState === "waiting_input" || i.factoryState === "awaiting_input").length;
|
|
16
17
|
const intervention = aggregateSource.filter((i) => i.sessionState === "failed" || i.factoryState === "failed" || i.factoryState === "escalated").length;
|
|
17
18
|
const running = aggregateSource.filter((i) => i.sessionState === "running").length;
|