patchrelay 0.15.0 → 0.17.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.
- package/README.md +11 -8
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +20 -2
- package/dist/cli/watch/HelpBar.js +1 -1
- package/dist/cli/watch/IssueDetailView.js +27 -11
- package/dist/cli/watch/Timeline.js +14 -0
- package/dist/cli/watch/TimelineRow.js +62 -0
- package/dist/cli/watch/timeline-builder.js +363 -0
- package/dist/cli/watch/use-detail-stream.js +29 -107
- package/dist/cli/watch/watch-state.js +62 -182
- package/dist/config.js +1 -1
- package/dist/db/migrations.js +2 -0
- package/dist/db.js +5 -0
- package/dist/http.js +19 -0
- package/dist/issue-query-service.js +23 -0
- package/dist/linear-session-reporting.js +28 -0
- package/dist/merge-queue.js +39 -3
- package/dist/run-orchestrator.js +45 -1
- package/dist/service.js +40 -1
- package/dist/webhook-handler.js +43 -1
- package/dist/webhooks.js +16 -0
- package/package.json +1 -1
- package/dist/cli/watch/ThreadView.js +0 -26
- package/dist/cli/watch/TurnSection.js +0 -20
package/dist/merge-queue.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { buildMergePrepActivity, buildMergePrepEscalationActivity } from "./linear-session-reporting.js";
|
|
1
2
|
import { execCommand } from "./utils.js";
|
|
3
|
+
const DEFAULT_MERGE_PREP_BUDGET = 3;
|
|
2
4
|
/**
|
|
3
5
|
* Merge queue steward — keeps PatchRelay-managed PR branches up to date
|
|
4
6
|
* with the base branch and enables auto-merge so GitHub merges when CI passes.
|
|
@@ -14,12 +16,14 @@ export class MergeQueue {
|
|
|
14
16
|
enqueueIssue;
|
|
15
17
|
logger;
|
|
16
18
|
feed;
|
|
17
|
-
|
|
19
|
+
onLinearActivity;
|
|
20
|
+
constructor(config, db, enqueueIssue, logger, feed, onLinearActivity) {
|
|
18
21
|
this.config = config;
|
|
19
22
|
this.db = db;
|
|
20
23
|
this.enqueueIssue = enqueueIssue;
|
|
21
24
|
this.logger = logger;
|
|
22
25
|
this.feed = feed;
|
|
26
|
+
this.onLinearActivity = onLinearActivity;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Prepare the front-of-queue issue for merge:
|
|
@@ -43,11 +47,37 @@ export class MergeQueue {
|
|
|
43
47
|
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false });
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
50
|
+
// Retry budget — escalate after repeated infrastructure failures
|
|
51
|
+
const attempts = issue.mergePrepAttempts + 1;
|
|
52
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, mergePrepAttempts: attempts });
|
|
53
|
+
if (attempts > DEFAULT_MERGE_PREP_BUDGET) {
|
|
54
|
+
this.logger.warn({ issueKey: issue.issueKey, attempts }, "Merge prep budget exhausted, escalating");
|
|
55
|
+
this.db.upsertIssue({
|
|
56
|
+
projectId: issue.projectId,
|
|
57
|
+
linearIssueId: issue.linearIssueId,
|
|
58
|
+
factoryState: "escalated",
|
|
59
|
+
pendingMergePrep: false,
|
|
60
|
+
});
|
|
61
|
+
this.feed?.publish({
|
|
62
|
+
level: "error",
|
|
63
|
+
kind: "workflow",
|
|
64
|
+
issueKey: issue.issueKey,
|
|
65
|
+
projectId: issue.projectId,
|
|
66
|
+
stage: "awaiting_queue",
|
|
67
|
+
status: "escalated",
|
|
68
|
+
summary: `Merge prep failed ${attempts - 1} times — escalating for human help`,
|
|
69
|
+
});
|
|
70
|
+
this.onLinearActivity?.(issue, buildMergePrepEscalationActivity(attempts - 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
46
73
|
const repoFullName = project.github?.repoFullName;
|
|
47
74
|
const baseBranch = project.github?.baseBranch ?? "main";
|
|
48
75
|
const gitBin = this.config.runner.gitBin;
|
|
49
76
|
// Enable auto-merge (idempotent)
|
|
50
77
|
const autoMergeOk = repoFullName ? await this.enableAutoMerge(issue, repoFullName) : false;
|
|
78
|
+
if (autoMergeOk) {
|
|
79
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("auto_merge"), { ephemeral: true });
|
|
80
|
+
}
|
|
51
81
|
// Fetch latest base branch
|
|
52
82
|
const fetchResult = await execCommand(gitBin, ["-C", issue.worktreePath, "fetch", "origin", baseBranch], {
|
|
53
83
|
timeoutMs: 60_000,
|
|
@@ -55,6 +85,7 @@ export class MergeQueue {
|
|
|
55
85
|
if (fetchResult.exitCode !== 0) {
|
|
56
86
|
// Transient failure — leave pendingMergePrep set so the next event retries.
|
|
57
87
|
this.logger.warn({ issueKey: issue.issueKey, stderr: fetchResult.stderr?.slice(0, 300) }, "Merge prep: fetch failed, will retry on next event");
|
|
88
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("fetch_retry"), { ephemeral: true });
|
|
58
89
|
return;
|
|
59
90
|
}
|
|
60
91
|
// Merge base branch into the PR branch
|
|
@@ -72,6 +103,7 @@ export class MergeQueue {
|
|
|
72
103
|
pendingRunType: "queue_repair",
|
|
73
104
|
pendingRunContextJson: JSON.stringify({ failureReason: "merge_conflict" }),
|
|
74
105
|
pendingMergePrep: false,
|
|
106
|
+
mergePrepAttempts: 0,
|
|
75
107
|
});
|
|
76
108
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
77
109
|
this.feed?.publish({
|
|
@@ -83,12 +115,13 @@ export class MergeQueue {
|
|
|
83
115
|
status: "conflict",
|
|
84
116
|
summary: `Merge conflict with ${baseBranch} — queue repair enqueued`,
|
|
85
117
|
});
|
|
118
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("conflict"));
|
|
86
119
|
return;
|
|
87
120
|
}
|
|
88
121
|
// Check if merge was a no-op (already up to date)
|
|
89
122
|
if (mergeResult.stdout?.includes("Already up to date")) {
|
|
90
123
|
this.logger.debug({ issueKey: issue.issueKey }, "Merge prep: branch already up to date");
|
|
91
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false });
|
|
124
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false, mergePrepAttempts: 0 });
|
|
92
125
|
if (!autoMergeOk) {
|
|
93
126
|
this.feed?.publish({
|
|
94
127
|
level: "warn",
|
|
@@ -99,6 +132,7 @@ export class MergeQueue {
|
|
|
99
132
|
status: "blocked",
|
|
100
133
|
summary: "Branch up to date but auto-merge not enabled — check gh auth and repo settings",
|
|
101
134
|
});
|
|
135
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("blocked"));
|
|
102
136
|
}
|
|
103
137
|
return;
|
|
104
138
|
}
|
|
@@ -109,10 +143,11 @@ export class MergeQueue {
|
|
|
109
143
|
if (pushResult.exitCode !== 0) {
|
|
110
144
|
// Push failed — leave pendingMergePrep set so the next event retries.
|
|
111
145
|
this.logger.warn({ issueKey: issue.issueKey, stderr: pushResult.stderr?.slice(0, 300) }, "Merge prep: push failed, will retry on next event");
|
|
146
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("push_retry"), { ephemeral: true });
|
|
112
147
|
return;
|
|
113
148
|
}
|
|
114
149
|
this.logger.info({ issueKey: issue.issueKey, baseBranch }, "Merge prep: branch updated and pushed");
|
|
115
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false });
|
|
150
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false, mergePrepAttempts: 0 });
|
|
116
151
|
this.feed?.publish({
|
|
117
152
|
level: "info",
|
|
118
153
|
kind: "workflow",
|
|
@@ -122,6 +157,7 @@ export class MergeQueue {
|
|
|
122
157
|
status: "prepared",
|
|
123
158
|
summary: `Branch updated to latest ${baseBranch} — CI will run`,
|
|
124
159
|
});
|
|
160
|
+
this.onLinearActivity?.(issue, buildMergePrepActivity("branch_update", baseBranch), { ephemeral: true });
|
|
125
161
|
}
|
|
126
162
|
/**
|
|
127
163
|
* Seed the merge queue on startup: for each project, ensure the front-of-queue
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -72,6 +72,7 @@ function buildRunPrompt(issue, runType, repoPath, context) {
|
|
|
72
72
|
}
|
|
73
73
|
return lines.join("\n");
|
|
74
74
|
}
|
|
75
|
+
const PROGRESS_THROTTLE_MS = 10_000;
|
|
75
76
|
export class RunOrchestrator {
|
|
76
77
|
config;
|
|
77
78
|
db;
|
|
@@ -81,6 +82,7 @@ export class RunOrchestrator {
|
|
|
81
82
|
logger;
|
|
82
83
|
feed;
|
|
83
84
|
worktreeManager;
|
|
85
|
+
progressThrottle = new Map();
|
|
84
86
|
botIdentity;
|
|
85
87
|
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed) {
|
|
86
88
|
this.config = config;
|
|
@@ -253,6 +255,8 @@ export class RunOrchestrator {
|
|
|
253
255
|
eventJson: JSON.stringify(notification.params),
|
|
254
256
|
});
|
|
255
257
|
}
|
|
258
|
+
// Emit ephemeral progress activity to Linear for notable in-flight events
|
|
259
|
+
this.maybeEmitProgressActivity(notification, run);
|
|
256
260
|
if (notification.method !== "turn/completed")
|
|
257
261
|
return;
|
|
258
262
|
const thread = await this.readThreadWithRetry(threadId);
|
|
@@ -286,6 +290,7 @@ export class RunOrchestrator {
|
|
|
286
290
|
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
287
291
|
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType));
|
|
288
292
|
void this.syncLinearSession(failedIssue, { activeRunType: run.runType });
|
|
293
|
+
this.progressThrottle.delete(run.id);
|
|
289
294
|
return;
|
|
290
295
|
}
|
|
291
296
|
// Complete the run
|
|
@@ -304,7 +309,7 @@ export class RunOrchestrator {
|
|
|
304
309
|
postRunState = "done";
|
|
305
310
|
}
|
|
306
311
|
else {
|
|
307
|
-
postRunState = "
|
|
312
|
+
postRunState = "awaiting_review";
|
|
308
313
|
}
|
|
309
314
|
}
|
|
310
315
|
this.db.transaction(() => {
|
|
@@ -347,6 +352,45 @@ export class RunOrchestrator {
|
|
|
347
352
|
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
348
353
|
}));
|
|
349
354
|
void this.syncLinearSession(updatedIssue);
|
|
355
|
+
this.progressThrottle.delete(run.id);
|
|
356
|
+
}
|
|
357
|
+
// ─── In-flight progress ──────────────────────────────────────────
|
|
358
|
+
maybeEmitProgressActivity(notification, run) {
|
|
359
|
+
const activity = this.resolveProgressActivity(notification);
|
|
360
|
+
if (!activity)
|
|
361
|
+
return;
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const lastEmit = this.progressThrottle.get(run.id) ?? 0;
|
|
364
|
+
if (now - lastEmit < PROGRESS_THROTTLE_MS)
|
|
365
|
+
return;
|
|
366
|
+
this.progressThrottle.set(run.id, now);
|
|
367
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
368
|
+
if (issue) {
|
|
369
|
+
void this.emitLinearActivity(issue, activity, { ephemeral: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
resolveProgressActivity(notification) {
|
|
373
|
+
if (notification.method === "item/started") {
|
|
374
|
+
const item = notification.params.item;
|
|
375
|
+
if (!item)
|
|
376
|
+
return undefined;
|
|
377
|
+
const type = typeof item.type === "string" ? item.type : undefined;
|
|
378
|
+
if (type === "commandExecution") {
|
|
379
|
+
const cmd = item.command;
|
|
380
|
+
const cmdStr = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
381
|
+
return { type: "action", action: "Running", parameter: cmdStr?.slice(0, 120) ?? "command" };
|
|
382
|
+
}
|
|
383
|
+
if (type === "mcpToolCall") {
|
|
384
|
+
const server = typeof item.server === "string" ? item.server : "";
|
|
385
|
+
const tool = typeof item.tool === "string" ? item.tool : "";
|
|
386
|
+
return { type: "action", action: "Using", parameter: `${server}/${tool}` };
|
|
387
|
+
}
|
|
388
|
+
if (type === "dynamicToolCall") {
|
|
389
|
+
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
390
|
+
return { type: "action", action: "Using", parameter: tool };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
350
394
|
}
|
|
351
395
|
// ─── Active status for query ──────────────────────────────────────
|
|
352
396
|
async getActiveRunStatus(issueKey) {
|
package/dist/service.js
CHANGED
|
@@ -35,7 +35,27 @@ export class PatchRelayService {
|
|
|
35
35
|
throw new Error("Service runtime enqueueIssue is not initialized");
|
|
36
36
|
};
|
|
37
37
|
this.orchestrator = new RunOrchestrator(config, db, codex, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed);
|
|
38
|
-
this.mergeQueue = new MergeQueue(config, db, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed)
|
|
38
|
+
this.mergeQueue = new MergeQueue(config, db, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed, (issue, content, options) => {
|
|
39
|
+
if (!issue.agentSessionId)
|
|
40
|
+
return;
|
|
41
|
+
void (async () => {
|
|
42
|
+
try {
|
|
43
|
+
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
44
|
+
if (!linear)
|
|
45
|
+
return;
|
|
46
|
+
const allowEphemeral = content.type === "thought" || content.type === "action";
|
|
47
|
+
await linear.createAgentActivity({
|
|
48
|
+
agentSessionId: issue.agentSessionId,
|
|
49
|
+
content,
|
|
50
|
+
...(options?.ephemeral && allowEphemeral ? { ephemeral: true } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
55
|
+
logger.warn({ issueKey: issue.issueKey, type: content.type, error: msg }, "Failed to emit merge-prep Linear activity");
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
});
|
|
39
59
|
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed);
|
|
40
60
|
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), this.mergeQueue, logger, this.feed);
|
|
41
61
|
const runtime = new ServiceRuntime(codex, logger, this.orchestrator, { listIssuesReadyForExecution: () => db.listIssuesReadyForExecution() }, this.webhookHandler, {
|
|
@@ -198,6 +218,22 @@ export class PatchRelayService {
|
|
|
198
218
|
this.codex.on("notification", handler);
|
|
199
219
|
return () => { this.codex.off("notification", handler); };
|
|
200
220
|
}
|
|
221
|
+
retryIssue(issueKey) {
|
|
222
|
+
const issue = this.db.getIssueByKey(issueKey);
|
|
223
|
+
if (!issue)
|
|
224
|
+
return undefined;
|
|
225
|
+
if (issue.activeRunId)
|
|
226
|
+
return { error: "Issue already has an active run" };
|
|
227
|
+
const runType = "implementation";
|
|
228
|
+
this.db.upsertIssue({
|
|
229
|
+
projectId: issue.projectId,
|
|
230
|
+
linearIssueId: issue.linearIssueId,
|
|
231
|
+
pendingRunType: runType,
|
|
232
|
+
factoryState: "delegated",
|
|
233
|
+
});
|
|
234
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
235
|
+
return { issueKey, runType };
|
|
236
|
+
}
|
|
201
237
|
listOperatorFeed(options) {
|
|
202
238
|
return this.feed.list(options);
|
|
203
239
|
}
|
|
@@ -253,6 +289,9 @@ export class PatchRelayService {
|
|
|
253
289
|
async getIssueReport(issueKey) {
|
|
254
290
|
return await this.queryService.getIssueReport(issueKey);
|
|
255
291
|
}
|
|
292
|
+
async getIssueTimeline(issueKey) {
|
|
293
|
+
return await this.queryService.getIssueTimeline(issueKey);
|
|
294
|
+
}
|
|
256
295
|
async getRunEvents(issueKey, runId) {
|
|
257
296
|
return await this.queryService.getRunEvents(issueKey, runId);
|
|
258
297
|
}
|
package/dist/webhook-handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildAgentSessionPlanForIssue, } from "./agent-session-plan.js";
|
|
2
2
|
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
3
|
-
import { buildAlreadyRunningThought, buildDelegationThought, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
|
|
3
|
+
import { buildAlreadyRunningThought, buildDelegationThought, buildPromptDeliveredThought, buildStopConfirmationActivity, } from "./linear-session-reporting.js";
|
|
4
4
|
import { resolveProject, triggerEventAllowed, trustedActorAllowed } from "./project-resolution.js";
|
|
5
5
|
import { normalizeWebhook } from "./webhooks.js";
|
|
6
6
|
import { InstallationWebhookHandler } from "./webhook-installation-handler.js";
|
|
@@ -210,6 +210,11 @@ export class WebhookHandler {
|
|
|
210
210
|
});
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
|
+
// Stop signal — halt active work and confirm disengagement
|
|
214
|
+
if (normalized.triggerEvent === "agentSignal" && normalized.agentSession.signal === "stop") {
|
|
215
|
+
await this.handleStopSignal(normalized, project, trackedIssue, existingIssue, activeRun, linear);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
213
218
|
if (normalized.triggerEvent !== "agentPrompted")
|
|
214
219
|
return;
|
|
215
220
|
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
@@ -251,6 +256,43 @@ export class WebhookHandler {
|
|
|
251
256
|
await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(desiredStage, "prompt"), { ephemeral: true });
|
|
252
257
|
}
|
|
253
258
|
}
|
|
259
|
+
// ─── Stop signal handling ────────────────────────────────────────
|
|
260
|
+
async handleStopSignal(normalized, project, trackedIssue, existingIssue, activeRun, linear) {
|
|
261
|
+
const issueId = normalized.issue.id;
|
|
262
|
+
const sessionId = normalized.agentSession.id;
|
|
263
|
+
// Best-effort halt: steer the active Codex turn with a stop instruction
|
|
264
|
+
if (activeRun?.threadId && activeRun.turnId) {
|
|
265
|
+
try {
|
|
266
|
+
await this.codex.steerTurn({
|
|
267
|
+
threadId: activeRun.threadId,
|
|
268
|
+
turnId: activeRun.turnId,
|
|
269
|
+
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop signal");
|
|
274
|
+
}
|
|
275
|
+
this.db.finishRun(activeRun.id, { status: "released", threadId: activeRun.threadId, turnId: activeRun.turnId });
|
|
276
|
+
}
|
|
277
|
+
this.db.upsertIssue({
|
|
278
|
+
projectId: project.id,
|
|
279
|
+
linearIssueId: issueId,
|
|
280
|
+
activeRunId: null,
|
|
281
|
+
factoryState: "awaiting_input",
|
|
282
|
+
agentSessionId: sessionId,
|
|
283
|
+
});
|
|
284
|
+
this.feed?.publish({
|
|
285
|
+
level: "info",
|
|
286
|
+
kind: "agent",
|
|
287
|
+
projectId: project.id,
|
|
288
|
+
issueKey: trackedIssue?.issueKey,
|
|
289
|
+
status: "stopped",
|
|
290
|
+
summary: "Stop signal received — work halted",
|
|
291
|
+
});
|
|
292
|
+
const updatedIssue = this.db.getIssue(project.id, issueId);
|
|
293
|
+
await this.publishAgentActivity(linear, sessionId, buildStopConfirmationActivity());
|
|
294
|
+
await this.syncAgentSession(linear, sessionId, updatedIssue ?? trackedIssue);
|
|
295
|
+
}
|
|
254
296
|
// ─── Comment handling (inlined) ───────────────────────────────────
|
|
255
297
|
async handleComment(normalized, project, trackedIssue) {
|
|
256
298
|
if ((normalized.triggerEvent !== "commentCreated" && normalized.triggerEvent !== "commentUpdated") ||
|
package/dist/webhooks.js
CHANGED
|
@@ -46,6 +46,18 @@ function deriveTriggerEvent(payload) {
|
|
|
46
46
|
["resource", "agentSession"],
|
|
47
47
|
])) || Boolean(getString(data, "agentSessionId"));
|
|
48
48
|
if (payload.type === "AgentSessionEvent" || payload.type === "AgentSession" || hasAgentSession) {
|
|
49
|
+
// Detect signal-bearing payloads (e.g. stop signal from Linear)
|
|
50
|
+
const agentActivityForSignal = getFirstNestedRecord(data, [
|
|
51
|
+
["agentActivity"],
|
|
52
|
+
["agentSession", "agentActivity"],
|
|
53
|
+
["session", "agentActivity"],
|
|
54
|
+
["agentSessionEvent", "agentActivity"],
|
|
55
|
+
["payload", "agentActivity"],
|
|
56
|
+
["resource", "agentActivity"],
|
|
57
|
+
]);
|
|
58
|
+
if (agentActivityForSignal && getString(agentActivityForSignal, "signal")) {
|
|
59
|
+
return "agentSignal";
|
|
60
|
+
}
|
|
49
61
|
if (payload.action === "created" || payload.action === "create") {
|
|
50
62
|
return "agentSessionCreated";
|
|
51
63
|
}
|
|
@@ -310,11 +322,15 @@ function extractAgentSessionMetadata(payload) {
|
|
|
310
322
|
getString(commentRecord ?? {}, "body") ??
|
|
311
323
|
getString(data, "body");
|
|
312
324
|
const issueCommentId = getString(commentRecord ?? {}, "id") ?? getString(data, "issueCommentId");
|
|
325
|
+
const signal = getString(agentActivity ?? {}, "signal");
|
|
326
|
+
const signalMetadata = asRecord((agentActivity ?? {}).signalMetadata);
|
|
313
327
|
return {
|
|
314
328
|
id,
|
|
315
329
|
...(promptContext ? { promptContext } : {}),
|
|
316
330
|
...(promptBody ? { promptBody } : {}),
|
|
317
331
|
...(issueCommentId ? { issueCommentId } : {}),
|
|
332
|
+
...(signal ? { signal } : {}),
|
|
333
|
+
...(signalMetadata ? { signalMetadata } : {}),
|
|
318
334
|
};
|
|
319
335
|
}
|
|
320
336
|
function extractInstallationMetadata(payload) {
|
package/package.json
CHANGED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { TurnSection } from "./TurnSection.js";
|
|
4
|
-
function planStepSymbol(status) {
|
|
5
|
-
if (status === "completed")
|
|
6
|
-
return "\u2713";
|
|
7
|
-
if (status === "inProgress")
|
|
8
|
-
return "\u25b8";
|
|
9
|
-
return " ";
|
|
10
|
-
}
|
|
11
|
-
function planStepColor(status) {
|
|
12
|
-
if (status === "completed")
|
|
13
|
-
return "green";
|
|
14
|
-
if (status === "inProgress")
|
|
15
|
-
return "yellow";
|
|
16
|
-
return "white";
|
|
17
|
-
}
|
|
18
|
-
export function ThreadView({ thread, follow }) {
|
|
19
|
-
const visibleTurns = follow && thread.turns.length > 1
|
|
20
|
-
? thread.turns.slice(-1)
|
|
21
|
-
: thread.turns;
|
|
22
|
-
const turnOffset = follow && thread.turns.length > 1
|
|
23
|
-
? thread.turns.length - 1
|
|
24
|
-
: 0;
|
|
25
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["Thread: ", thread.threadId.slice(0, 16)] }), _jsxs(Text, { dimColor: true, children: ["Status: ", thread.status] }), _jsxs(Text, { dimColor: true, children: ["Turns: ", thread.turns.length] })] }), thread.plan && thread.plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Plan:" }), thread.plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`)))] })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visibleTurns.map((turn, i) => (_jsx(TurnSection, { turn: turn, index: i + turnOffset, follow: follow }, turn.id))) })] }));
|
|
26
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { ItemLine } from "./ItemLine.js";
|
|
4
|
-
function turnStatusColor(status) {
|
|
5
|
-
if (status === "completed")
|
|
6
|
-
return "green";
|
|
7
|
-
if (status === "failed" || status === "interrupted")
|
|
8
|
-
return "red";
|
|
9
|
-
if (status === "inProgress")
|
|
10
|
-
return "yellow";
|
|
11
|
-
return "white";
|
|
12
|
-
}
|
|
13
|
-
const FOLLOW_TAIL_SIZE = 8;
|
|
14
|
-
export function TurnSection({ turn, index, follow }) {
|
|
15
|
-
const items = follow && turn.items.length > FOLLOW_TAIL_SIZE
|
|
16
|
-
? turn.items.slice(-FOLLOW_TAIL_SIZE)
|
|
17
|
-
: turn.items;
|
|
18
|
-
const skipped = turn.items.length - items.length;
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Turn #", index + 1] }), _jsx(Text, { color: turnStatusColor(turn.status), children: turn.status }), _jsxs(Text, { dimColor: true, children: ["(", turn.items.length, " items)"] })] }), skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier items"] }), items.map((item, i) => (_jsx(ItemLine, { item: item, isLast: i === items.length - 1 }, item.id)))] }));
|
|
20
|
-
}
|