patchrelay 0.25.4 → 0.25.6

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.25.4",
4
- "commit": "b0b9d22851d4",
5
- "builtAt": "2026-03-26T20:41:12.209Z"
3
+ "version": "0.25.6",
4
+ "commit": "427f9c51ab49",
5
+ "builtAt": "2026-03-26T21:20:50.822Z"
6
6
  }
@@ -1,9 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- function truncate(text, max) {
4
- const line = text.replace(/\s+/g, " ").trim();
5
- return line.length > max ? `${line.slice(0, Math.max(0, max - 3))}...` : line;
6
- }
7
3
  function cleanCommand(raw) {
8
4
  const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
9
5
  if (bashMatch?.[1])
@@ -21,7 +17,7 @@ function summarizeToolCall(item) {
21
17
  return `used ${item.toolName ?? item.type}`;
22
18
  }
23
19
  function summarizeText(item) {
24
- return truncate(item.text ?? "", 160);
20
+ return (item.text ?? "").replace(/\s+/g, " ").trim();
25
21
  }
26
22
  function itemPrefix(item) {
27
23
  if (item.type === "commandExecution")
@@ -35,7 +31,7 @@ function itemText(item) {
35
31
  case "reasoning":
36
32
  return summarizeText(item);
37
33
  case "commandExecution":
38
- return truncate(cleanCommand(item.command ?? "?"), 140);
34
+ return cleanCommand(item.command ?? "?");
39
35
  case "fileChange":
40
36
  return summarizeFileChange(item);
41
37
  case "mcpToolCall":
@@ -62,5 +58,5 @@ export function ItemLine({ item }) {
62
58
  return _jsx(_Fragment, {});
63
59
  }
64
60
  const color = itemColor(item);
65
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "wrap", ...(color ? { color } : {}), children: [itemPrefix(item), text] }), item.output && item.status === "inProgress" && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: truncate(item.output.split("\n").filter(Boolean).at(-1) ?? "", 120) }))] }));
61
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { wrap: "wrap", bold: item.type === "agentMessage", ...(color ? { color } : {}), children: [itemPrefix(item), text] }), item.output && item.status === "inProgress" && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: item.output.split("\n").filter(Boolean).at(-1) ?? "" }) }))] }));
66
62
  }
@@ -21,5 +21,5 @@ export function Timeline({ entries, follow, mode }) {
21
21
  if (displayRows.length === 0) {
22
22
  return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
23
23
  }
24
- 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)))] }));
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)))] }));
25
25
  }
@@ -51,31 +51,53 @@ function detailPrefix(detail) {
51
51
  return "$ ";
52
52
  return "";
53
53
  }
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
+ }
54
75
  function FeedRow({ entry }) {
55
76
  const label = entry.feed.status ?? entry.feed.feedKind;
56
- return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: "cyan", children: label.padEnd(12) }), _jsxs(Text, { children: [" ", entry.feed.summary] })] }));
77
+ 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: _jsx(Text, { wrap: "wrap", children: entry.feed.summary }) })] }));
57
78
  }
58
- function RunRow({ entry }) {
79
+ function RunRow({ entry, mode, }) {
59
80
  const run = entry.run;
60
81
  const color = runStatusColor(run.status);
61
82
  const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
62
- return (_jsxs(Box, { flexDirection: "column", 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, { color: color, children: [" ", runStatusLabel(run.status)] }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), entry.details.map((detail, index) => (_jsxs(Box, { paddingLeft: 6, children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : {}), children: [detailPrefix(detail), detail.text] })] }, `${entry.id}-detail-${index}`)))] }));
83
+ const showVerboseItems = mode === "verbose" && entry.items.length > 0;
84
+ 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}`)))] }));
63
85
  }
64
- function ItemRow({ entry }) {
65
- return (_jsx(Box, { paddingLeft: 6, children: _jsx(ItemLine, { item: entry.item }) }));
86
+ function ItemRow({ entry, mode, }) {
87
+ 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 }) })] }));
66
88
  }
67
89
  function CIChecksRow({ entry }) {
68
90
  const ci = entry.ciChecks;
69
- return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", children: "checks".padEnd(12) }), _jsx(Text, { children: " " }), ci.checks.map((check, i) => (_jsxs(Text, { children: [_jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsxs(Text, { dimColor: true, children: [check.name, " "] })] }, `c-${i}`)))] }));
91
+ 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}`))) })] }));
70
92
  }
