@levistudio/redline 0.1.0 → 0.3.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 +27 -7
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent.ts +8 -4
- package/src/cli.ts +41 -4
- package/src/client/cards.ts +12 -2
- package/src/client/diff.ts +58 -23
- package/src/client/diffToggle.ts +32 -0
- package/src/client/lib.ts +147 -72
- package/src/client/main.ts +10 -4
- package/src/client/render.ts +17 -5
- package/src/client/selection.ts +1 -1
- package/src/client/sse.ts +33 -1
- package/src/client/state.ts +22 -0
- package/src/client/styles.css +134 -93
- package/src/parseReply.ts +9 -1
- package/src/render.ts +9 -0
- package/src/resolve.ts +174 -91
- package/src/reviewSummary.ts +93 -0
- package/src/server-page.ts +5 -7
- package/src/server.ts +65 -14
- package/src/sidecar.ts +5 -0
package/src/client/main.ts
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
import { state } from "./state";
|
|
17
17
|
import { initSelectionHandlers } from "./selection";
|
|
18
18
|
import { initSSE } from "./sse";
|
|
19
|
-
import {
|
|
19
|
+
import { enableDiffMode, initDiffHandlers } from "./diff";
|
|
20
|
+
import { diffStateKey } from "./diffToggle";
|
|
20
21
|
import { observeCardSizes } from "./cards";
|
|
21
22
|
|
|
22
23
|
// Wire card callbacks (breaks circular dependency between cards and render)
|
|
@@ -89,9 +90,14 @@ positionCards();
|
|
|
89
90
|
updateNav();
|
|
90
91
|
applyRoundState();
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const justRevised = sessionStorage.getItem("just-revised");
|
|
94
|
+
if (justRevised) sessionStorage.removeItem("just-revised");
|
|
95
|
+
const diffPersisted = (() => {
|
|
96
|
+
try { return sessionStorage.getItem(diffStateKey(window.__REDLINE__.contextTitle)) === "1"; }
|
|
97
|
+
catch { return false; }
|
|
98
|
+
})();
|
|
99
|
+
if (justRevised || diffPersisted) {
|
|
100
|
+
void enableDiffMode();
|
|
95
101
|
}
|
|
96
102
|
if (sessionStorage.getItem("rl-no-changes")) {
|
|
97
103
|
sessionStorage.removeItem("rl-no-changes");
|
package/src/client/render.ts
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
highlightText as _highlightText,
|
|
4
4
|
preserveScroll as _preserveScroll,
|
|
5
5
|
latestVerdict,
|
|
6
|
+
isEscalated,
|
|
6
7
|
} from "./lib";
|
|
7
|
-
import { state, apiFetch, showError } from "./state";
|
|
8
|
+
import { state, apiFetch, showError, reportMutationFailure } from "./state";
|
|
8
9
|
import {
|
|
9
10
|
buildCommentCard,
|
|
10
11
|
captureTypingState,
|
|
@@ -171,7 +172,7 @@ export async function saveComment(
|
|
|
171
172
|
showError(data.error || "Failed to save comment");
|
|
172
173
|
}
|
|
173
174
|
} catch (err: unknown) {
|
|
174
|
-
|
|
175
|
+
reportMutationFailure("Failed to save comment", err);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
@@ -195,7 +196,7 @@ export async function submitReply(id: string, message: string): Promise<void> {
|
|
|
195
196
|
showError(data.error || "Failed to save reply");
|
|
196
197
|
}
|
|
197
198
|
} catch (err: unknown) {
|
|
198
|
-
|
|
199
|
+
reportMutationFailure("Failed to save reply", err);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -220,7 +221,7 @@ export async function resolveComment(id: string): Promise<void> {
|
|
|
220
221
|
showError(data.error || "Failed to resolve comment");
|
|
221
222
|
}
|
|
222
223
|
} catch (err: unknown) {
|
|
223
|
-
|
|
224
|
+
reportMutationFailure("Failed to resolve", err);
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
|
|
@@ -240,7 +241,7 @@ export async function reopenComment(id: string): Promise<void> {
|
|
|
240
241
|
showError(data.error || "Failed to reopen comment");
|
|
241
242
|
}
|
|
242
243
|
} catch (err: unknown) {
|
|
243
|
-
|
|
244
|
+
reportMutationFailure("Failed to reopen", err);
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -345,6 +346,17 @@ export function applyRoundState(): void {
|
|
|
345
346
|
bannerText = `${reviseCount} of ${total} comments imply edits.`;
|
|
346
347
|
}
|
|
347
348
|
banner.textContent = bannerText;
|
|
349
|
+
// Escalations are orthogonal to the revise/accept verdict — a comment
|
|
350
|
+
// can be answered in-thread yet still need the launching agent. Call
|
|
351
|
+
// it out on its own line so it survives all three banner branches.
|
|
352
|
+
const escCount = state.comments.filter(isEscalated).length;
|
|
353
|
+
if (escCount > 0) {
|
|
354
|
+
const escLine = document.createElement("div");
|
|
355
|
+
escLine.className = "banner-escalation";
|
|
356
|
+
escLine.textContent =
|
|
357
|
+
`↑ ${escCount} comment${escCount !== 1 ? "s" : ""} escalated to the launching agent.`;
|
|
358
|
+
banner.appendChild(escLine);
|
|
359
|
+
}
|
|
348
360
|
banner.style.display = "block";
|
|
349
361
|
}
|
|
350
362
|
|
package/src/client/selection.ts
CHANGED
|
@@ -197,7 +197,7 @@ export function initSelectionHandlers(): void {
|
|
|
197
197
|
|
|
198
198
|
const captured = captureSelection(sel, text);
|
|
199
199
|
if (!captured) {
|
|
200
|
-
showError("
|
|
200
|
+
showError("Couldn't anchor that selection \u2014 try highlighting the passage again.");
|
|
201
201
|
sel.removeAllRanges();
|
|
202
202
|
return;
|
|
203
203
|
}
|
package/src/client/sse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { state } from "./state";
|
|
1
|
+
import { state, markSessionEnded } from "./state";
|
|
2
2
|
import {
|
|
3
3
|
renderComments,
|
|
4
4
|
applyHighlights,
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
let sseHasConnectedOnce = false;
|
|
11
11
|
let currentEs: EventSource | null = null;
|
|
12
12
|
let lastEventAt = Date.now();
|
|
13
|
+
// Consecutive failed connection attempts with no successful open in between.
|
|
14
|
+
// A transient blip resolves on the next reconnect (resetting this to 0); a
|
|
15
|
+
// dead server never reconnects, so a sustained run means the server is gone.
|
|
16
|
+
let consecutiveSseErrors = 0;
|
|
17
|
+
const MAX_SSE_ERRORS = 4;
|
|
13
18
|
|
|
14
19
|
export async function softRefresh({ rehighlight = false } = {}): Promise<void> {
|
|
15
20
|
try {
|
|
@@ -53,6 +58,24 @@ export function initSSE(): void {
|
|
|
53
58
|
document.addEventListener("visibilitychange", onVisibleOrFocus);
|
|
54
59
|
window.addEventListener("focus", onVisibleOrFocus);
|
|
55
60
|
|
|
61
|
+
// Tell the server explicitly when this tab is going away, so it can
|
|
62
|
+
// distinguish a real close from a bare SSE drop (sleep, network blip) and
|
|
63
|
+
// not abandon a session the user means to keep. `keepalive` lets the POST
|
|
64
|
+
// survive unload; `pagehide` is more reliable than `beforeunload`. Skip the
|
|
65
|
+
// bfcache case (e.persisted) — the page may be restored and reconnect.
|
|
66
|
+
window.addEventListener("pagehide", (e) => {
|
|
67
|
+
if ((e as PageTransitionEvent).persisted) return;
|
|
68
|
+
try {
|
|
69
|
+
fetch("/api/tab-closed", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
keepalive: true,
|
|
72
|
+
headers: { "X-Redline-Token": state.csrfToken },
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
/* unload is best-effort */
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
56
79
|
setInterval(() => {
|
|
57
80
|
const banner = document.getElementById("sidebar-status-banner");
|
|
58
81
|
const revising = banner?.classList.contains("revising");
|
|
@@ -74,6 +97,7 @@ export function initSSE(): void {
|
|
|
74
97
|
});
|
|
75
98
|
es.onopen = () => {
|
|
76
99
|
lastEventAt = Date.now();
|
|
100
|
+
consecutiveSseErrors = 0;
|
|
77
101
|
if (sseHasConnectedOnce) {
|
|
78
102
|
softRefresh({ rehighlight: true });
|
|
79
103
|
}
|
|
@@ -173,6 +197,14 @@ export function initSSE(): void {
|
|
|
173
197
|
es.onerror = () => {
|
|
174
198
|
es.close();
|
|
175
199
|
if (currentEs === es) currentEs = null;
|
|
200
|
+
consecutiveSseErrors += 1;
|
|
201
|
+
// A run of failures with no successful open in between means the server
|
|
202
|
+
// is gone for good (it exited, or restarted on a fresh port this tab
|
|
203
|
+
// can't reach). Stop the silent retry loop and tell the user.
|
|
204
|
+
if (consecutiveSseErrors >= MAX_SSE_ERRORS) {
|
|
205
|
+
markSessionEnded();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
176
208
|
setTimeout(connectEvents, 3000);
|
|
177
209
|
};
|
|
178
210
|
})();
|
package/src/client/state.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const state = {
|
|
|
25
25
|
pendingSelection: null as PendingSelection | null,
|
|
26
26
|
navIdx: 0,
|
|
27
27
|
deliberateScrollUntil: 0,
|
|
28
|
+
sessionEnded: false,
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export type PendingSelection = {
|
|
@@ -54,3 +55,24 @@ export function showError(msg: string): void {
|
|
|
54
55
|
el.style.display = "block";
|
|
55
56
|
setTimeout(() => (el.style.display = "none"), 4000);
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
// The redline server has exited (session ended, or process killed). This tab
|
|
60
|
+
// can no longer do anything useful — show a persistent banner instead of
|
|
61
|
+
// letting actions fail with a cryptic "Failed to fetch". Idempotent.
|
|
62
|
+
export function markSessionEnded(): void {
|
|
63
|
+
if (state.sessionEnded) return;
|
|
64
|
+
state.sessionEnded = true;
|
|
65
|
+
const el = document.getElementById("session-ended-banner");
|
|
66
|
+
if (el) el.style.display = "block";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A fetch network failure (TypeError) on a mutating request means the server
|
|
70
|
+
// is unreachable — treat it as a definitively ended session. A non-network
|
|
71
|
+
// failure (server replied with an error) is shown as a transient toast.
|
|
72
|
+
export function reportMutationFailure(action: string, err: unknown): void {
|
|
73
|
+
if (err instanceof TypeError) {
|
|
74
|
+
markSessionEnded();
|
|
75
|
+
} else {
|
|
76
|
+
showError(action + ": " + (err as Error).message);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/client/styles.css
CHANGED
|
@@ -58,7 +58,6 @@ body {
|
|
|
58
58
|
font-weight: 500;
|
|
59
59
|
color: var(--text-muted);
|
|
60
60
|
letter-spacing: 0.02em;
|
|
61
|
-
text-transform: uppercase;
|
|
62
61
|
display: flex;
|
|
63
62
|
align-items: center;
|
|
64
63
|
gap: 10px;
|
|
@@ -110,6 +109,16 @@ body {
|
|
|
110
109
|
.round-picker-item:hover { background: var(--thread-bg); }
|
|
111
110
|
.round-picker-item.current { font-weight: 600; pointer-events: none; color: var(--text-muted); }
|
|
112
111
|
.round-picker-meta { font-size: 11px; color: var(--text-muted); margin-left: auto; }
|
|
112
|
+
.round-picker-action {
|
|
113
|
+
width: 100%;
|
|
114
|
+
text-align: left;
|
|
115
|
+
background: none;
|
|
116
|
+
border: none;
|
|
117
|
+
border-top: 1px solid var(--border);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
font-family: inherit;
|
|
120
|
+
color: var(--accent);
|
|
121
|
+
}
|
|
113
122
|
|
|
114
123
|
.btn-resolve {
|
|
115
124
|
background: var(--accent);
|
|
@@ -141,6 +150,23 @@ body {
|
|
|
141
150
|
.btn-accept:hover:not(:disabled) { border-color: #9ca3af; color: #111827; }
|
|
142
151
|
.btn-accept:disabled { opacity: 0.4; cursor: default; }
|
|
143
152
|
|
|
153
|
+
.btn-toggle-diff {
|
|
154
|
+
background: white;
|
|
155
|
+
color: #374151;
|
|
156
|
+
border: 1.5px solid #d1d5db;
|
|
157
|
+
padding: 6px 12px;
|
|
158
|
+
border-radius: var(--radius);
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
163
|
+
}
|
|
164
|
+
.btn-toggle-diff:hover { border-color: #9ca3af; color: #111827; }
|
|
165
|
+
.btn-toggle-diff.active {
|
|
166
|
+
background: #f3f4f6;
|
|
167
|
+
color: #111827;
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
.header-actions { display: flex; gap: 8px; align-items: center; }
|
|
145
171
|
|
|
146
172
|
/* Asymmetric status: hidden when healthy, visible only when the agent process
|
|
@@ -475,6 +501,57 @@ mark.rl-highlight.rl-img:hover, mark.rl-highlight.rl-img.active {
|
|
|
475
501
|
font-size: 13.5px;
|
|
476
502
|
line-height: 1.5;
|
|
477
503
|
color: var(--text);
|
|
504
|
+
word-wrap: break-word;
|
|
505
|
+
overflow-wrap: anywhere;
|
|
506
|
+
}
|
|
507
|
+
.thread-message > :first-child { margin-top: 0; }
|
|
508
|
+
.thread-message > :last-child { margin-bottom: 0; }
|
|
509
|
+
.thread-message p {
|
|
510
|
+
margin: 0 0 8px;
|
|
511
|
+
}
|
|
512
|
+
.thread-message ul,
|
|
513
|
+
.thread-message ol {
|
|
514
|
+
margin: 4px 0 8px;
|
|
515
|
+
padding-left: 22px;
|
|
516
|
+
}
|
|
517
|
+
.thread-message li { margin: 2px 0; }
|
|
518
|
+
.thread-message li > p { margin: 0; }
|
|
519
|
+
.thread-message code {
|
|
520
|
+
background: rgba(0, 0, 0, 0.05);
|
|
521
|
+
padding: 1px 4px;
|
|
522
|
+
border-radius: 3px;
|
|
523
|
+
font-size: 12.5px;
|
|
524
|
+
}
|
|
525
|
+
.thread-message pre {
|
|
526
|
+
background: rgba(0, 0, 0, 0.05);
|
|
527
|
+
padding: 8px 10px;
|
|
528
|
+
border-radius: 4px;
|
|
529
|
+
overflow-x: auto;
|
|
530
|
+
font-size: 12.5px;
|
|
531
|
+
line-height: 1.45;
|
|
532
|
+
margin: 4px 0 8px;
|
|
533
|
+
}
|
|
534
|
+
.thread-message pre code {
|
|
535
|
+
background: transparent;
|
|
536
|
+
padding: 0;
|
|
537
|
+
}
|
|
538
|
+
.thread-message blockquote {
|
|
539
|
+
margin: 4px 0 8px;
|
|
540
|
+
padding-left: 10px;
|
|
541
|
+
border-left: 3px solid var(--border, #ddd);
|
|
542
|
+
color: var(--text-muted, #666);
|
|
543
|
+
}
|
|
544
|
+
.thread-message h1,
|
|
545
|
+
.thread-message h2,
|
|
546
|
+
.thread-message h3,
|
|
547
|
+
.thread-message h4 {
|
|
548
|
+
font-size: 13.5px;
|
|
549
|
+
font-weight: 600;
|
|
550
|
+
margin: 4px 0;
|
|
551
|
+
}
|
|
552
|
+
.thread-message a {
|
|
553
|
+
color: var(--accent);
|
|
554
|
+
text-decoration: underline;
|
|
478
555
|
}
|
|
479
556
|
|
|
480
557
|
/* ── Verdict footer on agent replies ── */
|
|
@@ -492,6 +569,7 @@ mark.rl-highlight.rl-img:hover, mark.rl-highlight.rl-img.active {
|
|
|
492
569
|
line-height: 1.5;
|
|
493
570
|
}
|
|
494
571
|
.verdict.revise { color: #92400e; }
|
|
572
|
+
.verdict.escalate { color: #5b21b6; }
|
|
495
573
|
|
|
496
574
|
/* Warm-tinted resolve button when the latest verdict implies an edit */
|
|
497
575
|
.btn-resolve-comment.revise {
|
|
@@ -519,6 +597,22 @@ mark.rl-highlight.rl-img:hover, mark.rl-highlight.rl-img.active {
|
|
|
519
597
|
.verdict-badge.revise { background: #fef3c7; color: #92400e; }
|
|
520
598
|
.verdict-badge.accept { background: #e5e7eb; color: var(--text-muted); }
|
|
521
599
|
|
|
600
|
+
/* Escalation badge on the quote line — comment routed to the launching agent */
|
|
601
|
+
.escalate-badge {
|
|
602
|
+
display: inline-flex;
|
|
603
|
+
align-items: center;
|
|
604
|
+
margin-left: 6px;
|
|
605
|
+
padding: 1px 6px;
|
|
606
|
+
font-size: 10.5px;
|
|
607
|
+
font-weight: 600;
|
|
608
|
+
text-transform: uppercase;
|
|
609
|
+
letter-spacing: 0.04em;
|
|
610
|
+
border-radius: 3px;
|
|
611
|
+
font-style: normal;
|
|
612
|
+
background: #ede9fe;
|
|
613
|
+
color: #5b21b6;
|
|
614
|
+
}
|
|
615
|
+
|
|
522
616
|
/* Round-level secondary action (under the primary banner button) */
|
|
523
617
|
.round-secondary {
|
|
524
618
|
margin-top: 8px;
|
|
@@ -709,6 +803,16 @@ mark.rl-highlight.rl-img:hover, mark.rl-highlight.rl-img.active {
|
|
|
709
803
|
align-items: center;
|
|
710
804
|
gap: 8px;
|
|
711
805
|
}
|
|
806
|
+
/* Escalation sub-line under the round banner — purple to match the
|
|
807
|
+
↑ Escalated comment badges, distinct from the green verdict copy. */
|
|
808
|
+
.banner-escalation {
|
|
809
|
+
margin-top: 6px;
|
|
810
|
+
padding-top: 6px;
|
|
811
|
+
border-top: 1px solid #a5d6a7;
|
|
812
|
+
color: #5b21b6;
|
|
813
|
+
font-size: 12.5px;
|
|
814
|
+
font-weight: 600;
|
|
815
|
+
}
|
|
712
816
|
#sidebar-status-banner.revising {
|
|
713
817
|
background: #fff3e0;
|
|
714
818
|
border-bottom-color: #ffb74d;
|
|
@@ -821,22 +925,12 @@ mark.rl-highlight.rl-img:hover, mark.rl-highlight.rl-img.active {
|
|
|
821
925
|
.diff-prose li { margin: 0.3em 0; line-height: 1.7; }
|
|
822
926
|
.diff-block { border-radius: 4px; margin: 2px -12px; padding: 2px 12px; }
|
|
823
927
|
.diff-block-add { background: #e6ffed; border-left: 3px solid #28a745; }
|
|
824
|
-
.diff-block-del { background: #ffeef0; border-left: 3px solid #d73a49; opacity: 0.8; }
|
|
928
|
+
.diff-block-del { background: #ffeef0; border-left: 3px solid #d73a49; opacity: 0.8; text-decoration: line-through; text-decoration-color: #b91c2c; text-decoration-thickness: 1.5px; }
|
|
929
|
+
.diff-block-del a { text-decoration: line-through; }
|
|
825
930
|
.diff-block-mod { background: #fffbe6; border-left: 3px solid #f0ad00; }
|
|
826
931
|
ins.diff-word-add { background: #acf2bd; text-decoration: none; border-radius: 2px; padding: 0 1px; }
|
|
827
932
|
del.diff-word-del { background: #fdb8c0; border-radius: 2px; padding: 0 1px; }
|
|
828
933
|
.diff-no-changes { padding: 24px 0; color: var(--text-muted); }
|
|
829
|
-
.btn-diff-compare {
|
|
830
|
-
font-size: 12px;
|
|
831
|
-
padding: 4px 10px;
|
|
832
|
-
border-radius: var(--radius);
|
|
833
|
-
border: 1px solid var(--border);
|
|
834
|
-
background: white;
|
|
835
|
-
cursor: pointer;
|
|
836
|
-
color: var(--text-muted);
|
|
837
|
-
transition: all 0.1s;
|
|
838
|
-
}
|
|
839
|
-
.btn-diff-compare:hover { color: var(--text); border-color: #aaa; }
|
|
840
934
|
.btn-diff-accept {
|
|
841
935
|
background: #2e7d32;
|
|
842
936
|
color: white;
|
|
@@ -880,84 +974,34 @@ del.diff-word-del { background: #fdb8c0; border-radius: 2px; padding: 0 1px; }
|
|
|
880
974
|
opacity: 0.75;
|
|
881
975
|
}
|
|
882
976
|
|
|
883
|
-
/* ──
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
align-items: center;
|
|
887
|
-
gap: 10px;
|
|
888
|
-
padding: 8px 14px;
|
|
889
|
-
background: #eff6ff;
|
|
890
|
-
border: 1px solid #bfdbfe;
|
|
891
|
-
border-radius: var(--radius);
|
|
892
|
-
margin-bottom: 14px;
|
|
893
|
-
font-size: 13.5px;
|
|
894
|
-
color: #1e40af;
|
|
895
|
-
}
|
|
896
|
-
.revision-banner-text { flex: 1; }
|
|
897
|
-
.revision-banner-link {
|
|
898
|
-
background: none;
|
|
899
|
-
border: none;
|
|
900
|
-
cursor: pointer;
|
|
901
|
-
color: #1d4ed8;
|
|
902
|
-
font-size: 13.5px;
|
|
903
|
-
font-weight: 500;
|
|
904
|
-
padding: 0;
|
|
905
|
-
text-decoration: underline;
|
|
906
|
-
text-underline-offset: 2px;
|
|
907
|
-
}
|
|
908
|
-
.revision-banner-link:hover { color: #1e40af; }
|
|
909
|
-
.revision-banner-dismiss {
|
|
910
|
-
background: none;
|
|
911
|
-
border: none;
|
|
912
|
-
cursor: pointer;
|
|
913
|
-
color: #60a5fa;
|
|
914
|
-
font-size: 13px;
|
|
915
|
-
padding: 1px 4px;
|
|
916
|
-
border-radius: 3px;
|
|
917
|
-
line-height: 1;
|
|
918
|
-
opacity: 0.7;
|
|
919
|
-
flex-shrink: 0;
|
|
920
|
-
}
|
|
921
|
-
.revision-banner-dismiss:hover { opacity: 1; background: rgba(96,165,250,0.15); }
|
|
922
|
-
|
|
923
|
-
/* ── Context banner ── */
|
|
977
|
+
/* ── Context banner ──
|
|
978
|
+
Muted left-accent stripe; the styling itself signals "framing for what
|
|
979
|
+
follows", so we don't need a CONTEXT label or a saturated fill. */
|
|
924
980
|
.context-banner {
|
|
925
981
|
display: flex;
|
|
926
982
|
align-items: flex-start;
|
|
927
983
|
gap: 10px;
|
|
928
|
-
padding:
|
|
929
|
-
|
|
930
|
-
border: 1px solid #fde68a;
|
|
931
|
-
border-radius: var(--radius);
|
|
984
|
+
padding: 8px 12px 8px 16px;
|
|
985
|
+
border-left: 3px solid var(--accent);
|
|
932
986
|
margin-bottom: 14px;
|
|
933
|
-
font-size:
|
|
934
|
-
color:
|
|
935
|
-
line-height: 1.
|
|
936
|
-
}
|
|
937
|
-
.context-label {
|
|
938
|
-
font-weight: 600;
|
|
939
|
-
text-transform: uppercase;
|
|
940
|
-
letter-spacing: 0.05em;
|
|
941
|
-
font-size: 10.5px;
|
|
942
|
-
color: #b45309;
|
|
943
|
-
white-space: nowrap;
|
|
944
|
-
padding-top: 2px;
|
|
945
|
-
flex-shrink: 0;
|
|
987
|
+
font-size: 14px;
|
|
988
|
+
color: var(--text);
|
|
989
|
+
line-height: 1.55;
|
|
946
990
|
}
|
|
947
991
|
.context-text { flex: 1; }
|
|
948
992
|
.context-dismiss {
|
|
949
993
|
background: none;
|
|
950
994
|
border: none;
|
|
951
995
|
cursor: pointer;
|
|
952
|
-
color:
|
|
953
|
-
font-size:
|
|
996
|
+
color: var(--text-muted);
|
|
997
|
+
font-size: 12px;
|
|
954
998
|
padding: 1px 4px;
|
|
955
999
|
border-radius: 3px;
|
|
956
1000
|
line-height: 1;
|
|
957
|
-
opacity: 0.
|
|
1001
|
+
opacity: 0.4;
|
|
958
1002
|
flex-shrink: 0;
|
|
959
1003
|
}
|
|
960
|
-
.context-dismiss:hover { opacity:
|
|
1004
|
+
.context-dismiss:hover { opacity: 0.9; background: rgba(0,0,0,0.04); }
|
|
961
1005
|
|
|
962
1006
|
/* ── First-run security banner ── */
|
|
963
1007
|
/* Visible only on the first run on a given browser. Dismissal sets
|
|
@@ -967,28 +1011,25 @@ del.diff-word-del { background: #fdb8c0; border-radius: 2px; padding: 0 1px; }
|
|
|
967
1011
|
.first-run-banner[hidden] { display: none; }
|
|
968
1012
|
.first-run-banner {
|
|
969
1013
|
display: flex;
|
|
970
|
-
align-items:
|
|
971
|
-
gap:
|
|
972
|
-
padding:
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
margin-bottom: 14px;
|
|
977
|
-
font-size: 13px;
|
|
978
|
-
color: #7f1d1d;
|
|
1014
|
+
align-items: baseline;
|
|
1015
|
+
gap: 8px;
|
|
1016
|
+
padding: 4px 4px 4px 16px;
|
|
1017
|
+
margin-bottom: 10px;
|
|
1018
|
+
font-size: 11.5px;
|
|
1019
|
+
color: var(--text-muted);
|
|
979
1020
|
line-height: 1.5;
|
|
1021
|
+
opacity: 0.75;
|
|
980
1022
|
}
|
|
981
|
-
.first-run-icon { font-size:
|
|
1023
|
+
.first-run-icon { font-size: 10px; flex-shrink: 0; opacity: 0.6; }
|
|
982
1024
|
.first-run-text { flex: 1; }
|
|
983
1025
|
.first-run-dismiss {
|
|
984
|
-
background:
|
|
985
|
-
border:
|
|
986
|
-
color:
|
|
1026
|
+
background: none;
|
|
1027
|
+
border: none;
|
|
1028
|
+
color: var(--text-muted);
|
|
987
1029
|
cursor: pointer;
|
|
988
|
-
font-size:
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
border-radius: 4px;
|
|
1030
|
+
font-size: 11.5px;
|
|
1031
|
+
padding: 0 4px;
|
|
1032
|
+
text-decoration: underline;
|
|
992
1033
|
flex-shrink: 0;
|
|
993
1034
|
}
|
|
994
|
-
.first-run-dismiss:hover {
|
|
1035
|
+
.first-run-dismiss:hover { color: var(--text); }
|
package/src/parseReply.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// text into the UI. The delimiter form needs no escaping:
|
|
8
8
|
//
|
|
9
9
|
// REQUIRES_REVISION: <true|false>
|
|
10
|
+
// ESCALATE: <true|false> (optional — absent means false)
|
|
10
11
|
// REASON: <one short sentence, or empty>
|
|
11
12
|
// ---MESSAGE---
|
|
12
13
|
// <free-form prose, may contain anything>
|
|
@@ -25,6 +26,10 @@ export interface ParsedReply {
|
|
|
25
26
|
message: string;
|
|
26
27
|
requires_revision: boolean;
|
|
27
28
|
reason: string;
|
|
29
|
+
// True when the agent flagged the comment for the launching ("outer") agent
|
|
30
|
+
// — something it couldn't act on from inside the review. Optional in the
|
|
31
|
+
// envelope; absent means false.
|
|
32
|
+
escalate: boolean;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function tryDelimiterEnvelope(s: string): ParsedReply | null {
|
|
@@ -37,11 +42,13 @@ function tryDelimiterEnvelope(s: string): ParsedReply | null {
|
|
|
37
42
|
|
|
38
43
|
const reqMatch = header.match(/REQUIRES_REVISION\s*:\s*(true|false)\b/i);
|
|
39
44
|
const reasonMatch = header.match(/REASON\s*:\s*(.*?)\s*(?:\n|$)/i);
|
|
45
|
+
const escMatch = header.match(/ESCALATE\s*:\s*(true|false)\b/i);
|
|
40
46
|
|
|
41
47
|
return {
|
|
42
48
|
message,
|
|
43
49
|
requires_revision: reqMatch ? reqMatch[1].toLowerCase() === "true" : true,
|
|
44
50
|
reason: reasonMatch ? reasonMatch[1].trim() : "",
|
|
51
|
+
escalate: escMatch ? escMatch[1].toLowerCase() === "true" : false,
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -93,6 +100,7 @@ export function parseReply(raw: string): ParsedReply {
|
|
|
93
100
|
message: obj.message.trim(),
|
|
94
101
|
requires_revision: obj.requires_revision !== false, // default true if missing/non-bool
|
|
95
102
|
reason: typeof obj.reason === "string" ? obj.reason.trim() : "",
|
|
103
|
+
escalate: obj.escalate === true,
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
} catch { /* fall through */ }
|
|
@@ -111,5 +119,5 @@ export function parseReply(raw: string): ParsedReply {
|
|
|
111
119
|
if (obj) return obj;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
return { message: trimmed, requires_revision: true, reason: "" };
|
|
122
|
+
return { message: trimmed, requires_revision: true, reason: "", escalate: false };
|
|
115
123
|
}
|
package/src/render.ts
CHANGED
|
@@ -57,6 +57,15 @@ export function renderMarkdown(content: string): string {
|
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Render a comment-thread message through the same sanitized markdown
|
|
61
|
+
// pipeline as the document body. Agents reply in markdown (bold, lists,
|
|
62
|
+
// inline code, occasional fenced blocks) and the chat-style rendering in
|
|
63
|
+
// the sidebar previously displayed the raw `**bold**` and `1.` syntax,
|
|
64
|
+
// which looked like the agent had failed to render its own output.
|
|
65
|
+
export function renderMessageMarkdown(content: string): string {
|
|
66
|
+
return renderMarkdown(content);
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/**
|
|
61
70
|
* Locate where a quoted passage starts inside the flat text of a document.
|
|
62
71
|
*
|