patchrelay 0.12.8 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-session-plan.js +155 -11
- package/dist/agent-session-presentation.js +19 -14
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands/watch.js +30 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/watch/App.js +43 -0
- package/dist/cli/watch/HelpBar.js +7 -0
- package/dist/cli/watch/IssueDetailView.js +11 -0
- package/dist/cli/watch/IssueListView.js +9 -0
- package/dist/cli/watch/IssueRow.js +42 -0
- package/dist/cli/watch/ItemLine.js +72 -0
- package/dist/cli/watch/StatusBar.js +6 -0
- package/dist/cli/watch/ThreadView.js +20 -0
- package/dist/cli/watch/TurnSection.js +15 -0
- package/dist/cli/watch/use-detail-stream.js +160 -0
- package/dist/cli/watch/use-watch-stream.js +102 -0
- package/dist/cli/watch/watch-state.js +261 -0
- package/dist/codex-app-server.js +1 -4
- package/dist/config.js +2 -0
- package/dist/github-webhook-handler.js +44 -19
- package/dist/http.js +81 -0
- package/dist/issue-query-service.js +17 -2
- package/dist/linear-session-reporting.js +134 -0
- package/dist/run-orchestrator.js +65 -18
- package/dist/run-reporting.js +31 -0
- package/dist/service.js +60 -0
- package/dist/webhook-handler.js +49 -28
- package/package.json +4 -1
|
@@ -8,27 +8,171 @@ function titleCase(value) {
|
|
|
8
8
|
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
9
9
|
.join(" ");
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
export function formatRunTypeLabel(runType) {
|
|
12
|
+
return titleCase(formatRunLabel(runType));
|
|
13
|
+
}
|
|
14
|
+
function implementationPlan() {
|
|
15
|
+
return [
|
|
16
|
+
{ content: "Prepare workspace", status: "pending" },
|
|
17
|
+
{ content: "Implement or update branch", status: "pending" },
|
|
18
|
+
{ content: "Await review", status: "pending" },
|
|
19
|
+
{ content: "Land change", status: "pending" },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function reviewFixPlan() {
|
|
23
|
+
return [
|
|
24
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
25
|
+
{ content: "Address review feedback", status: "pending" },
|
|
26
|
+
{ content: "Await re-review", status: "pending" },
|
|
27
|
+
{ content: "Land change", status: "pending" },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function ciRepairPlan(attempt) {
|
|
31
|
+
return [
|
|
32
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
33
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
34
|
+
{ content: `Repair failing checks (${attemptLabel(attempt)})`, status: "pending" },
|
|
35
|
+
{ content: "Return to merge flow", status: "pending" },
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function queueRepairPlan(attempt) {
|
|
39
|
+
return [
|
|
40
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
41
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
42
|
+
{ content: "Review approved", status: "completed" },
|
|
43
|
+
{ content: `Repair merge queue (${attemptLabel(attempt)})`, status: "pending" },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function awaitingInputPlan() {
|
|
47
|
+
return [
|
|
48
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
49
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
50
|
+
{ content: "Review approved", status: "completed" },
|
|
51
|
+
{ content: "Waiting for guidance", status: "inProgress" },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function failedPlan(label) {
|
|
13
55
|
return [
|
|
14
|
-
{ content: "Prepare workspace", status:
|
|
15
|
-
{ content:
|
|
16
|
-
{ content: "Review
|
|
56
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
57
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
58
|
+
{ content: "Review approved", status: "completed" },
|
|
59
|
+
{ content: label, status: "inProgress" },
|
|
17
60
|
];
|
|
18
61
|
}
|
|
62
|
+
function attemptLabel(attempt) {
|
|
63
|
+
const safeAttempt = Math.max(1, attempt);
|
|
64
|
+
return `attempt ${safeAttempt}`;
|
|
65
|
+
}
|
|
66
|
+
function setStatuses(plan, statuses) {
|
|
67
|
+
return plan.map((step, index) => ({ ...step, status: statuses[index] ?? step.status }));
|
|
68
|
+
}
|
|
69
|
+
function resolvePlanRunType(params) {
|
|
70
|
+
if (params.activeRunType) {
|
|
71
|
+
return params.activeRunType;
|
|
72
|
+
}
|
|
73
|
+
if (params.pendingRunType) {
|
|
74
|
+
return params.pendingRunType;
|
|
75
|
+
}
|
|
76
|
+
switch (params.factoryState) {
|
|
77
|
+
case "changes_requested":
|
|
78
|
+
return "review_fix";
|
|
79
|
+
case "repairing_ci":
|
|
80
|
+
return "ci_repair";
|
|
81
|
+
case "repairing_queue":
|
|
82
|
+
return "queue_repair";
|
|
83
|
+
default:
|
|
84
|
+
return "implementation";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function buildAgentSessionPlan(params) {
|
|
88
|
+
const runType = resolvePlanRunType(params);
|
|
89
|
+
switch (params.factoryState) {
|
|
90
|
+
case "delegated":
|
|
91
|
+
case "preparing":
|
|
92
|
+
return setStatuses(planForRunType(runType, params), ["inProgress", "pending", "pending", "pending"]);
|
|
93
|
+
case "implementing":
|
|
94
|
+
return setStatuses(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
95
|
+
case "pr_open":
|
|
96
|
+
case "awaiting_review":
|
|
97
|
+
return setStatuses(implementationPlan(), ["completed", "completed", "inProgress", "pending"]);
|
|
98
|
+
case "changes_requested":
|
|
99
|
+
return setStatuses(reviewFixPlan(), ["completed", "inProgress", "pending", "pending"]);
|
|
100
|
+
case "repairing_ci":
|
|
101
|
+
return setStatuses(ciRepairPlan(params.ciRepairAttempts ?? 1), ["completed", "completed", "inProgress", "pending"]);
|
|
102
|
+
case "awaiting_queue":
|
|
103
|
+
return setStatuses([
|
|
104
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
105
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
106
|
+
{ content: "Review approved", status: "completed" },
|
|
107
|
+
{ content: "Queued for merge", status: "inProgress" },
|
|
108
|
+
], ["completed", "completed", "completed", "inProgress"]);
|
|
109
|
+
case "repairing_queue":
|
|
110
|
+
return setStatuses(queueRepairPlan(params.queueRepairAttempts ?? 1), ["completed", "completed", "completed", "inProgress"]);
|
|
111
|
+
case "awaiting_input":
|
|
112
|
+
return awaitingInputPlan();
|
|
113
|
+
case "escalated":
|
|
114
|
+
return failedPlan("Needs human help");
|
|
115
|
+
case "failed":
|
|
116
|
+
return failedPlan("Recovery needed");
|
|
117
|
+
case "done":
|
|
118
|
+
return setStatuses([
|
|
119
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
120
|
+
{ content: "Implement or update branch", status: "completed" },
|
|
121
|
+
{ content: "Review approved", status: "completed" },
|
|
122
|
+
{ content: "Merged", status: "completed" },
|
|
123
|
+
], ["completed", "completed", "completed", "completed"]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function planForRunType(runType, params) {
|
|
127
|
+
switch (runType) {
|
|
128
|
+
case "review_fix":
|
|
129
|
+
return reviewFixPlan();
|
|
130
|
+
case "ci_repair":
|
|
131
|
+
return ciRepairPlan(params.ciRepairAttempts ?? 1);
|
|
132
|
+
case "queue_repair":
|
|
133
|
+
return queueRepairPlan(params.queueRepairAttempts ?? 1);
|
|
134
|
+
case "implementation":
|
|
135
|
+
default:
|
|
136
|
+
return implementationPlan();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function buildAgentSessionPlanForIssue(issue, options) {
|
|
140
|
+
return buildAgentSessionPlan({
|
|
141
|
+
factoryState: issue.factoryState,
|
|
142
|
+
ciRepairAttempts: issue.ciRepairAttempts,
|
|
143
|
+
queueRepairAttempts: issue.queueRepairAttempts,
|
|
144
|
+
...(issue.pendingRunType ? { pendingRunType: issue.pendingRunType } : {}),
|
|
145
|
+
...(options?.activeRunType ? { activeRunType: options.activeRunType } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
19
148
|
export function buildPreparingSessionPlan(runType) {
|
|
20
|
-
return
|
|
149
|
+
return buildAgentSessionPlan({
|
|
150
|
+
factoryState: "preparing",
|
|
151
|
+
pendingRunType: runType,
|
|
152
|
+
});
|
|
21
153
|
}
|
|
22
154
|
export function buildRunningSessionPlan(runType) {
|
|
23
|
-
return
|
|
155
|
+
return buildAgentSessionPlan({
|
|
156
|
+
factoryState: runType === "ci_repair" ? "repairing_ci"
|
|
157
|
+
: runType === "review_fix" ? "changes_requested"
|
|
158
|
+
: runType === "queue_repair" ? "repairing_queue"
|
|
159
|
+
: "implementing",
|
|
160
|
+
activeRunType: runType,
|
|
161
|
+
});
|
|
24
162
|
}
|
|
25
163
|
export function buildCompletedSessionPlan(runType) {
|
|
26
|
-
|
|
164
|
+
if (runType === "ci_repair" || runType === "queue_repair") {
|
|
165
|
+
return buildAgentSessionPlan({ factoryState: "awaiting_queue" });
|
|
166
|
+
}
|
|
167
|
+
return buildAgentSessionPlan({ factoryState: "awaiting_review" });
|
|
27
168
|
}
|
|
28
169
|
export function buildAwaitingHandoffSessionPlan(runType) {
|
|
29
|
-
return
|
|
170
|
+
return buildCompletedSessionPlan(runType);
|
|
30
171
|
}
|
|
31
172
|
export function buildFailedSessionPlan(runType, run) {
|
|
32
|
-
|
|
33
|
-
return
|
|
173
|
+
void run;
|
|
174
|
+
return buildAgentSessionPlan({
|
|
175
|
+
factoryState: "failed",
|
|
176
|
+
activeRunType: runType,
|
|
177
|
+
});
|
|
34
178
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { buildSessionStatusUrl, createSessionStatusToken, deriveSessionStatusSigningSecret, } from "./public-agent-session-status.js";
|
|
2
2
|
const SESSION_STATUS_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
3
|
-
export function buildAgentSessionExternalUrls(config,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
3
|
+
export function buildAgentSessionExternalUrls(config, params) {
|
|
4
|
+
const urls = [];
|
|
5
|
+
if (params.issueKey && config.server.publicBaseUrl) {
|
|
6
|
+
const token = createSessionStatusToken({
|
|
7
|
+
issueKey: params.issueKey,
|
|
8
|
+
secret: deriveSessionStatusSigningSecret(config.linear.tokenEncryptionKey),
|
|
9
|
+
ttlSeconds: SESSION_STATUS_TTL_SECONDS,
|
|
10
|
+
});
|
|
11
|
+
urls.push({
|
|
14
12
|
label: "PatchRelay status",
|
|
15
13
|
url: buildSessionStatusUrl({
|
|
16
14
|
publicBaseUrl: config.server.publicBaseUrl,
|
|
17
|
-
issueKey,
|
|
15
|
+
issueKey: params.issueKey,
|
|
18
16
|
token: token.token,
|
|
19
17
|
}),
|
|
20
|
-
}
|
|
21
|
-
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (params.prUrl) {
|
|
21
|
+
urls.push({
|
|
22
|
+
label: "Pull request",
|
|
23
|
+
url: params.prUrl,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return urls.length > 0 ? urls : undefined;
|
|
22
27
|
}
|
package/dist/build-info.json
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function resolveBaseUrl(config) {
|
|
2
|
+
const bind = config.server.bind;
|
|
3
|
+
let host;
|
|
4
|
+
if (bind === "0.0.0.0") {
|
|
5
|
+
host = "127.0.0.1";
|
|
6
|
+
}
|
|
7
|
+
else if (bind === "::") {
|
|
8
|
+
host = "[::1]";
|
|
9
|
+
}
|
|
10
|
+
else if (bind.includes(":") && !bind.startsWith("[")) {
|
|
11
|
+
host = `[${bind}]`;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
host = bind;
|
|
15
|
+
}
|
|
16
|
+
return `http://${host}:${config.server.port}`;
|
|
17
|
+
}
|
|
18
|
+
export async function handleWatchCommand(params) {
|
|
19
|
+
const { render } = await import("ink");
|
|
20
|
+
const { createElement } = await import("react");
|
|
21
|
+
const { App } = await import("../watch/App.js");
|
|
22
|
+
const baseUrl = resolveBaseUrl(params.config);
|
|
23
|
+
const bearerToken = params.config.operatorApi.bearerToken ?? undefined;
|
|
24
|
+
const issueKey = typeof params.parsed.flags.get("issue") === "string"
|
|
25
|
+
? String(params.parsed.flags.get("issue"))
|
|
26
|
+
: undefined;
|
|
27
|
+
const instance = render(createElement(App, { baseUrl, bearerToken, initialIssueKey: issueKey }));
|
|
28
|
+
await instance.waitUntilExit();
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
package/dist/cli/help.js
CHANGED
|
@@ -39,6 +39,7 @@ export function rootHelpText() {
|
|
|
39
39
|
" installations [--json] Show connected Linear installations",
|
|
40
40
|
" feed [--follow] [--limit <count>] [--issue <issueKey>] [--project <projectId>] [--kind <kind>] [--stage <stage>] [--status <status>] [--workflow <id>] [--json]",
|
|
41
41
|
" Show a live operator feed from the daemon",
|
|
42
|
+
" watch [--issue <issueKey>] Live TUI dashboard of issues and runs",
|
|
42
43
|
" serve Run the local PatchRelay service",
|
|
43
44
|
" inspect <issueKey> Show the latest known issue state",
|
|
44
45
|
" live <issueKey> [--watch] [--json] Show the active run status",
|
package/dist/cli/index.js
CHANGED
|
@@ -21,6 +21,7 @@ function getCommandConfigProfile(command) {
|
|
|
21
21
|
case "connect":
|
|
22
22
|
case "installations":
|
|
23
23
|
case "feed":
|
|
24
|
+
case "watch":
|
|
24
25
|
return "operator_cli";
|
|
25
26
|
case "inspect":
|
|
26
27
|
case "live":
|
|
@@ -90,6 +91,9 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
90
91
|
case "feed":
|
|
91
92
|
assertKnownFlags(parsed, command, ["follow", "limit", "issue", "project", "kind", "stage", "status", "workflow", "json"]);
|
|
92
93
|
return;
|
|
94
|
+
case "watch":
|
|
95
|
+
assertKnownFlags(parsed, command, ["issue"]);
|
|
96
|
+
return;
|
|
93
97
|
case "install-service":
|
|
94
98
|
assertKnownFlags(parsed, command, ["force", "write-only", "json"]);
|
|
95
99
|
return;
|
|
@@ -316,6 +320,10 @@ export async function runCli(argv, options) {
|
|
|
316
320
|
data: operatorData,
|
|
317
321
|
});
|
|
318
322
|
}
|
|
323
|
+
if (command === "watch") {
|
|
324
|
+
const { handleWatchCommand } = await import("./commands/watch.js");
|
|
325
|
+
return await handleWatchCommand({ config, parsed });
|
|
326
|
+
}
|
|
319
327
|
if (command === "retry") {
|
|
320
328
|
const issueData = await ensureIssueDataAccess(data, config);
|
|
321
329
|
if (!data) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useReducer } from "react";
|
|
3
|
+
import { Box, useApp, useInput } from "ink";
|
|
4
|
+
import { watchReducer, initialWatchState } from "./watch-state.js";
|
|
5
|
+
import { useWatchStream } from "./use-watch-stream.js";
|
|
6
|
+
import { useDetailStream } from "./use-detail-stream.js";
|
|
7
|
+
import { IssueListView } from "./IssueListView.js";
|
|
8
|
+
import { IssueDetailView } from "./IssueDetailView.js";
|
|
9
|
+
export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [state, dispatch] = useReducer(watchReducer, {
|
|
12
|
+
...initialWatchState,
|
|
13
|
+
...(initialIssueKey ? { view: "detail", activeDetailKey: initialIssueKey } : {}),
|
|
14
|
+
});
|
|
15
|
+
useWatchStream({ baseUrl, bearerToken, dispatch });
|
|
16
|
+
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch });
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (input === "q") {
|
|
19
|
+
exit();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (state.view === "list") {
|
|
23
|
+
if (input === "j" || key.downArrow) {
|
|
24
|
+
dispatch({ type: "select", index: state.selectedIndex + 1 });
|
|
25
|
+
}
|
|
26
|
+
else if (input === "k" || key.upArrow) {
|
|
27
|
+
dispatch({ type: "select", index: state.selectedIndex - 1 });
|
|
28
|
+
}
|
|
29
|
+
else if (key.return) {
|
|
30
|
+
const issue = state.issues[state.selectedIndex];
|
|
31
|
+
if (issue?.issueKey) {
|
|
32
|
+
dispatch({ type: "enter-detail", issueKey: issue.issueKey });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (state.view === "detail") {
|
|
37
|
+
if (key.escape || key.backspace || key.delete) {
|
|
38
|
+
dispatch({ type: "exit-detail" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { state: state })) : (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), thread: state.thread })) }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function HelpBar({ view }) {
|
|
4
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: view === "list"
|
|
5
|
+
? "j/k: navigate Enter: detail q: quit"
|
|
6
|
+
: "Esc: back q: quit" }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { ThreadView } from "./ThreadView.js";
|
|
4
|
+
import { HelpBar } from "./HelpBar.js";
|
|
5
|
+
export function IssueDetailView({ issue, thread }) {
|
|
6
|
+
if (!issue) {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail" })] }));
|
|
8
|
+
}
|
|
9
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["PR #", issue.prNumber] })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), thread ? (_jsx(ThreadView, { thread: thread })) : (_jsx(Text, { dimColor: true, children: "Waiting for thread data..." })), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "detail" })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { IssueRow } from "./IssueRow.js";
|
|
4
|
+
import { StatusBar } from "./StatusBar.js";
|
|
5
|
+
import { HelpBar } from "./HelpBar.js";
|
|
6
|
+
export function IssueListView({ state }) {
|
|
7
|
+
const { issues, selectedIndex, connected } = state;
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, connected: connected }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No tracked issues." })) : (_jsx(Box, { flexDirection: "column", children: issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.issueKey ?? `${issue.projectId}-${index}`))) })), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "list" })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
const STATE_COLORS = {
|
|
4
|
+
delegated: "blue",
|
|
5
|
+
preparing: "blue",
|
|
6
|
+
implementing: "yellow",
|
|
7
|
+
pr_open: "cyan",
|
|
8
|
+
awaiting_review: "cyan",
|
|
9
|
+
changes_requested: "magenta",
|
|
10
|
+
repairing_ci: "magenta",
|
|
11
|
+
awaiting_queue: "green",
|
|
12
|
+
repairing_queue: "magenta",
|
|
13
|
+
done: "green",
|
|
14
|
+
failed: "red",
|
|
15
|
+
escalated: "red",
|
|
16
|
+
awaiting_input: "yellow",
|
|
17
|
+
};
|
|
18
|
+
function stateColor(state) {
|
|
19
|
+
return STATE_COLORS[state] ?? "white";
|
|
20
|
+
}
|
|
21
|
+
function formatPr(issue) {
|
|
22
|
+
if (!issue.prNumber)
|
|
23
|
+
return "";
|
|
24
|
+
const parts = [`PR #${issue.prNumber}`];
|
|
25
|
+
if (issue.prReviewState === "approved")
|
|
26
|
+
parts.push("approved");
|
|
27
|
+
else if (issue.prReviewState === "changes_requested")
|
|
28
|
+
parts.push("changes");
|
|
29
|
+
if (issue.prCheckStatus === "passed")
|
|
30
|
+
parts.push("checks ok");
|
|
31
|
+
else if (issue.prCheckStatus === "failed")
|
|
32
|
+
parts.push("checks fail");
|
|
33
|
+
return parts.join(" ");
|
|
34
|
+
}
|
|
35
|
+
export function IssueRow({ issue, selected }) {
|
|
36
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
37
|
+
const state = issue.factoryState;
|
|
38
|
+
const run = issue.activeRunType ?? issue.latestRunType;
|
|
39
|
+
const runStatus = issue.activeRunType ? "running" : issue.latestRunStatus;
|
|
40
|
+
const pr = formatPr(issue);
|
|
41
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "▸" : " " }), _jsx(Text, { bold: true, children: key.padEnd(10) }), _jsx(Text, { color: stateColor(state), children: state.padEnd(20) }), _jsx(Text, { dimColor: true, children: run ? `${run}${runStatus ? `:${runStatus}` : ""}`.padEnd(25) : "".padEnd(25) }), pr ? _jsx(Text, { dimColor: true, children: pr }) : null] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
const STATUS_SYMBOL = {
|
|
4
|
+
completed: "\u2713",
|
|
5
|
+
failed: "\u2717",
|
|
6
|
+
declined: "\u2717",
|
|
7
|
+
inProgress: "\u25cf",
|
|
8
|
+
};
|
|
9
|
+
function statusChar(status) {
|
|
10
|
+
return STATUS_SYMBOL[status] ?? " ";
|
|
11
|
+
}
|
|
12
|
+
function statusColor(status) {
|
|
13
|
+
if (status === "completed")
|
|
14
|
+
return "green";
|
|
15
|
+
if (status === "failed" || status === "declined")
|
|
16
|
+
return "red";
|
|
17
|
+
if (status === "inProgress")
|
|
18
|
+
return "yellow";
|
|
19
|
+
return "white";
|
|
20
|
+
}
|
|
21
|
+
function truncate(text, max) {
|
|
22
|
+
const line = text.replace(/\n/g, " ").trim();
|
|
23
|
+
return line.length > max ? `${line.slice(0, max - 3)}...` : line;
|
|
24
|
+
}
|
|
25
|
+
function renderAgentMessage(item) {
|
|
26
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "message: " }), _jsx(Text, { children: truncate(item.text ?? "", 120) })] }));
|
|
27
|
+
}
|
|
28
|
+
function renderCommand(item) {
|
|
29
|
+
const cmd = item.command ?? "?";
|
|
30
|
+
const exit = item.exitCode !== undefined ? ` exit:${item.exitCode}` : "";
|
|
31
|
+
const duration = item.durationMs !== undefined ? ` ${(item.durationMs / 1000).toFixed(1)}s` : "";
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: truncate(cmd, 60) }), exit && _jsx(Text, { dimColor: true, children: exit }), duration && _jsx(Text, { dimColor: true, children: duration })] }), item.output && item.status === "inProgress" && (_jsxs(Text, { dimColor: true, children: [" ", truncate(item.output.split("\n").filter(Boolean).at(-1) ?? "", 100)] }))] }));
|
|
33
|
+
}
|
|
34
|
+
function renderFileChange(item) {
|
|
35
|
+
const count = item.changes?.length ?? 0;
|
|
36
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "files: " }), _jsxs(Text, { children: [count, " change", count !== 1 ? "s" : ""] })] }));
|
|
37
|
+
}
|
|
38
|
+
function renderToolCall(item) {
|
|
39
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "tool: " }), _jsx(Text, { children: item.toolName ?? item.type })] }));
|
|
40
|
+
}
|
|
41
|
+
function renderPlan(item) {
|
|
42
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan: " }), _jsx(Text, { children: truncate(item.text ?? "", 120) })] }));
|
|
43
|
+
}
|
|
44
|
+
function renderDefault(item) {
|
|
45
|
+
return (_jsxs(Text, { dimColor: true, children: [item.type, item.text ? `: ${truncate(item.text, 80)}` : ""] }));
|
|
46
|
+
}
|
|
47
|
+
export function ItemLine({ item, isLast }) {
|
|
48
|
+
const prefix = isLast ? "\u2514" : "\u251c";
|
|
49
|
+
let content;
|
|
50
|
+
switch (item.type) {
|
|
51
|
+
case "agentMessage":
|
|
52
|
+
content = renderAgentMessage(item);
|
|
53
|
+
break;
|
|
54
|
+
case "commandExecution":
|
|
55
|
+
content = renderCommand(item);
|
|
56
|
+
break;
|
|
57
|
+
case "fileChange":
|
|
58
|
+
content = renderFileChange(item);
|
|
59
|
+
break;
|
|
60
|
+
case "mcpToolCall":
|
|
61
|
+
case "dynamicToolCall":
|
|
62
|
+
content = renderToolCall(item);
|
|
63
|
+
break;
|
|
64
|
+
case "plan":
|
|
65
|
+
content = renderPlan(item);
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
content = renderDefault(item);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: prefix }), _jsx(Text, { color: statusColor(item.status), children: statusChar(item.status) }), content] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function StatusBar({ issues, connected }) {
|
|
4
|
+
const active = issues.filter((i) => i.activeRunType).length;
|
|
5
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: issues.length }), _jsx(Text, { children: " issues tracked" }), active > 0 && (_jsxs(Text, { children: [", ", _jsx(Text, { bold: true, color: "green", children: active }), " active"] }))] }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "● connected" : "○ disconnected" })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TurnSection } from "./TurnSection.js";
|
|
4
|
+
function planStepSymbol(status) {
|
|
5
|
+
if (status === "completed")
|
|
6
|
+
return "\u2713";
|
|
7
|
+
if (status === "inProgress")
|
|
8
|
+
return "\u25b8";
|
|
9
|
+
return " ";
|
|
10
|
+
}
|
|
11
|
+
function planStepColor(status) {
|
|
12
|
+
if (status === "completed")
|
|
13
|
+
return "green";
|
|
14
|
+
if (status === "inProgress")
|
|
15
|
+
return "yellow";
|
|
16
|
+
return "white";
|
|
17
|
+
}
|
|
18
|
+
export function ThreadView({ thread }) {
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["Thread: ", thread.threadId.slice(0, 16)] }), _jsxs(Text, { dimColor: true, children: ["Status: ", thread.status] }), _jsxs(Text, { dimColor: true, children: ["Turns: ", thread.turns.length] })] }), thread.plan && thread.plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Plan:" }), thread.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, { flexDirection: "column", marginTop: 1, children: thread.turns.map((turn, i) => (_jsx(TurnSection, { turn: turn, index: i }, turn.id))) })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { ItemLine } from "./ItemLine.js";
|
|
4
|
+
function turnStatusColor(status) {
|
|
5
|
+
if (status === "completed")
|
|
6
|
+
return "green";
|
|
7
|
+
if (status === "failed" || status === "interrupted")
|
|
8
|
+
return "red";
|
|
9
|
+
if (status === "inProgress")
|
|
10
|
+
return "yellow";
|
|
11
|
+
return "white";
|
|
12
|
+
}
|
|
13
|
+
export function TurnSection({ turn, index }) {
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Turn #", index + 1] }), _jsx(Text, { color: turnStatusColor(turn.status), children: turn.status }), _jsxs(Text, { dimColor: true, children: ["(", turn.items.length, " items)"] })] }), turn.items.map((item, i) => (_jsx(ItemLine, { item: item, isLast: i === turn.items.length - 1 }, item.id)))] }));
|
|
15
|
+
}
|