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.
@@ -8,27 +8,171 @@ function titleCase(value) {
8
8
  .map((word) => word[0].toUpperCase() + word.slice(1))
9
9
  .join(" ");
10
10
  }
11
- function buildPlan(runType, statuses) {
12
- const label = titleCase(formatRunLabel(runType));
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: statuses[0] },
15
- { content: `Run ${label}`, status: statuses[1] },
16
- { content: "Review outcome", status: statuses[2] },
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 buildPlan(runType, ["inProgress", "pending", "pending"]);
149
+ return buildAgentSessionPlan({
150
+ factoryState: "preparing",
151
+ pendingRunType: runType,
152
+ });
21
153
  }
22
154
  export function buildRunningSessionPlan(runType) {
23
- return buildPlan(runType, ["completed", "inProgress", "pending"]);
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
- return buildPlan(runType, ["completed", "completed", "completed"]);
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 buildPlan(runType, ["completed", "completed", "inProgress"]);
170
+ return buildCompletedSessionPlan(runType);
30
171
  }
31
172
  export function buildFailedSessionPlan(runType, run) {
32
- const workflowStepStatus = run?.threadId || run?.turnId ? "completed" : "inProgress";
33
- return buildPlan(runType, ["completed", workflowStepStatus, "pending"]);
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, issueKey) {
4
- if (!issueKey || !config.server.publicBaseUrl) {
5
- return undefined;
6
- }
7
- const token = createSessionStatusToken({
8
- issueKey,
9
- secret: deriveSessionStatusSigningSecret(config.linear.tokenEncryptionKey),
10
- ttlSeconds: SESSION_STATUS_TTL_SECONDS,
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.12.8",
4
- "commit": "711863ef2d94",
5
- "builtAt": "2026-03-24T22:49:07.152Z"
3
+ "version": "0.13.0",
4
+ "commit": "e106318f8abc",
5
+ "builtAt": "2026-03-25T08:10:05.542Z"
6
6
  }
package/dist/cli/args.js CHANGED
@@ -16,6 +16,7 @@ export const KNOWN_COMMANDS = new Set([
16
16
  "connect",
17
17
  "installations",
18
18
  "feed",
19
+ "watch",
19
20
  "install-service",
20
21
  "restart-service",
21
22
  "help",
@@ -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
+ }