patchrelay 0.35.11 → 0.35.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +41 -9
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/args.js +19 -1
  4. package/dist/cli/commands/issues.js +18 -56
  5. package/dist/cli/commands/watch.js +5 -0
  6. package/dist/cli/data.js +160 -47
  7. package/dist/cli/formatters/text.js +51 -90
  8. package/dist/cli/help.js +15 -8
  9. package/dist/cli/index.js +3 -58
  10. package/dist/cli/operator-client.js +0 -82
  11. package/dist/cli/watch/App.js +21 -12
  12. package/dist/cli/watch/HelpBar.js +3 -3
  13. package/dist/cli/watch/IssueDetailView.js +63 -130
  14. package/dist/cli/watch/IssueRow.js +82 -27
  15. package/dist/cli/watch/StatusBar.js +8 -4
  16. package/dist/cli/watch/detail-rows.js +589 -0
  17. package/dist/cli/watch/render-rich-text.js +226 -0
  18. package/dist/cli/watch/state-visualization.js +48 -23
  19. package/dist/cli/watch/timeline-builder.js +2 -1
  20. package/dist/cli/watch/use-detail-stream.js +10 -104
  21. package/dist/cli/watch/use-watch-stream.js +11 -102
  22. package/dist/cli/watch/watch-state.js +129 -56
  23. package/dist/codex-thread-utils.js +3 -0
  24. package/dist/db/migrations.js +239 -2
  25. package/dist/db.js +628 -39
  26. package/dist/github-app-token.js +7 -0
  27. package/dist/github-failure-context.js +44 -1
  28. package/dist/github-rollup.js +47 -0
  29. package/dist/github-webhook-handler.js +423 -52
  30. package/dist/github-webhooks.js +7 -0
  31. package/dist/http.js +12 -264
  32. package/dist/idle-reconciliation.js +268 -76
  33. package/dist/issue-query-service.js +221 -129
  34. package/dist/issue-session-events.js +151 -0
  35. package/dist/issue-session.js +99 -0
  36. package/dist/linear-client.js +39 -25
  37. package/dist/linear-session-reporting.js +12 -0
  38. package/dist/linear-session-sync.js +253 -24
  39. package/dist/linear-workflow.js +33 -0
  40. package/dist/merge-queue-protocol.js +0 -51
  41. package/dist/preflight.js +1 -4
  42. package/dist/queue-health-monitor.js +11 -7
  43. package/dist/run-orchestrator.js +1364 -147
  44. package/dist/run-reporting.js +5 -3
  45. package/dist/service.js +279 -102
  46. package/dist/status-note.js +56 -0
  47. package/dist/waiting-reason.js +65 -0
  48. package/dist/webhook-handler.js +270 -79
  49. package/package.json +3 -2
  50. package/dist/cli/commands/feed.js +0 -60
  51. package/dist/cli/watch/FeedView.js +0 -28
  52. package/dist/cli/watch/use-feed-stream.js +0 -92
@@ -1,138 +1,71 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useReducer } from "react";
3
- import { Box, Text } from "ink";
4
- import { Timeline } from "./Timeline.js";
5
- import { StateHistoryView } from "./StateHistoryView.js";
6
- import { buildStateHistory } from "./history-builder.js";
3
+ import { Box, Text, useStdout } from "ink";
7
4
  import { HelpBar } from "./HelpBar.js";
