patchrelay 0.45.1 → 0.46.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.
@@ -0,0 +1,119 @@
1
+ import { relativeTime, truncate } from "./format-utils.js";
2
+ export { relativeTime };
3
+ const RUN_LABEL = {
4
+ implementation: "implementation",
5
+ ci_repair: "ci repair",
6
+ review_fix: "review fix",
7
+ branch_upkeep: "branch upkeep",
8
+ queue_repair: "queue repair",
9
+ };
10
+ export function buildEventLogLines(source) {
11
+ const lines = [];
12
+ for (const run of source.rawRuns) {
13
+ const label = RUN_LABEL[run.runType] ?? run.runType;
14
+ lines.push({
15
+ id: `run-start-${run.id}`,
16
+ at: run.startedAt,
17
+ category: "run",
18
+ phrase: `${label} started`,
19
+ });
20
+ if (run.endedAt) {
21
+ const failed = run.status === "failed" || run.status === "errored";
22
+ const reason = run.report && typeof run.report === "object"
23
+ ? extractFailureReason(run.report)
24
+ : undefined;
25
+ lines.push({
26
+ id: `run-end-${run.id}`,
27
+ at: run.endedAt,
28
+ category: "run",
29
+ phrase: `${label} ended · ${humanStatus(run.status)}`,
30
+ color: failed ? "red" : run.status === "completed" ? "green" : undefined,
31
+ ...(failed && reason ? { continuation: reason } : {}),
32
+ });
33
+ }
34
+ }
35
+ for (const event of source.rawFeedEvents) {
36
+ const mapped = mapFeedEvent(event);
37
+ if (mapped)
38
+ lines.push(mapped);
39
+ }
40
+ lines.sort((left, right) => {
41
+ const lt = new Date(left.at).getTime();
42
+ const rt = new Date(right.at).getTime();
43
+ if (lt !== rt)
44
+ return lt - rt;
45
+ return left.id.localeCompare(right.id);
46
+ });
47
+ return lines;
48
+ }
49
+ function mapFeedEvent(event) {
50
+ const base = { id: `feed-${event.id}`, at: event.at };
51
+ switch (event.kind) {
52
+ case "stage": {
53
+ const phrase = formatStagePhrase(event);
54
+ if (!phrase)
55
+ return null;
56
+ return {
57
+ ...base,
58
+ category: "stage",
59
+ phrase,
60
+ ...(event.level === "error" ? { color: "red" } : event.level === "warn" ? { color: "yellow" } : {}),
61
+ };
62
+ }
63
+ case "github":
64
+ return { ...base, category: "github", phrase: compactSummary(event), ...(event.level === "error" ? { color: "red" } : {}) };
65
+ case "linear":
66
+ return { ...base, category: "github", phrase: compactSummary(event) };
67
+ case "comment":
68
+ return { ...base, category: "review", phrase: compactSummary(event) };
69
+ case "agent":
70
+ case "turn": {
71
+ const summary = event.summary?.toLowerCase() ?? "";
72
+ if (summary.startsWith("prompt")) {
73
+ return { ...base, category: "human", phrase: compactSummary(event) };
74
+ }
75
+ return null;
76
+ }
77
+ default:
78
+ return null;
79
+ }
80
+ }
81
+ function formatStagePhrase(event) {
82
+ const from = event.stage;
83
+ const to = event.nextStage;
84
+ if (from && to)
85
+ return `${from} → ${to}`;
86
+ if (event.summary)
87
+ return compactSummary(event);
88
+ return null;
89
+ }
90
+ function compactSummary(event) {
91
+ const summary = event.summary?.trim() ?? "";
92
+ if (!summary)
93
+ return event.kind;
94
+ return summary;
95
+ }
96
+ function extractFailureReason(report) {
97
+ const reason = report["failureReason"] ?? report["error"] ?? report["message"];
98
+ if (typeof reason === "string" && reason.trim().length > 0) {
99
+ return truncate(reason.replace(/\s+/g, " ").trim(), 140);
100
+ }
101
+ return undefined;
102
+ }
103
+ function humanStatus(status) {
104
+ switch (status) {
105
+ case "completed":
106
+ case "succeeded":
107
+ return "success";
108
+ case "failed":
109
+ case "errored":
110
+ return "failed";
111
+ case "running":
112
+ return "running";
113
+ default:
114
+ return status;
115
+ }
116
+ }
117
+ export function formatEventAge(at) {
118
+ return relativeTime(at);
119
+ }
@@ -0,0 +1,81 @@
1
+ const GLYPH = {
2
+ running: "\u25cf",
3
+ queued: "\u25cb",
4
+ approved: "\u2713",
5
+ declined: "\u2717",
6
+ attention: "\u26a0",
7
+ };
8
+ const COLOR = {
9
+ running: "yellow",
10
+ queued: "gray",
11
+ approved: "green",
12
+ declined: "red",
13
+ attention: "red",
14
+ };
15
+ export function issueTokenFor(issue) {
16
+ if (issue.factoryState === "done") {
17
+ return { glyph: GLYPH.approved, color: COLOR.approved, kind: "approved", phrase: "done" };
18
+ }
19
+ if (issue.factoryState === "failed") {
20
+ return { glyph: GLYPH.declined, color: COLOR.declined, kind: "declined", phrase: "failed" };
21
+ }
22
+ if (issue.factoryState === "escalated") {
23
+ return { glyph: GLYPH.attention, color: COLOR.attention, kind: "attention", phrase: "escalated" };
24
+ }
25
+ if (issue.factoryState === "awaiting_input" || issue.sessionState === "waiting_input") {
26
+ return { glyph: GLYPH.attention, color: COLOR.attention, kind: "attention", phrase: "needs human" };
27
+ }
28
+ if (issue.factoryState === "delegated") {
29
+ return { glyph: GLYPH.queued, color: COLOR.queued, kind: "queued", phrase: "delegated" };
30
+ }
31
+ return {
32
+ glyph: GLYPH.running,
33
+ color: COLOR.running,
34
+ kind: "running",
35
+ phrase: phraseForRunning(issue),
36
+ };
37
+ }
38
+ function phraseForRunning(issue) {
39
+ switch (issue.factoryState) {
40
+ case "implementing":
41
+ return "implementing";
42
+ case "pr_open":
43
+ return "pr open";
44
+ case "changes_requested":
45
+ return "changes requested";
46
+ case "repairing_ci":
47
+ return "repairing ci";
48
+ case "awaiting_queue":
49
+ return "awaiting queue";
50
+ case "repairing_queue":
51
+ return "repairing queue";
52
+ default:
53
+ return issue.factoryState;
54
+ }
55
+ }
56
+ export function prTokenFor(issue) {
57
+ if (issue.prNumber === undefined)
58
+ return null;
59
+ const kind = prKind(issue);
60
+ return {
61
+ prNumber: issue.prNumber,
62
+ glyph: GLYPH[kind],
63
+ color: COLOR[kind],
64
+ kind,
65
+ };
66
+ }
67
+ function prKind(issue) {
68
+ if (issue.prState === "merged")
69
+ return "approved";
70
+ if (issue.prState === "closed")
71
+ return "declined";
72
+ if (issue.prReviewState === "approved")
73
+ return "approved";
74
+ if (issue.prReviewState === "changes_requested")
75
+ return "declined";
76
+ if (issue.prChecksSummary?.overall === "failure" || issue.prCheckStatus === "failure")
77
+ return "declined";
78
+ if (issue.prChecksSummary?.overall === "success" || issue.prCheckStatus === "success")
79
+ return "approved";
80
+ return "running";
81
+ }
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { buildDetailLines } from "./detail-rows.js";
6
+ import { buildCodexLogLines } from "./codex-log-rows.js";
6
7
  import { lineToPlainText } from "./render-rich-text.js";
