@levistudio/redline 0.2.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 +22 -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 +19 -21
- package/src/agentProvider.ts +267 -0
- package/src/cli.ts +109 -36
- package/src/client/cards.ts +10 -1
- package/src/client/lib.ts +143 -78
- package/src/client/render.ts +17 -5
- package/src/client/selection.ts +1 -1
- package/src/client/sse.ts +34 -2
- package/src/client/state.ts +22 -0
- package/src/client/styles.css +27 -0
- package/src/parseReply.ts +9 -1
- package/src/pickModel.ts +6 -4
- package/src/promptEnvelope.ts +1 -1
- package/src/resolve.ts +134 -97
- package/src/reviewSummary.ts +93 -0
- package/src/server-page.ts +5 -3
- package/src/server.ts +50 -16
- package/src/sidecar.ts +5 -0
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);
|
|
@@ -129,12 +129,23 @@ export function createServer(
|
|
|
129
129
|
|
|
130
130
|
// Abandonment detection: if no browser is connected for ABANDON_GRACE_MS after
|
|
131
131
|
// the first one ever connected, fire onAbandonCallback so the CLI can exit.
|
|
132
|
-
// Default 10min —
|
|
133
|
-
//
|
|
134
|
-
//
|
|
132
|
+
// Default 10min — this is now only the *backstop* for the no-beacon case
|
|
133
|
+
// (browser crash, kill -9, OS-killed tab). A cleanly closed tab fires an
|
|
134
|
+
// explicit /api/tab-closed beacon and takes the much shorter TAB_CLOSE_GRACE_MS
|
|
135
|
+
// path instead. The long backstop must stay generous: a bare SSE drop (laptop
|
|
136
|
+
// sleep, network blip, DevTools offline) is NOT a closed tab, and exiting on
|
|
137
|
+
// it kills a session the user means to continue. Override with
|
|
138
|
+
// REDLINE_ABANDON_MS for tests.
|
|
135
139
|
const ABANDON_GRACE_MS = process.env.REDLINE_ABANDON_MS
|
|
136
140
|
? parseInt(process.env.REDLINE_ABANDON_MS, 10)
|
|
137
141
|
: 10 * 60 * 1000;
|
|
142
|
+
// Grace applied after an explicit tab-close beacon. A reload also fires the
|
|
143
|
+
// beacon, so we can't exit immediately — but a reload reconnects its SSE
|
|
144
|
+
// within ~1s, while a real close never does. A few seconds covers the
|
|
145
|
+
// reconnect. Override with REDLINE_TABCLOSE_MS for tests.
|
|
146
|
+
const TAB_CLOSE_GRACE_MS = process.env.REDLINE_TABCLOSE_MS
|
|
147
|
+
? parseInt(process.env.REDLINE_TABCLOSE_MS, 10)
|
|
148
|
+
: 8000;
|
|
138
149
|
let hadBrowser = false;
|
|
139
150
|
let abandonTimer: ReturnType<typeof setTimeout> | null = null;
|
|
140
151
|
let onAbandonCallback: (() => void) | undefined;
|
|
@@ -194,15 +205,24 @@ export function createServer(
|
|
|
194
205
|
}, REVISION_TIMEOUT_MS);
|
|
195
206
|
}
|
|
196
207
|
|
|
208
|
+
function armAbandonTimer(graceMs: number) {
|
|
209
|
+
if (abandonTimer) clearTimeout(abandonTimer);
|
|
210
|
+
abandonTimer = setTimeout(() => {
|
|
211
|
+
abandonTimer = null;
|
|
212
|
+
// Re-check at fire time: a tab may have (re)connected during the grace
|
|
213
|
+
// — a reload, or a second tab — in which case nothing is abandoned.
|
|
214
|
+
if (browserClients.size > 0) return;
|
|
215
|
+
console.log(`\n[redline] No browser connected for ${graceMs / 1000}s — assuming abandoned.`);
|
|
216
|
+
onAbandonCallback?.();
|
|
217
|
+
}, graceMs);
|
|
218
|
+
}
|
|
219
|
+
|
|
197
220
|
function checkBrowserPresence() {
|
|
198
221
|
if (browserClients.size > 0) {
|
|
199
222
|
hadBrowser = true;
|
|
200
223
|
if (abandonTimer) { clearTimeout(abandonTimer); abandonTimer = null; }
|
|
201
224
|
} else if (hadBrowser && !abandonTimer) {
|
|
202
|
-
|
|
203
|
-
console.log(`\n[redline] No browser connected for ${ABANDON_GRACE_MS / 1000}s — assuming abandoned.`);
|
|
204
|
-
onAbandonCallback?.();
|
|
205
|
-
}, ABANDON_GRACE_MS);
|
|
225
|
+
armAbandonTimer(ABANDON_GRACE_MS);
|
|
206
226
|
}
|
|
207
227
|
}
|
|
208
228
|
|
|
@@ -255,7 +275,7 @@ export function createServer(
|
|
|
255
275
|
const agentRepliedAt = latestRound?.agent_replied_at ?? null;
|
|
256
276
|
const roundNumber = latestRound?.round ?? 1;
|
|
257
277
|
const totalRounds = sidecar.rounds.length;
|
|
258
|
-
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));
|
|
259
279
|
});
|
|
260
280
|
|
|
261
281
|
// Add a comment to the active round
|
|
@@ -410,7 +430,7 @@ export function createServer(
|
|
|
410
430
|
});
|
|
411
431
|
|
|
412
432
|
// CLI signals the agent subprocess is gone for good (restart cap exhausted,
|
|
413
|
-
// missing
|
|
433
|
+
// missing provider CLI, etc). Surfaces a small persistent indicator in the
|
|
414
434
|
// header so the user knows replies aren't coming and can restart redline.
|
|
415
435
|
// No paired "agent-available" event — recovery requires a restart, so the
|
|
416
436
|
// indicator stays until the page reloads.
|
|
@@ -424,6 +444,17 @@ export function createServer(
|
|
|
424
444
|
return c.json({ ok: true });
|
|
425
445
|
});
|
|
426
446
|
|
|
447
|
+
// A browser tab fired pagehide — it is closing, reloading, or navigating away.
|
|
448
|
+
// This is a hint, not proof of abandonment (a reload fires it too), so we
|
|
449
|
+
// shorten the abandon grace rather than exiting outright. On a real close the
|
|
450
|
+
// SSE never reconnects and the short timer fires; on a reload the new page
|
|
451
|
+
// reconnects within ~1s and checkBrowserPresence clears the timer. Crashes
|
|
452
|
+
// and kill -9 send no beacon and fall back to the long ABANDON_GRACE_MS.
|
|
453
|
+
app.post("/api/tab-closed", (c) => {
|
|
454
|
+
if (hadBrowser) armAbandonTimer(TAB_CLOSE_GRACE_MS);
|
|
455
|
+
return c.json({ ok: true });
|
|
456
|
+
});
|
|
457
|
+
|
|
427
458
|
// Agent signals it is composing a reply (shows typing indicator in thread)
|
|
428
459
|
app.post("/api/comment/:id/thinking", async (c) => {
|
|
429
460
|
const id = c.req.param("id");
|
|
@@ -440,6 +471,7 @@ export function createServer(
|
|
|
440
471
|
name?: string;
|
|
441
472
|
requires_revision?: boolean;
|
|
442
473
|
revision_reason?: string;
|
|
474
|
+
escalate?: boolean;
|
|
443
475
|
}>();
|
|
444
476
|
if (!body.message?.trim()) return c.json({ ok: false, error: "message is required" }, 400);
|
|
445
477
|
const role = (body.role === "human" ? "human" : "agent") as "human" | "agent";
|
|
@@ -453,9 +485,12 @@ export function createServer(
|
|
|
453
485
|
const entry: import("./sidecar").ThreadEntry = { role, message: body.message.trim(), at: new Date().toISOString() };
|
|
454
486
|
if (name) entry.name = name;
|
|
455
487
|
// Verdict only meaningful on agent replies; ignore on human entries.
|
|
456
|
-
if (role === "agent"
|
|
457
|
-
|
|
458
|
-
|
|
488
|
+
if (role === "agent") {
|
|
489
|
+
if (typeof body.requires_revision === "boolean") {
|
|
490
|
+
entry.requires_revision = body.requires_revision;
|
|
491
|
+
if (body.revision_reason?.trim()) entry.revision_reason = body.revision_reason.trim();
|
|
492
|
+
}
|
|
493
|
+
if (body.escalate === true) entry.escalate = true;
|
|
459
494
|
}
|
|
460
495
|
comment.thread.push(entry);
|
|
461
496
|
return { skip: false as const, roundNumber: round.round, comment };
|
|
@@ -553,7 +588,8 @@ export function createServer(
|
|
|
553
588
|
sidecar.context,
|
|
554
589
|
true, // readOnly
|
|
555
590
|
csrfToken,
|
|
556
|
-
opts.noAgent ?? false
|
|
591
|
+
opts.noAgent ?? false,
|
|
592
|
+
opts.agentName
|
|
557
593
|
));
|
|
558
594
|
});
|
|
559
595
|
|
|
@@ -646,5 +682,3 @@ export function createServer(
|
|
|
646
682
|
onRevisionRecovered(cb: () => void) { onRevisionRecoveredCallback = cb; },
|
|
647
683
|
};
|
|
648
684
|
}
|
|
649
|
-
|
|
650
|
-
|
package/src/sidecar.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface ThreadEntry {
|
|
|
13
13
|
// action defaults to "Revise" or "Accept as-is". Only set on agent entries.
|
|
14
14
|
requires_revision?: boolean;
|
|
15
15
|
revision_reason?: string;
|
|
16
|
+
// Set true on an agent reply when the comment can't be acted on from inside
|
|
17
|
+
// this review — it needs the agent that *launched* redline (project context,
|
|
18
|
+
// tools, or authority the inline agent lacks). Surfaced in the closeout
|
|
19
|
+
// summary so the launching agent picks it up. Only set on agent entries.
|
|
20
|
+
escalate?: boolean;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
// Latest agent verdict on a comment thread:
|