8
- import { planStepSymbol, planStepColor } from "./plan-helpers.js";
9
- import { progressBar } from "./format-utils.js";
10
- import { FactoryStateGraph } from "./FactoryStateGraph.js";
11
- import { QueueObservationView } from "./QueueObservationView.js";
12
- import { buildPatchRelayQueueObservations, buildPatchRelayStateGraph } from "./state-visualization.js";
13
- import { FreshnessBadge } from "./FreshnessBadge.js";
14
- function formatTokens(n) {
15
- if (n >= 1_000_000)
16
- return `${(n / 1_000_000).toFixed(1)}M`;
17
- if (n >= 1_000)
18
- return `${(n / 1_000).toFixed(1)}k`;
19
- return String(n);
20
- }
21
- function formatReviewState(reviewState) {
22
- switch (reviewState) {
23
- case "approved":
24
- return "approved";
25
- case "changes_requested":
26
- return "changes requested";
27
- case "commented":
28
- return "commented";
29
- default:
30
- return reviewState ? reviewState.replaceAll("_", " ") : null;
31
- }
32
- }
33
- function formatCheckState(checkState) {
34
- switch (checkState) {
35
- case "passed":
36
- case "success":
37
- return "checks passed";
38
- case "failed":
39
- case "failure":
40
- return "checks failed";
41
- case "pending":
42
- case "in_progress":
43
- case "queued":
44
- return "checks pending";
45
- default:
46
- return null;
47
- }
48
- }
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;
70
- }
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";
82
- if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
83
- const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "checks";
84
- return `${check} failed`;
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";
90
- return null;
91
- }
92
- function ElapsedTime({ startedAt }) {
93
- const [, tick] = useReducer((c) => c + 1, 0);
5
+ import { buildDetailLines } from "./detail-rows.js";
6
+ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0, onLayoutChange, }) {
7
+ const [, tick] = useReducer((value) => value + 1, 0);
8
+ const { stdout } = useStdout();
9
+ const width = Math.max(20, stdout?.columns ?? 80);
10
+ const totalRows = stdout?.rows ?? 24;
11
+ const footerRows = 1 + (unreadBelow > 0 ? 1 : 0);
12
+ const viewportRows = Math.max(4, totalRows - reservedRows - footerRows);
94
13
  useEffect(() => {
95
- const id = setInterval(tick, 1000);
14
+ const id = setInterval(tick, 1_000);
96
15
  return () => clearInterval(id);
97
16
  }, []);
98
- const elapsed = Math.max(0, Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000));
99
- const minutes = Math.floor(elapsed / 60);
100
- const seconds = elapsed % 60;
101
- return _jsxs(Text, { dimColor: true, children: [minutes, "m ", String(seconds).padStart(2, "0"), "s"] });
17
+ const lines = useMemo(() => {
18
+ if (!issue) {
19
+ return [{ key: "loading", segments: [{ text: "Loading issue…", dimColor: true }] }];
20
+ }
21
+ return buildDetailLines({
22
+ issue,
23
+ timeline,
24
+ activeRunStartedAt,
25
+ activeRunId,
26
+ tokenUsage,
27
+ diffSummary,
28
+ plan,
29
+ issueContext,
30
+ detailTab,
31
+ rawRuns,
32
+ rawFeedEvents,
33
+ follow,
34
+ connected,
35
+ lastServerMessageAt,
36
+ width,
37
+ });
38
+ }, [
39
+ issue,
40
+ timeline,
41
+ activeRunStartedAt,
42
+ activeRunId,
43
+ tokenUsage,
44
+ diffSummary,
45
+ plan,
46
+ issueContext,
47
+ detailTab,
48
+ rawRuns,
49
+ rawFeedEvents,
50
+ follow,
51
+ connected,
52
+ lastServerMessageAt,
53
+ width,
54
+ ]);
55
+ useEffect(() => {
56
+ onLayoutChange(viewportRows, lines.length);
57
+ }, [lines.length, onLayoutChange, viewportRows]);
58
+ const maxOffset = Math.max(0, lines.length - viewportRows);
59
+ const start = Math.min(scrollOffset, maxOffset);
60
+ const visibleLines = lines.slice(start, start + viewportRows);
61
+ const fillerCount = Math.max(0, viewportRows - visibleLines.length);
62
+ return (_jsxs(Box, { flexDirection: "column", children: [visibleLines.map((line) => (_jsx(RenderedLine, { line: line }, line.key))), Array.from({ length: fillerCount }, (_, index) => (_jsx(Text, { children: " " }, `detail-fill-${index}`))), unreadBelow > 0 && (_jsx(Text, { color: "yellow", children: `${unreadBelow} below · End jumps back to live` })), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
102
63
  }
103
- export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, }) {
104
- if (!issue) {
105
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
106
- }
107
- const key = issue.issueKey ?? issue.projectId;
108
- const meta = [];
109
- if (tokenUsage)
110
- meta.push(`${formatTokens(tokenUsage.inputTokens)} in / ${formatTokens(tokenUsage.outputTokens)} out`);
111
- if (diffSummary && diffSummary.filesChanged > 0)
112
- meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
113
- if (issueContext?.runCount)
114
- meta.push(`${issueContext.runCount} runs`);
115
- const state = STATE_DISPLAY[effectiveState(issue)] ?? { label: issue.factoryState, color: "white" };
116
- const blocker = blockerText(issue, issueContext);
117
- const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
118
- const graph = useMemo(() => buildPatchRelayStateGraph(history, issue.factoryState), [history, issue.factoryState]);
119
- const queueObservations = useMemo(() => buildPatchRelayQueueObservations(issue, rawFeedEvents), [issue, rawFeedEvents]);
120
- // 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}`);
64
+ function RenderedLine({ line }) {
65
+ if (line.segments.length === 0) {
66
+ return _jsx(Text, { children: " " });
136
67
  }
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 }) })] }));
68
+ return (_jsx(Text, { children: line.segments.map((segment, index) => (_jsx(Text
69
+ // eslint-disable-next-line react/no-array-index-key
70
+ , { ...(segment.color ? { color: segment.color } : {}), ...(segment.dimColor ? { dimColor: true } : {}), ...(segment.bold ? { bold: true } : {}), children: segment.text }, `${line.key}-${index}`))) }));
138
71
  }
