@fullstackhouse/cerber 0.9.1 → 0.10.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/README.md +36 -0
- package/dist/core/artifact.js +67 -0
- package/dist/core/revise.js +166 -0
- package/dist/runner/chat.js +215 -0
- package/dist/runner/claude.js +3 -0
- package/dist/runner/prompt.js +132 -0
- package/dist/runner/review.js +19 -1
- package/dist/server/daemon.js +2 -0
- package/dist/server/index.js +62 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-Dmz5lQnG.css → index-BGy7f14m.css} +1 -1
- package/web/dist/assets/index-CmpTqzKD.js +236 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CsWW0EBv.js +0 -236
package/dist/runner/review.js
CHANGED
|
@@ -121,10 +121,17 @@ async function runReview(ref, opts) {
|
|
|
121
121
|
error: null,
|
|
122
122
|
withSource: source !== null,
|
|
123
123
|
trusted: trusted && source !== null,
|
|
124
|
+
sessionId: null,
|
|
124
125
|
},
|
|
125
126
|
sent: null,
|
|
126
127
|
refresh: null,
|
|
127
128
|
calibration: null,
|
|
129
|
+
// The conversation is the user's writing, so a re-review keeps it — the
|
|
130
|
+
// chat prompt replays the transcript, which is what makes it survive the
|
|
131
|
+
// session change. The snapshot does NOT carry over: it describes the draft
|
|
132
|
+
// this run just replaced, and resetting to it would restore a dead review.
|
|
133
|
+
chat: existing?.chat ?? [],
|
|
134
|
+
preChat: null,
|
|
128
135
|
};
|
|
129
136
|
await saveArtifact(artifact);
|
|
130
137
|
const { prompt, truncated } = buildReviewPrompt(pr, diff, { source: source !== null, trusted });
|
|
@@ -170,6 +177,9 @@ async function runReview(ref, opts) {
|
|
|
170
177
|
error: null,
|
|
171
178
|
withSource: source !== null,
|
|
172
179
|
trusted: trusted && source !== null,
|
|
180
|
+
// Only worth keeping when there is a checkout to resume into: a session
|
|
181
|
+
// whose working directory was empty has nothing a chat turn could read.
|
|
182
|
+
sessionId: source ? review.sessionId : null,
|
|
173
183
|
},
|
|
174
184
|
};
|
|
175
185
|
// A re-review regenerates the AI's comments, but the human's own comments
|
|
@@ -236,17 +246,25 @@ async function runAiReview(prompt, opts, source, trusted) {
|
|
|
236
246
|
try {
|
|
237
247
|
const first = await runClaude(prompt, claudeOpts);
|
|
238
248
|
try {
|
|
239
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
ai: AiReviewSchema.parse(extractJson(first.text)),
|
|
251
|
+
costUsd: first.costUsd,
|
|
252
|
+
model: first.model,
|
|
253
|
+
sessionId: first.sessionId,
|
|
254
|
+
};
|
|
240
255
|
}
|
|
241
256
|
catch (err) {
|
|
242
257
|
const message = err instanceof Error ? err.message : String(err);
|
|
243
258
|
opts.onProgress?.("Model output failed validation — retrying once…");
|
|
259
|
+
// A fresh run, not a resume: the retry prompt already carries the whole
|
|
260
|
+
// original prompt, so resuming would send the diff twice.
|
|
244
261
|
const second = await runClaude(buildRetryPrompt(prompt, first.text, message), claudeOpts);
|
|
245
262
|
const cost = (first.costUsd ?? 0) + (second.costUsd ?? 0);
|
|
246
263
|
return {
|
|
247
264
|
ai: AiReviewSchema.parse(extractJson(second.text)),
|
|
248
265
|
costUsd: cost > 0 ? cost : null,
|
|
249
266
|
model: second.model,
|
|
267
|
+
sessionId: second.sessionId ?? first.sessionId,
|
|
250
268
|
};
|
|
251
269
|
}
|
|
252
270
|
}
|
package/dist/server/daemon.js
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import { buildReviewPayload, computeCalibration } from "../core/send.js";
|
|
|
17
17
|
import { listArtifacts, loadArtifactByKey, reconcileRunning, updateArtifactByKey, } from "../core/state.js";
|
|
18
18
|
import { isReviewRunning } from "../runner/inflight.js";
|
|
19
19
|
import { reviewPr } from "../runner/review.js";
|
|
20
|
+
import { runChatTurn } from "../runner/chat.js";
|
|
21
|
+
import { mergeConcurrentEdits, restoreReview } from "../core/revise.js";
|
|
22
|
+
import { ChatTurnSchema } from "../core/artifact.js";
|
|
20
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
24
|
export async function buildApp(opts) {
|
|
22
25
|
const app = new Hono();
|
|
@@ -294,6 +297,7 @@ export async function buildApp(opts) {
|
|
|
294
297
|
error: null,
|
|
295
298
|
withSource: withSource !== false,
|
|
296
299
|
trusted: false,
|
|
300
|
+
sessionId: null,
|
|
297
301
|
},
|
|
298
302
|
}));
|
|
299
303
|
// Minutes-long: run it detached and let the cockpit poll the artifact.
|
|
@@ -315,6 +319,64 @@ export async function buildApp(opts) {
|
|
|
315
319
|
});
|
|
316
320
|
return c.json(running, 202);
|
|
317
321
|
});
|
|
322
|
+
// ---- Chat: argue with the review before sending it ----
|
|
323
|
+
// Local only. The transcript never reaches GitHub — Send still builds its
|
|
324
|
+
// payload from the summary, comments and verdict alone.
|
|
325
|
+
const ChatRequestSchema = z.object({
|
|
326
|
+
message: z.string().min(1),
|
|
327
|
+
refs: ChatTurnSchema.shape.refs.optional(),
|
|
328
|
+
/** The user said, in so many words, that the run may touch their comments. */
|
|
329
|
+
allowUserComments: z.boolean().optional(),
|
|
330
|
+
});
|
|
331
|
+
app.post("/api/reviews/:key/chat", async (c) => {
|
|
332
|
+
const key = c.req.param("key");
|
|
333
|
+
const artifact = await loadArtifactByKey(key);
|
|
334
|
+
if (!artifact)
|
|
335
|
+
return c.json({ error: "not found" }, 404);
|
|
336
|
+
// A sent review is a record of what was posted — arguing with it now would
|
|
337
|
+
// rewrite history that GitHub already has.
|
|
338
|
+
if (artifact.sent)
|
|
339
|
+
return c.json({ error: `already sent at ${artifact.sent.at}` }, 409);
|
|
340
|
+
if (isReviewRunning(artifact.id)) {
|
|
341
|
+
return c.json({ error: "a run for this PR is already in flight" }, 409);
|
|
342
|
+
}
|
|
343
|
+
let request;
|
|
344
|
+
try {
|
|
345
|
+
request = ChatRequestSchema.parse(await c.req.json());
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const result = await runChatTurn(artifact, request.message, {
|
|
352
|
+
refs: request.refs,
|
|
353
|
+
allowUserComments: request.allowUserComments,
|
|
354
|
+
onProgress: (m) => console.log(`[chat ${artifact.id}] ${m}`),
|
|
355
|
+
});
|
|
356
|
+
// A turn takes minutes and the cockpit stays live throughout, so fold the
|
|
357
|
+
// result onto whatever is on disk now rather than overwriting it — an
|
|
358
|
+
// edit the user made while waiting must not vanish when the answer lands.
|
|
359
|
+
const saved = await updateArtifactByKey(key, (current) => mergeConcurrentEdits(artifact, result.artifact, current));
|
|
360
|
+
return c.json({ artifact: saved, applied: result.applied, refused: result.refused });
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 502);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// Put the review back the way it was before the conversation started. The
|
|
367
|
+
// conversation itself stays — you lose the edits, not the reasoning.
|
|
368
|
+
app.post("/api/reviews/:key/chat/reset", async (c) => {
|
|
369
|
+
const key = c.req.param("key");
|
|
370
|
+
const artifact = await loadArtifactByKey(key);
|
|
371
|
+
if (!artifact)
|
|
372
|
+
return c.json({ error: "not found" }, 404);
|
|
373
|
+
if (artifact.sent)
|
|
374
|
+
return c.json({ error: `already sent at ${artifact.sent.at}` }, 409);
|
|
375
|
+
if (!artifact.preChat)
|
|
376
|
+
return c.json({ error: "this review has not been chatted about" }, 409);
|
|
377
|
+
const updated = await updateArtifactByKey(key, (a) => a.preChat ? restoreReview(a, a.preChat) : a);
|
|
378
|
+
return c.json(updated);
|
|
379
|
+
});
|
|
318
380
|
// ---- Export (local file download) ----
|
|
319
381
|
app.get("/api/reviews/:key/export", async (c) => {
|
|
320
382
|
const artifact = await loadArtifactByKey(c.req.param("key"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:fullstackhouse/cerber",
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
|
|
8
8
|
Outdated base version: https://github.com/primer/github-syntax-light
|
|
9
9
|
Current colors taken from GitHub's CSS
|
|
10
|
-
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}:root{--bg: #ffffff;--fg: #1f2328;--muted: #656d76;--border: #d0d7de;--green: #1a7f37;--red: #cf222e;--yellow: #9a6700;--accent: #0969da}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;color:var(--fg);background:var(--bg)}.app{max-width:1100px;margin:0 auto;padding:0 16px 64px}.topbar{display:flex;align-items:baseline;gap:12px;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:16px}.brand{font-size:20px;font-weight:700;text-decoration:none;color:var(--fg)}.tagline{color:var(--muted);font-size:13px}a{color:var(--accent)}.muted{color:var(--muted);font-size:.92em}.error{color:var(--red);background:#ffebe9;border:1px solid rgba(207,34,46,.3);padding:8px 12px;border-radius:6px}.empty{text-align:center;color:var(--muted);margin-top:64px}.empty pre{display:inline-block;background:#f6f8fa;padding:8px 16px;border-radius:6px}.md{line-height:1.55}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:8px 0}.md code{font-family:ui-monospace,SFMono-Regular,monospace;font-size:.9em;background:#f0f2f5;padding:1px 5px;border-radius:4px}.md pre{background:#f6f8fa;border:1px solid var(--border);border-radius:6px;padding:10px 12px;overflow-x:auto}.md pre code{background:none;padding:0;font-size:12px}.md ul,.md ol{margin:8px 0;padding-left:24px}.md blockquote{margin:8px 0;padding-left:12px;border-left:3px solid var(--border);color:var(--muted)}.md h1,.md h2,.md h3{font-size:1.05em;margin:12px 0 4px}.daemon-strip{background:#ddf4ff;border:1px solid rgba(9,105,218,.3);border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:13px;display:flex;gap:12px;flex-wrap:wrap}.queue-stats{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}.queue{width:100%;border-collapse:collapse;font-size:14px}.queue th{text-align:left;color:var(--muted);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--border)}.queue td{padding:8px;border-bottom:1px solid var(--border);vertical-align:top}.pr-link{font-weight:600;text-decoration:none}.status{font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--border);white-space:nowrap}.status-ready{background:#dafbe1;border-color:#1a7f3766}.status-running{background:#ddf4ff;border-color:#0969da66}.status-failed{background:#ffebe9;border-color:#cf222e66}.status-awaiting{background:#fff8c5;border-color:#9a670066}.review-now{font-size:12px;padding:2px 10px}.row-archived{opacity:.6}.archived{margin-top:20px}.archived-toggle{background:none;border:none;color:var(--muted);font-size:13px;padding:4px 0;cursor:pointer}.inbox-toggle{display:block;margin:6px 0}.badge{font-size:12px;padding:2px 8px;border-radius:999px;white-space:nowrap}.badge-green{background:#dafbe1;color:var(--green)}.badge-yellow{background:#fff8c5;color:var(--yellow)}.badge-red{background:#ffebe9;color:var(--red)}.badge-muted{color:var(--muted)}.badge-warn{background:#fff8c5;color:var(--yellow)}.freshness{background:#fff8c5;border:1px solid rgba(154,103,0,.3);color:#57430a;padding:10px 12px;border-radius:6px;margin:12px 0;line-height:1.5}.freshness code{background:#0000000f;padding:1px 4px;border-radius:4px}.freshness-running{background:#ddf4ff;border-color:#0969da4d;color:#0a3069}.rerun-btn{margin-left:8px}.add{color:var(--green)}.del{color:var(--red)}.back{display:inline-block;margin:8px 0;text-decoration:none}.detail h1{font-size:22px;margin:4px 0}.verdict{border:1px solid var(--border);border-left-width:4px;border-radius:6px;padding:8px 12px;margin:12px 0}.verdict-approve{border-left-color:var(--green)}.verdict-comment{border-left-color:var(--yellow)}.verdict-request_changes{border-left-color:var(--red)}.verdict p{margin:4px 0 0;color:var(--muted);font-size:14px}.summary-body{white-space:pre-wrap;line-height:1.55}.chapter{border:1px solid var(--border);border-radius:6px;margin:12px 0;padding:4px 16px 12px}.chapter-title{cursor:pointer;-webkit-user-select:none;user-select:none;font-size:16px;position:sticky;top:0;z-index:5;background:var(--bg);margin:0 -16px;padding:10px 16px;border-radius:6px 6px 0 0}.chapter-title-open{border-bottom:1px solid var(--border)}.chapter-explanation{white-space:pre-wrap;line-height:1.5}.comment{border:1px solid var(--border);border-left:4px solid var(--yellow);border-radius:6px;padding:8px 12px;margin:8px 0;font-size:14px}.comment-loc{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:var(--muted);margin-bottom:4px}.comment-body{white-space:pre-wrap;line-height:1.5}.diff{margin-top:8px;font-size:12px}.inline-comment-row td{padding:8px 12px;background:#f6f8fa;border-top:1px solid var(--border);border-bottom:1px solid var(--border);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;white-space:normal}.inline-comment-row .comment{margin:0;max-width:820px;background:var(--bg)}button{font:inherit;font-size:12px;padding:2px 10px;border:1px solid var(--border);border-radius:6px;background:#f6f8fa;cursor:pointer}button:hover{background:#eef1f4}.comment-actions{margin-left:8px;display:inline-flex;gap:4px}.comment-dropped{opacity:.5}.comment-approved{border-left-color:var(--green)}.comment-edit,.add-comment textarea{width:100%;font:inherit;font-size:14px;padding:6px 8px;border:1px solid var(--border);border-radius:6px}.add-comment{border:1px dashed var(--border);border-radius:6px;padding:8px 12px;margin:8px 0;display:flex;flex-direction:column;gap:6px}.add-comment-row{display:flex;gap:8px;align-items:center}.add-comment select,.add-comment input{font:inherit;font-size:13px;padding:4px 6px;border:1px solid var(--border);border-radius:6px}.add-comment-btn{margin:8px 0}.verdict-override{margin-left:12px;font-size:13px;padding:2px 6px;border:1px solid var(--border);border-radius:6px}.send-panel{border:2px solid var(--accent);border-radius:8px;padding:12px 16px;margin:24px 0}.send-panel.sent{border-color:var(--green);background:#dafbe1}.send-panel h2{margin:0}.send-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:10px}.send-as{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--muted)}.send-spacer{flex:1}.send-controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.send-controls select{font:inherit;font-size:14px;padding:4px 8px;border:1px solid var(--border);border-radius:6px}.send-btn{background:var(--accent);color:#fff;border-color:var(--accent);font-size:14px;padding:6px 14px}.send-btn:hover{background:#0757b3}.send-btn:disabled{opacity:.6;cursor:default}.body-preview{background:#f6f8fa;border:1px solid var(--border);border-radius:6px;padding:12px;white-space:pre-wrap;font-size:13px;max-height:400px;overflow:auto}.export-link{font-size:13px}.post-actions{margin-top:16px}.diff .d2h-file-header{font-family:ui-monospace,SFMono-Regular,monospace}.diff .hljs{display:inline;padding:0;background:transparent;color:inherit}.topbar-link{margin-left:auto;font-size:13px;color:var(--muted);text-decoration:none}.topbar-link:hover{color:var(--fg);text-decoration:underline}.settings{max-width:760px}.settings h2{font-size:15px;margin-top:32px}.settings p{line-height:1.55}.trust-add{display:flex;gap:8px;margin:20px 0 12px}.trust-add input{flex:1;padding:7px 10px;font:inherit;color:var(--fg);background:var(--bg-alt, transparent);border:1px solid var(--border);border-radius:6px}.trust-table{border-collapse:collapse;width:100%}.trust-table td{padding:6px 10px 6px 0;border-bottom:1px solid var(--border);vertical-align:baseline}.trust-table td:last-child{text-align:right}.trust-deny code{text-decoration:line-through}
|
|
10
|
+
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}:root{--bg: #ffffff;--fg: #1f2328;--muted: #656d76;--border: #d0d7de;--green: #1a7f37;--red: #cf222e;--yellow: #9a6700;--accent: #0969da}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;color:var(--fg);background:var(--bg)}.app{max-width:1100px;margin:0 auto;padding:0 16px 64px}.topbar{display:flex;align-items:baseline;gap:12px;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:16px}.brand{font-size:20px;font-weight:700;text-decoration:none;color:var(--fg)}.tagline{color:var(--muted);font-size:13px}a{color:var(--accent)}.muted{color:var(--muted);font-size:.92em}.error{color:var(--red);background:#ffebe9;border:1px solid rgba(207,34,46,.3);padding:8px 12px;border-radius:6px}.empty{text-align:center;color:var(--muted);margin-top:64px}.empty pre{display:inline-block;background:#f6f8fa;padding:8px 16px;border-radius:6px}.md{line-height:1.55}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:8px 0}.md code{font-family:ui-monospace,SFMono-Regular,monospace;font-size:.9em;background:#f0f2f5;padding:1px 5px;border-radius:4px}.md pre{background:#f6f8fa;border:1px solid var(--border);border-radius:6px;padding:10px 12px;overflow-x:auto}.md pre code{background:none;padding:0;font-size:12px}.md ul,.md ol{margin:8px 0;padding-left:24px}.md blockquote{margin:8px 0;padding-left:12px;border-left:3px solid var(--border);color:var(--muted)}.md h1,.md h2,.md h3{font-size:1.05em;margin:12px 0 4px}.daemon-strip{background:#ddf4ff;border:1px solid rgba(9,105,218,.3);border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:13px;display:flex;gap:12px;flex-wrap:wrap}.queue-stats{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}.queue{width:100%;border-collapse:collapse;font-size:14px}.queue th{text-align:left;color:var(--muted);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--border)}.queue td{padding:8px;border-bottom:1px solid var(--border);vertical-align:top}.pr-link{font-weight:600;text-decoration:none}.status{font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--border);white-space:nowrap}.status-ready{background:#dafbe1;border-color:#1a7f3766}.status-running{background:#ddf4ff;border-color:#0969da66}.status-failed{background:#ffebe9;border-color:#cf222e66}.status-awaiting{background:#fff8c5;border-color:#9a670066}.review-now{font-size:12px;padding:2px 10px}.row-archived{opacity:.6}.archived{margin-top:20px}.archived-toggle{background:none;border:none;color:var(--muted);font-size:13px;padding:4px 0;cursor:pointer}.inbox-toggle{display:block;margin:6px 0}.badge{font-size:12px;padding:2px 8px;border-radius:999px;white-space:nowrap}.badge-green{background:#dafbe1;color:var(--green)}.badge-yellow{background:#fff8c5;color:var(--yellow)}.badge-red{background:#ffebe9;color:var(--red)}.badge-muted{color:var(--muted)}.badge-warn{background:#fff8c5;color:var(--yellow)}.freshness{background:#fff8c5;border:1px solid rgba(154,103,0,.3);color:#57430a;padding:10px 12px;border-radius:6px;margin:12px 0;line-height:1.5}.freshness code{background:#0000000f;padding:1px 4px;border-radius:4px}.freshness-running{background:#ddf4ff;border-color:#0969da4d;color:#0a3069}.rerun-btn{margin-left:8px}.add{color:var(--green)}.del{color:var(--red)}.back{display:inline-block;margin:8px 0;text-decoration:none}.detail h1{font-size:22px;margin:4px 0}.verdict{border:1px solid var(--border);border-left-width:4px;border-radius:6px;padding:8px 12px;margin:12px 0}.verdict-approve{border-left-color:var(--green)}.verdict-comment{border-left-color:var(--yellow)}.verdict-request_changes{border-left-color:var(--red)}.verdict p{margin:4px 0 0;color:var(--muted);font-size:14px}.summary-body{white-space:pre-wrap;line-height:1.55}.chapter{border:1px solid var(--border);border-radius:6px;margin:12px 0;padding:4px 16px 12px}.chapter-title{cursor:pointer;-webkit-user-select:none;user-select:none;font-size:16px;position:sticky;top:0;z-index:5;background:var(--bg);margin:0 -16px;padding:10px 16px;border-radius:6px 6px 0 0}.chapter-title-open{border-bottom:1px solid var(--border)}.chapter-explanation{white-space:pre-wrap;line-height:1.5}.comment{border:1px solid var(--border);border-left:4px solid var(--yellow);border-radius:6px;padding:8px 12px;margin:8px 0;font-size:14px}.comment-loc{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:var(--muted);margin-bottom:4px}.comment-body{white-space:pre-wrap;line-height:1.5}.diff{margin-top:8px;font-size:12px}.inline-comment-row td{padding:8px 12px;background:#f6f8fa;border-top:1px solid var(--border);border-bottom:1px solid var(--border);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;white-space:normal}.inline-comment-row .comment{margin:0;max-width:820px;background:var(--bg)}button{font:inherit;font-size:12px;padding:2px 10px;border:1px solid var(--border);border-radius:6px;background:#f6f8fa;cursor:pointer}button:hover{background:#eef1f4}.comment-actions{margin-left:8px;display:inline-flex;gap:4px}.comment-dropped{opacity:.5}.comment-approved{border-left-color:var(--green)}.comment-edit,.add-comment textarea{width:100%;font:inherit;font-size:14px;padding:6px 8px;border:1px solid var(--border);border-radius:6px}.add-comment{border:1px dashed var(--border);border-radius:6px;padding:8px 12px;margin:8px 0;display:flex;flex-direction:column;gap:6px}.add-comment-row{display:flex;gap:8px;align-items:center}.add-comment select,.add-comment input{font:inherit;font-size:13px;padding:4px 6px;border:1px solid var(--border);border-radius:6px}.add-comment-btn{margin:8px 0}.verdict-override{margin-left:12px;font-size:13px;padding:2px 6px;border:1px solid var(--border);border-radius:6px}.send-panel{border:2px solid var(--accent);border-radius:8px;padding:12px 16px;margin:24px 0}.send-panel.sent{border-color:var(--green);background:#dafbe1}.send-panel h2{margin:0}.send-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:10px}.send-as{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--muted)}.send-spacer{flex:1}.send-controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.send-controls select{font:inherit;font-size:14px;padding:4px 8px;border:1px solid var(--border);border-radius:6px}.send-btn{background:var(--accent);color:#fff;border-color:var(--accent);font-size:14px;padding:6px 14px}.send-btn:hover{background:#0757b3}.send-btn:disabled{opacity:.6;cursor:default}.body-preview{background:#f6f8fa;border:1px solid var(--border);border-radius:6px;padding:12px;white-space:pre-wrap;font-size:13px;max-height:400px;overflow:auto}.export-link{font-size:13px}.post-actions{margin-top:16px}.diff .d2h-file-header{font-family:ui-monospace,SFMono-Regular,monospace}.diff .hljs{display:inline;padding:0;background:transparent;color:inherit}.topbar-link{margin-left:auto;font-size:13px;color:var(--muted);text-decoration:none}.topbar-link:hover{color:var(--fg);text-decoration:underline}.settings{max-width:760px}.settings h2{font-size:15px;margin-top:32px}.settings p{line-height:1.55}.trust-add{display:flex;gap:8px;margin:20px 0 12px}.trust-add input{flex:1;padding:7px 10px;font:inherit;color:var(--fg);background:var(--bg-alt, transparent);border:1px solid var(--border);border-radius:6px}.trust-table{border-collapse:collapse;width:100%}.trust-table td{padding:6px 10px 6px 0;border-bottom:1px solid var(--border);vertical-align:baseline}.trust-table td:last-child{text-align:right}.trust-deny code{text-decoration:line-through}.chat-panel{border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:24px 0}.chat-panel h2{margin:0}.chat-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:10px}.chat-log{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}.chat-turn{border-left:3px solid var(--border);padding-left:12px}.chat-turn-user{border-left-color:var(--accent)}.chat-turn-who{font-size:12px;font-weight:600;color:var(--muted);margin-bottom:2px}.chat-turn-body p:first-child{margin-top:0}.chat-turn-body p:last-child{margin-bottom:0}.chat-revision{font-size:13px;color:var(--green);margin-top:4px}.chat-refusal{font-size:13px;color:var(--yellow);margin-top:4px}.chat-refs{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;margin-bottom:6px}.chat-ref{font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:2px 6px;border:1px solid var(--border);border-radius:999px;color:var(--muted);background:transparent}.chat-refs-pending .chat-ref{cursor:pointer;color:var(--accent);border-color:var(--accent)}.chat-input{width:100%;box-sizing:border-box;padding:8px 10px;font:inherit;color:var(--fg);background:transparent;border:1px solid var(--border);border-radius:6px;resize:vertical}.chat-actions{display:flex;align-items:center;gap:10px;margin-top:8px}.chat-reset{margin-left:auto}.discuss-btn{margin:8px 0;font-size:13px}
|