patchrelay 0.35.11 → 0.35.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -9
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +0 -1
- package/dist/cli/commands/issues.js +2 -56
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/data.js +110 -47
- package/dist/cli/formatters/text.js +6 -90
- package/dist/cli/help.js +3 -8
- package/dist/cli/index.js +0 -48
- package/dist/cli/operator-client.js +0 -82
- package/dist/cli/watch/App.js +1 -12
- package/dist/cli/watch/HelpBar.js +2 -2
- package/dist/cli/watch/IssueDetailView.js +57 -26
- package/dist/cli/watch/IssueRow.js +71 -27
- package/dist/cli/watch/StatusBar.js +7 -4
- package/dist/cli/watch/state-visualization.js +48 -23
- package/dist/cli/watch/timeline-builder.js +2 -1
- package/dist/cli/watch/use-detail-stream.js +10 -104
- package/dist/cli/watch/use-watch-stream.js +11 -102
- package/dist/cli/watch/watch-state.js +18 -50
- package/dist/codex-thread-utils.js +3 -0
- package/dist/db/migrations.js +239 -2
- package/dist/db.js +628 -39
- package/dist/github-app-token.js +7 -0
- package/dist/github-failure-context.js +44 -1
- package/dist/github-rollup.js +47 -0
- package/dist/github-webhook-handler.js +248 -51
- package/dist/github-webhooks.js +5 -0
- package/dist/http.js +12 -264
- package/dist/idle-reconciliation.js +268 -76
- package/dist/issue-query-service.js +221 -129
- package/dist/issue-session-events.js +151 -0
- package/dist/issue-session.js +99 -0
- package/dist/linear-client.js +39 -25
- package/dist/linear-session-reporting.js +12 -0
- package/dist/linear-session-sync.js +253 -24
- package/dist/linear-workflow.js +33 -0
- package/dist/merge-queue-protocol.js +0 -51
- package/dist/preflight.js +1 -4
- package/dist/queue-health-monitor.js +11 -7
- package/dist/run-orchestrator.js +1295 -146
- package/dist/run-reporting.js +5 -3
- package/dist/service.js +279 -102
- package/dist/status-note.js +56 -0
- package/dist/waiting-reason.js +65 -0
- package/dist/webhook-handler.js +270 -79
- package/package.json +1 -1
- package/dist/cli/commands/feed.js +0 -60
- package/dist/cli/watch/FeedView.js +0 -28
- package/dist/cli/watch/use-feed-stream.js +0 -92
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { getBuildInfo } from "../build-info.js";
|
|
3
3
|
import { assertKnownFlags, hasHelpFlag, parseArgs, resolveCommand } from "./args.js";
|
|
4
|
-
import { handleFeedCommand } from "./commands/feed.js";
|
|
5
4
|
import { handleIssueCommand, } from "./commands/issues.js";
|
|
6
5
|
import { handleLinearCommand } from "./commands/linear.js";
|
|
7
6
|
import { handleRepoCommand } from "./commands/repo.js";
|
|
@@ -19,7 +18,6 @@ function getCommandConfigProfile(command) {
|
|
|
19
18
|
case "service":
|
|
20
19
|
return "doctor";
|
|
21
20
|
case "linear":
|
|
22
|
-
case "feed":
|
|
23
21
|
case "dashboard":
|
|
24
22
|
return "operator_cli";
|
|
25
23
|
case "repo":
|
|
@@ -133,9 +131,6 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
133
131
|
}
|
|
134
132
|
assertKnownFlags(parsed, "service", []);
|
|
135
133
|
return;
|
|
136
|
-
case "feed":
|
|
137
|
-
assertKnownFlags(parsed, command, ["follow", "limit", "issue", "repo", "kind", "stage", "status", "workflow", "json"]);
|
|
138
|
-
return;
|
|
139
134
|
case "dashboard":
|
|
140
135
|
assertKnownFlags(parsed, command, ["issue"]);
|
|
141
136
|
return;
|
|
@@ -323,21 +318,6 @@ export async function runCli(argv, options) {
|
|
|
323
318
|
writeOutput(stderr, `${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.\n`);
|
|
324
319
|
return 1;
|
|
325
320
|
}
|
|
326
|
-
if (command === "feed") {
|
|
327
|
-
const operatorData = parsed.flags.get("follow") === true
|
|
328
|
-
? await ensureFeedFollowDataAccess(data, config)
|
|
329
|
-
: await ensureFeedListDataAccess(data, config);
|
|
330
|
-
if (!data) {
|
|
331
|
-
data = operatorData;
|
|
332
|
-
ownsData = true;
|
|
333
|
-
}
|
|
334
|
-
return await handleFeedCommand({
|
|
335
|
-
parsed,
|
|
336
|
-
json,
|
|
337
|
-
stdout,
|
|
338
|
-
data: operatorData,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
321
|
if (command === "dashboard") {
|
|
342
322
|
const { handleWatchCommand } = await import("./commands/watch.js");
|
|
343
323
|
return await handleWatchCommand({ config, parsed });
|
|
@@ -362,10 +342,6 @@ async function createCliDataAccess(config) {
|
|
|
362
342
|
const { CliDataAccess } = await import("./data.js");
|
|
363
343
|
return new CliDataAccess(config);
|
|
364
344
|
}
|
|
365
|
-
async function createCliOperatorDataAccess(config) {
|
|
366
|
-
const { CliOperatorApiClient } = await import("./operator-client.js");
|
|
367
|
-
return new CliOperatorApiClient(config);
|
|
368
|
-
}
|
|
369
345
|
async function ensureIssueDataAccess(data, config) {
|
|
370
346
|
if (data) {
|
|
371
347
|
if (isIssueDataAccess(data)) {
|
|
@@ -378,27 +354,3 @@ async function ensureIssueDataAccess(data, config) {
|
|
|
378
354
|
function isIssueDataAccess(data) {
|
|
379
355
|
return !!data && typeof data === "object" && "inspect" in data && typeof data.inspect === "function";
|
|
380
356
|
}
|
|
381
|
-
async function ensureFeedListDataAccess(data, config) {
|
|
382
|
-
if (data) {
|
|
383
|
-
if (hasFeedListDataAccess(data)) {
|
|
384
|
-
return data;
|
|
385
|
-
}
|
|
386
|
-
throw new Error("The feed command requires listOperatorFeed() data access.");
|
|
387
|
-
}
|
|
388
|
-
return await createCliOperatorDataAccess(config);
|
|
389
|
-
}
|
|
390
|
-
async function ensureFeedFollowDataAccess(data, config) {
|
|
391
|
-
if (data) {
|
|
392
|
-
if (hasFeedFollowDataAccess(data)) {
|
|
393
|
-
return data;
|
|
394
|
-
}
|
|
395
|
-
throw new Error("The feed --follow command requires followOperatorFeed() data access.");
|
|
396
|
-
}
|
|
397
|
-
return await createCliOperatorDataAccess(config);
|
|
398
|
-
}
|
|
399
|
-
function hasFeedListDataAccess(data) {
|
|
400
|
-
return !!data && typeof data === "object" && "listOperatorFeed" in data && typeof data.listOperatorFeed === "function";
|
|
401
|
-
}
|
|
402
|
-
function hasFeedFollowDataAccess(data) {
|
|
403
|
-
return !!data && typeof data === "object" && "followOperatorFeed" in data && typeof data.followOperatorFeed === "function";
|
|
404
|
-
}
|
|
@@ -29,88 +29,6 @@ export class CliOperatorApiClient {
|
|
|
29
29
|
async disconnectLinearWorkspace(workspace) {
|
|
30
30
|
return await this.requestJson(`/api/linear/workspaces/${encodeURIComponent(workspace)}`, undefined, { method: "DELETE" });
|
|
31
31
|
}
|
|
32
|
-
async listOperatorFeed(options) {
|
|
33
|
-
return await this.requestJson("/api/feed", {
|
|
34
|
-
...(options?.limit && options.limit > 0 ? { limit: String(options.limit) } : {}),
|
|
35
|
-
...(options?.issueKey ? { issue: options.issueKey } : {}),
|
|
36
|
-
...(options?.projectId ? { project: options.projectId } : {}),
|
|
37
|
-
...(options?.kind ? { kind: options.kind } : {}),
|
|
38
|
-
...(options?.stage ? { stage: options.stage } : {}),
|
|
39
|
-
...(options?.status ? { status: options.status } : {}),
|
|
40
|
-
...(options?.workflowId ? { workflow: options.workflowId } : {}),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
async followOperatorFeed(onEvent, options) {
|
|
44
|
-
const url = new URL("/api/feed", this.getOperatorBaseUrl());
|
|
45
|
-
url.searchParams.set("follow", "1");
|
|
46
|
-
if (options?.limit && options.limit > 0) {
|
|
47
|
-
url.searchParams.set("limit", String(options.limit));
|
|
48
|
-
}
|
|
49
|
-
if (options?.issueKey) {
|
|
50
|
-
url.searchParams.set("issue", options.issueKey);
|
|
51
|
-
}
|
|
52
|
-
if (options?.projectId) {
|
|
53
|
-
url.searchParams.set("project", options.projectId);
|
|
54
|
-
}
|
|
55
|
-
if (options?.kind) {
|
|
56
|
-
url.searchParams.set("kind", options.kind);
|
|
57
|
-
}
|
|
58
|
-
if (options?.stage) {
|
|
59
|
-
url.searchParams.set("stage", options.stage);
|
|
60
|
-
}
|
|
61
|
-
if (options?.status) {
|
|
62
|
-
url.searchParams.set("status", options.status);
|
|
63
|
-
}
|
|
64
|
-
if (options?.workflowId) {
|
|
65
|
-
url.searchParams.set("workflow", options.workflowId);
|
|
66
|
-
}
|
|
67
|
-
const response = await fetch(url, {
|
|
68
|
-
method: "GET",
|
|
69
|
-
headers: {
|
|
70
|
-
accept: "text/event-stream",
|
|
71
|
-
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
if (!response.ok || !response.body) {
|
|
75
|
-
const body = await response.text().catch(() => "");
|
|
76
|
-
const message = this.readErrorMessage(body);
|
|
77
|
-
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
78
|
-
}
|
|
79
|
-
const reader = response.body.getReader();
|
|
80
|
-
const decoder = new TextDecoder();
|
|
81
|
-
let buffer = "";
|
|
82
|
-
let dataLines = [];
|
|
83
|
-
while (true) {
|
|
84
|
-
const { done, value } = await reader.read();
|
|
85
|
-
if (done) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
buffer += decoder.decode(value, { stream: true });
|
|
89
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
90
|
-
while (newlineIndex !== -1) {
|
|
91
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
92
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
93
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
94
|
-
if (!line) {
|
|
95
|
-
if (dataLines.length > 0) {
|
|
96
|
-
const parsed = JSON.parse(dataLines.join("\n"));
|
|
97
|
-
onEvent(parsed);
|
|
98
|
-
dataLines = [];
|
|
99
|
-
}
|
|
100
|
-
newlineIndex = buffer.indexOf("\n");
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (line.startsWith(":")) {
|
|
104
|
-
newlineIndex = buffer.indexOf("\n");
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (line.startsWith("data:")) {
|
|
108
|
-
dataLines.push(line.slice(5).trimStart());
|
|
109
|
-
}
|
|
110
|
-
newlineIndex = buffer.indexOf("\n");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
32
|
getOperatorBaseUrl() {
|
|
115
33
|
const host = this.normalizeLocalHost(this.config.server.bind);
|
|
116
34
|
return `http://${host}:${this.config.server.port}/`;
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -4,10 +4,8 @@ import { Box, Text, useApp, useInput } 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
|
-
import { useFeedStream } from "./use-feed-stream.js";
|
|
8
7
|
import { IssueListView } from "./IssueListView.js";
|
|
9
8
|
import { IssueDetailView } from "./IssueDetailView.js";
|
|
10
|
-
import { FeedView } from "./FeedView.js";
|
|
11
9
|
async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
12
10
|
const headers = { "content-type": "application/json" };
|
|
13
11
|
if (bearerToken)
|
|
@@ -71,7 +69,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
71
69
|
const [frozen, setFrozen] = useState(false);
|
|
72
70
|
useWatchStream({ baseUrl, bearerToken, dispatch, active: !frozen });
|
|
73
71
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch, active: !frozen });
|
|
74
|
-
useFeedStream({ baseUrl, bearerToken, active: state.view === "feed" && !frozen, dispatch });
|
|
75
72
|
const [promptMode, setPromptMode] = useState(false);
|
|
76
73
|
const [promptBuffer, setPromptBuffer] = useState("");
|
|
77
74
|
const handleRetry = useCallback(() => {
|
|
@@ -154,9 +151,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
154
151
|
else if (key.tab) {
|
|
155
152
|
dispatch({ type: "cycle-filter" });
|
|
156
153
|
}
|
|
157
|
-
else if (input === "F" || input === "f") {
|
|
158
|
-
dispatch({ type: "enter-feed" });
|
|
159
|
-
}
|
|
160
154
|
}
|
|
161
155
|
else if (state.view === "detail") {
|
|
162
156
|
if (key.escape || key.backspace || key.delete) {
|
|
@@ -193,11 +187,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
193
187
|
dispatch({ type: "detail-navigate", direction: "prev", filtered });
|
|
194
188
|
}
|
|
195
189
|
}
|
|
196
|
-
else if (state.view === "feed") {
|
|
197
|
-
if (key.escape || key.backspace || key.delete) {
|
|
198
|
-
dispatch({ type: "exit-feed" });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
190
|
});
|
|
202
|
-
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, 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 }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) :
|
|
191
|
+
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, 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 }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : null }));
|
|
203
192
|
}
|
|
@@ -9,10 +9,10 @@ export function HelpBar({ view, follow, detailTab }) {
|
|
|
9
9
|
.join(" ");
|
|
10
10
|
}
|
|
11
11
|
else if (view === "feed") {
|
|
12
|
-
text = "";
|
|
12
|
+
text = "Legacy feed view Esc: back";
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
text = "
|
|
15
|
+
text = "Enter: detail Tab: filter";
|
|
16
16
|
}
|
|
17
17
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
18
18
|
}
|
|
@@ -7,9 +7,6 @@ import { buildStateHistory } from "./history-builder.js";
|
|
|
7
7
|
import { HelpBar } from "./HelpBar.js";
|
|
8
8
|
import { planStepSymbol, planStepColor } from "./plan-helpers.js";
|
|
9
9
|
import { progressBar } from "./format-utils.js";
|
|
10
|
-
import { FactoryStateGraph } from "./FactoryStateGraph.js";
|
|
11
|
-
import { QueueObservationView } from "./QueueObservationView.js";
|
|
12
|
-
import { buildPatchRelayQueueObservations, buildPatchRelayStateGraph } from "./state-visualization.js";
|
|
13
10
|
import { FreshnessBadge } from "./FreshnessBadge.js";
|
|
14
11
|
function formatTokens(n) {
|
|
15
12
|
if (n >= 1_000_000)
|
|
@@ -46,46 +43,74 @@ function formatCheckState(checkState) {
|
|
|
46
43
|
return null;
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
pr_open: { label: "PR open", color: "cyan" },
|
|
55
|
-
changes_requested: { label: "review changes", color: "yellow" },
|
|
56
|
-
repairing_ci: { label: "repairing CI", color: "yellow" },
|
|
57
|
-
awaiting_queue: { label: "queued for merge", color: "cyan" },
|
|
58
|
-
repairing_queue: { label: "repairing queue", color: "yellow" },
|
|
59
|
-
done: { label: "merged", color: "green" },
|
|
46
|
+
const SESSION_DISPLAY = {
|
|
47
|
+
idle: { label: "idle", color: "blueBright" },
|
|
48
|
+
running: { label: "running", color: "cyan" },
|
|
49
|
+
waiting_input: { label: "needs input", color: "yellow" },
|
|
50
|
+
done: { label: "done", color: "green" },
|
|
60
51
|
failed: { label: "failed", color: "red" },
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
};
|
|
53
|
+
const STAGE_DISPLAY = {
|
|
54
|
+
blocked: "blocked",
|
|
55
|
+
ready: "ready",
|
|
56
|
+
delegated: "delegated",
|
|
57
|
+
implementing: "implementing",
|
|
58
|
+
pr_open: "PR open",
|
|
59
|
+
changes_requested: "review changes",
|
|
60
|
+
repairing_ci: "repairing CI",
|
|
61
|
+
awaiting_queue: "waiting downstream",
|
|
62
|
+
repairing_queue: "repairing queue",
|
|
63
|
+
done: "merged",
|
|
64
|
+
failed: "failed",
|
|
65
|
+
escalated: "escalated",
|
|
66
|
+
awaiting_input: "needs input",
|
|
63
67
|
};
|
|
64
68
|
function effectiveState(issue) {
|
|
69
|
+
if (issue.sessionState === "done")
|
|
70
|
+
return "done";
|
|
71
|
+
if (issue.sessionState === "failed")
|
|
72
|
+
return "failed";
|
|
65
73
|
if (issue.blockedByCount > 0 && !issue.activeRunType)
|
|
66
74
|
return "blocked";
|
|
67
75
|
if (issue.readyForExecution && !issue.activeRunType)
|
|
68
76
|
return "ready";
|
|
77
|
+
if (issue.sessionState === "waiting_input")
|
|
78
|
+
return "awaiting_input";
|
|
69
79
|
return issue.factoryState;
|
|
70
80
|
}
|
|
81
|
+
function sessionDisplay(issue) {
|
|
82
|
+
const state = issue.sessionState ?? "unknown";
|
|
83
|
+
return SESSION_DISPLAY[state] ?? { label: state, color: "white" };
|
|
84
|
+
}
|
|
85
|
+
function stageDisplay(issue) {
|
|
86
|
+
const state = effectiveState(issue);
|
|
87
|
+
return STAGE_DISPLAY[state] ?? issue.factoryState;
|
|
88
|
+
}
|
|
71
89
|
function blockerText(issue, issueContext) {
|
|
90
|
+
const rereviewNeeded = issue.prReviewState === "changes_requested"
|
|
91
|
+
&& (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
92
|
+
&& !issue.activeRunType;
|
|
93
|
+
if (issue.sessionState === "waiting_input")
|
|
94
|
+
return issue.waitingReason ?? "Waiting for input";
|
|
95
|
+
if (issue.waitingReason && !issue.activeRunType)
|
|
96
|
+
return issue.waitingReason;
|
|
72
97
|
if (issue.blockedByCount > 0)
|
|
73
98
|
return `Waiting on ${issue.blockedByKeys.join(", ")}`;
|
|
74
|
-
if (issue
|
|
99
|
+
if (effectiveState(issue) === "repairing_queue")
|
|
75
100
|
return "Merge queue conflict, repairing branch";
|
|
76
|
-
if (issue
|
|
101
|
+
if (effectiveState(issue) === "repairing_ci") {
|
|
77
102
|
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "CI";
|
|
78
103
|
return `Repairing ${check}`;
|
|
79
104
|
}
|
|
80
|
-
if (issue.factoryState === "awaiting_queue")
|
|
81
|
-
return "Waiting for merge queue";
|
|
82
105
|
if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
83
106
|
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
|
|
84
107
|
return `${check} failed`;
|
|
85
108
|
}
|
|
109
|
+
if (rereviewNeeded)
|
|
110
|
+
return "Awaiting re-review after requested changes";
|
|
86
111
|
if (issue.prReviewState === "changes_requested")
|
|
87
112
|
return "Review changes requested";
|
|
88
|
-
if (issue.prNumber !== undefined && !issue.prReviewState && issue
|
|
113
|
+
if (issue.prNumber !== undefined && !issue.prReviewState && effectiveState(issue) !== "done")
|
|
89
114
|
return "Awaiting review";
|
|
90
115
|
return null;
|
|
91
116
|
}
|
|
@@ -102,7 +127,7 @@ function ElapsedTime({ startedAt }) {
|
|
|
102
127
|
}
|
|
103
128
|
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, }) {
|
|
104
129
|
if (!issue) {
|
|
105
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, {
|
|
130
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Loading issue\u2026" }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
|
|
106
131
|
}
|
|
107
132
|
const key = issue.issueKey ?? issue.projectId;
|
|
108
133
|
const meta = [];
|
|
@@ -112,19 +137,25 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
|
|
|
112
137
|
meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
|
|
113
138
|
if (issueContext?.runCount)
|
|
114
139
|
meta.push(`${issueContext.runCount} runs`);
|
|
115
|
-
const
|
|
140
|
+
const session = sessionDisplay(issue);
|
|
141
|
+
const stage = stageDisplay(issue);
|
|
116
142
|
const blocker = blockerText(issue, issueContext);
|
|
117
143
|
const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
|
|
118
|
-
const graph = useMemo(() => buildPatchRelayStateGraph(history, issue.factoryState), [history, issue.factoryState]);
|
|
119
|
-
const queueObservations = useMemo(() => buildPatchRelayQueueObservations(issue, rawFeedEvents), [issue, rawFeedEvents]);
|
|
120
144
|
// Build compact facts for the header
|
|
121
145
|
const facts = [];
|
|
146
|
+
const rereviewNeeded = issue.prReviewState === "changes_requested"
|
|
147
|
+
&& (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
148
|
+
&& !issue.activeRunType;
|
|
122
149
|
if (issue.prNumber !== undefined)
|
|
123
150
|
facts.push(`PR #${issue.prNumber}`);
|
|
124
151
|
if (issue.prReviewState === "approved")
|
|
125
152
|
facts.push("approved");
|
|
153
|
+
else if (rereviewNeeded)
|
|
154
|
+
facts.push("re-review needed");
|
|
126
155
|
else if (issue.prReviewState === "changes_requested")
|
|
127
156
|
facts.push("changes requested");
|
|
157
|
+
if (issue.waitingReason && issue.sessionState === "waiting_input")
|
|
158
|
+
facts.push(issue.waitingReason);
|
|
128
159
|
if (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
129
160
|
facts.push("checks passed");
|
|
130
161
|
else if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
@@ -134,5 +165,5 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
|
|
|
134
165
|
else if (issue.prChecksSummary?.total) {
|
|
135
166
|
facts.push(`checks ${issue.prChecksSummary.completed}/${issue.prChecksSummary.total}`);
|
|
136
167
|
}
|
|
137
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color:
|
|
168
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: session.color, children: session.label }), _jsx(Text, { dimColor: true, children: ` debug stage ${stage}` }), facts.length > 0 && _jsx(Text, { dimColor: true, children: facts.join(" \u00b7 ") }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), follow && _jsx(Text, { color: "yellow", children: "follow" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), issue.title && _jsx(Text, { children: issue.title }), blocker && _jsx(Text, { color: "yellow", children: blocker }), issue.statusNote && issue.statusNote !== blocker && (_jsx(Text, { dimColor: true, wrap: "wrap", children: issue.statusNote })), issueContext?.latestFailureSummary && (_jsxs(Text, { color: issueContext.latestFailureSource === "queue_eviction" ? "yellow" : "red", children: ["Latest failure: ", issueContext.latestFailureSummary, issueContext.latestFailureHeadSha ? ` @ ${issueContext.latestFailureHeadSha.slice(0, 8)}` : ""] })), detailTab === "timeline" ? (_jsxs(_Fragment, { children: [plan && plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Plan" }), _jsx(Text, { children: progressBar(plan.filter((s) => s.status === "completed").length, plan.length, 16) }), _jsxs(Text, { dimColor: true, children: [plan.filter((s) => s.status === "completed").length, "/", plan.length] })] }), plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`)))] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow }) })] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "PatchRelay activity history." }), _jsx(Text, { dimColor: true, children: "Runs, waits, and wake-ups are shown here in PatchRelay order." })] }), _jsx(Box, { marginTop: 1, children: _jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab }) })] }));
|
|
138
169
|
}
|
|
@@ -3,48 +3,85 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { summarizeIssueStatusNote } from "./issue-status-note.js";
|
|
4
4
|
import { relativeTime, truncate } from "./format-utils.js";
|
|
5
5
|
// ─── State display ──────────────────────────────────────────────
|
|
6
|
-
const TERMINAL_STATES = new Set(["done", "failed", "escalated"
|
|
6
|
+
const TERMINAL_STATES = new Set(["done", "failed", "escalated"]);
|
|
7
7
|
function effectiveState(issue) {
|
|
8
|
+
if (issue.sessionState === "done")
|
|
9
|
+
return "done";
|
|
10
|
+
if (issue.sessionState === "failed")
|
|
11
|
+
return "failed";
|
|
8
12
|
if (issue.blockedByCount > 0 && !issue.activeRunType)
|
|
9
13
|
return "blocked";
|
|
10
14
|
if (issue.readyForExecution && !issue.activeRunType)
|
|
11
15
|
return "ready";
|
|
16
|
+
if (issue.sessionState === "waiting_input")
|
|
17
|
+
return "awaiting_input";
|
|
12
18
|
return issue.factoryState;
|
|
13
19
|
}
|
|
14
|
-
function
|
|
20
|
+
function sessionDisplay(issue) {
|
|
21
|
+
switch (issue.sessionState) {
|
|
22
|
+
case "running":
|
|
23
|
+
return { label: "running", color: "cyan" };
|
|
24
|
+
case "idle":
|
|
25
|
+
return { label: "idle", color: "blueBright" };
|
|
26
|
+
case "waiting_input":
|
|
27
|
+
return { label: "needs input", color: "yellow" };
|
|
28
|
+
case "done":
|
|
29
|
+
return { label: "done", color: "green" };
|
|
30
|
+
case "failed":
|
|
31
|
+
return { label: "failed", color: "red" };
|
|
32
|
+
default:
|
|
33
|
+
return { label: "unknown", color: "white" };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function stageLabel(issue) {
|
|
15
37
|
const state = effectiveState(issue);
|
|
16
38
|
switch (state) {
|
|
17
|
-
case "blocked": return
|
|
18
|
-
case "ready": return
|
|
19
|
-
case "delegated": return
|
|
20
|
-
case "implementing": return
|
|
21
|
-
case "pr_open": return
|
|
22
|
-
case "changes_requested": return
|
|
23
|
-
case "repairing_ci": return
|
|
24
|
-
case "awaiting_queue": return
|
|
25
|
-
case "repairing_queue": return
|
|
26
|
-
case "done": return
|
|
27
|
-
case "failed": return
|
|
28
|
-
case "escalated": return
|
|
29
|
-
case "awaiting_input": return
|
|
30
|
-
default: return
|
|
39
|
+
case "blocked": return "blocked";
|
|
40
|
+
case "ready": return "ready";
|
|
41
|
+
case "delegated": return "delegated";
|
|
42
|
+
case "implementing": return "implementing";
|
|
43
|
+
case "pr_open": return "PR open";
|
|
44
|
+
case "changes_requested": return "review changes";
|
|
45
|
+
case "repairing_ci": return "repairing CI";
|
|
46
|
+
case "awaiting_queue": return "waiting downstream";
|
|
47
|
+
case "repairing_queue": return "repairing queue";
|
|
48
|
+
case "done": return "merged";
|
|
49
|
+
case "failed": return "failed";
|
|
50
|
+
case "escalated": return "escalated";
|
|
51
|
+
case "awaiting_input": return "needs input";
|
|
52
|
+
default: return state;
|
|
31
53
|
}
|
|
32
54
|
}
|
|
33
55
|
// ─── Context facts (what matters right now) ─────────────────────
|
|
34
|
-
function buildFacts(issue) {
|
|
56
|
+
function buildFacts(issue, selected) {
|
|
35
57
|
const facts = [];
|
|
58
|
+
const rereviewNeeded = issue.prReviewState === "changes_requested"
|
|
59
|
+
&& (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
60
|
+
&& !issue.activeRunType;
|
|
36
61
|
// PR number
|
|
37
62
|
if (issue.prNumber !== undefined) {
|
|
38
63
|
facts.push({ text: `PR #${issue.prNumber}` });
|
|
39
64
|
}
|
|
65
|
+
if (!issue.sessionState) {
|
|
66
|
+
facts.push({ text: `stage ${stageLabel(issue)}` });
|
|
67
|
+
}
|
|
68
|
+
else if (selected) {
|
|
69
|
+
facts.push({ text: `internal stage ${stageLabel(issue)}` });
|
|
70
|
+
}
|
|
71
|
+
if (issue.waitingReason && issue.sessionState === "waiting_input") {
|
|
72
|
+
facts.push({ text: issue.waitingReason, color: "yellow" });
|
|
73
|
+
}
|
|
40
74
|
// Review state — only show when it matters (not yet approved, or changes requested)
|
|
41
75
|
if (issue.prReviewState === "approved") {
|
|
42
76
|
facts.push({ text: "approved", color: "green" });
|
|
43
77
|
}
|
|
78
|
+
else if (rereviewNeeded) {
|
|
79
|
+
facts.push({ text: "re-review needed", color: "yellow" });
|
|
80
|
+
}
|
|
44
81
|
else if (issue.prReviewState === "changes_requested") {
|
|
45
82
|
facts.push({ text: "changes requested", color: "yellow" });
|
|
46
83
|
}
|
|
47
|
-
else if (issue.prNumber !== undefined && !issue.prReviewState && !TERMINAL_STATES.has(issue
|
|
84
|
+
else if (issue.prNumber !== undefined && !issue.prReviewState && !TERMINAL_STATES.has(effectiveState(issue))) {
|
|
48
85
|
facts.push({ text: "awaiting review", color: "yellow" });
|
|
49
86
|
}
|
|
50
87
|
// Check status — compact
|
|
@@ -74,20 +111,27 @@ function buildFacts(issue) {
|
|
|
74
111
|
}
|
|
75
112
|
// ─── What's blocking progress ───────────────────────────────────
|
|
76
113
|
function blockerText(issue) {
|
|
114
|
+
const rereviewNeeded = issue.prReviewState === "changes_requested"
|
|
115
|
+
&& (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
116
|
+
&& !issue.activeRunType;
|
|
117
|
+
if (issue.sessionState === "waiting_input")
|
|
118
|
+
return issue.waitingReason ?? "Waiting for input";
|
|
119
|
+
if (issue.waitingReason && !issue.activeRunType)
|
|
120
|
+
return issue.waitingReason;
|
|
77
121
|
if (issue.blockedByCount > 0)
|
|
78
122
|
return `Waiting on ${issue.blockedByKeys.join(", ")}`;
|
|
79
|
-
if (issue
|
|
123
|
+
if (effectiveState(issue) === "repairing_queue")
|
|
80
124
|
return "Merge queue conflict, repairing branch";
|
|
81
|
-
if (issue
|
|
125
|
+
if (effectiveState(issue) === "repairing_ci") {
|
|
82
126
|
const check = issue.latestFailureCheckName ?? "CI";
|
|
83
127
|
return `Repairing ${check}`;
|
|
84
128
|
}
|
|
85
|
-
if (issue.factoryState === "awaiting_queue")
|
|
86
|
-
return "Waiting for merge queue";
|
|
87
129
|
if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
88
130
|
const check = issue.latestFailureCheckName ?? "checks";
|
|
89
131
|
return `${check} failed`;
|
|
90
132
|
}
|
|
133
|
+
if (rereviewNeeded)
|
|
134
|
+
return "Awaiting re-review after requested changes";
|
|
91
135
|
if (issue.prReviewState === "changes_requested")
|
|
92
136
|
return "Review changes requested";
|
|
93
137
|
return null;
|
|
@@ -98,13 +142,13 @@ export function IssueRow({ issue, selected, titleWidth }) {
|
|
|
98
142
|
const tw = titleWidth ?? 60;
|
|
99
143
|
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
100
144
|
const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
|
|
101
|
-
const
|
|
102
|
-
const facts = buildFacts(issue);
|
|
145
|
+
const session = sessionDisplay(issue);
|
|
146
|
+
const facts = buildFacts(issue, selected);
|
|
103
147
|
const blocker = selected ? blockerText(issue) : null;
|
|
104
|
-
const isTerminal = TERMINAL_STATES.has(issue
|
|
148
|
+
const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
|
|
105
149
|
// Terminal issues: compact single line
|
|
106
150
|
if (isTerminal && !selected) {
|
|
107
|
-
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color:
|
|
151
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label })] }));
|
|
108
152
|
}
|
|
109
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color:
|
|
153
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label }), facts.length > 0 && (_jsx(Text, { dimColor: true, children: ` \u00b7 ` })), facts.map((fact, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: ` \u00b7 ` }) : null, _jsx(Text, { color: fact.color ?? "white", dimColor: !fact.color, children: fact.text })] }, i)))] }), title ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: title }) })) : null, blocker ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: blocker }) })) : null, detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null, selected && issue.factoryState && issue.sessionState ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: `Debug stage: ${stageLabel(issue)}` }) })) : null] }));
|
|
110
154
|
}
|
|
@@ -9,8 +9,11 @@ const FILTER_LABELS = {
|
|
|
9
9
|
};
|
|
10
10
|
export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, frozen, }) {
|
|
11
11
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
12
|
+
const aggregateSource = filter === "all" ? allIssues : issues;
|
|
13
|
+
const agg = computeAggregates(aggregateSource);
|
|
14
|
+
const withPr = aggregateSource.filter((i) => i.prNumber !== undefined).length;
|
|
15
|
+
const waitingInput = aggregateSource.filter((i) => i.sessionState === "waiting_input" || i.factoryState === "awaiting_input").length;
|
|
16
|
+
const running = aggregateSource.filter((i) => i.sessionState === "running").length;
|
|
17
|
+
const idle = aggregateSource.filter((i) => i.sessionState === "idle").length;
|
|
18
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), running > 0 && _jsxs(Text, { color: "cyan", children: [running, " running"] }), idle > 0 && _jsxs(Text, { color: "blueBright", children: [idle, " idle"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), waitingInput > 0 && _jsxs(Text, { color: "yellow", children: [waitingInput, " needs input"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] }), frozen && _jsx(Text, { color: "magenta", children: "frozen" })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
|
|
16
19
|
}
|