patchrelay 0.19.0 → 0.20.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/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +43 -4
- package/dist/cli/watch/HelpBar.js +1 -1
- package/dist/db.js +4 -0
- package/dist/github-webhook-handler.js +57 -1
- package/dist/http.js +16 -0
- package/dist/service.js +19 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useReducer, useMemo, useCallback } from "react";
|
|
3
|
-
import { Box, useApp, useInput } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useReducer, useMemo, useCallback, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { watchReducer, initialWatchState, filterIssues } from "./watch-state.js";
|
|
5
5
|
import { useWatchStream } from "./use-watch-stream.js";
|
|
6
6
|
import { useDetailStream } from "./use-detail-stream.js";
|
|
@@ -8,6 +8,17 @@ import { useFeedStream } from "./use-feed-stream.js";
|
|
|
8
8
|
import { IssueListView } from "./IssueListView.js";
|
|
9
9
|
import { IssueDetailView } from "./IssueDetailView.js";
|
|
10
10
|
import { FeedView } from "./FeedView.js";
|
|
11
|
+
async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
12
|
+
const headers = { "content-type": "application/json" };
|
|
13
|
+
if (bearerToken)
|
|
14
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
15
|
+
await fetch(new URL(`/api/issues/${encodeURIComponent(issueKey)}/prompt`, baseUrl), {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers,
|
|
18
|
+
body: JSON.stringify({ text }),
|
|
19
|
+
signal: AbortSignal.timeout(5000),
|
|
20
|
+
}).catch(() => { });
|
|
21
|
+
}
|
|
11
22
|
async function postRetry(baseUrl, issueKey, bearerToken) {
|
|
12
23
|
const headers = { "content-type": "application/json" };
|
|
13
24
|
if (bearerToken)
|
|
@@ -28,12 +39,37 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
28
39
|
useWatchStream({ baseUrl, bearerToken, dispatch });
|
|
29
40
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch });
|
|
30
41
|
useFeedStream({ baseUrl, bearerToken, active: state.view === "feed", dispatch });
|
|
42
|
+
const [promptMode, setPromptMode] = useState(false);
|
|
43
|
+
const [promptBuffer, setPromptBuffer] = useState("");
|
|
31
44
|
const handleRetry = useCallback(() => {
|
|
32
45
|
if (state.activeDetailKey) {
|
|
33
46
|
void postRetry(baseUrl, state.activeDetailKey, bearerToken);
|
|
34
47
|
}
|
|
35
48
|
}, [baseUrl, bearerToken, state.activeDetailKey]);
|
|
49
|
+
const handlePromptSubmit = useCallback(() => {
|
|
50
|
+
if (state.activeDetailKey && promptBuffer.trim()) {
|
|
51
|
+
void postPrompt(baseUrl, state.activeDetailKey, promptBuffer.trim(), bearerToken);
|
|
52
|
+
}
|
|
53
|
+
setPromptMode(false);
|
|
54
|
+
setPromptBuffer("");
|
|
55
|
+
}, [baseUrl, bearerToken, state.activeDetailKey, promptBuffer]);
|
|
36
56
|
useInput((input, key) => {
|
|
57
|
+
if (promptMode) {
|
|
58
|
+
if (key.escape) {
|
|
59
|
+
setPromptMode(false);
|
|
60
|
+
setPromptBuffer("");
|
|
61
|
+
}
|
|
62
|
+
else if (key.return) {
|
|
63
|
+
handlePromptSubmit();
|
|
64
|
+
}
|
|
65
|
+
else if (key.backspace || key.delete) {
|
|
66
|
+
setPromptBuffer((b) => b.slice(0, -1));
|
|
67
|
+
}
|
|
68
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
69
|
+
setPromptBuffer((b) => b + input);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
37
73
|
if (input === "q") {
|
|
38
74
|
exit();
|
|
39
75
|
return;
|
|
@@ -68,6 +104,9 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
68
104
|
else if (input === "r") {
|
|
69
105
|
handleRetry();
|
|
70
106
|
}
|
|
107
|
+
else if (input === "p") {
|
|
108
|
+
setPromptMode(true);
|
|
109
|
+
}
|
|
71
110
|
else if (input === "j" || key.downArrow) {
|
|
72
111
|
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
73
112
|
}
|
|
@@ -81,5 +120,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
});
|
|
84
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, allIssues: filtered, activeDetailKey: state.activeDetailKey })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
123
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, allIssues: filtered, activeDetailKey: state.activeDetailKey }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
85
124
|
}
|
|
@@ -7,7 +7,7 @@ const HELP_TEXT = {
|
|
|
7
7
|
};
|
|
8
8
|
export function HelpBar({ view, follow }) {
|
|
9
9
|
const text = view === "detail"
|
|
10
|
-
? `j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} r: retry q: quit`
|
|
10
|
+
? `j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} p: prompt r: retry q: quit`
|
|
11
11
|
: HELP_TEXT[view];
|
|
12
12
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
13
13
|
}
|
package/dist/db.js
CHANGED
|
@@ -233,6 +233,10 @@ export class PatchRelayDatabase {
|
|
|
233
233
|
const row = this.connection.prepare("SELECT * FROM issues WHERE branch_name = ?").get(branchName);
|
|
234
234
|
return row ? mapIssueRow(row) : undefined;
|
|
235
235
|
}
|
|
236
|
+
getIssueByPrNumber(prNumber) {
|
|
237
|
+
const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
|
|
238
|
+
return row ? mapIssueRow(row) : undefined;
|
|
239
|
+
}
|
|
236
240
|
listIssuesReadyForExecution() {
|
|
237
241
|
const rows = this.connection
|
|
238
242
|
.prepare("SELECT project_id, linear_issue_id FROM issues WHERE (pending_run_type IS NOT NULL OR pending_merge_prep = 1) AND active_run_id IS NULL")
|
|
@@ -25,14 +25,16 @@ export class GitHubWebhookHandler {
|
|
|
25
25
|
enqueueIssue;
|
|
26
26
|
mergeQueue;
|
|
27
27
|
logger;
|
|
28
|
+
codex;
|
|
28
29
|
feed;
|
|
29
|
-
constructor(config, db, linearProvider, enqueueIssue, mergeQueue, logger, feed) {
|
|
30
|
+
constructor(config, db, linearProvider, enqueueIssue, mergeQueue, logger, codex, feed) {
|
|
30
31
|
this.config = config;
|
|
31
32
|
this.db = db;
|
|
32
33
|
this.linearProvider = linearProvider;
|
|
33
34
|
this.enqueueIssue = enqueueIssue;
|
|
34
35
|
this.mergeQueue = mergeQueue;
|
|
35
36
|
this.logger = logger;
|
|
37
|
+
this.codex = codex;
|
|
36
38
|
this.feed = feed;
|
|
37
39
|
}
|
|
38
40
|
async acceptGitHubWebhook(params) {
|
|
@@ -100,6 +102,10 @@ export class GitHubWebhookHandler {
|
|
|
100
102
|
}
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
105
|
+
if (params.eventType === "issue_comment") {
|
|
106
|
+
await this.handlePrComment(payload);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
103
109
|
const event = normalizeGitHubWebhook({
|
|
104
110
|
eventType: params.eventType,
|
|
105
111
|
payload: payload,
|
|
@@ -282,6 +288,56 @@ export class GitHubWebhookHandler {
|
|
|
282
288
|
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear session from GitHub webhook");
|
|
283
289
|
}
|
|
284
290
|
}
|
|
291
|
+
async handlePrComment(payload) {
|
|
292
|
+
if (payload.action !== "created")
|
|
293
|
+
return;
|
|
294
|
+
const issuePayload = payload.issue;
|
|
295
|
+
const comment = payload.comment;
|
|
296
|
+
if (!issuePayload || !comment)
|
|
297
|
+
return;
|
|
298
|
+
if (!issuePayload.pull_request)
|
|
299
|
+
return; // only PR comments
|
|
300
|
+
const body = typeof comment.body === "string" ? comment.body : "";
|
|
301
|
+
if (!body.trim())
|
|
302
|
+
return;
|
|
303
|
+
const user = comment.user;
|
|
304
|
+
const author = typeof user?.login === "string" ? user.login : "unknown";
|
|
305
|
+
if (typeof user?.type === "string" && user.type === "Bot")
|
|
306
|
+
return;
|
|
307
|
+
const prNumber = typeof issuePayload.number === "number" ? issuePayload.number : undefined;
|
|
308
|
+
if (!prNumber)
|
|
309
|
+
return;
|
|
310
|
+
const issue = this.db.getIssueByPrNumber(prNumber);
|
|
311
|
+
if (!issue)
|
|
312
|
+
return;
|
|
313
|
+
this.feed?.publish({
|
|
314
|
+
level: "info",
|
|
315
|
+
kind: "comment",
|
|
316
|
+
issueKey: issue.issueKey,
|
|
317
|
+
projectId: issue.projectId,
|
|
318
|
+
stage: issue.factoryState,
|
|
319
|
+
status: "pr_comment",
|
|
320
|
+
summary: `GitHub PR comment from ${author}`,
|
|
321
|
+
detail: body.slice(0, 200),
|
|
322
|
+
});
|
|
323
|
+
if (issue.activeRunId) {
|
|
324
|
+
const run = this.db.getRun(issue.activeRunId);
|
|
325
|
+
if (run?.threadId && run.turnId) {
|
|
326
|
+
try {
|
|
327
|
+
await this.codex.steerTurn({
|
|
328
|
+
threadId: run.threadId,
|
|
329
|
+
turnId: run.turnId,
|
|
330
|
+
input: `GitHub PR comment from ${author}:\n\n${body}`,
|
|
331
|
+
});
|
|
332
|
+
this.logger.info({ issueKey: issue.issueKey, author }, "Forwarded GitHub PR comment to active run");
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
336
|
+
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to forward GitHub PR comment");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
285
341
|
}
|
|
286
342
|
function resolveCheckClass(checkName, project) {
|
|
287
343
|
if (!checkName || !project)
|
package/dist/http.js
CHANGED
|
@@ -309,6 +309,22 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
309
309
|
}
|
|
310
310
|
return reply.send({ ok: true, ...result });
|
|
311
311
|
});
|
|
312
|
+
app.post("/api/issues/:issueKey/prompt", async (request, reply) => {
|
|
313
|
+
const issueKey = request.params.issueKey;
|
|
314
|
+
const body = request.body;
|
|
315
|
+
const text = body?.text;
|
|
316
|
+
if (!text || typeof text !== "string") {
|
|
317
|
+
return reply.code(400).send({ ok: false, reason: "missing text field" });
|
|
318
|
+
}
|
|
319
|
+
const result = await service.promptIssue(issueKey, text);
|
|
320
|
+
if (!result) {
|
|
321
|
+
return reply.code(404).send({ ok: false, reason: "issue_not_found" });
|
|
322
|
+
}
|
|
323
|
+
if ("error" in result) {
|
|
324
|
+
return reply.code(409).send({ ok: false, reason: result.error });
|
|
325
|
+
}
|
|
326
|
+
return reply.send({ ok: true, delivered: true });
|
|
327
|
+
});
|
|
312
328
|
app.get("/api/feed", async (request, reply) => {
|
|
313
329
|
const feedQuery = {
|
|
314
330
|
limit: getPositiveIntegerQueryParam(request, "limit") ?? 50,
|
package/dist/service.js
CHANGED
|
@@ -57,7 +57,7 @@ export class PatchRelayService {
|
|
|
57
57
|
})();
|
|
58
58
|
});
|
|
59
59
|
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed);
|
|
60
|
-
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), this.mergeQueue, logger, this.feed);
|
|
60
|
+
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), this.mergeQueue, logger, codex, this.feed);
|
|
61
61
|
const runtime = new ServiceRuntime(codex, logger, this.orchestrator, { listIssuesReadyForExecution: () => db.listIssuesReadyForExecution() }, this.webhookHandler, {
|
|
62
62
|
processIssue: async (item) => {
|
|
63
63
|
const issue = db.getIssue(item.projectId, item.issueId);
|
|
@@ -218,6 +218,24 @@ export class PatchRelayService {
|
|
|
218
218
|
this.codex.on("notification", handler);
|
|
219
219
|
return () => { this.codex.off("notification", handler); };
|
|
220
220
|
}
|
|
221
|
+
async promptIssue(issueKey, text) {
|
|
222
|
+
const issue = this.db.getIssueByKey(issueKey);
|
|
223
|
+
if (!issue)
|
|
224
|
+
return undefined;
|
|
225
|
+
if (!issue.activeRunId)
|
|
226
|
+
return { error: "No active run" };
|
|
227
|
+
const run = this.db.getRun(issue.activeRunId);
|
|
228
|
+
if (!run?.threadId || !run.turnId)
|
|
229
|
+
return { error: "No active thread or turn" };
|
|
230
|
+
try {
|
|
231
|
+
await this.codex.steerTurn({ threadId: run.threadId, turnId: run.turnId, input: `Operator prompt from watch TUI:\n\n${text}` });
|
|
232
|
+
return { delivered: true };
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
236
|
+
return { error: `Failed to deliver prompt: ${msg}` };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
221
239
|
retryIssue(issueKey) {
|
|
222
240
|
const issue = this.db.getIssueByKey(issueKey);
|
|
223
241
|
if (!issue)
|