71
- export function TimelineRow({ entry }) {
93
+ export function TimelineRow({ entry, mode }) {
72
94
  switch (entry.kind) {
73
95
  case "feed":
74
96
  return _jsx(FeedRow, { entry: entry });
75
97
  case "run":
76
- return _jsx(RunRow, { entry: entry });
98
+ return _jsx(RunRow, { entry: entry, mode: mode });
77
99
  case "item":
78
- return _jsx(ItemRow, { entry: entry });
100
+ return _jsx(ItemRow, { entry: entry, mode: mode });
79
101
  case "ci-checks":
80
102
  return _jsx(CIChecksRow, { entry: entry });
81
103
  }
@@ -2,52 +2,98 @@ export function buildTimelineRows(entries, mode) {
2
2
  return mode === "compact" ? buildCompactTimelineRows(entries) : buildVerboseTimelineRows(entries);
3
3
  }
4
4
  function buildVerboseTimelineRows(entries) {
5
- return entries.flatMap((entry) => {
5
+ const rows = [];
6
+ const runs = new Map();
7
+ for (const entry of entries) {
8
+ if (entry.kind === "run-start" && entry.runId !== undefined) {
9
+ const existing = runs.get(entry.runId);
10
+ if (!existing) {
11
+ const run = { ...entry.run };
12
+ runs.set(entry.runId, {
13
+ id: `run-${entry.runId}`,
14
+ at: run.startedAt,
15
+ run,
16
+ items: [],
17
+ endedAt: run.endedAt,
18
+ });
19
+ }
20
+ continue;
21
+ }
22
+ if (entry.kind === "run-end" && entry.runId !== undefined) {
23
+ const existing = runs.get(entry.runId);
24
+ if (existing) {
25
+ existing.run = { ...entry.run };
26
+ existing.endedAt = entry.run?.endedAt;
27
+ }
28
+ else {
29
+ const run = { ...entry.run };
30
+ runs.set(entry.runId, {
31
+ id: `run-${entry.runId}`,
32
+ at: run.startedAt,
33
+ run,
34
+ items: [],
35
+ endedAt: run.endedAt,
36
+ });
37
+ }
38
+ continue;
39
+ }
40
+ if (entry.kind === "item" && entry.runId !== undefined && runs.has(entry.runId)) {
41
+ runs.get(entry.runId).items.push(entry.item);
42
+ continue;
43
+ }
6
44
  switch (entry.kind) {
7
- case "run-start":
8
- return [{
9
- id: entry.id,
10
- kind: "run",
11
- at: entry.at,
12
- finalized: false,
13
- run: entry.run,
14
- details: [],
15
- }];
16
- case "run-end":
17
- return [{
18
- id: entry.id,
19
- kind: "run",
20
- at: entry.at,
21
- finalized: true,
22
- run: entry.run,
23
- details: [],
24
- }];
25
45
  case "feed":
26
- return [{
27
- id: entry.id,
28
- kind: "feed",
29
- at: entry.at,
30
- finalized: true,
31
- feed: entry.feed,
32
- }];
46
+ rows.push({
47
+ id: entry.id,
48
+ kind: "feed",
49
+ at: entry.at,
50
+ finalized: true,
51
+ feed: entry.feed,
52
+ });
53
+ break;
33
54
  case "ci-checks":
34
- return [{
35
- id: entry.id,
36
- kind: "ci-checks",
37
- at: entry.at,
38
- finalized: true,
39
- ciChecks: entry.ciChecks,
40
- }];
55
+ rows.push({
56
+ id: entry.id,
57
+ kind: "ci-checks",
58
+ at: entry.at,
59
+ finalized: true,
60
+ ciChecks: entry.ciChecks,
61
+ });
62
+ break;
41
63
  case "item":
42
- return [{
43
- id: entry.id,
44
- kind: "item",
45
- at: entry.at,
46
- finalized: entry.item?.status !== "inProgress",
47
- item: entry.item,
48
- }];
64
+ rows.push({
65
+ id: entry.id,
66
+ kind: "item",
67
+ at: entry.at,
68
+ finalized: entry.item?.status !== "inProgress",
69
+ item: entry.item,
70
+ });
71
+ break;
49
72
  }
73
+ }
74
+ for (const [runId, run] of runs) {
75
+ rows.push({
76
+ id: run.id,
77
+ kind: "run",
78
+ at: run.at,
79
+ finalized: run.items.every((item) => item.status !== "inProgress") && run.run.status !== "running",
80
+ run: { ...run.run, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
81
+ details: [],
82
+ items: entries
83
+ .filter((entry) => entry.kind === "item" && entry.runId === runId)
84
+ .map((entry) => ({ at: entry.at, item: entry.item })),
85
+ });
86
+ }
87
+ rows.sort((left, right) => {
88
+ const cmp = left.at.localeCompare(right.at);
89
+ if (cmp !== 0)
90
+ return cmp;
91
+ const kindCmp = rowKindOrder(left.kind) - rowKindOrder(right.kind);
92
+ if (kindCmp !== 0)
93
+ return kindCmp;
94
+ return left.id.localeCompare(right.id);
50
95
  });
96
+ return rows;
51
97
  }
52
98
  function buildCompactTimelineRows(entries) {
53
99
  const rows = [];
@@ -130,7 +176,8 @@ function buildCompactTimelineRows(entries) {
130
176
  at: run.at,
131
177
  finalized: status !== "running",
132
178
  run: { ...run.run, status, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
133
- details: summarizeRunDetails(run.items, status),
179
+ details: summarizeRunDetails(run.items),
180
+ items: [],
134
181
  });
135
182
  }
136
183
  rows.sort((left, right) => {
@@ -162,7 +209,7 @@ function resolveCompactRunStatus(run, items) {
162
209
  }
163
210
  return run.status === "queued" ? "queued" : "running";
164
211
  }
165
- function summarizeRunDetails(items, status) {
212
+ function summarizeRunDetails(items) {
166
213
  const details = [];
167
214
  const latestAgentMessage = findLatest(items, (item) => item.type === "agentMessage" && Boolean(item.text?.trim()));
168
215
  const latestUserMessage = findLatest(items, (item) => item.type === "userMessage" && Boolean(item.text?.trim()));
@@ -172,13 +219,13 @@ function summarizeRunDetails(items, status) {
172
219
  if (latestUserMessage && !latestAgentMessage) {
173
220
  details.push({
174
221
  tone: "user",
175
- text: `you: ${summarizeNarrative(latestUserMessage.text ?? "", 120)}`,
222
+ text: `you: ${summarizeNarrative(latestUserMessage.text ?? "")}`,
176
223
  });
177
224
  }
178
225
  if (latestAgentMessage) {
179
226
  details.push({
180
227
  tone: "message",
181
- text: summarizeNarrative(latestAgentMessage.text ?? "", status === "running" ? 140 : 180),
228
+ text: summarizeNarrative(latestAgentMessage.text ?? ""),
182
229
  });
183
230
  }
184
231
  if (latestCommand?.command) {
@@ -204,7 +251,7 @@ function summarizeRunDetails(items, status) {
204
251
  }
205
252
  return dedupeDetails(details).slice(0, 3);
206
253
  }
207
- function summarizeNarrative(input, max) {
254
+ function summarizeNarrative(input) {
208
255
  const normalized = input
209
256
  .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
210
257
  .replace(/`([^`]+)`/g, "$1")
@@ -212,8 +259,7 @@ function summarizeNarrative(input, max) {
212
259
  .trim();
213
260
  if (!normalized)
214
261
  return "";
215
- const sentence = normalized.match(/^(.+?[.!?])(?:\s|$)/)?.[1] ?? normalized;
216
- return truncate(sentence, max);
262
+ return normalized.match(/^(.+?[.!?])(?:\s|$)/)?.[1] ?? normalized;
217
263
  }
218
264
  function summarizeFileChanges(changes) {
219
265
  const files = Array.from(new Set(changes
@@ -275,15 +321,12 @@ function rowKindOrder(kind) {
275
321
  return 3;
276
322
  }
277
323
  }
278
- function truncate(text, max) {
279
- return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
280
- }
281
324
  function cleanCommand(raw) {
282
325
  const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
283
326
  if (bashMatch?.[1])
284
- return truncate(bashMatch[1], 120);
327
+ return bashMatch[1];
285
328
  const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
286
329
  if (bashMatch2?.[1])
287
- return truncate(bashMatch2[1], 120);
288
- return truncate(raw, 120);
330
+ return bashMatch2[1];
331
+ return raw;
289
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.25.4",
3
+ "version": "0.25.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {