@levistudio/redline 0.3.0 → 0.4.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/AGENTS.md +227 -0
- package/CHANGELOG.md +11 -1
- package/CLAUDE.md +9 -0
- package/README.md +39 -21
- package/SECURITY.md +3 -3
- package/bin/redline.cjs +4 -1
- package/package.json +4 -1
- package/scripts/install-skill.sh +104 -39
- package/skills/redline-review/SKILL.md +9 -9
- package/src/agent.ts +13 -19
- package/src/agentProvider.ts +267 -0
- package/src/cli.ts +68 -32
- package/src/client/sse.ts +1 -1
- package/src/pickModel.ts +6 -4
- package/src/promptEnvelope.ts +1 -1
- package/src/resolve.ts +19 -65
- package/src/server-page.ts +5 -4
- package/src/server.ts +5 -6
package/src/cli.ts
CHANGED
|
@@ -45,24 +45,25 @@ preflightDependencies();
|
|
|
45
45
|
// Dynamic imports so preflight runs before module resolution pulls in third-party deps.
|
|
46
46
|
const { createServer } = await import("./server");
|
|
47
47
|
const { resolve } = await import("./resolve");
|
|
48
|
+
const {
|
|
49
|
+
getAgentProvider,
|
|
50
|
+
invalidProviderMessage,
|
|
51
|
+
parseAgentProviderId,
|
|
52
|
+
resolveProviderId,
|
|
53
|
+
} = await import("./agentProvider");
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"\n[redline] Could not find the `claude` CLI on PATH.\n" +
|
|
62
|
-
"Redline shells out to Claude Code for agent replies and revisions.\n" +
|
|
63
|
-
"Install it from https://claude.com/claude-code and re-run.\n"
|
|
64
|
-
);
|
|
65
|
-
process.exit(1);
|
|
55
|
+
function argValue(args: string[], flag: string): string | undefined {
|
|
56
|
+
const idx = args.indexOf(flag);
|
|
57
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectProvider(args: string[]) {
|
|
61
|
+
const raw = argValue(args, "--agent") ?? process.env.REDLINE_AGENT;
|
|
62
|
+
if (raw && !parseAgentProviderId(raw)) {
|
|
63
|
+
console.error(invalidProviderMessage(raw));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
return getAgentProvider(resolveProviderId(raw));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// Walk up from `start` looking for a git root (a `.git` directory or file —
|
|
@@ -100,11 +101,33 @@ function maybePrintGitignoreHint(filePath: string) {
|
|
|
100
101
|
|
|
101
102
|
const args = process.argv.slice(2);
|
|
102
103
|
|
|
104
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
105
|
+
const pkgPath = path.resolve(import.meta.dir, "..", "package.json");
|
|
106
|
+
try {
|
|
107
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
|
|
108
|
+
console.log(pkg.version ?? "unknown");
|
|
109
|
+
} catch {
|
|
110
|
+
console.log("unknown");
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// redline install-skill [--agent claude|codex|both]
|
|
116
|
+
if (args[0] === "install-skill") {
|
|
117
|
+
const script = path.resolve(import.meta.dir, "..", "scripts", "install-skill.sh");
|
|
118
|
+
if (!existsSync(script)) {
|
|
119
|
+
console.error(`Install script not found: ${script}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const result = spawnSync("bash", [script, ...args.slice(1)], { stdio: "inherit" });
|
|
123
|
+
process.exit(result.status ?? 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
// redline resolve <file> [--model <id>]
|
|
104
127
|
if (args[0] === "resolve") {
|
|
105
128
|
const filePath = args[1];
|
|
106
129
|
if (!filePath) {
|
|
107
|
-
console.error("Usage: redline resolve <file.md> [--model <model-id>]");
|
|
130
|
+
console.error("Usage: redline resolve <file.md> [--model <model-id>] [--agent claude|codex]");
|
|
108
131
|
process.exit(1);
|
|
109
132
|
}
|
|
110
133
|
const resolved = path.resolve(filePath);
|
|
@@ -112,15 +135,20 @@ if (args[0] === "resolve") {
|
|
|
112
135
|
console.error(`File not found: ${resolved}`);
|
|
113
136
|
process.exit(1);
|
|
114
137
|
}
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
const model = argValue(args, "--model");
|
|
139
|
+
const provider = selectProvider(args);
|
|
140
|
+
try {
|
|
141
|
+
provider.preflight();
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
resolve(resolved, { model, agentProvider: provider.id });
|
|
119
147
|
} else {
|
|
120
148
|
// redline <file> — open review reader
|
|
121
149
|
const filePath = args[0];
|
|
122
150
|
if (!filePath) {
|
|
123
|
-
console.error("Usage: redline <file.md>");
|
|
151
|
+
console.error("Usage: redline <file.md>\n redline resolve <file.md> [--model <model-id>] [--agent claude|codex]\n redline install-skill [--agent claude|codex|both]");
|
|
124
152
|
process.exit(1);
|
|
125
153
|
}
|
|
126
154
|
const resolved = path.resolve(filePath);
|
|
@@ -129,14 +157,21 @@ if (args[0] === "resolve") {
|
|
|
129
157
|
process.exit(1);
|
|
130
158
|
}
|
|
131
159
|
const noAgent = args.includes("--no-agent");
|
|
160
|
+
const provider = selectProvider(args);
|
|
132
161
|
|
|
133
162
|
// Manual annotation mode skips both the preflight and the agent spawn —
|
|
134
|
-
// the user just wants inline comments without
|
|
135
|
-
// requiring
|
|
136
|
-
if (!noAgent)
|
|
163
|
+
// the user just wants inline comments without an agent conversation, so
|
|
164
|
+
// requiring a provider CLI on PATH would be a hostile gate.
|
|
165
|
+
if (!noAgent) {
|
|
166
|
+
try {
|
|
167
|
+
provider.preflight();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
137
173
|
|
|
138
|
-
const
|
|
139
|
-
const context = contextFlag !== -1 ? args[contextFlag + 1] : undefined;
|
|
174
|
+
const context = argValue(args, "--context");
|
|
140
175
|
const autoOpen = args.includes("--open");
|
|
141
176
|
|
|
142
177
|
const resultFile = path.join(path.dirname(resolved), ".review", path.basename(resolved) + ".result");
|
|
@@ -164,7 +199,7 @@ if (args[0] === "resolve") {
|
|
|
164
199
|
// caller can pin the token if it needs to.
|
|
165
200
|
const csrfToken = process.env.REDLINE_TOKEN ?? crypto.randomUUID();
|
|
166
201
|
|
|
167
|
-
const app = createServer(resolved, { context, csrfToken, noAgent });
|
|
202
|
+
const app = createServer(resolved, { context, csrfToken, noAgent, agentName: provider.displayName });
|
|
168
203
|
const server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: app.fetch, idleTimeout: 0 });
|
|
169
204
|
const url = `http://localhost:${server.port}`;
|
|
170
205
|
|
|
@@ -182,6 +217,7 @@ if (args[0] === "resolve") {
|
|
|
182
217
|
started_at: new Date().toISOString(),
|
|
183
218
|
pid: process.pid,
|
|
184
219
|
csrf_token: csrfToken,
|
|
220
|
+
agent_provider: provider.id,
|
|
185
221
|
}, null, 2));
|
|
186
222
|
} catch (e) {
|
|
187
223
|
console.error("[redline] Failed to write startup file:", e);
|
|
@@ -194,14 +230,14 @@ if (args[0] === "resolve") {
|
|
|
194
230
|
console.log(` URL: ${url}`);
|
|
195
231
|
console.log(` Result: ${resultFile}`);
|
|
196
232
|
console.log(`${bar}`);
|
|
197
|
-
if (noAgent) console.log(` Mode: manual annotation (--no-agent — no
|
|
233
|
+
if (noAgent) console.log(` Mode: manual annotation (--no-agent — no ${provider.displayName} replies, no revision pass)`);
|
|
198
234
|
if (!autoOpen) console.log(`\n → cmd-click the URL when you're ready to review\n`);
|
|
199
235
|
else console.log("");
|
|
200
236
|
|
|
201
237
|
maybePrintGitignoreHint(resolved);
|
|
202
238
|
|
|
203
239
|
// Auto-restart the agent if it dies unexpectedly (harness reaping, OOM,
|
|
204
|
-
// a transient
|
|
240
|
+
// a transient provider-CLI auth blip, etc). Capped to MAX_RESTARTS within
|
|
205
241
|
// RESTART_WINDOW_MS so a permanently-broken environment doesn't loop forever.
|
|
206
242
|
const RESTART_WINDOW_MS = 60_000;
|
|
207
243
|
// Cap is overrideable via env so integration tests can exercise the
|
|
@@ -216,7 +252,7 @@ if (args[0] === "resolve") {
|
|
|
216
252
|
[process.execPath, "run", path.join(import.meta.dir, "agent.ts"), resolved],
|
|
217
253
|
{
|
|
218
254
|
stdout: "inherit", stderr: "inherit", stdin: "ignore",
|
|
219
|
-
env: { ...process.env, REDLINE_PORT: String(server.port), REDLINE_TOKEN: csrfToken },
|
|
255
|
+
env: { ...process.env, REDLINE_PORT: String(server.port), REDLINE_TOKEN: csrfToken, REDLINE_AGENT: provider.id },
|
|
220
256
|
}
|
|
221
257
|
);
|
|
222
258
|
agentProc = proc;
|
package/src/client/sse.ts
CHANGED
|
@@ -192,7 +192,7 @@ export function initSSE(): void {
|
|
|
192
192
|
});
|
|
193
193
|
on("finished", () => {
|
|
194
194
|
document.body.innerHTML =
|
|
195
|
-
'<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:system-ui;flex-direction:column;gap:16px;color:#374151"><div style="font-size:48px">\u2713</div><div style="font-size:20px;font-weight:600">Review complete</div><div style="color:#6b7280">You can close this tab and continue in
|
|
195
|
+
'<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:system-ui;flex-direction:column;gap:16px;color:#374151"><div style="font-size:48px">\u2713</div><div style="font-size:20px;font-weight:600">Review complete</div><div style="color:#6b7280">You can close this tab and continue in your agent environment.</div></div>';
|
|
196
196
|
});
|
|
197
197
|
es.onerror = () => {
|
|
198
198
|
es.close();
|
package/src/pickModel.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
// Heuristic for picking
|
|
2
|
-
//
|
|
1
|
+
// Heuristic for picking how much model capacity to use based on the human's
|
|
2
|
+
// message. Providers map these tiers to concrete model ids.
|
|
3
3
|
//
|
|
4
4
|
// Used in two places:
|
|
5
5
|
// - agent.ts picks per-reply, looking at the last human message in the thread
|
|
6
6
|
// - resolve.ts picks per-revision, scanning every human message across settled comments
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export type ModelTier = "fast" | "smart";
|
|
9
|
+
|
|
10
|
+
export const FAST_MODEL: ModelTier = "fast";
|
|
11
|
+
export const SMART_MODEL: ModelTier = "smart";
|
|
10
12
|
|
|
11
13
|
export const REPLY_INVOLVED_PATTERNS =
|
|
12
14
|
/\b(what|why|how|which|who|suggest|suggestion|alternative|option|idea|propose|explain|think|consider|recommend|help|could|would|should)\b/i;
|
package/src/promptEnvelope.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Per-prompt envelope around user-controlled strings before they reach
|
|
2
|
-
//
|
|
2
|
+
// the selected local agent provider. Same delimiter-over-JSON principle the agent reply path
|
|
3
3
|
// already uses (see retro entry 2026-05-07 — "delimiter envelope for agent
|
|
4
4
|
// replies"), applied to the *input* side: comment text, document body,
|
|
5
5
|
// thread messages.
|
package/src/resolve.ts
CHANGED
|
@@ -5,11 +5,13 @@ import type { Round, Comment } from "./sidecar";
|
|
|
5
5
|
import { pickRevisionModel } from "./pickModel";
|
|
6
6
|
import { newEnvelope } from "./promptEnvelope";
|
|
7
7
|
import { contextBlock } from "./contextBlock";
|
|
8
|
+
import { getAgentProvider, resolveProviderId, type AgentProviderId } from "./agentProvider";
|
|
8
9
|
|
|
9
10
|
const serverBase = () => `http://localhost:${process.env.REDLINE_PORT ?? "3000"}`;
|
|
10
11
|
const csrfHeader = (): Record<string, string> => ({ "X-Redline-Token": process.env.REDLINE_TOKEN ?? "" });
|
|
11
12
|
|
|
12
|
-
export async function resolve(filePath: string, options: { model?: string } = {}) {
|
|
13
|
+
export async function resolve(filePath: string, options: { model?: string; agentProvider?: AgentProviderId } = {}) {
|
|
14
|
+
const provider = getAgentProvider(options.agentProvider ?? resolveProviderId());
|
|
13
15
|
const model = options.model ?? null;
|
|
14
16
|
const sidecar = await loadSidecar(filePath);
|
|
15
17
|
|
|
@@ -20,7 +22,7 @@ export async function resolve(filePath: string, options: { model?: string } = {}
|
|
|
20
22
|
}
|
|
21
23
|
const round: Round = resolvedRounds[resolvedRounds.length - 1];
|
|
22
24
|
const settled = round.comments.filter((c) => c.resolved);
|
|
23
|
-
const chosenModel = model ?? pickRevisionModel(settled);
|
|
25
|
+
const chosenModel = model ?? provider.modelForTier(pickRevisionModel(settled));
|
|
24
26
|
|
|
25
27
|
const docText = await readFile(filePath, "utf-8");
|
|
26
28
|
|
|
@@ -89,9 +91,6 @@ export async function resolve(filePath: string, options: { model?: string } = {}
|
|
|
89
91
|
contextBlock(sidecar.context, env) +
|
|
90
92
|
`<comments-to-apply>\n${commentsBlock}\n</comments-to-apply>${priorChangesBlock}\n\n<document>\n${env.wrap("document", docText)}\n</document>`;
|
|
91
93
|
|
|
92
|
-
// Call the claude CLI (inherits auth from the user's Claude Code session — no API key needed)
|
|
93
|
-
const cliBin = process.env.CLAUDE_CODE_EXECPATH ?? "claude";
|
|
94
|
-
|
|
95
94
|
const broadcastChunk = (text: string, kind: "thinking" | "text") => {
|
|
96
95
|
fetch(`${serverBase()}/api/revision-chunk`, {
|
|
97
96
|
method: "POST",
|
|
@@ -109,7 +108,7 @@ export async function resolve(filePath: string, options: { model?: string } = {}
|
|
|
109
108
|
const fail = async (reason: string) => {
|
|
110
109
|
await logRevisionFailure(filePath, {
|
|
111
110
|
reason,
|
|
112
|
-
model: chosenModel
|
|
111
|
+
model: `${provider.id}/${chosenModel}`,
|
|
113
112
|
exitCode,
|
|
114
113
|
stderr: stderrText.trim(),
|
|
115
114
|
stdoutSample: revised.slice(0, 2000),
|
|
@@ -128,72 +127,27 @@ export async function resolve(filePath: string, options: { model?: string } = {}
|
|
|
128
127
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
129
128
|
console.log(
|
|
130
129
|
attempt === 1
|
|
131
|
-
? `Revising with ${chosenModel}...\n`
|
|
132
|
-
: `\nRetrying revision with ${chosenModel} (attempt ${attempt}/${MAX_ATTEMPTS})...\n`
|
|
130
|
+
? `Revising with ${provider.id}/${chosenModel}...\n`
|
|
131
|
+
: `\nRetrying revision with ${provider.id}/${chosenModel} (attempt ${attempt}/${MAX_ATTEMPTS})...\n`
|
|
133
132
|
);
|
|
134
133
|
console.log("─".repeat(60));
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Drain stderr concurrently so we can include it in any error report.
|
|
147
|
-
stderrText = "";
|
|
148
|
-
const stderrDone = (async () => {
|
|
149
|
-
const r = proc.stderr.getReader();
|
|
150
|
-
const dec = new TextDecoder();
|
|
151
|
-
while (true) {
|
|
152
|
-
const { done, value } = await r.read();
|
|
153
|
-
if (done) break;
|
|
154
|
-
const chunk = dec.decode(value);
|
|
155
|
-
stderrText += chunk;
|
|
156
|
-
process.stderr.write(chunk);
|
|
157
|
-
}
|
|
158
|
-
})();
|
|
159
|
-
|
|
160
|
-
revised = "";
|
|
161
|
-
let buffer = "";
|
|
162
|
-
const reader = proc.stdout.getReader();
|
|
163
|
-
while (true) {
|
|
164
|
-
const { done, value } = await reader.read();
|
|
165
|
-
if (done) break;
|
|
166
|
-
buffer += new TextDecoder().decode(value);
|
|
167
|
-
const lines = buffer.split("\n");
|
|
168
|
-
buffer = lines.pop() ?? "";
|
|
169
|
-
for (const line of lines) {
|
|
170
|
-
if (!line.trim()) continue;
|
|
171
|
-
try {
|
|
172
|
-
const obj = JSON.parse(line);
|
|
173
|
-
if (obj.type === "stream_event" && obj.event?.type === "content_block_delta") {
|
|
174
|
-
const delta = obj.event.delta;
|
|
175
|
-
if (delta?.type === "text_delta" && delta.text) {
|
|
176
|
-
revised += delta.text;
|
|
177
|
-
process.stdout.write(delta.text);
|
|
178
|
-
broadcastChunk(delta.text, "text");
|
|
179
|
-
} else if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
180
|
-
broadcastChunk(delta.thinking, "thinking");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch { /* malformed JSON line, skip */ }
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
exitCode = await proc.exited;
|
|
188
|
-
await stderrDone;
|
|
189
|
-
const revisionDurationMs = Date.now() - revisionStartedAt;
|
|
134
|
+
const run = await provider.runRevision({
|
|
135
|
+
systemPrompt,
|
|
136
|
+
userMessage,
|
|
137
|
+
model: chosenModel,
|
|
138
|
+
cwd: process.cwd(),
|
|
139
|
+
onChunk: broadcastChunk,
|
|
140
|
+
});
|
|
141
|
+
revised = run.revised;
|
|
142
|
+
stderrText = run.stderr;
|
|
143
|
+
exitCode = run.exitCode;
|
|
190
144
|
console.log("\n" + "─".repeat(60));
|
|
191
|
-
console.log(`Model: ${chosenModel} · Duration: ${(
|
|
145
|
+
console.log(`Model: ${provider.id}/${chosenModel} · Duration: ${(run.durationMs / 1000).toFixed(1)}s · Exit: ${exitCode}`);
|
|
192
146
|
console.log("─".repeat(60) + "\n");
|
|
193
147
|
|
|
194
148
|
// A CLI crash is not retryable — fail immediately.
|
|
195
149
|
if (exitCode !== 0) {
|
|
196
|
-
await fail(
|
|
150
|
+
await fail(`${provider.id} CLI exited with code ${exitCode}${stderrText.trim() ? ` — ${stderrText.trim().split("\n").slice(-3).join(" | ")}` : ""}`);
|
|
197
151
|
}
|
|
198
152
|
|
|
199
153
|
const result = validateRevision(revised, docText, settled);
|
package/src/server-page.ts
CHANGED
|
@@ -17,7 +17,8 @@ function pageTemplate(
|
|
|
17
17
|
context?: string,
|
|
18
18
|
readOnly = false,
|
|
19
19
|
csrfToken = "",
|
|
20
|
-
noAgent = false
|
|
20
|
+
noAgent = false,
|
|
21
|
+
agentName = "selected local"
|
|
21
22
|
): string {
|
|
22
23
|
const commentsJson = JSON.stringify(comments);
|
|
23
24
|
|
|
@@ -51,7 +52,7 @@ function pageTemplate(
|
|
|
51
52
|
</span>
|
|
52
53
|
<div class="header-actions">
|
|
53
54
|
<span id="agent-status" class="agent-status" hidden></span>
|
|
54
|
-
${noAgent ? `<span class="manual-mode-pill" title="Started with --no-agent. No
|
|
55
|
+
${noAgent ? `<span class="manual-mode-pill" title="Started with --no-agent. No ${escapeHtml(agentName)} replies, no revision pass.">Manual mode</span>` : ''}
|
|
55
56
|
${!readOnly && totalRounds > 1 ? `<button class="btn-toggle-diff" id="btn-toggle-diff" type="button" aria-pressed="false">Show changes</button>` : ''}
|
|
56
57
|
${readOnly
|
|
57
58
|
? `<span style="font-size:13px;color:var(--text-muted);font-style:italic">Read-only — <a href="/" style="color:var(--accent)">back to current</a></span>`
|
|
@@ -65,7 +66,7 @@ function pageTemplate(
|
|
|
65
66
|
</div>` : ''}
|
|
66
67
|
${!readOnly ? `<div class="first-run-banner" id="first-run-banner" hidden>
|
|
67
68
|
<span class="first-run-icon" aria-hidden="true">⚠</span>
|
|
68
|
-
<span class="first-run-text">Redline sends document and comment text to your
|
|
69
|
+
<span class="first-run-text">Redline sends document and comment text to your ${escapeHtml(agentName)} agent. Use trusted docs.</span>
|
|
69
70
|
<button class="first-run-dismiss" id="first-run-dismiss" aria-label="Dismiss">Got it</button>
|
|
70
71
|
</div>` : ''}
|
|
71
72
|
<article class="prose" id="prose">
|
|
@@ -97,7 +98,7 @@ function pageTemplate(
|
|
|
97
98
|
</div>
|
|
98
99
|
</div>
|
|
99
100
|
<div id="error-banner" style="display:none;position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#b71c1c;color:white;padding:12px 24px;border-radius:6px;font-size:14px;font-weight:500;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:999;white-space:nowrap;"></div>
|
|
100
|
-
<div id="session-ended-banner" style="display:none;position:fixed;top:0;left:0;right:0;background:#92400e;color:white;padding:10px 24px;font-size:14px;font-weight:500;text-align:center;z-index:1000;box-shadow:0 1px 4px rgba(0,0,0,0.15);">Review session ended — the redline server is no longer running. Your changes up to this point are saved; close this tab and continue in
|
|
101
|
+
<div id="session-ended-banner" style="display:none;position:fixed;top:0;left:0;right:0;background:#92400e;color:white;padding:10px 24px;font-size:14px;font-weight:500;text-align:center;z-index:1000;box-shadow:0 1px 4px rgba(0,0,0,0.15);">Review session ended — the redline server is no longer running. Your changes up to this point are saved; close this tab and continue in your agent environment.</div>
|
|
101
102
|
|
|
102
103
|
<script>
|
|
103
104
|
window.__REDLINE__ = {
|
package/src/server.ts
CHANGED
|
@@ -50,7 +50,7 @@ function getClientBundle(): Promise<string> {
|
|
|
50
50
|
|
|
51
51
|
export function createServer(
|
|
52
52
|
filePath: string,
|
|
53
|
-
opts: { context?: string; csrfToken?: string; noAgent?: boolean } = {}
|
|
53
|
+
opts: { context?: string; csrfToken?: string; noAgent?: boolean; agentName?: string } = {}
|
|
54
54
|
) {
|
|
55
55
|
const app = new Hono();
|
|
56
56
|
const fileName = path.basename(filePath);
|
|
@@ -275,7 +275,7 @@ export function createServer(
|
|
|
275
275
|
const agentRepliedAt = latestRound?.agent_replied_at ?? null;
|
|
276
276
|
const roundNumber = latestRound?.round ?? 1;
|
|
277
277
|
const totalRounds = sidecar.rounds.length;
|
|
278
|
-
return c.html(pageTemplate(fileName, html, serializeCommentsForClient(comments), roundResolved, agentRepliedAt, roundNumber, totalRounds, sidecar.context, false, csrfToken, opts.noAgent ?? false));
|
|
278
|
+
return c.html(pageTemplate(fileName, html, serializeCommentsForClient(comments), roundResolved, agentRepliedAt, roundNumber, totalRounds, sidecar.context, false, csrfToken, opts.noAgent ?? false, opts.agentName));
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
// Add a comment to the active round
|
|
@@ -430,7 +430,7 @@ export function createServer(
|
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
// CLI signals the agent subprocess is gone for good (restart cap exhausted,
|
|
433
|
-
// missing
|
|
433
|
+
// missing provider CLI, etc). Surfaces a small persistent indicator in the
|
|
434
434
|
// header so the user knows replies aren't coming and can restart redline.
|
|
435
435
|
// No paired "agent-available" event — recovery requires a restart, so the
|
|
436
436
|
// indicator stays until the page reloads.
|
|
@@ -588,7 +588,8 @@ export function createServer(
|
|
|
588
588
|
sidecar.context,
|
|
589
589
|
true, // readOnly
|
|
590
590
|
csrfToken,
|
|
591
|
-
opts.noAgent ?? false
|
|
591
|
+
opts.noAgent ?? false,
|
|
592
|
+
opts.agentName
|
|
592
593
|
));
|
|
593
594
|
});
|
|
594
595
|
|
|
@@ -681,5 +682,3 @@ export function createServer(
|
|
|
681
682
|
onRevisionRecovered(cb: () => void) { onRevisionRecoveredCallback = cb; },
|
|
682
683
|
};
|
|
683
684
|
}
|
|
684
|
-
|
|
685
|
-
|