7
8
  export function findLastAssistantMessage(timeline) {
8
9
  return findLastItemField(timeline, (entry) => entry.item?.type === "agentMessage", "text");
@@ -14,11 +15,14 @@ export function findLastCommandOutput(timeline) {
14
15
  return findLastItemField(timeline, (entry) => entry.item?.type === "commandExecution" && Boolean(entry.item?.output?.trim()), "output");
15
16
  }
16
17
  export function buildWatchDetailExportText(input) {
17
- const lines = buildDetailLines({
18
- ...input,
19
- width: input.width ?? 100,
20
- });
21
- return `${lines.map(lineToPlainText).join("\n").trimEnd()}\n`;
18
+ const width = input.width ?? 100;
19
+ const detail = buildDetailLines({ ...input, width });
20
+ const log = buildCodexLogLines(input.timeline, width);
21
+ const sections = [detail.map(lineToPlainText).join("\n").trimEnd()];
22
+ if (log.length > 0) {
23
+ sections.push(`app-server log\n${log.map(lineToPlainText).join("\n").trimEnd()}`);
24
+ }
25
+ return `${sections.join("\n\n").trimEnd()}\n`;
22
26
  }
23
27
  export function writeTextToClipboard(text, stream = process.stderr) {
24
28
  if (!stream.isTTY || text.length === 0) {
@@ -162,6 +162,14 @@ export function watchReducer(state, action) {
162
162
  return { ...state, view: "detail", activeDetailKey: action.issueKey, follow: true, ...DETAIL_INITIAL };
163
163
  case "exit-detail":
164
164
  return { ...state, view: "list", activeDetailKey: null, ...DETAIL_INITIAL };
165
+ case "enter-log":
166
+ if (state.view !== "detail" || !state.activeDetailKey)
167
+ return state;
168
+ return { ...state, view: "log", follow: true, detailScrollOffset: 0 };
169
+ case "exit-log":
170
+ if (state.view !== "log")
171
+ return state;
172
+ return { ...state, view: "detail", follow: true, detailScrollOffset: 0 };
165
173
  case "detail-navigate": {
166
174
  const list = action.filtered;
167
175
  if (list.length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {