patchrelay 0.33.0 → 0.35.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.33.0",
4
- "commit": "90189d83b85f",
5
- "builtAt": "2026-04-02T00:44:41.741Z"
3
+ "version": "0.35.0",
4
+ "commit": "e1a5b7d3b9e0",
5
+ "builtAt": "2026-04-02T21:55:42.542Z"
6
6
  }
@@ -186,9 +186,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
186
186
  else if (input === "t") {
187
187
  dispatch({ type: "switch-detail-tab", tab: "timeline" });
188
188
  }
189
- else if (input === "v") {
190
- dispatch({ type: "toggle-timeline-mode" });
191
- }
192
189
  else if (input === "j" || key.downArrow) {
193
190
  dispatch({ type: "detail-navigate", direction: "next", filtered });
194
191
  }
@@ -202,5 +199,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
202
199
  }
203
200
  }
204
201
  });
205
- 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, timelineMode: state.timelineMode, 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 }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: "Operator Feed" })] }), _jsx(FeedView, { events: state.feedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt })] })) }));
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 }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: "Operator Feed" })] }), _jsx(FeedView, { events: state.feedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt })] })) }));
206
203
  }
@@ -1,21 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- const HELP_TEXT = {
4
- list: "j/k: navigate Enter: detail F: feed Tab: filter x: freeze q: quit",
5
- detail: "",
6
- feed: "Esc: list q: quit",
7
- };
8
- export function HelpBar({ view, follow, detailTab, timelineMode }) {
3
+ export function HelpBar({ view, follow, detailTab }) {
9
4
  let text;
10
5
  if (view === "detail") {
11
6
  const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
12
- const timelineHint = detailTab === "timeline" ? `v: ${timelineMode === "verbose" ? "compact" : "verbose"}` : undefined;
13
- text = [tabHint, timelineHint, "j/k: prev/next", "Esc: list", `f: follow ${follow ? "on" : "off"}`, "x: freeze", "p: prompt", "s: stop", "r: retry", "q: quit"]
7
+ text = [tabHint, `f: follow ${follow ? "on" : "off"}`, "p: prompt", "s: stop", "r: retry"]
14
8
  .filter(Boolean)
15
9
  .join(" ");
16
10
  }
11
+ else if (view === "feed") {
12
+ text = "";
13
+ }
17
14
  else {
18
- text = HELP_TEXT[view];
15
+ text = "F: feed Tab: filter";
19
16
  }
20
17
  return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
21
18
  }
@@ -46,82 +46,47 @@ function formatCheckState(checkState) {
46
46
  return null;
47
47
  }
48
48
  }