@@ -3,48 +3,94 @@ import { Box, Text } from "ink";
3
3
  import { summarizeIssueStatusNote } from "./issue-status-note.js";
4
4
  import { relativeTime, truncate } from "./format-utils.js";
5
5
  // ─── State display ──────────────────────────────────────────────
6
- const TERMINAL_STATES = new Set(["done", "failed", "escalated", "awaiting_input"]);
6
+ const TERMINAL_STATES = new Set(["done", "failed", "escalated"]);
7
+ function needsOperatorIntervention(issue) {
8
+ return issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated";
9
+ }
7
10
  function effectiveState(issue) {
11
+ if (issue.sessionState === "done")
12
+ return "done";
13
+ if (issue.sessionState === "failed")
14
+ return "failed";
8
15
  if (issue.blockedByCount > 0 && !issue.activeRunType)
9
16
  return "blocked";
10
17
  if (issue.readyForExecution && !issue.activeRunType)
11
18
  return "ready";
19
+ if (issue.sessionState === "waiting_input")
20
+ return "awaiting_input";
12
21
  return issue.factoryState;
13
22
  }
14
- function stateDisplay(issue) {
23
+ function sessionDisplay(issue) {
24
+ if (needsOperatorIntervention(issue)) {
25
+ return { label: "needs help", color: "red" };
26
+ }
27
+ switch (issue.sessionState) {
28
+ case "running":
29
+ return { label: "running", color: "cyan" };
30
+ case "idle":
31
+ return { label: "idle", color: "blueBright" };
32
+ case "waiting_input":
33
+ return { label: "needs input", color: "yellow" };
34
+ case "done":
35
+ return { label: "done", color: "green" };
36
+ case "failed":
37
+ return { label: "failed", color: "red" };
38
+ default:
39
+ return { label: "unknown", color: "white" };
40
+ }
41
+ }
42
+ function stageLabel(issue) {
15
43
  const state = effectiveState(issue);
16
44
  switch (state) {
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" };
45
+ case "blocked": return "blocked";
46
+ case "ready": return "ready";
47
+ case "delegated": return "delegated";
48
+ case "implementing": return "implementing";
49
+ case "pr_open": return "PR open";
50
+ case "changes_requested": return "review changes";
51
+ case "repairing_ci": return "repairing CI";
52
+ case "awaiting_queue": return "waiting downstream";
53
+ case "repairing_queue": return "repairing queue";
54
+ case "done": return "merged";
55
+ case "failed": return "failed";
56
+ case "escalated": return "escalated";
57
+ case "awaiting_input": return "needs input";
58
+ default: return state;
31
59
  }
32
60
  }
33
61
  // ─── Context facts (what matters right now) ─────────────────────
34
- function buildFacts(issue) {
62
+ function buildFacts(issue, selected) {
35
63
  const facts = [];
64
+ const rereviewNeeded = issue.prReviewState === "changes_requested"
65
+ && (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
66
+ && !issue.activeRunType;
36
67
  // PR number
37
68
  if (issue.prNumber !== undefined) {
38
69
  facts.push({ text: `PR #${issue.prNumber}` });
39
70
  }
71
+ if (!issue.sessionState) {
72
+ facts.push({ text: `stage ${stageLabel(issue)}` });
73
+ }
74
+ else if (selected) {
75
+ facts.push({ text: `internal stage ${stageLabel(issue)}` });
76
+ }
77
+ if (issue.waitingReason && issue.sessionState === "waiting_input") {
78
+ facts.push({ text: issue.waitingReason, color: "yellow" });
79
+ }
80
+ if (needsOperatorIntervention(issue)) {
81
+ facts.push({ text: "operator action needed", color: "red" });
82
+ }
40
83
  // Review state — only show when it matters (not yet approved, or changes requested)
41
84
  if (issue.prReviewState === "approved") {
42
85
  facts.push({ text: "approved", color: "green" });
43
86
  }
87
+ else if (rereviewNeeded) {
88
+ facts.push({ text: "re-review needed", color: "yellow" });
89
+ }
44
90
  else if (issue.prReviewState === "changes_requested") {
45
91
  facts.push({ text: "changes requested", color: "yellow" });
46
92
  }
47
- else if (issue.prNumber !== undefined && !issue.prReviewState && !TERMINAL_STATES.has(issue.factoryState)) {
93
+ else if (issue.prNumber !== undefined && !issue.prReviewState && !TERMINAL_STATES.has(effectiveState(issue))) {
48
94
  facts.push({ text: "awaiting review", color: "yellow" });
49
95
  }
50
96
  // Check status — compact
@@ -74,20 +120,29 @@ function buildFacts(issue) {
74
120
  }
75
121
  // ─── What's blocking progress ───────────────────────────────────
76
122
  function blockerText(issue) {
123
+ const rereviewNeeded = issue.prReviewState === "changes_requested"
124
+ && (issue.prCheckStatus === "passed" || issue.prCheckStatus === "success")
125
+ && !issue.activeRunType;
126
+ if (issue.sessionState === "waiting_input")
127
+ return issue.waitingReason ?? "Waiting for input";
128
+ if (needsOperatorIntervention(issue))
129
+ return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
130
+ if (issue.waitingReason && !issue.activeRunType)
131
+ return issue.waitingReason;
77
132
  if (issue.blockedByCount > 0)
78
133
  return `Waiting on ${issue.blockedByKeys.join(", ")}`;
79
- if (issue.factoryState === "repairing_queue")
134
+ if (effectiveState(issue) === "repairing_queue")
80
135
  return "Merge queue conflict, repairing branch";
81
- if (issue.factoryState === "repairing_ci") {
136
+ if (effectiveState(issue) === "repairing_ci") {
82
137
  const check = issue.latestFailureCheckName ?? "CI";
83
138
  return `Repairing ${check}`;
84
139
  }
85
- if (issue.factoryState === "awaiting_queue")
86
- return "Waiting for merge queue";
87
140
  if (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
88
141
  const check = issue.latestFailureCheckName ?? "checks";
89
142
  return `${check} failed`;
90
143
  }
144
+ if (rereviewNeeded)
145
+ return "Awaiting re-review after requested changes";
91
146
  if (issue.prReviewState === "changes_requested")
92
147
  return "Review changes requested";
93
148
  return null;
@@ -98,13 +153,13 @@ export function IssueRow({ issue, selected, titleWidth }) {
98
153
  const tw = titleWidth ?? 60;
99
154
  const title = issue.title ? truncate(issue.title, tw) : "";
100
155
  const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
101
- const state = stateDisplay(issue);
102
- const facts = buildFacts(issue);
156
+ const session = sessionDisplay(issue);
157
+ const facts = buildFacts(issue, selected);
103
158
  const blocker = selected ? blockerText(issue) : null;
104
- const isTerminal = TERMINAL_STATES.has(issue.factoryState);
159
+ const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
105
160
  // Terminal issues: compact single line
106
161
  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 })] }));
162
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label })] }));
108
163
  }
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] }));
164
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label }), facts.length > 0 && (_jsx(Text, { dimColor: true, children: ` \u00b7 ` })), facts.map((fact, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: ` \u00b7 ` }) : null, _jsx(Text, { color: fact.color ?? "white", dimColor: !fact.color, children: fact.text })] }, i)))] }), title ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: title }) })) : null, blocker ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: blocker }) })) : null, detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null, selected && issue.factoryState && issue.sessionState ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: `Debug stage: ${stageLabel(issue)}` }) })) : null] }));
110
165
  }
