patchrelay 0.35.12 → 0.35.13
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/args.js +19 -0
- package/dist/cli/commands/issues.js +17 -1
- package/dist/cli/data.js +50 -0
- package/dist/cli/formatters/text.js +45 -0
- package/dist/cli/help.js +12 -0
- package/dist/cli/index.js +3 -10
- package/dist/cli/watch/App.js +22 -2
- package/dist/cli/watch/HelpBar.js +1 -1
- package/dist/cli/watch/IssueDetailView.js +63 -161
- package/dist/cli/watch/IssueRow.js +11 -0
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +589 -0
- package/dist/cli/watch/render-rich-text.js +226 -0
- package/dist/cli/watch/watch-state.js +111 -6
- package/dist/github-webhook-handler.js +176 -2
- package/dist/github-webhooks.js +2 -0
- package/dist/linear-session-sync.js +1 -1
- package/dist/run-orchestrator.js +75 -7
- package/dist/status-note.js +1 -1
- package/package.json +3 -2
package/dist/build-info.json
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -55,6 +55,25 @@ export function resolveCommand(parsed) {
|
|
|
55
55
|
return { command: "help", commandArgs: [] };
|
|
56
56
|
}
|
|
57
57
|
if (KNOWN_COMMANDS.has(requestedCommand)) {
|
|
58
|
+
if (requestedCommand === "attach") {
|
|
59
|
+
return { command: "repo", commandArgs: ["link", ...parsed.positionals.slice(1)] };
|
|
60
|
+
}
|
|
61
|
+
if (requestedCommand === "repos") {
|
|
62
|
+
const rest = parsed.positionals.slice(1);
|
|
63
|
+
if (rest.length === 0) {
|
|
64
|
+
return { command: "repo", commandArgs: ["list"] };
|
|
65
|
+
}
|
|
66
|
+
if (["list", "show", "link", "unlink", "sync"].includes(rest[0])) {
|
|
67
|
+
return { command: "repo", commandArgs: rest };
|
|
68
|
+
}
|
|
69
|
+
return { command: "repo", commandArgs: ["show", ...rest] };
|
|
70
|
+
}
|
|
71
|
+
if (requestedCommand === "connect") {
|
|
72
|
+
return { command: "linear", commandArgs: ["connect", ...parsed.positionals.slice(1)] };
|
|
73
|
+
}
|
|
74
|
+
if (requestedCommand === "installations") {
|
|
75
|
+
return { command: "linear", commandArgs: ["list", ...parsed.positionals.slice(1)] };
|
|
76
|
+
}
|
|
58
77
|
const command = requestedCommand === "dash" || requestedCommand === "d"
|
|
59
78
|
? "dashboard"
|
|
60
79
|
: requestedCommand;
|
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
import { getRunTypeFlag } from "../args.js";
|
|
3
3
|
import { CliUsageError } from "../errors.js";
|
|
4
4
|
import { formatJson } from "../formatters/json.js";
|
|
5
|
-
import { formatInspect, formatList, formatLive, formatOpen, formatRetry, formatWorktree } from "../formatters/text.js";
|
|
5
|
+
import { formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatWorktree } from "../formatters/text.js";
|
|
6
6
|
import { buildOpenCommand } from "../interactive.js";
|
|
7
7
|
import { writeOutput } from "../output.js";
|
|
8
8
|
export async function handleIssueCommand(params) {
|
|
@@ -34,6 +34,8 @@ export async function handleIssueCommand(params) {
|
|
|
34
34
|
return await handleWorktreeCommand(nested);
|
|
35
35
|
case "open":
|
|
36
36
|
return await handleOpenCommand(nested);
|
|
37
|
+
case "sessions":
|
|
38
|
+
return await handleSessionsCommand(nested);
|
|
37
39
|
case "retry":
|
|
38
40
|
return await handleRetryCommand(nested);
|
|
39
41
|
default:
|
|
@@ -112,6 +114,20 @@ export async function handleOpenCommand(params) {
|
|
|
112
114
|
const openCommand = buildOpenCommand(params.config, result.worktreePath, result.resumeThreadId);
|
|
113
115
|
return await params.runInteractive(openCommand.command, openCommand.args);
|
|
114
116
|
}
|
|
117
|
+
export async function handleSessionsCommand(params) {
|
|
118
|
+
const issueKey = params.commandArgs[0];
|
|
119
|
+
if (!issueKey) {
|
|
120
|
+
throw new Error("sessions requires <issueKey>.");
|
|
121
|
+
}
|
|
122
|
+
const result = params.data.sessions(issueKey);
|
|
123
|
+
if (!result) {
|
|
124
|
+
throw new Error(`Issue not found: ${issueKey}`);
|
|
125
|
+
}
|
|
126
|
+
writeOutput(params.stdout, params.json
|
|
127
|
+
? formatJson(result)
|
|
128
|
+
: formatSessionHistory(result, (threadId) => buildOpenCommand(params.config, result.worktreePath ?? "", threadId)));
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
115
131
|
export async function handleRetryCommand(params) {
|
|
116
132
|
const issueKey = params.commandArgs[0];
|
|
117
133
|
if (!issueKey) {
|
package/dist/cli/data.js
CHANGED
|
@@ -56,6 +56,23 @@ function parseObjectJson(value) {
|
|
|
56
56
|
return undefined;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
function summarizeRun(run) {
|
|
60
|
+
const summary = parseObjectJson(run.summaryJson);
|
|
61
|
+
if (typeof summary?.latestAssistantMessage === "string" && summary.latestAssistantMessage.trim()) {
|
|
62
|
+
return summary.latestAssistantMessage.trim();
|
|
63
|
+
}
|
|
64
|
+
const report = parseObjectJson(run.reportJson);
|
|
65
|
+
const assistantMessages = report?.assistantMessages;
|
|
66
|
+
if (Array.isArray(assistantMessages)) {
|
|
67
|
+
for (let index = assistantMessages.length - 1; index >= 0; index -= 1) {
|
|
68
|
+
const value = assistantMessages[index];
|
|
69
|
+
if (typeof value === "string" && value.trim()) {
|
|
70
|
+
return value.trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return run.failureReason?.trim() || undefined;
|
|
75
|
+
}
|
|
59
76
|
export class CliDataAccess extends CliOperatorApiClient {
|
|
60
77
|
config;
|
|
61
78
|
db;
|
|
@@ -192,6 +209,39 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
192
209
|
const updated = this.db.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
193
210
|
return { issue: updated, runType, ...(options?.reason ? { reason: options.reason } : {}) };
|
|
194
211
|
}
|
|
212
|
+
sessions(issueKey) {
|
|
213
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
214
|
+
if (!issue)
|
|
215
|
+
return undefined;
|
|
216
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
217
|
+
const runs = this.db.listRunsForIssue(issue.projectId, issue.linearIssueId);
|
|
218
|
+
const sessions = runs
|
|
219
|
+
.slice()
|
|
220
|
+
.reverse()
|
|
221
|
+
.map((run) => {
|
|
222
|
+
const summary = summarizeRun(run);
|
|
223
|
+
return {
|
|
224
|
+
runId: run.id,
|
|
225
|
+
runType: run.runType,
|
|
226
|
+
status: run.status,
|
|
227
|
+
...(run.threadId ? { threadId: run.threadId } : {}),
|
|
228
|
+
...(run.turnId ? { turnId: run.turnId } : {}),
|
|
229
|
+
...(run.parentThreadId ? { parentThreadId: run.parentThreadId } : {}),
|
|
230
|
+
...(summary ? { summary } : {}),
|
|
231
|
+
...(run.failureReason ? { failureReason: run.failureReason } : {}),
|
|
232
|
+
eventCount: this.db.listThreadEvents(run.id).length,
|
|
233
|
+
startedAt: run.startedAt,
|
|
234
|
+
...(run.endedAt ? { endedAt: run.endedAt } : {}),
|
|
235
|
+
isCurrentThread: run.threadId !== undefined && run.threadId === dbIssue.threadId,
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
issue,
|
|
240
|
+
...(dbIssue.worktreePath ? { worktreePath: dbIssue.worktreePath } : {}),
|
|
241
|
+
...(dbIssue.threadId ? { currentThreadId: dbIssue.threadId } : {}),
|
|
242
|
+
sessions,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
195
245
|
appendRetryWake(issue, runType) {
|
|
196
246
|
if (runType === "queue_repair") {
|
|
197
247
|
const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
|
|
@@ -75,6 +75,51 @@ export function formatRetry(result) {
|
|
|
75
75
|
.filter(Boolean)
|
|
76
76
|
.join("\n")}\n`;
|
|
77
77
|
}
|
|
78
|
+
function formatTimestampRange(startedAt, endedAt) {
|
|
79
|
+
return endedAt ? `${startedAt} -> ${endedAt}` : `${startedAt} -> running`;
|
|
80
|
+
}
|
|
81
|
+
export function formatSessionHistory(result, buildOpenForThread) {
|
|
82
|
+
const lines = [
|
|
83
|
+
`${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
|
|
84
|
+
value("Worktree", result.worktreePath),
|
|
85
|
+
value("Current thread", result.currentThreadId),
|
|
86
|
+
];
|
|
87
|
+
if (result.sessions.length === 0) {
|
|
88
|
+
lines.push("No recorded app-server sessions.");
|
|
89
|
+
return `${lines.join("\n")}\n`;
|
|
90
|
+
}
|
|
91
|
+
for (const session of result.sessions) {
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push([
|
|
94
|
+
`run #${session.runId}`,
|
|
95
|
+
session.runType,
|
|
96
|
+
session.status,
|
|
97
|
+
formatTimestampRange(session.startedAt, session.endedAt),
|
|
98
|
+
session.isCurrentThread ? "current" : undefined,
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(" "));
|
|
102
|
+
lines.push(value("Thread", session.threadId));
|
|
103
|
+
if (session.parentThreadId) {
|
|
104
|
+
lines.push(value("Parent thread", session.parentThreadId));
|
|
105
|
+
}
|
|
106
|
+
if (session.turnId) {
|
|
107
|
+
lines.push(value("Turn", session.turnId));
|
|
108
|
+
}
|
|
109
|
+
lines.push(value("Events", session.eventCount));
|
|
110
|
+
if (session.summary) {
|
|
111
|
+
lines.push(value("Summary", truncateLine(session.summary)));
|
|
112
|
+
}
|
|
113
|
+
else if (session.failureReason) {
|
|
114
|
+
lines.push(value("Failure", truncateLine(session.failureReason)));
|
|
115
|
+
}
|
|
116
|
+
if (session.threadId && result.worktreePath && buildOpenForThread) {
|
|
117
|
+
const command = buildOpenForThread(session.threadId);
|
|
118
|
+
lines.push(value("Open", formatCommand(command.command, command.args)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return `${lines.join("\n")}\n`;
|
|
122
|
+
}
|
|
78
123
|
export function formatList(items) {
|
|
79
124
|
return `${items
|
|
80
125
|
.map((item) => [
|
package/dist/cli/help.js
CHANGED
|
@@ -36,6 +36,7 @@ export function rootHelpText() {
|
|
|
36
36
|
" issue show <issueKey> [--json] Show the latest known issue state",
|
|
37
37
|
" issue watch <issueKey> [--json] Follow the active run until it settles",
|
|
38
38
|
" issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
39
|
+
" issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
|
|
39
40
|
" service status [--json] Show systemd state and local health",
|
|
40
41
|
" service logs [--lines <count>] [--json] Show recent service logs",
|
|
41
42
|
" serve Run the local PatchRelay service",
|
|
@@ -93,6 +94,10 @@ export function linearHelpText() {
|
|
|
93
94
|
" patchrelay linear connect",
|
|
94
95
|
" patchrelay linear list",
|
|
95
96
|
" patchrelay linear sync usertold",
|
|
97
|
+
"",
|
|
98
|
+
"Compatibility aliases:",
|
|
99
|
+
" patchrelay connect Alias for `patchrelay linear connect`",
|
|
100
|
+
" patchrelay installations Alias for `patchrelay linear list`",
|
|
96
101
|
].join("\n");
|
|
97
102
|
}
|
|
98
103
|
export function repoHelpText() {
|
|
@@ -121,6 +126,11 @@ export function repoHelpText() {
|
|
|
121
126
|
" patchrelay repo link krasnoperov/usertold --workspace usertold --team USE",
|
|
122
127
|
" patchrelay repo show krasnoperov/usertold",
|
|
123
128
|
" patchrelay repo sync",
|
|
129
|
+
"",
|
|
130
|
+
"Compatibility aliases:",
|
|
131
|
+
" patchrelay attach ... Alias for `patchrelay repo link ...`",
|
|
132
|
+
" patchrelay repos Alias for `patchrelay repo list`",
|
|
133
|
+
" patchrelay repos <repo> Alias for `patchrelay repo show <repo>`",
|
|
124
134
|
].join("\n");
|
|
125
135
|
}
|
|
126
136
|
export function issueHelpText() {
|
|
@@ -134,12 +144,14 @@ export function issueHelpText() {
|
|
|
134
144
|
" watch <issueKey> Follow PatchRelay-owned activity until it settles",
|
|
135
145
|
" path <issueKey> Print the issue worktree path",
|
|
136
146
|
" open <issueKey> Open Codex in the issue worktree",
|
|
147
|
+
" sessions <issueKey> Show recorded Codex app-server sessions",
|
|
137
148
|
" retry <issueKey> Requeue a run",
|
|
138
149
|
"",
|
|
139
150
|
"Examples:",
|
|
140
151
|
" patchrelay issue list --active",
|
|
141
152
|
" patchrelay issue show USE-54",
|
|
142
153
|
" patchrelay issue watch USE-54",
|
|
154
|
+
" patchrelay issue sessions USE-54",
|
|
143
155
|
].join("\n");
|
|
144
156
|
}
|
|
145
157
|
export function serviceHelpText() {
|
package/dist/cli/index.js
CHANGED
|
@@ -59,6 +59,9 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
59
59
|
case "open":
|
|
60
60
|
assertKnownFlags(parsed, "issue", ["print", "json"]);
|
|
61
61
|
return;
|
|
62
|
+
case "sessions":
|
|
63
|
+
assertKnownFlags(parsed, "issue", ["json"]);
|
|
64
|
+
return;
|
|
62
65
|
case "retry":
|
|
63
66
|
assertKnownFlags(parsed, "issue", ["run-type", "reason", "json"]);
|
|
64
67
|
return;
|
|
@@ -106,12 +109,6 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
106
109
|
assertKnownFlags(parsed, "repo", []);
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
|
-
case "attach":
|
|
110
|
-
case "repos":
|
|
111
|
-
case "connect":
|
|
112
|
-
case "installations":
|
|
113
|
-
throw new CliUsageError(`${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.`);
|
|
114
|
-
return;
|
|
115
112
|
case "service":
|
|
116
113
|
if (commandArgs[0] === "install") {
|
|
117
114
|
assertKnownFlags(parsed, "service", ["force", "write-only", "json"]);
|
|
@@ -314,10 +311,6 @@ export async function runCli(argv, options) {
|
|
|
314
311
|
runInteractive,
|
|
315
312
|
});
|
|
316
313
|
}
|
|
317
|
-
if (command === "attach" || command === "repos" || command === "connect" || command === "installations") {
|
|
318
|
-
writeOutput(stderr, `${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.\n`);
|
|
319
|
-
return 1;
|
|
320
|
-
}
|
|
321
314
|
if (command === "dashboard") {
|
|
322
315
|
const { handleWatchCommand } = await import("./commands/watch.js");
|
|
323
316
|
return await handleWatchCommand({ config, parsed });
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -181,12 +181,32 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
181
181
|
dispatch({ type: "switch-detail-tab", tab: "timeline" });
|
|
182
182
|
}
|
|
183
183
|
else if (input === "j" || key.downArrow) {
|
|
184
|
-
dispatch({ type: "detail-
|
|
184
|
+
dispatch({ type: "detail-scroll", delta: 1 });
|
|
185
185
|
}
|
|
186
186
|
else if (input === "k" || key.upArrow) {
|
|
187
|
+
dispatch({ type: "detail-scroll", delta: -1 });
|
|
188
|
+
}
|
|
189
|
+
else if (key.pageDown || (key.ctrl && input === "d")) {
|
|
190
|
+
dispatch({ type: "detail-page", direction: "down" });
|
|
191
|
+
}
|
|
192
|
+
else if (key.pageUp || (key.ctrl && input === "u")) {
|
|
193
|
+
dispatch({ type: "detail-page", direction: "up" });
|
|
194
|
+
}
|
|
195
|
+
else if (key.home) {
|
|
196
|
+
dispatch({ type: "detail-jump", target: "start" });
|
|
197
|
+
}
|
|
198
|
+
else if (key.end) {
|
|
199
|
+
dispatch({ type: "detail-jump", target: "end" });
|
|
200
|
+
}
|
|
201
|
+
else if (input === "[" || key.leftArrow) {
|
|
187
202
|
dispatch({ type: "detail-navigate", direction: "prev", filtered });
|
|
188
203
|
}
|
|
204
|
+
else if (input === "]" || key.rightArrow) {
|
|
205
|
+
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
206
|
+
}
|
|
189
207
|
}
|
|
190
208
|
});
|
|
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
|
|
209
|
+
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: 1 + ((promptMode || promptStatus) ? 1 : 0), onLayoutChange: (viewportRows, contentRows) => {
|
|
210
|
+
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
211
|
+
} }), 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 }));
|
|
192
212
|
}
|
|
@@ -4,7 +4,7 @@ export function HelpBar({ view, follow, detailTab }) {
|
|
|
4
4
|
let text;
|
|
5
5
|
if (view === "detail") {
|
|
6
6
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
7
|
-
text = [tabHint, `f:
|
|
7
|
+
text = [tabHint, "j/k: scroll", "PgUp/PgDn: page", "[ ]: issue", "Home/End: jump", `f: live ${follow ? "on" : "off"}`, "p: prompt", "s: stop", "r: retry"]
|
|
8
8
|
.filter(Boolean)
|
|
9
9
|
.join(" ");
|
|
10
10
|
}
|
|
@@ -1,169 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useReducer } from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import { Timeline } from "./Timeline.js";
|
|
5
|
-
import { StateHistoryView } from "./StateHistoryView.js";
|
|
6
|
-
import { buildStateHistory } from "./history-builder.js";
|
|
3
|
+
import { Box, Text, useStdout } from "ink";
|
|
7
4
|
import { HelpBar } from "./HelpBar.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return String(n);
|
|
17
|
-
}
|
|
18
|
-
function formatReviewState(reviewState) {
|
|
19
|
-
switch (reviewState) {
|
|
20
|
-
case "approved":
|
|
21
|
-
return "approved";
|
|
22
|
-
case "changes_requested":
|
|
23
|
-
return "changes requested";
|
|
24
|
-
case "commented":
|
|
25
|
-
return "commented";
|
|
26
|
-
default:
|
|
27
|
-
return reviewState ? reviewState.replaceAll("_", " ") : null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function formatCheckState(checkState) {
|
|
31
|
-
switch (checkState) {
|
|
32
|
-
case "passed":
|
|
33
|
-
case "success":
|
|
34
|
-
return "checks passed";
|
|
35
|
-
case "failed":
|
|
36
|
-
case "failure":
|
|
37
|
-
return "checks failed";
|
|
38
|
-
case "pending":
|
|
39
|
-
case "in_progress":
|
|
40
|
-
case "queued":
|
|
41
|
-
return "checks pending";
|
|
42
|
-
default:
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
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" },
|
|
51
|
-
failed: { label: "failed", color: "red" },
|
|
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",
|
|
67
|
-
};
|
|
68
|
-
function effectiveState(issue) {
|
|
69
|
-
if (issue.sessionState === "done")
|
|
70
|
-
return "done";
|
|
71
|
-
if (issue.sessionState === "failed")
|
|
72
|
-
return "failed";
|
|
73
|
-
if (issue.blockedByCount > 0 && !issue.activeRunType)
|
|
74
|
-
return "blocked";
|
|
75
|
-
if (issue.readyForExecution && !issue.activeRunType)
|
|
76
|
-
return "ready";
|
|
77
|
-
if (issue.sessionState === "waiting_input")
|
|
78
|
-
return "awaiting_input";
|
|
79
|
-
return issue.factoryState;
|
|
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
|
-
}
|
|
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;
|
|
97
|
-
if (issue.blockedByCount > 0)
|
|
98
|
-
return `Waiting on ${issue.blockedByKeys.join(", ")}`;
|
|
99
|
-
if (effectiveState(issue) === "repairing_queue")
|
|
100
|
-
return "Merge queue conflict, repairing branch";
|
|
101
|
-
if (effectiveState(issue) === "repairing_ci") {
|
|
102
|
-
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "CI";
|
|
103
|
-
return `Repairing ${check}`;
|
|
104
|
-
}
|
|
105
|
-
if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
106
|
-
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
|
|
107
|
-
return `${check} failed`;
|
|
108
|
-
}
|
|
109
|
-
if (rereviewNeeded)
|
|
110
|
-
return "Awaiting re-review after requested changes";
|
|
111
|
-
if (issue.prReviewState === "changes_requested")
|
|
112
|
-
return "Review changes requested";
|
|
113
|
-
if (issue.prNumber !== undefined && !issue.prReviewState && effectiveState(issue) !== "done")
|
|
114
|
-
return "Awaiting review";
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
function ElapsedTime({ startedAt }) {
|
|
118
|
-
const [, tick] = useReducer((c) => c + 1, 0);
|
|
5
|
+
import { buildDetailLines } from "./detail-rows.js";
|
|
6
|
+
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
|
+
const { stdout } = useStdout();
|
|
9
|
+
const width = Math.max(20, stdout?.columns ?? 80);
|
|
10
|
+
const totalRows = stdout?.rows ?? 24;
|
|
11
|
+
const footerRows = 1 + (unreadBelow > 0 ? 1 : 0);
|
|
12
|
+
const viewportRows = Math.max(4, totalRows - reservedRows - footerRows);
|
|
119
13
|
useEffect(() => {
|
|
120
|
-
const id = setInterval(tick,
|
|
14
|
+
const id = setInterval(tick, 1_000);
|
|
121
15
|
return () => clearInterval(id);
|
|
122
16
|
}, []);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
17
|
+
const lines = useMemo(() => {
|
|
18
|
+
if (!issue) {
|
|
19
|
+
return [{ key: "loading", segments: [{ text: "Loading issue…", dimColor: true }] }];
|
|
20
|
+
}
|
|
21
|
+
return buildDetailLines({
|
|
22
|
+
issue,
|
|
23
|
+
timeline,
|
|
24
|
+
activeRunStartedAt,
|
|
25
|
+
activeRunId,
|
|
26
|
+
tokenUsage,
|
|
27
|
+
diffSummary,
|
|
28
|
+
plan,
|
|
29
|
+
issueContext,
|
|
30
|
+
detailTab,
|
|
31
|
+
rawRuns,
|
|
32
|
+
rawFeedEvents,
|
|
33
|
+
follow,
|
|
34
|
+
connected,
|
|
35
|
+
lastServerMessageAt,
|
|
36
|
+
width,
|
|
37
|
+
});
|
|
38
|
+
}, [
|
|
39
|
+
issue,
|
|
40
|
+
timeline,
|
|
41
|
+
activeRunStartedAt,
|
|
42
|
+
activeRunId,
|
|
43
|
+
tokenUsage,
|
|
44
|
+
diffSummary,
|
|
45
|
+
plan,
|
|
46
|
+
issueContext,
|
|
47
|
+
detailTab,
|
|
48
|
+
rawRuns,
|
|
49
|
+
rawFeedEvents,
|
|
50
|
+
follow,
|
|
51
|
+
connected,
|
|
52
|
+
lastServerMessageAt,
|
|
53
|
+
width,
|
|
54
|
+
]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
onLayoutChange(viewportRows, lines.length);
|
|
57
|
+
}, [lines.length, onLayoutChange, viewportRows]);
|
|
58
|
+
const maxOffset = Math.max(0, lines.length - viewportRows);
|
|
59
|
+
const start = Math.min(scrollOffset, maxOffset);
|
|
60
|
+
const visibleLines = lines.slice(start, start + viewportRows);
|
|
61
|
+
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}`))), unreadBelow > 0 && (_jsx(Text, { color: "yellow", children: `${unreadBelow} below · End jumps back to live` })), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
|
|
127
63
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
const key = issue.issueKey ?? issue.projectId;
|
|
133
|
-
const meta = [];
|
|
134
|
-
if (tokenUsage)
|
|
135
|
-
meta.push(`${formatTokens(tokenUsage.inputTokens)} in / ${formatTokens(tokenUsage.outputTokens)} out`);
|
|
136
|
-
if (diffSummary && diffSummary.filesChanged > 0)
|
|
137
|
-
meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
|
|
138
|
-
if (issueContext?.runCount)
|
|
139
|
-
meta.push(`${issueContext.runCount} runs`);
|
|
140
|
-
const session = sessionDisplay(issue);
|
|
141
|
-
const stage = stageDisplay(issue);
|
|
142
|
-
const blocker = blockerText(issue, issueContext);
|
|
143
|
-
const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
|
|
144
|
-
// Build compact facts for the header
|
|
145
|
-
const facts = [];
|
|
146
|
-
const rereviewNeeded = issue.prReviewState === "changes_requested"
|
|
147
|
-
&& (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
148
|
-
&& !issue.activeRunType;
|
|
149
|
-
if (issue.prNumber !== undefined)
|
|
150
|
-
facts.push(`PR #${issue.prNumber}`);
|
|
151
|
-
if (issue.prReviewState === "approved")
|
|
152
|
-
facts.push("approved");
|
|
153
|
-
else if (rereviewNeeded)
|
|
154
|
-
facts.push("re-review needed");
|
|
155
|
-
else if (issue.prReviewState === "changes_requested")
|
|
156
|
-
facts.push("changes requested");
|
|
157
|
-
if (issue.waitingReason && issue.sessionState === "waiting_input")
|
|
158
|
-
facts.push(issue.waitingReason);
|
|
159
|
-
if (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
|
|
160
|
-
facts.push("checks passed");
|
|
161
|
-
else if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
162
|
-
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
|
|
163
|
-
facts.push(`${check} failed`);
|
|
164
|
-
}
|
|
165
|
-
else if (issue.prChecksSummary?.total) {
|
|
166
|
-
facts.push(`checks ${issue.prChecksSummary.completed}/${issue.prChecksSummary.total}`);
|
|
64
|
+
function RenderedLine({ line }) {
|
|
65
|
+
if (line.segments.length === 0) {
|
|
66
|
+
return _jsx(Text, { children: " " });
|
|
167
67
|
}
|
|
168
|
-
return (
|
|
68
|
+
return (_jsx(Text, { children: line.segments.map((segment, index) => (_jsx(Text
|
|
69
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
70
|
+
, { ...(segment.color ? { color: segment.color } : {}), ...(segment.dimColor ? { dimColor: true } : {}), ...(segment.bold ? { bold: true } : {}), children: segment.text }, `${line.key}-${index}`))) }));
|
|
169
71
|
}
|
|
@@ -4,6 +4,9 @@ import { summarizeIssueStatusNote } from "./issue-status-note.js";
|
|
|
4
4
|
import { relativeTime, truncate } from "./format-utils.js";
|
|
5
5
|
// ─── State display ──────────────────────────────────────────────
|
|
6
6
|
const TERMINAL_STATES = new Set(["done", "failed", "escalated"]);
|
|
7
|
+
function needsOperatorIntervention(issue) {
|
|
8
|
+
return issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated";
|
|
9
|
+
}
|
|
7
10
|
function effectiveState(issue) {
|
|
8
11
|
if (issue.sessionState === "done")
|
|
9
12
|
return "done";
|
|
@@ -18,6 +21,9 @@ function effectiveState(issue) {
|
|
|
18
21
|
return issue.factoryState;
|
|
19
22
|
}
|
|
20
23
|
function sessionDisplay(issue) {
|
|
24
|
+
if (needsOperatorIntervention(issue)) {
|
|
25
|
+
return { label: "needs help", color: "red" };
|
|
26
|
+
}
|
|
21
27
|
switch (issue.sessionState) {
|
|
22
28
|
case "running":
|
|
23
29
|
return { label: "running", color: "cyan" };
|
|
@@ -71,6 +77,9 @@ function buildFacts(issue, selected) {
|
|
|
71
77
|
if (issue.waitingReason && issue.sessionState === "waiting_input") {
|
|
72
78
|
facts.push({ text: issue.waitingReason, color: "yellow" });
|
|
73
79
|
}
|
|
80
|
+
if (needsOperatorIntervention(issue)) {
|
|
81
|
+
facts.push({ text: "operator action needed", color: "red" });
|
|
82
|
+
}
|
|
74
83
|
// Review state — only show when it matters (not yet approved, or changes requested)
|
|
75
84
|
if (issue.prReviewState === "approved") {
|
|
76
85
|
facts.push({ text: "approved", color: "green" });
|
|
@@ -116,6 +125,8 @@ function blockerText(issue) {
|
|
|
116
125
|
&& !issue.activeRunType;
|
|
117
126
|
if (issue.sessionState === "waiting_input")
|
|
118
127
|
return issue.waitingReason ?? "Waiting for input";
|
|
128
|
+
if (needsOperatorIntervention(issue))
|
|
129
|
+
return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
|
|
119
130
|
if (issue.waitingReason && !issue.activeRunType)
|
|
120
131
|
return issue.waitingReason;
|
|
121
132
|
if (issue.blockedByCount > 0)
|
|
@@ -13,7 +13,8 @@ export function StatusBar({ issues, totalCount, filter, connected, lastServerMes
|
|
|
13
13
|
const agg = computeAggregates(aggregateSource);
|
|
14
14
|
const withPr = aggregateSource.filter((i) => i.prNumber !== undefined).length;
|
|
15
15
|
const waitingInput = aggregateSource.filter((i) => i.sessionState === "waiting_input" || i.factoryState === "awaiting_input").length;
|
|
16
|
+
const intervention = aggregateSource.filter((i) => i.sessionState === "failed" || i.factoryState === "failed" || i.factoryState === "escalated").length;
|
|
16
17
|
const running = aggregateSource.filter((i) => i.sessionState === "running").length;
|
|
17
18
|
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 })] }));
|
|
19
|
+
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"] }), intervention > 0 && _jsxs(Text, { color: "red", children: [intervention, " needs help"] }), 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 })] }));
|
|
19
20
|
}
|