49
- function buildPrStatusSummary(issue, issueContext) {
50
- if (issue.prNumber === undefined)
51
- return [];
52
- const summary = [`PR #${issue.prNumber}`];
53
- const checkState = formatCheckState(issue.prCheckStatus);
54
- const reviewState = formatReviewState(issue.prReviewState);
55
- const failedCheck = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName;
56
- if (checkState === "checks failed" && failedCheck) {
57
- summary.push(`${failedCheck} failed`);
58
- }
59
- else if (checkState) {
60
- summary.push(checkState);
61
- }
62
- if (issue.prChecksSummary?.total) {
63
- if (issue.prChecksSummary.failed > 0) {
64
- summary.push(`${issue.prChecksSummary.failed}/${issue.prChecksSummary.total} checks failing`);
65
- }
66
- else if (issue.prChecksSummary.pending > 0) {
67
- summary.push(`${issue.prChecksSummary.completed}/${issue.prChecksSummary.total} checks settled`);
68
- }
69
- else {
70
- summary.push(`${issue.prChecksSummary.passed}/${issue.prChecksSummary.total} checks passed`);
71
- }
72
- }
73
- if (reviewState) {
74
- summary.push(`review ${reviewState}`);
75
- }
76
- else if (issue.factoryState === "pr_open" || issue.factoryState === "repairing_ci" || issue.factoryState === "awaiting_queue") {
77
- summary.push("review pending");
78
- }
79
- if (issue.factoryState === "awaiting_queue") {
80
- summary.push("queued for merge");
81
- }
82
- else if (issue.factoryState === "repairing_queue") {
83
- summary.push("merge queue repair needed");
84
- }
85
- else if (issue.factoryState === "done") {
86
- summary.push("merged");
87
- }
88
- else if (issue.prCheckStatus === "failed" || issue.prReviewState === undefined || issue.prReviewState === "changes_requested") {
89
- summary.push("not mergeable");
90
- }
91
- return summary;
49
+ const STATE_DISPLAY = {
50
+ blocked: { label: "blocked", color: "yellow" },
51
+ ready: { label: "ready", color: "blueBright" },
52
+ delegated: { label: "delegated", color: "cyan" },
53
+ implementing: { label: "implementing", color: "cyan" },
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" },
60
+ failed: { label: "failed", color: "red" },
61
+ escalated: { label: "escalated", color: "red" },
62
+ awaiting_input: { label: "awaiting input", color: "yellow" },
63
+ };
64
+ function effectiveState(issue) {
65
+ if (issue.blockedByCount > 0 && !issue.activeRunType)
66
+ return "blocked";
67
+ if (issue.readyForExecution && !issue.activeRunType)
68
+ return "ready";
69
+ return issue.factoryState;
92
70
  }
93
- function resolvePrimaryBlocker(issue, issueContext) {
94
- if (issue.blockedByCount > 0) {
95
- return {
96
- text: `Waiting on blockers: ${issue.blockedByKeys.join(", ")}`,
97
- color: "yellow",
98
- };
99
- }
71
+ function blockerText(issue, issueContext) {
72
+ if (issue.blockedByCount > 0)
73
+ return `Waiting on ${issue.blockedByKeys.join(", ")}`;
74
+ if (issue.factoryState === "repairing_queue")
75
+ return "Merge queue conflict, repairing branch";
76
+ if (issue.factoryState === "repairing_ci") {
77
+ const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "CI";
78
+ return `Repairing ${check}`;
79
+ }
80
+ if (issue.factoryState === "awaiting_queue")
81
+ return "Waiting for merge queue";
100
82
  if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
101
- const failedChecks = issue.prChecksSummary?.failedNames ?? [];
102
- const failedCheck = issueContext?.latestFailureCheckName
103
- ?? issue.latestFailureCheckName
104
- ?? (failedChecks.length > 0 ? failedChecks.slice(0, 2).join(", ") : undefined);
105
- return {
106
- text: failedCheck ? `Blocked by failed check: ${failedCheck}` : "Blocked by failed PR checks",
107
- color: "red",
108
- };
109
- }
110
- if (issue.prCheckStatus === "pending" || issue.prCheckStatus === "in_progress" || issue.prCheckStatus === "queued") {
111
- return { text: "Waiting for PR checks to finish", color: "yellow" };
112
- }
113
- if (issue.prReviewState === "changes_requested") {
114
- return { text: "Blocked by requested review changes", color: "yellow" };
115
- }
116
- if (issue.factoryState === "repairing_queue") {
117
- return { text: "Blocked by merge queue refresh failure", color: "yellow" };
118
- }
119
- if (issue.factoryState === "awaiting_queue") {
120
- return { text: "Waiting in merge queue", color: "yellow" };
121
- }
122
- if (issue.prNumber !== undefined && !issue.prReviewState && issue.factoryState !== "done") {
123
- return { text: "Blocked pending review approval", color: "yellow" };
83
+ const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
84
+ return `${check} failed`;
124
85
  }
86
+ if (issue.prReviewState === "changes_requested")
87
+ return "Review changes requested";
88
+ if (issue.prNumber !== undefined && !issue.prReviewState && issue.factoryState !== "done")
89
+ return "Awaiting review";
125
90
  return null;
126
91
  }
127
92
  function ElapsedTime({ startedAt }) {
@@ -135,9 +100,9 @@ function ElapsedTime({ startedAt }) {
135
100
  const seconds = elapsed % 60;
136
101
  return _jsxs(Text, { dimColor: true, children: [minutes, "m ", String(seconds).padStart(2, "0"), "s"] });
137
102
  }
138
- export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, timelineMode, rawRuns, rawFeedEvents, connected, lastServerMessageAt, }) {
103
+ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, }) {
139
104
  if (!issue) {
140
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode })] }));
105
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
141
106
  }
142
107
  const key = issue.issueKey ?? issue.projectId;
143
108
  const meta = [];
@@ -147,10 +112,27 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
147
112
  meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
148
113
  if (issueContext?.runCount)
149
114
  meta.push(`${issueContext.runCount} runs`);
115
+ const state = STATE_DISPLAY[effectiveState(issue)] ?? { label: issue.factoryState, color: "white" };
116
+ const blocker = blockerText(issue, issueContext);
150
117
  const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
151
118
  const graph = useMemo(() => buildPatchRelayStateGraph(history, issue.factoryState), [history, issue.factoryState]);
152
119
  const queueObservations = useMemo(() => buildPatchRelayQueueObservations(issue, rawFeedEvents), [issue, rawFeedEvents]);
153
- const prStatusSummary = useMemo(() => buildPrStatusSummary(issue, issueContext), [issue, issueContext]);
154
- const primaryBlocker = useMemo(() => resolvePrimaryBlocker(issue, issueContext), [issue, issueContext]);
155
- 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.blockedByCount > 0 && _jsxs(Text, { color: "yellow", children: ["blocked by ", issue.blockedByKeys.join(", ")] }), issue.readyForExecution && !issue.activeRunType && issue.blockedByCount === 0 && _jsx(Text, { color: "blueBright", children: "ready" }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), detailTab === "timeline" && _jsx(Text, { dimColor: true, children: timelineMode }), follow && _jsx(Text, { color: "yellow", children: "follow" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), issue.title && _jsx(Text, { children: issue.title }), prStatusSummary.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: prStatusSummary.join(" | ") }) })), primaryBlocker && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: primaryBlocker.color, children: ["Blocked by: ", primaryBlocker.text] }) })), issueContext?.latestFailureSummary && (_jsx(Box, { marginTop: 1, children: _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, mode: timelineMode }) })] })) : (_jsxs(_Fragment, { children: [_jsx(FactoryStateGraph, { main: graph.main, prLoops: graph.prLoops, queueLoop: graph.queueLoop, exits: graph.exits }), _jsx(QueueObservationView, { observations: queueObservations }), _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, timelineMode: timelineMode }) })] }));
120
+ // Build compact facts for the header
121
+ const facts = [];
122
+ if (issue.prNumber !== undefined)
123
+ facts.push(`PR #${issue.prNumber}`);
124
+ if (issue.prReviewState === "approved")
125
+ facts.push("approved");
126
+ else if (issue.prReviewState === "changes_requested")
127
+ facts.push("changes requested");
128
+ if (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
129
+ facts.push("checks passed");
130
+ else if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
131
+ const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
132
+ facts.push(`${check} failed`);
133
+ }
134
+ else if (issue.prChecksSummary?.total) {
135
+ facts.push(`checks ${issue.prChecksSummary.completed}/${issue.prChecksSummary.total}`);
136
+ }
137
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: state.color, children: state.label }), 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 }), 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: [_jsx(FactoryStateGraph, { main: graph.main, prLoops: graph.prLoops, queueLoop: graph.queueLoop, exits: graph.exits }), _jsx(QueueObservationView, { observations: queueObservations }), _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 }) })] }));
156
138
  }