@@ -9,8 +9,12 @@ const FILTER_LABELS = {
9
9
  };
10
10
  export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, frozen, }) {
11
11
  const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
12
- const agg = computeAggregates(allIssues);
13
- const withPr = allIssues.filter((i) => i.prNumber !== undefined).length;
14
- const awaitingInput = allIssues.filter((i) => i.factoryState === "awaiting_input").length;
15
- return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), agg.active > 0 && _jsxs(Text, { color: "cyan", children: [agg.active, " active"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), awaitingInput > 0 && _jsxs(Text, { color: "yellow", children: [awaitingInput, " awaiting input"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] }), frozen && _jsx(Text, { color: "magenta", children: "frozen" })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
12
+ const aggregateSource = filter === "all" ? allIssues : issues;
13
+ const agg = computeAggregates(aggregateSource);
14
+ const withPr = aggregateSource.filter((i) => i.prNumber !== undefined).length;
15
+ const waitingInput = aggregateSource.filter((i) => i.sessionState === "waiting_input" || i.factoryState === "awaiting_input").length;
16
+ const intervention = aggregateSource.filter((i) => i.sessionState === "failed" || i.factoryState === "failed" || i.factoryState === "escalated").length;
17
+ const running = aggregateSource.filter((i) => i.sessionState === "running").length;
18
+ const idle = aggregateSource.filter((i) => i.sessionState === "idle").length;
19
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), running > 0 && _jsxs(Text, { color: "cyan", children: [running, " running"] }), idle > 0 && _jsxs(Text, { color: "blueBright", children: [idle, " idle"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), waitingInput > 0 && _jsxs(Text, { color: "yellow", children: [waitingInput, " needs input"] }), intervention > 0 && _jsxs(Text, { color: "red", children: [intervention, " needs help"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] }), frozen && _jsx(Text, { color: "magenta", children: "frozen" })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
16
20
  }