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.
- package/README.md +41 -9
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +19 -1
- package/dist/cli/commands/issues.js +18 -56
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/data.js +160 -47
- package/dist/cli/formatters/text.js +51 -90
- package/dist/cli/help.js +15 -8
- package/dist/cli/index.js +3 -58
- package/dist/cli/operator-client.js +0 -82
- package/dist/cli/watch/App.js +21 -12
- package/dist/cli/watch/HelpBar.js +3 -3
- package/dist/cli/watch/IssueDetailView.js +63 -130
- package/dist/cli/watch/IssueRow.js +82 -27
- package/dist/cli/watch/StatusBar.js +8 -4
- package/dist/cli/watch/detail-rows.js +589 -0
- package/dist/cli/watch/render-rich-text.js +226 -0
- package/dist/cli/watch/state-visualization.js +48 -23
- package/dist/cli/watch/timeline-builder.js +2 -1
- package/dist/cli/watch/use-detail-stream.js +10 -104
- package/dist/cli/watch/use-watch-stream.js +11 -102
- package/dist/cli/watch/watch-state.js +129 -56
- package/dist/codex-thread-utils.js +3 -0
- package/dist/db/migrations.js +239 -2
- package/dist/db.js +628 -39
- package/dist/github-app-token.js +7 -0
- package/dist/github-failure-context.js +44 -1
- package/dist/github-rollup.js +47 -0
- package/dist/github-webhook-handler.js +423 -52
- package/dist/github-webhooks.js +7 -0
- package/dist/http.js +12 -264
- package/dist/idle-reconciliation.js +268 -76
- package/dist/issue-query-service.js +221 -129
- package/dist/issue-session-events.js +151 -0
- package/dist/issue-session.js +99 -0
- package/dist/linear-client.js +39 -25
- package/dist/linear-session-reporting.js +12 -0
- package/dist/linear-session-sync.js +253 -24
- package/dist/linear-workflow.js +33 -0
- package/dist/merge-queue-protocol.js +0 -51
- package/dist/preflight.js +1 -4
- package/dist/queue-health-monitor.js +11 -7
- package/dist/run-orchestrator.js +1364 -147
- package/dist/run-reporting.js +5 -3
- package/dist/service.js +279 -102
- package/dist/status-note.js +56 -0
- package/dist/waiting-reason.js +65 -0
- package/dist/webhook-handler.js +270 -79
- package/package.json +3 -2
- package/dist/cli/commands/feed.js +0 -60
- package/dist/cli/watch/FeedView.js +0 -28
- package/dist/cli/watch/use-feed-stream.js +0 -92
|
@@ -15,7 +15,9 @@ export function formatInspect(result) {
|
|
|
15
15
|
const lines = [
|
|
16
16
|
header,
|
|
17
17
|
value("Title", result.issue?.title),
|
|
18
|
-
value("
|
|
18
|
+
value("Session", result.issue?.sessionState),
|
|
19
|
+
value("Waiting reason", result.issue?.waitingReason ?? result.issue?.statusNote),
|
|
20
|
+
value("Debug stage", result.issue?.factoryState),
|
|
19
21
|
result.activeRun ? value("Active run", `${result.activeRun.runType} (${result.activeRun.status})`) : undefined,
|
|
20
22
|
result.latestRun && !result.activeRun ? value("Latest run", `${result.latestRun.runType} (${result.latestRun.status})`) : undefined,
|
|
21
23
|
result.prNumber ? value("PR", `#${result.prNumber}${result.prReviewState ? ` [${result.prReviewState}]` : ""}`) : undefined,
|
|
@@ -35,39 +37,6 @@ export function formatLive(result) {
|
|
|
35
37
|
].filter(Boolean);
|
|
36
38
|
return `${lines.join("\n")}\n`;
|
|
37
39
|
}
|
|
38
|
-
export function formatReport(result) {
|
|
39
|
-
const sections = result.runs.map(({ run, report, summary }) => {
|
|
40
|
-
const changedFiles = report?.fileChanges
|
|
41
|
-
.map((entry) => (typeof entry.path === "string" ? entry.path : undefined))
|
|
42
|
-
.filter(Boolean)
|
|
43
|
-
.join(", ");
|
|
44
|
-
const commands = report?.commands.map((command) => command.command).join(" | ");
|
|
45
|
-
const tools = report?.toolCalls.map((tool) => `${tool.type}:${tool.name}`).join(", ");
|
|
46
|
-
return [
|
|
47
|
-
`${run.runType} #${run.id} ${run.status}`,
|
|
48
|
-
value("Started", run.startedAt),
|
|
49
|
-
value("Ended", run.endedAt),
|
|
50
|
-
value("Thread", run.threadId),
|
|
51
|
-
summary?.latestAssistantMessage ? value("Summary", truncateLine(String(summary.latestAssistantMessage))) : undefined,
|
|
52
|
-
report?.assistantMessages.at(-1) ? value("Assistant conclusion", truncateLine(report.assistantMessages.at(-1))) : undefined,
|
|
53
|
-
commands ? value("Commands", commands) : undefined,
|
|
54
|
-
changedFiles ? value("Changed files", changedFiles) : undefined,
|
|
55
|
-
tools ? value("Tool calls", tools) : undefined,
|
|
56
|
-
]
|
|
57
|
-
.filter(Boolean)
|
|
58
|
-
.join("\n");
|
|
59
|
-
});
|
|
60
|
-
return `${sections.join("\n\n")}\n`;
|
|
61
|
-
}
|
|
62
|
-
export function formatEvents(result) {
|
|
63
|
-
const sections = result.events.map((event) => [
|
|
64
|
-
`#${event.id} ${event.createdAt} ${event.method}`,
|
|
65
|
-
value("Thread", event.threadId),
|
|
66
|
-
value("Turn", event.turnId),
|
|
67
|
-
event.parsedEvent ? JSON.stringify(event.parsedEvent, null, 2) : event.eventJson,
|
|
68
|
-
].join("\n"));
|
|
69
|
-
return `${value("Run", result.run.id)}\n${value("Run type", result.run.runType)}\n\n${sections.join("\n\n")}\n`;
|
|
70
|
-
}
|
|
71
40
|
export function formatWorktree(result, cdOnly) {
|
|
72
41
|
if (cdOnly) {
|
|
73
42
|
return `${result.worktreePath}\n`;
|
|
@@ -106,70 +75,62 @@ export function formatRetry(result) {
|
|
|
106
75
|
.filter(Boolean)
|
|
107
76
|
.join("\n")}\n`;
|
|
108
77
|
}
|
|
78
|
+
function formatTimestampRange(startedAt, endedAt) {
|
|
79
|
+
return endedAt ? `${startedAt} -> ${endedAt}` : `${startedAt} -> running`;
|
|
80
|
+
}
|
|
81
|
+
export function formatSessionHistory(result, buildOpenForThread) {
|
|
82
|
+
const lines = [
|
|
83
|
+
`${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
|
|
84
|
+
value("Worktree", result.worktreePath),
|
|
85
|
+
value("Current thread", result.currentThreadId),
|
|
86
|
+
];
|
|
87
|
+
if (result.sessions.length === 0) {
|
|
88
|
+
lines.push("No recorded app-server sessions.");
|
|
89
|
+
return `${lines.join("\n")}\n`;
|
|
90
|
+
}
|
|
91
|
+
for (const session of result.sessions) {
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push([
|
|
94
|
+
`run #${session.runId}`,
|
|
95
|
+
session.runType,
|
|
96
|
+
session.status,
|
|
97
|
+
formatTimestampRange(session.startedAt, session.endedAt),
|
|
98
|
+
session.isCurrentThread ? "current" : undefined,
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(" "));
|
|
102
|
+
lines.push(value("Thread", session.threadId));
|
|
103
|
+
if (session.parentThreadId) {
|
|
104
|
+
lines.push(value("Parent thread", session.parentThreadId));
|
|
105
|
+
}
|
|
106
|
+
if (session.turnId) {
|
|
107
|
+
lines.push(value("Turn", session.turnId));
|
|
108
|
+
}
|
|
109
|
+
lines.push(value("Events", session.eventCount));
|
|
110
|
+
if (session.summary) {
|
|
111
|
+
lines.push(value("Summary", truncateLine(session.summary)));
|
|
112
|
+
}
|
|
113
|
+
else if (session.failureReason) {
|
|
114
|
+
lines.push(value("Failure", truncateLine(session.failureReason)));
|
|
115
|
+
}
|
|
116
|
+
if (session.threadId && result.worktreePath && buildOpenForThread) {
|
|
117
|
+
const command = buildOpenForThread(session.threadId);
|
|
118
|
+
lines.push(value("Open", formatCommand(command.command, command.args)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return `${lines.join("\n")}\n`;
|
|
122
|
+
}
|
|
109
123
|
export function formatList(items) {
|
|
110
124
|
return `${items
|
|
111
125
|
.map((item) => [
|
|
112
126
|
item.issueKey ?? "-",
|
|
113
127
|
item.currentLinearState ?? "-",
|
|
114
|
-
item.
|
|
128
|
+
item.sessionState ?? "-",
|
|
129
|
+
item.waitingReason ?? "-",
|
|
115
130
|
item.activeRunType ?? "-",
|
|
116
131
|
item.latestRunType ? `${item.latestRunType}:${item.latestRunStatus ?? "-"}` : "-",
|
|
117
132
|
item.updatedAt,
|
|
133
|
+
item.factoryState,
|
|
118
134
|
].join("\t"))
|
|
119
135
|
.join("\n")}\n`;
|
|
120
136
|
}
|
|
121
|
-
function colorize(enabled, code, value) {
|
|
122
|
-
return enabled ? `\u001B[${code}m${value}\u001B[0m` : value;
|
|
123
|
-
}
|
|
124
|
-
function formatFeedStatus(event, color) {
|
|
125
|
-
const raw = event.status ?? event.kind;
|
|
126
|
-
const label = raw.replaceAll("_", " ");
|
|
127
|
-
const padded = label.padEnd(15);
|
|
128
|
-
if (event.level === "error" || raw === "failed" || raw === "delivery_failed") {
|
|
129
|
-
return colorize(color, "31", padded);
|
|
130
|
-
}
|
|
131
|
-
if (event.level === "warn" || raw === "ignored" || raw === "fallback" || raw === "handoff" || raw === "transition_suppressed") {
|
|
132
|
-
return colorize(color, "33", padded);
|
|
133
|
-
}
|
|
134
|
-
if (raw === "running" || raw === "started" || raw === "delegated" || raw === "transition_chosen" || raw === "completed") {
|
|
135
|
-
return colorize(color, "32", padded);
|
|
136
|
-
}
|
|
137
|
-
if (raw === "queued" || raw === "selected") {
|
|
138
|
-
return colorize(color, "36", padded);
|
|
139
|
-
}
|
|
140
|
-
return colorize(color, "2", padded);
|
|
141
|
-
}
|
|
142
|
-
function formatFeedMeta(event, color) {
|
|
143
|
-
const parts = [
|
|
144
|
-
event.workflowId ? `workflow:${event.workflowId}` : undefined,
|
|
145
|
-
event.stage ? `stage:${event.stage}` : undefined,
|
|
146
|
-
event.nextStage ? `next:${event.nextStage}` : undefined,
|
|
147
|
-
].filter(Boolean);
|
|
148
|
-
if (parts.length === 0) {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
return colorize(color, "2", `[${parts.join(" ")}]`);
|
|
152
|
-
}
|
|
153
|
-
export function formatOperatorFeedEvent(event, options) {
|
|
154
|
-
const color = options?.color === true;
|
|
155
|
-
const timestamp = new Date(event.at).toLocaleTimeString("en-GB", { hour12: false });
|
|
156
|
-
const issue = event.issueKey ?? event.projectId ?? "-";
|
|
157
|
-
const meta = formatFeedMeta(event, color);
|
|
158
|
-
const line = [
|
|
159
|
-
colorize(color, "2", timestamp),
|
|
160
|
-
colorize(color, "1", issue.padEnd(10)),
|
|
161
|
-
formatFeedStatus(event, color),
|
|
162
|
-
event.summary,
|
|
163
|
-
...(meta ? [meta] : []),
|
|
164
|
-
].join(" ");
|
|
165
|
-
if (!event.detail) {
|
|
166
|
-
return `${line}\n`;
|
|
167
|
-
}
|
|
168
|
-
return `${line}\n${colorize(color, "2", ` ${truncateLine(event.detail)}`)}\n`;
|
|
169
|
-
}
|
|
170
|
-
export function formatOperatorFeed(result, options) {
|
|
171
|
-
if (result.events.length === 0) {
|
|
172
|
-
return "No feed events yet.\n";
|
|
173
|
-
}
|
|
174
|
-
return result.events.map((event) => formatOperatorFeedEvent(event, options)).join("");
|
|
175
|
-
}
|
package/dist/cli/help.js
CHANGED
|
@@ -36,14 +36,13 @@ export function rootHelpText() {
|
|
|
36
36
|
" issue show <issueKey> [--json] Show the latest known issue state",
|
|
37
37
|
" issue watch <issueKey> [--json] Follow the active run until it settles",
|
|
38
38
|
" issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
39
|
+
" issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
|
|
39
40
|
" service status [--json] Show systemd state and local health",
|
|
40
41
|
" service logs [--lines <count>] [--json] Show recent service logs",
|
|
42
|
+
" serve Run the local PatchRelay service",
|
|
41
43
|
"",
|
|
42
44
|
"Operator commands:",
|
|
43
|
-
"
|
|
44
|
-
" Show operator activity from the daemon",
|
|
45
|
-
" dashboard [--issue <issueKey>] Open the TUI dashboard of issues and runs",
|
|
46
|
-
" serve Run the local PatchRelay service",
|
|
45
|
+
" dashboard [--issue <issueKey>] Open the PatchRelay session dashboard",
|
|
47
46
|
"",
|
|
48
47
|
"Environment options:",
|
|
49
48
|
" --help, -h Show help for the root command or current command group",
|
|
@@ -62,7 +61,6 @@ export function rootHelpText() {
|
|
|
62
61
|
" patchrelay repo list",
|
|
63
62
|
" patchrelay issue list --active",
|
|
64
63
|
" patchrelay issue watch USE-54",
|
|
65
|
-
" patchrelay dashboard",
|
|
66
64
|
" patchrelay service status",
|
|
67
65
|
" patchrelay version --json",
|
|
68
66
|
"",
|
|
@@ -96,6 +94,10 @@ export function linearHelpText() {
|
|
|
96
94
|
" patchrelay linear connect",
|
|
97
95
|
" patchrelay linear list",
|
|
98
96
|
" patchrelay linear sync usertold",
|
|
97
|
+
"",
|
|
98
|
+
"Compatibility aliases:",
|
|
99
|
+
" patchrelay connect Alias for `patchrelay linear connect`",
|
|
100
|
+
" patchrelay installations Alias for `patchrelay linear list`",
|
|
99
101
|
].join("\n");
|
|
100
102
|
}
|
|
101
103
|
export function repoHelpText() {
|
|
@@ -124,6 +126,11 @@ export function repoHelpText() {
|
|
|
124
126
|
" patchrelay repo link krasnoperov/usertold --workspace usertold --team USE",
|
|
125
127
|
" patchrelay repo show krasnoperov/usertold",
|
|
126
128
|
" patchrelay repo sync",
|
|
129
|
+
"",
|
|
130
|
+
"Compatibility aliases:",
|
|
131
|
+
" patchrelay attach ... Alias for `patchrelay repo link ...`",
|
|
132
|
+
" patchrelay repos Alias for `patchrelay repo list`",
|
|
133
|
+
" patchrelay repos <repo> Alias for `patchrelay repo show <repo>`",
|
|
127
134
|
].join("\n");
|
|
128
135
|
}
|
|
129
136
|
export function issueHelpText() {
|
|
@@ -134,17 +141,17 @@ export function issueHelpText() {
|
|
|
134
141
|
"Commands:",
|
|
135
142
|
" show <issueKey> Show the latest known issue state",
|
|
136
143
|
" list List tracked issues",
|
|
137
|
-
" watch <issueKey> Follow
|
|
138
|
-
" report <issueKey> Show finished run reports",
|
|
139
|
-
" events <issueKey> Show raw thread events",
|
|
144
|
+
" watch <issueKey> Follow PatchRelay-owned activity until it settles",
|
|
140
145
|
" path <issueKey> Print the issue worktree path",
|
|
141
146
|
" open <issueKey> Open Codex in the issue worktree",
|
|
147
|
+
" sessions <issueKey> Show recorded Codex app-server sessions",
|
|
142
148
|
" retry <issueKey> Requeue a run",
|
|
143
149
|
"",
|
|
144
150
|
"Examples:",
|
|
145
151
|
" patchrelay issue list --active",
|
|
146
152
|
" patchrelay issue show USE-54",
|
|
147
153
|
" patchrelay issue watch USE-54",
|
|
154
|
+
" patchrelay issue sessions USE-54",
|
|
148
155
|
].join("\n");
|
|
149
156
|
}
|
|
150
157
|
export function serviceHelpText() {
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { getBuildInfo } from "../build-info.js";
|
|
3
3
|
import { assertKnownFlags, hasHelpFlag, parseArgs, resolveCommand } from "./args.js";
|
|
4
|
-
import { handleFeedCommand } from "./commands/feed.js";
|
|
5
4
|
import { handleIssueCommand, } from "./commands/issues.js";
|
|
6
5
|
import { handleLinearCommand } from "./commands/linear.js";
|
|
7
6
|
import { handleRepoCommand } from "./commands/repo.js";
|
|
@@ -19,7 +18,6 @@ function getCommandConfigProfile(command) {
|
|
|
19
18
|
case "service":
|
|
20
19
|
return "doctor";
|
|
21
20
|
case "linear":
|
|
22
|
-
case "feed":
|
|
23
21
|
case "dashboard":
|
|
24
22
|
return "operator_cli";
|
|
25
23
|
case "repo":
|
|
@@ -61,6 +59,9 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
61
59
|
case "open":
|
|
62
60
|
assertKnownFlags(parsed, "issue", ["print", "json"]);
|
|
63
61
|
return;
|
|
62
|
+
case "sessions":
|
|
63
|
+
assertKnownFlags(parsed, "issue", ["json"]);
|
|
64
|
+
return;
|
|
64
65
|
case "retry":
|
|
65
66
|
assertKnownFlags(parsed, "issue", ["run-type", "reason", "json"]);
|
|
66
67
|
return;
|
|
@@ -108,12 +109,6 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
108
109
|
assertKnownFlags(parsed, "repo", []);
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
case "attach":
|
|
112
|
-
case "repos":
|
|
113
|
-
case "connect":
|
|
114
|
-
case "installations":
|
|
115
|
-
throw new CliUsageError(`${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.`);
|
|
116
|
-
return;
|
|
117
112
|
case "service":
|
|
118
113
|
if (commandArgs[0] === "install") {
|
|
119
114
|
assertKnownFlags(parsed, "service", ["force", "write-only", "json"]);
|
|
@@ -133,9 +128,6 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
133
128
|
}
|
|
134
129
|
assertKnownFlags(parsed, "service", []);
|
|
135
130
|
return;
|
|
136
|
-
case "feed":
|
|
137
|
-
assertKnownFlags(parsed, command, ["follow", "limit", "issue", "repo", "kind", "stage", "status", "workflow", "json"]);
|
|
138
|
-
return;
|
|
139
131
|
case "dashboard":
|
|
140
132
|
assertKnownFlags(parsed, command, ["issue"]);
|
|
141
133
|
return;
|
|
@@ -319,25 +311,6 @@ export async function runCli(argv, options) {
|
|
|
319
311
|
runInteractive,
|
|
320
312
|
});
|
|
321
313
|
}
|
|
322
|
-
if (command === "attach" || command === "repos" || command === "connect" || command === "installations") {
|
|
323
|
-
writeOutput(stderr, `${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.\n`);
|
|
324
|
-
return 1;
|
|
325
|
-
}
|
|
326
|
-
if (command === "feed") {
|
|
327
|
-
const operatorData = parsed.flags.get("follow") === true
|
|
328
|
-
? await ensureFeedFollowDataAccess(data, config)
|
|
329
|
-
: await ensureFeedListDataAccess(data, config);
|
|
330
|
-
if (!data) {
|
|
331
|
-
data = operatorData;
|
|
332
|
-
ownsData = true;
|
|
333
|
-
}
|
|
334
|
-
return await handleFeedCommand({
|
|
335
|
-
parsed,
|
|
336
|
-
json,
|
|
337
|
-
stdout,
|
|
338
|
-
data: operatorData,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
314
|
if (command === "dashboard") {
|
|
342
315
|
const { handleWatchCommand } = await import("./commands/watch.js");
|
|
343
316
|
return await handleWatchCommand({ config, parsed });
|
|
@@ -362,10 +335,6 @@ async function createCliDataAccess(config) {
|
|
|
362
335
|
const { CliDataAccess } = await import("./data.js");
|
|
363
336
|
return new CliDataAccess(config);
|
|
364
337
|
}
|
|
365
|
-
async function createCliOperatorDataAccess(config) {
|
|
366
|
-
const { CliOperatorApiClient } = await import("./operator-client.js");
|
|
367
|
-
return new CliOperatorApiClient(config);
|
|
368
|
-
}
|
|
369
338
|
async function ensureIssueDataAccess(data, config) {
|
|
370
339
|
if (data) {
|
|
371
340
|
if (isIssueDataAccess(data)) {
|
|
@@ -378,27 +347,3 @@ async function ensureIssueDataAccess(data, config) {
|
|
|
378
347
|
function isIssueDataAccess(data) {
|
|
379
348
|
return !!data && typeof data === "object" && "inspect" in data && typeof data.inspect === "function";
|
|
380
349
|
}
|
|
381
|
-
async function ensureFeedListDataAccess(data, config) {
|
|
382
|
-
if (data) {
|
|
383
|
-
if (hasFeedListDataAccess(data)) {
|
|
384
|
-
return data;
|
|
385
|
-
}
|
|
386
|
-
throw new Error("The feed command requires listOperatorFeed() data access.");
|
|
387
|
-
}
|
|
388
|
-
return await createCliOperatorDataAccess(config);
|
|
389
|
-
}
|
|
390
|
-
async function ensureFeedFollowDataAccess(data, config) {
|
|
391
|
-
if (data) {
|
|
392
|
-
if (hasFeedFollowDataAccess(data)) {
|
|
393
|
-
return data;
|
|
394
|
-
}
|
|
395
|
-
throw new Error("The feed --follow command requires followOperatorFeed() data access.");
|
|
396
|
-
}
|
|
397
|
-
return await createCliOperatorDataAccess(config);
|
|
398
|
-
}
|
|
399
|
-
function hasFeedListDataAccess(data) {
|
|
400
|
-
return !!data && typeof data === "object" && "listOperatorFeed" in data && typeof data.listOperatorFeed === "function";
|
|
401
|
-
}
|
|
402
|
-
function hasFeedFollowDataAccess(data) {
|
|
403
|
-
return !!data && typeof data === "object" && "followOperatorFeed" in data && typeof data.followOperatorFeed === "function";
|
|
404
|
-
}
|
|
@@ -29,88 +29,6 @@ export class CliOperatorApiClient {
|
|
|
29
29
|
async disconnectLinearWorkspace(workspace) {
|
|
30
30
|
return await this.requestJson(`/api/linear/workspaces/${encodeURIComponent(workspace)}`, undefined, { method: "DELETE" });
|
|
31
31
|
}
|
|
32
|
-
async listOperatorFeed(options) {
|
|
33
|
-
return await this.requestJson("/api/feed", {
|
|
34
|
-
...(options?.limit && options.limit > 0 ? { limit: String(options.limit) } : {}),
|
|
35
|
-
...(options?.issueKey ? { issue: options.issueKey } : {}),
|
|
36
|
-
...(options?.projectId ? { project: options.projectId } : {}),
|
|
37
|
-
...(options?.kind ? { kind: options.kind } : {}),
|
|
38
|
-
...(options?.stage ? { stage: options.stage } : {}),
|
|
39
|
-
...(options?.status ? { status: options.status } : {}),
|
|
40
|
-
...(options?.workflowId ? { workflow: options.workflowId } : {}),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
async followOperatorFeed(onEvent, options) {
|
|
44
|
-
const url = new URL("/api/feed", this.getOperatorBaseUrl());
|
|
45
|
-
url.searchParams.set("follow", "1");
|
|
46
|
-
if (options?.limit && options.limit > 0) {
|
|
47
|
-
url.searchParams.set("limit", String(options.limit));
|
|
48
|
-
}
|
|
49
|
-
if (options?.issueKey) {
|
|
50
|
-
url.searchParams.set("issue", options.issueKey);
|
|
51
|
-
}
|
|
52
|
-
if (options?.projectId) {
|
|
53
|
-
url.searchParams.set("project", options.projectId);
|
|
54
|
-
}
|
|
55
|
-
if (options?.kind) {
|
|
56
|
-
url.searchParams.set("kind", options.kind);
|
|
57
|
-
}
|
|
58
|
-
if (options?.stage) {
|
|
59
|
-
url.searchParams.set("stage", options.stage);
|
|
60
|
-
}
|
|
61
|
-
if (options?.status) {
|
|
62
|
-
url.searchParams.set("status", options.status);
|
|
63
|
-
}
|
|
64
|
-
if (options?.workflowId) {
|
|
65
|
-
url.searchParams.set("workflow", options.workflowId);
|
|
66
|
-
}
|
|
67
|
-
const response = await fetch(url, {
|
|
68
|
-
method: "GET",
|
|
69
|
-
headers: {
|
|
70
|
-
accept: "text/event-stream",
|
|
71
|
-
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
if (!response.ok || !response.body) {
|
|
75
|
-
const body = await response.text().catch(() => "");
|
|
76
|
-
const message = this.readErrorMessage(body);
|
|
77
|
-
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
78
|
-
}
|
|
79
|
-
const reader = response.body.getReader();
|
|
80
|
-
const decoder = new TextDecoder();
|
|
81
|
-
let buffer = "";
|
|
82
|
-
let dataLines = [];
|
|
83
|
-
while (true) {
|
|
84
|
-
const { done, value } = await reader.read();
|
|
85
|
-
if (done) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
buffer += decoder.decode(value, { stream: true });
|
|
89
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
90
|
-
while (newlineIndex !== -1) {
|
|
91
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
92
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
93
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
94
|
-
if (!line) {
|
|
95
|
-
if (dataLines.length > 0) {
|
|
96
|
-
const parsed = JSON.parse(dataLines.join("\n"));
|
|
97
|
-
onEvent(parsed);
|
|
98
|
-
dataLines = [];
|
|
99
|
-
}
|
|
100
|
-
newlineIndex = buffer.indexOf("\n");
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (line.startsWith(":")) {
|
|
104
|
-
newlineIndex = buffer.indexOf("\n");
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (line.startsWith("data:")) {
|
|
108
|
-
dataLines.push(line.slice(5).trimStart());
|
|
109
|
-
}
|
|
110
|
-
newlineIndex = buffer.indexOf("\n");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
32
|
getOperatorBaseUrl() {
|
|
115
33
|
const host = this.normalizeLocalHost(this.config.server.bind);
|
|
116
34
|
return `http://${host}:${this.config.server.port}/`;
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -4,10 +4,8 @@ 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";
|
|
7
|
-
import { useFeedStream } from "./use-feed-stream.js";
|
|
8
7
|
import { IssueListView } from "./IssueListView.js";
|
|
9
8
|
import { IssueDetailView } from "./IssueDetailView.js";
|
|
10
|
-
import { FeedView } from "./FeedView.js";
|
|
11
9
|
async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
12
10
|
const headers = { "content-type": "application/json" };
|
|
13
11
|
if (bearerToken)
|
|
@@ -71,7 +69,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
71
69
|
const [frozen, setFrozen] = useState(false);
|
|
72
70
|
useWatchStream({ baseUrl, bearerToken, dispatch, active: !frozen });
|
|
73
71
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch, active: !frozen });
|
|
74
|
-
useFeedStream({ baseUrl, bearerToken, active: state.view === "feed" && !frozen, dispatch });
|
|
75
72
|
const [promptMode, setPromptMode] = useState(false);
|
|
76
73
|
const [promptBuffer, setPromptBuffer] = useState("");
|
|
77
74
|
const handleRetry = useCallback(() => {
|
|
@@ -154,9 +151,6 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
154
151
|
else if (key.tab) {
|
|
155
152
|
dispatch({ type: "cycle-filter" });
|
|
156
153
|
}
|
|
157
|
-
else if (input === "F" || input === "f") {
|
|
158
|
-
dispatch({ type: "enter-feed" });
|
|
159
|
-
}
|
|
160
154
|
}
|
|
161
155
|
else if (state.view === "detail") {
|
|
162
156
|
if (key.escape || key.backspace || key.delete) {
|
|
@@ -187,17 +181,32 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
187
181
|
dispatch({ type: "switch-detail-tab", tab: "timeline" });
|
|
188
182
|
}
|
|
189
183
|
else if (input === "j" || key.downArrow) {
|
|
190
|
-
dispatch({ type: "detail-
|
|
184
|
+
dispatch({ type: "detail-scroll", delta: 1 });
|
|
191
185
|
}
|
|
192
186
|
else if (input === "k" || key.upArrow) {
|
|
187
|
+
dispatch({ type: "detail-scroll", delta: -1 });
|
|
188
|
+
}
|
|
189
|
+
else if (key.pageDown || (key.ctrl && input === "d")) {
|
|
190
|
+
dispatch({ type: "detail-page", direction: "down" });
|
|
191
|
+
}
|
|
192
|
+
else if (key.pageUp || (key.ctrl && input === "u")) {
|
|
193
|
+
dispatch({ type: "detail-page", direction: "up" });
|
|
194
|
+
}
|
|
195
|
+
else if (key.home) {
|
|
196
|
+
dispatch({ type: "detail-jump", target: "start" });
|
|
197
|
+
}
|
|
198
|
+
else if (key.end) {
|
|
199
|
+
dispatch({ type: "detail-jump", target: "end" });
|
|
200
|
+
}
|
|
201
|
+
else if (input === "[" || key.leftArrow) {
|
|
193
202
|
dispatch({ type: "detail-navigate", direction: "prev", filtered });
|
|
194
203
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (key.escape || key.backspace || key.delete) {
|
|
198
|
-
dispatch({ type: "exit-feed" });
|
|
204
|
+
else if (input === "]" || key.rightArrow) {
|
|
205
|
+
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
199
206
|
}
|
|
200
207
|
}
|
|
201
208
|
});
|
|
202
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt
|
|
209
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, scrollOffset: state.detailScrollOffset, unreadBelow: state.detailUnreadBelow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, reservedRows: 1 + ((promptMode || promptStatus) ? 1 : 0), onLayoutChange: (viewportRows, contentRows) => {
|
|
210
|
+
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
211
|
+
} }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : null }));
|
|
203
212
|
}
|
|
@@ -4,15 +4,15 @@ export function HelpBar({ view, follow, detailTab }) {
|
|
|
4
4
|
let text;
|
|
5
5
|
if (view === "detail") {
|
|
6
6
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
7
|
-
text = [tabHint, `f:
|
|
7
|
+
text = [tabHint, "j/k: scroll", "PgUp/PgDn: page", "[ ]: issue", "Home/End: jump", `f: live ${follow ? "on" : "off"}`, "p: prompt", "s: stop", "r: retry"]
|
|
8
8
|
.filter(Boolean)
|
|
9
9
|
.join(" ");
|
|
10
10
|
}
|
|
11
11
|
else if (view === "feed") {
|
|
12
|
-
text = "";
|
|
12
|
+
text = "Legacy feed view Esc: back";
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
text = "
|
|
15
|
+
text = "Enter: detail Tab: filter";
|
|
16
16
|
}
|
|
17
17
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
18
18
|
}
|