@@ -1,260 +1,110 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { summarizeIssueStatusNote } from "./issue-status-note.js";
4
- import { progressBar, relativeTime, truncate } from "./format-utils.js";
5
- const STATE_COLORS = {
6
- blocked: "yellow",
7
- ready: "blueBright",
8
- delegated: "cyan",
9
- implementing: "cyan",
10
- pr_open: "cyan",
11
- changes_requested: "yellow",
12
- repairing_ci: "cyan",
13
- awaiting_queue: "cyan",
14
- repairing_queue: "cyan",
15
- done: "green",
16
- failed: "red",
17
- escalated: "red",
18
- awaiting_input: "yellow",
19
- };
20
- const STATE_SHORT = {
21
- blocked: "blocked",
22
- ready: "ready",
23
- delegated: "delegated",
24
- implementing: "implementing",
25
- pr_open: "pr open",
26
- changes_requested: "review changes",
27
- repairing_ci: "repairing checks",
28
- awaiting_queue: "queued for merge",
29
- repairing_queue: "repairing merge queue",
30
- done: "done",
31
- failed: "failed",
32
- escalated: "escalated",
33
- awaiting_input: "awaiting input",
34
- };
35
- const STATUS_SHORT = {
36
- running: "\u25b8",
37
- completed: "\u2713",
38
- failed: "\u2717",
39
- released: "\u2013",
40
- };
41
- function stateColor(state) {
42
- return STATE_COLORS[state] ?? "white";
43
- }
4
+ import { relativeTime, truncate } from "./format-utils.js";
5
+ // ─── State display ──────────────────────────────────────────────
44
6
  const TERMINAL_STATES = new Set(["done", "failed", "escalated", "awaiting_input"]);
