@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.
@@ -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 { ai: AiReviewSchema.parse(extractJson(first.text)), costUsd: first.costUsd, model: first.model };
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
  }
@@ -43,6 +43,8 @@ export function stubArtifact(ref) {
43
43
  sent: null,
44
44
  refresh: null,
45
45
  calibration: null,
46
+ chat: [],
47
+ preChat: null,
46
48
  };
47
49
  }
48
50
  /** An artifact nothing has touched yet — safe to delete when its PR leaves the queue. */
@@ -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.9.1",
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}