@levistudio/redline 0.1.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/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/ROADMAP.md +39 -0
- package/SECURITY.md +33 -0
- package/bin/redline.cjs +61 -0
- package/package.json +61 -0
- package/scripts/install-skill.sh +78 -0
- package/skills/redline-review/SKILL.md +102 -0
- package/src/agent.ts +283 -0
- package/src/cli.ts +332 -0
- package/src/client/cards.ts +385 -0
- package/src/client/diff.ts +100 -0
- package/src/client/firstRunBanner.ts +26 -0
- package/src/client/lib.ts +299 -0
- package/src/client/main.ts +119 -0
- package/src/client/render.ts +413 -0
- package/src/client/selection.ts +253 -0
- package/src/client/sse.ts +179 -0
- package/src/client/state.ts +56 -0
- package/src/client/styles.css +994 -0
- package/src/contextBlock.ts +16 -0
- package/src/diff.ts +166 -0
- package/src/parseReply.ts +115 -0
- package/src/pickModel.ts +38 -0
- package/src/promptEnvelope.ts +58 -0
- package/src/render.ts +83 -0
- package/src/resolve.ts +290 -0
- package/src/server-page.ts +119 -0
- package/src/server.ts +634 -0
- package/src/sidecar.ts +190 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { state } from "./state";
|
|
2
|
+
import {
|
|
3
|
+
renderComments,
|
|
4
|
+
applyHighlights,
|
|
5
|
+
applyRoundState,
|
|
6
|
+
updateNav,
|
|
7
|
+
positionCards,
|
|
8
|
+
} from "./render";
|
|
9
|
+
|
|
10
|
+
let sseHasConnectedOnce = false;
|
|
11
|
+
let currentEs: EventSource | null = null;
|
|
12
|
+
let lastEventAt = Date.now();
|
|
13
|
+
|
|
14
|
+
export async function softRefresh({ rehighlight = false } = {}): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch("/api/comments");
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
if (typeof data.totalRounds === "number" && data.totalRounds > state.totalRounds) {
|
|
19
|
+
window.location.reload();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
state.comments = data.comments;
|
|
23
|
+
state.roundResolved = data.roundResolved;
|
|
24
|
+
renderComments();
|
|
25
|
+
if (rehighlight) applyHighlights();
|
|
26
|
+
positionCards();
|
|
27
|
+
updateNav();
|
|
28
|
+
applyRoundState();
|
|
29
|
+
} catch {
|
|
30
|
+
/* non-fatal */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function forceReconnect(reason: string): void {
|
|
35
|
+
try {
|
|
36
|
+
console.warn("[redline] forcing SSE reconnect:", reason);
|
|
37
|
+
} catch {}
|
|
38
|
+
if (currentEs) {
|
|
39
|
+
try {
|
|
40
|
+
currentEs.close();
|
|
41
|
+
} catch {}
|
|
42
|
+
currentEs = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onVisibleOrFocus(): void {
|
|
47
|
+
if (document.visibilityState === "visible") {
|
|
48
|
+
softRefresh({ rehighlight: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function initSSE(): void {
|
|
53
|
+
document.addEventListener("visibilitychange", onVisibleOrFocus);
|
|
54
|
+
window.addEventListener("focus", onVisibleOrFocus);
|
|
55
|
+
|
|
56
|
+
setInterval(() => {
|
|
57
|
+
const banner = document.getElementById("sidebar-status-banner");
|
|
58
|
+
const revising = banner?.classList.contains("revising");
|
|
59
|
+
if (!revising) return;
|
|
60
|
+
const silenceMs = Date.now() - lastEventAt;
|
|
61
|
+
if (silenceMs > 30_000) {
|
|
62
|
+
forceReconnect(`no events for ${Math.round(silenceMs / 1000)}s during revision`);
|
|
63
|
+
}
|
|
64
|
+
}, 5000);
|
|
65
|
+
|
|
66
|
+
(function connectEvents() {
|
|
67
|
+
const es = new EventSource("/api/events?client=browser");
|
|
68
|
+
currentEs = es;
|
|
69
|
+
lastEventAt = Date.now();
|
|
70
|
+
const on = (name: string, fn: (e: MessageEvent) => void) =>
|
|
71
|
+
es.addEventListener(name, (e) => {
|
|
72
|
+
lastEventAt = Date.now();
|
|
73
|
+
fn(e as MessageEvent);
|
|
74
|
+
});
|
|
75
|
+
es.onopen = () => {
|
|
76
|
+
lastEventAt = Date.now();
|
|
77
|
+
if (sseHasConnectedOnce) {
|
|
78
|
+
softRefresh({ rehighlight: true });
|
|
79
|
+
}
|
|
80
|
+
sseHasConnectedOnce = true;
|
|
81
|
+
};
|
|
82
|
+
on("comment-thinking", (e) => {
|
|
83
|
+
try {
|
|
84
|
+
state.thinkingCommentIds.add(JSON.parse(e.data).commentId);
|
|
85
|
+
} catch {}
|
|
86
|
+
renderComments();
|
|
87
|
+
positionCards();
|
|
88
|
+
updateNav();
|
|
89
|
+
});
|
|
90
|
+
on("agent-replied", () => {
|
|
91
|
+
state.thinkingCommentIds.clear();
|
|
92
|
+
softRefresh();
|
|
93
|
+
});
|
|
94
|
+
on("comment-added", () => softRefresh({ rehighlight: true }));
|
|
95
|
+
on("comment-reply", (e) => {
|
|
96
|
+
try {
|
|
97
|
+
state.thinkingCommentIds.delete(JSON.parse(e.data).commentId);
|
|
98
|
+
} catch {}
|
|
99
|
+
softRefresh();
|
|
100
|
+
});
|
|
101
|
+
on("comment-resolved", () => softRefresh({ rehighlight: true }));
|
|
102
|
+
on("reload", () => {
|
|
103
|
+
sessionStorage.setItem("just-revised", "1");
|
|
104
|
+
window.location.reload();
|
|
105
|
+
});
|
|
106
|
+
on("revision-chunk", (e) => {
|
|
107
|
+
try {
|
|
108
|
+
const { text, kind } = JSON.parse(e.data);
|
|
109
|
+
const stream = document.getElementById("revision-stream");
|
|
110
|
+
if (stream) {
|
|
111
|
+
if (stream.style.display === "none" || !stream.style.display) stream.style.display = "block";
|
|
112
|
+
const span = document.createElement("span");
|
|
113
|
+
span.className = kind === "thinking" ? "rs-thinking" : "rs-text";
|
|
114
|
+
span.textContent = text;
|
|
115
|
+
stream.appendChild(span);
|
|
116
|
+
stream.scrollTop = stream.scrollHeight;
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
});
|
|
120
|
+
on("revision-error", (e) => {
|
|
121
|
+
let msg = "Revision failed.";
|
|
122
|
+
try {
|
|
123
|
+
msg = "Revision failed: " + (JSON.parse(e.data).message ?? "unknown error");
|
|
124
|
+
} catch {}
|
|
125
|
+
softRefresh();
|
|
126
|
+
const banner = document.getElementById("sidebar-status-banner");
|
|
127
|
+
if (banner) {
|
|
128
|
+
banner.classList.remove("revising");
|
|
129
|
+
banner.classList.remove("error");
|
|
130
|
+
banner.classList.add("error");
|
|
131
|
+
banner.textContent = msg + ' Click "Revise document" to retry.';
|
|
132
|
+
banner.style.display = "block";
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
on("revision-stalled", (e) => {
|
|
136
|
+
let msg = "Revision did not complete.";
|
|
137
|
+
try {
|
|
138
|
+
msg = "Revision did not complete: " + (JSON.parse(e.data).message ?? "unknown");
|
|
139
|
+
} catch {}
|
|
140
|
+
softRefresh();
|
|
141
|
+
const banner = document.getElementById("sidebar-status-banner");
|
|
142
|
+
if (banner) {
|
|
143
|
+
banner.classList.remove("revising");
|
|
144
|
+
banner.classList.remove("error");
|
|
145
|
+
banner.classList.add("error");
|
|
146
|
+
banner.textContent = msg + ' Click "Revise document" to retry.';
|
|
147
|
+
banner.style.display = "block";
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
on("revision-no-changes", () => {
|
|
151
|
+
try {
|
|
152
|
+
sessionStorage.setItem("rl-no-changes", "1");
|
|
153
|
+
} catch {}
|
|
154
|
+
window.location.reload();
|
|
155
|
+
});
|
|
156
|
+
on("agent-unavailable", (e) => {
|
|
157
|
+
let reason = "Agent process unavailable. Restart redline to recover.";
|
|
158
|
+
try {
|
|
159
|
+
const data = JSON.parse(e.data);
|
|
160
|
+
if (data.reason) reason = data.reason;
|
|
161
|
+
} catch {}
|
|
162
|
+
const el = document.getElementById("agent-status");
|
|
163
|
+
if (el) {
|
|
164
|
+
el.textContent = "Agent offline";
|
|
165
|
+
el.setAttribute("title", reason);
|
|
166
|
+
el.removeAttribute("hidden");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
on("finished", () => {
|
|
170
|
+
document.body.innerHTML =
|
|
171
|
+
'<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 Claude Code.</div></div>';
|
|
172
|
+
});
|
|
173
|
+
es.onerror = () => {
|
|
174
|
+
es.close();
|
|
175
|
+
if (currentEs === es) currentEs = null;
|
|
176
|
+
setTimeout(connectEvents, 3000);
|
|
177
|
+
};
|
|
178
|
+
})();
|
|
179
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ClientComment } from "./lib";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
__REDLINE__: {
|
|
6
|
+
comments: ClientComment[];
|
|
7
|
+
roundResolved: boolean;
|
|
8
|
+
totalRounds: number;
|
|
9
|
+
contextTitle: string;
|
|
10
|
+
csrfToken: string;
|
|
11
|
+
noAgent?: boolean;
|
|
12
|
+
};
|
|
13
|
+
hljs?: { highlightElement(el: HTMLElement): void };
|
|
14
|
+
dismissContextBanner?: () => void;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const state = {
|
|
19
|
+
comments: window.__REDLINE__.comments as ClientComment[],
|
|
20
|
+
roundResolved: window.__REDLINE__.roundResolved,
|
|
21
|
+
totalRounds: window.__REDLINE__.totalRounds,
|
|
22
|
+
csrfToken: window.__REDLINE__.csrfToken || "",
|
|
23
|
+
noAgent: window.__REDLINE__.noAgent === true,
|
|
24
|
+
thinkingCommentIds: new Set<string>(),
|
|
25
|
+
pendingSelection: null as PendingSelection | null,
|
|
26
|
+
navIdx: 0,
|
|
27
|
+
deliberateScrollUntil: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PendingSelection = {
|
|
31
|
+
quote: string;
|
|
32
|
+
context_before: string;
|
|
33
|
+
context_after: string;
|
|
34
|
+
_rectTop?: number;
|
|
35
|
+
_range?: Range;
|
|
36
|
+
_img?: HTMLImageElement;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function apiFetch(url: string, init?: RequestInit): Promise<Response> {
|
|
40
|
+
init = init || {};
|
|
41
|
+
const m = (init.method || "GET").toUpperCase();
|
|
42
|
+
if (m !== "GET" && m !== "HEAD") {
|
|
43
|
+
init.headers = Object.assign({}, init.headers || {}, {
|
|
44
|
+
"X-Redline-Token": state.csrfToken,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return fetch(url, init);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function showError(msg: string): void {
|
|
51
|
+
const el = document.getElementById("error-banner");
|
|
52
|
+
if (!el) return;
|
|
53
|
+
el.textContent = msg;
|
|
54
|
+
el.style.display = "block";
|
|
55
|
+
setTimeout(() => (el.style.display = "none"), 4000);
|
|
56
|
+
}
|