45
- function formatStatus(issue) {
46
- const effectiveState = issue.blockedByCount > 0 && !issue.activeRunType
47
- ? "blocked"
48
- : issue.readyForExecution && !issue.activeRunType
49
- ? "ready"
50
- : issue.factoryState;
51
- const state = STATE_SHORT[effectiveState] ?? effectiveState;
52
- // Terminal states: just the label, no run symbol
53
- if (TERMINAL_STATES.has(issue.factoryState))
54
- return state;
55
- // Active/in-progress: show run status symbol
56
- const status = issue.activeRunType ? "running" : issue.latestRunStatus;
57
- const statusSym = status ? (STATUS_SHORT[status] ?? "") : "";
58
- if (statusSym)
59
- return `${state} ${statusSym}`;
60
- return state;
61
- }
62
- function buildStatusChips(issue) {
63
- const effectiveState = issue.blockedByCount > 0 && !issue.activeRunType
64
- ? "blocked"
65
- : issue.readyForExecution && !issue.activeRunType
66
- ? "ready"
67
- : issue.factoryState;
68
- const chips = [{
69
- text: `${stateIcon(effectiveState)} ${STATE_SHORT[effectiveState] ?? effectiveState}`,
70
- color: stateColor(effectiveState),
71
- }];
72
- if (issue.prNumber !== undefined) {
73
- chips.push({ text: `PR #${issue.prNumber}`, color: "cyan" });
74
- }
75
- const reviewChip = buildReviewChip(issue.prReviewState);
76
- if (reviewChip)
77
- chips.push(reviewChip);
78
- const checkChip = buildCheckChip(issue.prCheckStatus);
79
- if (checkChip)
80
- chips.push(checkChip);
81
- const checksProgressChip = buildChecksProgressChip(issue);
82
- if (checksProgressChip)
83
- chips.push(checksProgressChip);
84
- const mergeChip = buildMergeChip(issue);
85
- if (mergeChip)
86
- chips.push(mergeChip);
87
- if (issue.blockedByCount > 0) {
88
- chips.push({
89
- text: `blocked by ${issue.blockedByKeys.join(", ")}`,
90
- color: "yellow",
91
- });
92
- }
93
- return chips;
7
+ function effectiveState(issue) {
8
+ if (issue.blockedByCount > 0 && !issue.activeRunType)
9
+ return "blocked";
10
+ if (issue.readyForExecution && !issue.activeRunType)
11
+ return "ready";
12
+ return issue.factoryState;
94
13
  }
95
- function stateIcon(state) {
14
+ function stateDisplay(issue) {
15
+ const state = effectiveState(issue);
96
16
  switch (state) {
97
- case "implementing":
98
- case "repairing_ci":
99
- case "repairing_queue":
100
- return "\u25b8";
101
- case "awaiting_queue":
102
- return "\u25a4";
103
- case "done":
104
- return "\u2713";
105
- case "failed":
106
- case "escalated":
107
- return "\u2717";
108
- case "blocked":
109
- return "!";
110
- case "ready":
111
- return "+";
112
- default:
113
- return "\u2022";
17
+ case "blocked": return { label: "blocked", color: "yellow" };
18
+ case "ready": return { label: "ready", color: "blueBright" };
19
+ case "delegated": return { label: "delegated", color: "cyan" };
20
+ case "implementing": return { label: "implementing", color: "cyan" };
21
+ case "pr_open": return { label: "PR open", color: "cyan" };
22
+ case "changes_requested": return { label: "review changes", color: "yellow" };
23
+ case "repairing_ci": return { label: "repairing CI", color: "yellow" };
24
+ case "awaiting_queue": return { label: "queued for merge", color: "cyan" };
25
+ case "repairing_queue": return { label: "repairing queue", color: "yellow" };
26
+ case "done": return { label: "merged", color: "green" };
27
+ case "failed": return { label: "failed", color: "red" };
28
+ case "escalated": return { label: "escalated", color: "red" };
29
+ case "awaiting_input": return { label: "awaiting input", color: "yellow" };
30
+ default: return { label: state, color: "white" };
114
31
  }
115
32
  }
116
- function buildReviewChip(reviewState) {
117
- switch (reviewState) {
118
- case "approved":
119
- return { text: "\u2713 review approved", color: "green" };
120
- case "changes_requested":
121
- return { text: "\u2717 changes requested", color: "yellow" };
122
- case "commented":
123
- return { text: "\u2022 review commented", color: "yellow" };
124
- case "dismissed":
125
- return { text: "\u2013 review dismissed", color: "yellow" };
126
- default:
127
- return null;
33
+ // ─── Context facts (what matters right now) ─────────────────────
34
+ function buildFacts(issue) {
35
+ const facts = [];
36
+ // PR number
37
+ if (issue.prNumber !== undefined) {
38
+ facts.push({ text: `PR #${issue.prNumber}` });
128
39
  }
129
- }
130
- function buildCheckChip(checkState) {
131
- switch (checkState) {
132
- case "passed":
133
- case "success":
134
- return { text: "\u2713 checks passed", color: "green" };
135
- case "failed":
136
- case "failure":
137
- return { text: "\u2717 checks failed", color: "red" };
138
- case "pending":
139
- case "in_progress":
140
- case "queued":
141
- return { text: "\u25cf checks running", color: "yellow" };
142
- default:
143
- return null;
40
+ // Review state — only show when it matters (not yet approved, or changes requested)
41
+ if (issue.prReviewState === "approved") {
42
+ facts.push({ text: "approved", color: "green" });
144
43
  }
145
- }
146
- function buildChecksProgressChip(issue) {
147
- const summary = issue.prChecksSummary;
148
- if (!summary || summary.total <= 0)
149
- return null;
150
- const text = summary.failed > 0
151
- ? `checks ${summary.failed}/${summary.total} failed`
152
- : summary.pending > 0
153
- ? `checks ${summary.completed}/${summary.total} settled`
154
- : `checks ${summary.passed}/${summary.total} passed`;
155
- const color = summary.failed > 0 ? "red" : summary.pending > 0 ? "yellow" : "green";
156
- return { text, color };
157
- }
158
- function buildMergeChip(issue) {
159
- if (issue.prNumber === undefined)
160
- return null;
161
- switch (issue.factoryState) {
162
- case "awaiting_queue":
163
- return { text: "\u25a4 queued for merge", color: "cyan" };
164
- case "repairing_queue":
165
- return { text: "! merge queue repair", color: "yellow" };
166
- case "done":
167
- return { text: "\u2713 merged", color: "green" };
168
- case "pr_open":
169
- if (issue.prReviewState === "approved" && issue.prCheckStatus === "passed") {
170
- return { text: "\u2713 merge ready", color: "green" };
171
- }
172
- return { text: "\u2022 PR open", color: "cyan" };
173
- default:
174
- return null;
44
+ else if (issue.prReviewState === "changes_requested") {
45
+ facts.push({ text: "changes requested", color: "yellow" });
175
46
  }
176
- }
177
- function buildPrimaryBlocker(issue) {
178
- if (issue.blockedByCount > 0) {
179
- return {
180
- text: `Waiting on ${issue.blockedByKeys.join(", ")}`,
181
- color: "yellow",
182
- };
47
+ else if (issue.prNumber !== undefined && !issue.prReviewState && !TERMINAL_STATES.has(issue.factoryState)) {
48
+ facts.push({ text: "awaiting review", color: "yellow" });
183
49
  }
184
- if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
185
- const failedChecks = issue.prChecksSummary?.failedNames ?? [];
186
- const failedCheck = issue.latestFailureCheckName
187
- ?? (failedChecks.length > 0 ? failedChecks.slice(0, 2).join(", ") : undefined)
188
- ?? "PR checks";
189
- return {
190
- text: `${failedCheck} failed`,
191
- color: "red",
192
- };
50
+ // Check status compact
51
+ if (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success") {
52
+ facts.push({ text: "checks passed", color: "green" });
193
53
  }
194
- if (issue.prCheckStatus === "pending" || issue.prCheckStatus === "in_progress" || issue.prCheckStatus === "queued") {
195
- return {
196
- text: "Waiting for PR checks to finish",
197
- color: "yellow",
198
- };
54
+ else if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
55
+ const failedNames = issue.prChecksSummary?.failedNames ?? [];
56
+ const checkInfo = issue.latestFailureCheckName
57
+ ?? (failedNames.length > 0 ? failedNames.slice(0, 2).join(", ") : "checks");
58
+ facts.push({ text: `${checkInfo} failed`, color: "red" });
199
59
  }
200
- if (issue.prReviewState === "changes_requested") {
201
- return {
202
- text: "Review changes requested",
203
- color: "yellow",
204
- };
60
+ else if (issue.prCheckStatus === "pending" || issue.prCheckStatus === "in_progress") {
61
+ const summary = issue.prChecksSummary;
62
+ if (summary && summary.total > 0) {
63
+ facts.push({ text: `checks ${summary.completed}/${summary.total}`, color: "yellow" });
64
+ }
65
+ else {
66
+ facts.push({ text: "checks running", color: "yellow" });
67
+ }
205
68
  }
206
- if (issue.factoryState === "repairing_queue") {
207
- return {
208
- text: "Merge queue reported a branch refresh failure",
209
- color: "yellow",
210
- };
69
+ // Blocker
70
+ if (issue.blockedByCount > 0) {
71
+ facts.push({ text: `waiting on ${issue.blockedByKeys.join(", ")}`, color: "yellow" });
211
72
  }
212
- if (issue.factoryState === "awaiting_queue") {
213
- return {
214
- text: "Waiting for merge queue turn",
215
- color: "yellow",
216
- };
73
+ return facts;
74
+ }
75
+ // ─── What's blocking progress ───────────────────────────────────
76
+ function blockerText(issue) {
77
+ if (issue.blockedByCount > 0)
78
+ return `Waiting on ${issue.blockedByKeys.join(", ")}`;
79
+ if (issue.factoryState === "repairing_queue")
80
+ return "Merge queue conflict, repairing branch";
81
+ if (issue.factoryState === "repairing_ci") {
82
+ const check = issue.latestFailureCheckName ?? "CI";
83
+ return `Repairing ${check}`;
217
84
  }
218
- if (issue.prNumber !== undefined && !issue.prReviewState && issue.factoryState !== "done") {
219
- return {
220
- text: "Waiting for review approval",
221
- color: "yellow",
222
- };
85
+ if (issue.factoryState === "awaiting_queue")
86
+ return "Waiting for merge queue";
87
+ if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
88
+ const check = issue.latestFailureCheckName ?? "checks";
89
+ return `${check} failed`;
223
90
  }
91
+ if (issue.prReviewState === "changes_requested")
92
+ return "Review changes requested";
224
93
  return null;
225
94
  }
226
- function buildPipelineProgress(issue) {
227
- switch (issue.factoryState) {
228
- case "delegated":
229
- return { current: 1, total: 4, label: "delegated" };
230
- case "implementing":
231
- return { current: 1, total: 4, label: "implementing" };
232
- case "pr_open":
233
- case "changes_requested":
234
- case "repairing_ci":
235
- return { current: 2, total: 4, label: "pr checks" };
236
- case "awaiting_queue":
237
- case "repairing_queue":
238
- return { current: 3, total: 4, label: "merge queue" };
239
- case "done":
240
- return { current: 4, total: 4, label: "merged" };
241
- case "failed":
242
- case "escalated":
243
- case "awaiting_input":
244
- return { current: 4, total: 4, label: "stopped" };
245
- default:
246
- return { current: 1, total: 4, label: "queued" };
247
- }
248
- }
95
+ // ─── Render ─────────────────────────────────────────────────────
249
96
  export function IssueRow({ issue, selected, titleWidth }) {
250
97
  const key = issue.issueKey ?? issue.projectId;
251
- const ago = relativeTime(issue.updatedAt);
252
- const tw = titleWidth ?? 40;
98
+ const tw = titleWidth ?? 60;
253
99
  const title = issue.title ? truncate(issue.title, tw) : "";
254
100
  const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
255
- const status = formatStatus(issue);
256
- const chips = buildStatusChips(issue);
257
- const blocker = buildPrimaryBlocker(issue);
258
- const pipeline = buildPipelineProgress(issue);
259
- return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${ago}` }), _jsx(Text, { dimColor: true, children: ` ${status}` })] }), _jsx(Box, { paddingLeft: 2, flexWrap: "wrap", children: title ? _jsx(Text, { children: title }) : null }), _jsx(Box, { paddingLeft: 2, flexWrap: "wrap", children: chips.map((chip, index) => (_jsx(Box, { marginRight: 1, children: _jsxs(Text, { color: chip.color, children: ["[", chip.text, "]"] }) }, `${key}-chip-${index}`))) }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(pipeline.current, pipeline.total, 8) }), _jsx(Text, { dimColor: true, children: pipeline.label }), blocker ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { color: blocker.color, children: blocker.text })] })) : null] }), detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null] }));
101
+ const state = stateDisplay(issue);
102
+ const facts = buildFacts(issue);
103
+ const blocker = selected ? blockerText(issue) : null;
104
+ const isTerminal = TERMINAL_STATES.has(issue.factoryState);
105
+ // Terminal issues: compact single line
106
+ 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: state.color, children: state.label })] }));
108
+ }
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: state.color, children: state.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] }));
260
110
  }
@@ -24,19 +24,37 @@ function itemPrefix(item) {
24
24
  return "$ ";
25
25
  return "";
26
26
  }
27
+ function formatItemDuration(ms) {
28
+ if (ms === undefined || ms === null)
29
+ return "";
30
+ const seconds = Math.floor(ms / 1000);
31
+ if (seconds < 1)
32
+ return "";
33
+ if (seconds < 60)
34
+ return ` ${seconds}s`;
35
+ const minutes = Math.floor(seconds / 60);
36
+ return ` ${minutes}m`;
37
+ }
27
38
  function itemText(item) {
28
39
  switch (item.type) {
29
40
  case "agentMessage":
30
41
  case "plan":
31
42
  case "reasoning":
32
43
  return summarizeText(item);
33
- case "commandExecution":
34
- return cleanCommand(item.command ?? "?");
44
+ case "commandExecution": {
45
+ const cmd = cleanCommand(item.command ?? "?");
46
+ const exit = item.exitCode !== undefined && item.exitCode !== null && item.exitCode !== 0
47
+ ? ` exit ${item.exitCode}` : "";
48
+ const dur = formatItemDuration(item.durationMs);
49
+ return `${cmd}${exit}${dur}`;
50
+ }
35
51
  case "fileChange":
36
52
  return summarizeFileChange(item);
37
53
  case "mcpToolCall":
38
- case "dynamicToolCall":
39
- return summarizeToolCall(item);
54
+ case "dynamicToolCall": {
55
+ const dur = formatItemDuration(item.durationMs);
56
+ return `${summarizeToolCall(item)}${dur}`;
57
+ }
40
58
  case "userMessage":
41
59
  return `you: ${summarizeText(item)}`;
42
60
  default:
@@ -4,16 +4,14 @@ import { Box, Static, Text, useStdout } from "ink";
4
4
  import { buildTimelineRows } from "./timeline-presentation.js";
5
5
  import { TimelineRow } from "./TimelineRow.js";
6
6
  const ACTIVE_TAIL = 8;
7
- export function Timeline({ entries, follow, mode }) {
7
+ export function Timeline({ entries, follow }) {
8
8
  const { stdout } = useStdout();
9
9
  const rows = stdout?.rows ?? 24;
10
10
  const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
11
- const displayRows = useMemo(() => buildTimelineRows(entries, mode), [entries, mode]);
12
- // Split: finalized entries go to Static (terminal scrollback), active entries re-render
11
+ const displayRows = useMemo(() => buildTimelineRows(entries), [entries]);
13
12
  const splitIndex = useMemo(() => {
14
13
  if (!follow)
15
- return 0; // follow OFF: everything in active area (re-renders)
16
- // Find the boundary: keep the last maxActive entries in the active area
14
+ return 0;
17
15
  return Math.max(0, displayRows.length - maxActive);
18
16
  }, [displayRows.length, follow, maxActive]);
19
17
  const finalized = displayRows.slice(0, splitIndex);
@@ -21,5 +19,5 @@ export function Timeline({ entries, follow, mode }) {
21
19
  if (displayRows.length === 0) {
22
20
  return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
23
21
  }
24
- return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry, mode: mode }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry, mode: mode }, entry.id)))] }));
22
+ return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
25
23
  }
@@ -1,9 +1,6 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { ItemLine } from "./ItemLine.js";
4
- function formatTime(iso) {
5
- return new Date(iso).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
6
- }
7
4
  function formatDuration(startedAt, endedAt) {
8
5
  const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
9
6
  const seconds = Math.floor(ms / 1000);
@@ -21,7 +18,7 @@ const RUN_LABELS = {
21
18
  review_fix: "review fix",
22
19
  queue_repair: "merge fix",
23
20
  };
24
- function runStatusColor(status) {
21
+ function runDotColor(status) {
25
22
  if (status === "completed")
26
23
  return "green";
27
24
  if (status === "failed")
@@ -32,13 +29,6 @@ function runStatusColor(status) {
32
29
  return "yellow";
33
30
  return "white";
34
31
  }
35
- function runStatusLabel(status) {
36
- if (status === "running")
37
- return "running";
38
- if (status === "released")
39
- return "released";
40
- return status;
41
- }
42
32
  function detailColor(detail) {
43
33
  if (detail.tone === "command")
44
34
  return "white";
@@ -51,54 +41,35 @@ function detailPrefix(detail) {
51
41
  return "$ ";
52
42
  return "";
53
43
  }
54
- function verboseItemLabel(type) {
55
- switch (type) {
56
- case "agentMessage":
57
- return "message";
58
- case "commandExecution":
59
- return "command";
60
- case "fileChange":
61
- return "files";
62
- case "mcpToolCall":
63
- case "dynamicToolCall":
64
- return "tool";
65
- case "userMessage":
66
- return "you";
67
- case "plan":
68
- return "plan";
69
- case "reasoning":
70
- return "reasoning";
71
- default:
72
- return type;
73
- }
74
- }
75
44
  function FeedRow({ entry }) {
76
45
  const label = entry.feed.status ?? entry.feed.feedKind;
77
- const repeatSuffix = entry.repeatCount && entry.repeatCount > 1 ? ` ×${entry.repeatCount}` : "";
78
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: "cyan", bold: true, children: label.padEnd(12) })] }), _jsx(Box, { paddingLeft: 6, children: _jsxs(Text, { wrap: "wrap", children: [entry.feed.summary, repeatSuffix] }) })] }));
46
+ const repeatSuffix = entry.repeatCount && entry.repeatCount > 1 ? ` \u00d7${entry.repeatCount}` : "";
47
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u25cf" }), _jsx(Text, { color: "cyan", children: ` ${label}` }), _jsx(Text, { dimColor: true, children: ` ${entry.feed.summary}${repeatSuffix}` })] }));
79
48
  }
80
- function RunRow({ entry, mode, }) {
49
+ function RunRow({ entry, }) {
81
50
  const run = entry.run;
82
- const color = runStatusColor(run.status);
51
+ const dotColor = runDotColor(run.status);
83
52
  const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
84
- const showVerboseItems = mode === "verbose" && entry.items.length > 0;
85
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { bold: true, color: "yellow", children: (RUN_LABELS[run.runType] ?? run.runType).padEnd(12) }), _jsxs(Text, { bold: true, color: color, children: [" ", runStatusLabel(run.status)] }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), entry.details.length > 0 && _jsx(Text, { children: " " }), entry.details.map((detail, index) => (_jsx(Box, { paddingLeft: 6, marginBottom: index === entry.details.length - 1 ? 0 : 1, children: _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : {}), bold: detail.tone === "message", children: [detailPrefix(detail), detail.text] }) }, `${entry.id}-detail-${index}`))), showVerboseItems && _jsx(Text, { children: " " }), showVerboseItems && entry.items.map((itemEntry, index) => (_jsxs(Box, { flexDirection: "column", paddingLeft: 6, marginBottom: index === entry.items.length - 1 ? 0 : 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { dimColor: true, children: [formatTime(itemEntry.at), " "] }), _jsx(Text, { dimColor: true, children: verboseItemLabel(itemEntry.item.type) })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: itemEntry.item }) })] }, `${entry.id}-item-${index}`)))] }));
53
+ const showItems = entry.items.length > 0;
54
+ const showDetails = !showItems && entry.details.length > 0;
55
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { bold: true, color: "yellow", children: ` ${RUN_LABELS[run.runType] ?? run.runType}` }), _jsx(Text, { bold: true, color: dotColor, children: ` ${run.status}` }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), showItems && entry.items.map((itemEntry, index) => (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: itemEntry.item }) }, `${entry.id}-item-${index}`))), showDetails && entry.details.map((detail, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : { dimColor: true }), bold: detail.tone === "message", children: [detailPrefix(detail), detail.text] }) }, `${entry.id}-detail-${index}`)))] }));
86
56
  }
87
- function ItemRow({ entry, mode, }) {
88
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 6, marginBottom: mode === "verbose" ? 1 : 0, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { dimColor: true, children: entry.item.type })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: entry.item }) })] }));
57
+ function ItemRow({ entry, }) {
58
+ return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: entry.item }) }));
89
59
  }
90
60
  function CIChecksRow({ entry }) {
91
61
  const ci = entry.ciChecks;
92
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", bold: true, children: "checks".padEnd(12) })] }), _jsx(Box, { paddingLeft: 6, gap: 2, flexWrap: "wrap", children: ci.checks.map((check, i) => (_jsxs(Text, { children: [_jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsx(Text, { dimColor: true, children: check.name })] }, `c-${i}`))) })] }));
62
+ const dotColor = CHECK_COLORS[ci.overall] ?? "white";
63
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { color: dotColor, bold: true, children: ` checks` }), _jsx(Text, { children: ` ` }), ci.checks.map((check, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { children: ` ` }) : null, _jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsx(Text, { dimColor: true, children: ` ${check.name}` })] }, `c-${i}`)))] }));
93
64
  }
94
- export function TimelineRow({ entry, mode }) {
65
+ export function TimelineRow({ entry }) {
95
66
  switch (entry.kind) {
96
67
  case "feed":
97
68
  return _jsx(FeedRow, { entry: entry });
98
69
  case "run":
99
- return _jsx(RunRow, { entry: entry, mode: mode });
70
+ return _jsx(RunRow, { entry: entry });
100
71
  case "item":
101
- return _jsx(ItemRow, { entry: entry, mode: mode });
72
+ return _jsx(ItemRow, { entry: entry });
102
73
  case "ci-checks":
103
74
  return _jsx(CIChecksRow, { entry: entry });
104
75
  }
@@ -8,7 +8,7 @@ export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activ
8
8
  at: run.startedAt,
9
9
  kind: "run-start",
10
10
  runId: run.id,
11
- run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt },
11
+ run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt, threadId: run.threadId },
12
12
  });
13
13
  if (run.endedAt) {
14
14
  entries.push({
@@ -16,7 +16,7 @@ export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activ
16
16
  at: run.endedAt,
17
17
  kind: "run-end",
18
18
  runId: run.id,
19
- run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt },
19
+ run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt, threadId: run.threadId },
20
20
  });
21
21
  }
22
22
  // Items from completed run event history, with report fallback
@@ -1,103 +1,5 @@
1
- export function buildTimelineRows(entries, mode) {
2
- const rows = mode === "compact" ? buildCompactTimelineRows(entries) : buildVerboseTimelineRows(entries);
3
- return collapseRepeatedFeedRows(rows);
4
- }
5
- function buildVerboseTimelineRows(entries) {
6
- const rows = [];
7
- const runs = new Map();
8
- for (const entry of entries) {
9
- if (entry.kind === "run-start" && entry.runId !== undefined) {
10
- const existing = runs.get(entry.runId);
11
- if (!existing) {
12
- const run = { ...entry.run };
13
- runs.set(entry.runId, {
14
- id: `run-${entry.runId}`,
15
- at: run.startedAt,
16
- run,
17
- items: [],
18
- endedAt: run.endedAt,
19
- });
20
- }
21
- continue;
22
- }
23
- if (entry.kind === "run-end" && entry.runId !== undefined) {
24
- const existing = runs.get(entry.runId);
25
- if (existing) {
26
- existing.run = { ...entry.run };
27
- existing.endedAt = entry.run?.endedAt;
28
- }
29
- else {
30
- const run = { ...entry.run };
31
- runs.set(entry.runId, {
32
- id: `run-${entry.runId}`,
33
- at: run.startedAt,
34
- run,
35
- items: [],
36
- endedAt: run.endedAt,
37
- });
38
- }
39
- continue;
40
- }
41
- if (entry.kind === "item" && entry.runId !== undefined && runs.has(entry.runId)) {
42
- runs.get(entry.runId).items.push(entry.item);
43
- continue;
44
- }
45
- switch (entry.kind) {
46
- case "feed":
47
- if (shouldHideFeed(entry.feed)) {
48
- break;
49
- }
50
- rows.push({
51
- id: entry.id,
52
- kind: "feed",
53
- at: entry.at,
54
- finalized: true,
55
- feed: entry.feed,
56
- });
57
- break;
58
- case "ci-checks":
59
- rows.push({
60
- id: entry.id,
61
- kind: "ci-checks",
62
- at: entry.at,
63
- finalized: true,
64
- ciChecks: entry.ciChecks,
65
- });
66
- break;
67
- case "item":
68
- rows.push({
69
- id: entry.id,
70
- kind: "item",
71
- at: entry.at,
72
- finalized: entry.item?.status !== "inProgress",
73
- item: entry.item,
74
- });
75
- break;
76
- }
77
- }
78
- for (const [runId, run] of runs) {
79
- rows.push({
80
- id: run.id,
81
- kind: "run",
82
- at: run.at,
83
- finalized: run.items.every((item) => item.status !== "inProgress") && run.run.status !== "running",
84
- run: { ...run.run, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
85
- details: [],
86
- items: summarizeVerboseItems(entries
87
- .filter((entry) => entry.kind === "item" && entry.runId === runId)
88
- .map((entry) => ({ at: entry.at, item: entry.item }))),
89
- });
90
- }
91
- rows.sort((left, right) => {
92
- const cmp = left.at.localeCompare(right.at);
93
- if (cmp !== 0)
94
- return cmp;
95
- const kindCmp = rowKindOrder(left.kind) - rowKindOrder(right.kind);
96
- if (kindCmp !== 0)
97
- return kindCmp;
98
- return left.id.localeCompare(right.id);
99
- });
100
- return rows;
1
+ export function buildTimelineRows(entries) {
2
+ return collapseRepeatedFeedRows(buildCompactTimelineRows(entries));
101
3
  }
102
4
  function buildCompactTimelineRows(entries) {
103
5
  const rows = [];
@@ -181,7 +83,7 @@ function buildCompactTimelineRows(entries) {
181
83
  finalized: status !== "running",
182
84
  run: { ...run.run, status, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
183
85
  details: summarizeRunDetails(run.items),
184
- items: [],
86
+ items: run.items.map((item) => ({ at: run.at, item })),
185
87
  });
186
88
  }
187
89
  rows.sort((left, right) => {
@@ -335,39 +237,6 @@ function rowKindOrder(kind) {
335
237
  return 3;
336
238
  }
337
239
  }
338
- function summarizeVerboseItems(items) {
339
- const directTypes = new Set(["userMessage", "commandExecution", "fileChange", "plan"]);
340
- const kept = items.filter((entry) => directTypes.has(entry.item.type));
341
- const latestAgentMessage = findLatestVerboseItem(items, (entry) => entry.item.type === "agentMessage" && Boolean(entry.item.text?.trim()));
342
- if (latestAgentMessage) {
343
- kept.push(latestAgentMessage);
344
- }
345
- else {
346
- const latestReasoning = findLatestVerboseItem(items, (entry) => entry.item.type === "reasoning" && Boolean(entry.item.text?.trim()));
347
- if (latestReasoning) {
348
- kept.push(latestReasoning);
349
- }
350
- }
351
- const deduped = new Map();
352
- for (const entry of kept) {
353
- deduped.set(entry.item.id, entry);
354
- }
355
- return Array.from(deduped.values()).sort((left, right) => {
356
- const cmp = left.at.localeCompare(right.at);
357
- if (cmp !== 0)
358
- return cmp;
359
- return left.item.id.localeCompare(right.item.id);
360
- });
361
- }
362
- function findLatestVerboseItem(items, predicate) {
363
- for (let i = items.length - 1; i >= 0; i -= 1) {
364
- const item = items[i];
365
- if (predicate(item)) {
366
- return item;
367
- }
368
- }
369
- return undefined;
370
- }
371
240
  function collapseRepeatedFeedRows(rows) {
372
241
  const collapsed = [];
373
242
  for (const row of rows) {
@@ -8,7 +8,6 @@ function capArray(arr, max) {
8
8
  }
9
9
  const DETAIL_INITIAL = {
10
10
  detailTab: "timeline",
11
- timelineMode: "compact",
12
11
  timeline: [],
13
12
  rawRuns: [],
14
13
  rawFeedEvents: [],
@@ -140,8 +139,8 @@ export function watchReducer(state, action) {
140
139
  return { ...state, feedEvents: capArray([...state.feedEvents, action.event], MAX_FEED_EVENTS) };
141
140
  case "switch-detail-tab":
142
141
  return { ...state, detailTab: action.tab };
143
- case "toggle-timeline-mode":
144
- return { ...state, timelineMode: state.timelineMode === "compact" ? "verbose" : "compact" };
142
+ default:
143
+ return state;
145
144
  }
146
145
  }
147
146
  // ─── Feed Event → Issue List + Timeline ───────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {