agent-feedbacker 0.1.0 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -4
- package/bin/feedbacker.mjs +8 -1
- package/hooks/hooks.json +13 -0
- package/package.json +2 -1
- package/skills/review/SKILL.md +1 -3
- package/src/baseline.mjs +122 -0
- package/src/core.mjs +24 -6
- package/src/dist/app.css +1 -1
- package/src/dist/app.js +14 -14
- package/src/mcp.mjs +5 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedbacker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Review an agent's changes across several git repos in a local MR-style page and send line comments back to it.",
|
|
5
5
|
"author": { "name": "Pavel Polivka" },
|
|
6
6
|
"repository": "https://github.com/PavlikPolivka/feedbacker",
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ This gives Claude:
|
|
|
22
22
|
|
|
23
23
|
- the MCP tool `request_review` (it shows up as `mcp__plugin_feedbacker_feedbacker__request_review`)
|
|
24
24
|
- the skill `/feedbacker:review`, which tells Claude when and how to ask for a review
|
|
25
|
+
- hooks for the **session baseline**: each repo's state is recorded when the session first touches it (at session start for the current repo, and before the first edit in any other repo). When Claude asks for a review without a base, the review shows exactly what changed in this session, even if Claude committed along the way, and repos Claude changed but forgot to pass are added. Uncommitted work from before the session is left out, because the baseline is a snapshot of it (`git stash create`, which changes neither your working tree nor your stash list). New untracked files from before the session still show up.
|
|
25
26
|
|
|
26
27
|
Then just say "let me review it" or run `/feedbacker:review`. To try it from a checkout without installing: `claude --plugin-dir /path/to/feedbacker`.
|
|
27
28
|
|
|
@@ -64,6 +65,8 @@ feedbacker ~/Projects/api ~/Projects/web@main \
|
|
|
64
65
|
|
|
65
66
|
In the browser:
|
|
66
67
|
|
|
68
|
+
- In changed lines, the changed words are marked.
|
|
69
|
+
- Tick **Viewed** on a file when you're done with it: it collapses, dims in the file tree, and the top bar counts how many you've viewed.
|
|
67
70
|
- Click a line number to comment on that line (a highlighter "+" shows where); shift-click a second one to comment on a range. Ctrl+Enter saves, Esc cancels.
|
|
68
71
|
- With several repos, the picker in the top bar switches between all of them and a single one (the summary, questions and your comments in other repos stay). The top bar also shows the size of the change and keeps **Approve** and **Request changes** in reach; files can be collapsed, and the file tree shows where your comments are.
|
|
69
72
|
- Comments, answers and the general comment are kept in the browser's local storage until you submit, so a refresh loses nothing.
|
|
@@ -75,7 +78,7 @@ In the browser:
|
|
|
75
78
|
|
|
76
79
|
| Argument | Type | |
|
|
77
80
|
|---|---|---|
|
|
78
|
-
| `repos` | `[{ path, base?, worktrees? }]` | Required. Absolute paths; `base` defaults to `HEAD
|
|
81
|
+
| `repos` | `[{ path, base?, worktrees? }]` | Required. Absolute paths; `base` defaults to `HEAD` (or, with the plugin's hooks, where the repo stood when the session started); `worktrees: true` adds all worktrees of that repo. (`exact` and `session` are set by the hook.) |
|
|
79
82
|
| `summary` | string | Shown at the top of the page. |
|
|
80
83
|
| `questions` | string[] | Shown with an answer box each. |
|
|
81
84
|
|
|
@@ -86,6 +89,10 @@ It blocks until the user submits (Claude Code keeps long MCP calls open; it send
|
|
|
86
89
|
|
|
87
90
|
Verdict: **changes requested**
|
|
88
91
|
|
|
92
|
+
Reviewed:
|
|
93
|
+
|
|
94
|
+
- /home/me/Projects/api (feature/config): 2 files changed (src/server.ts, src/config.ts), changes since the session started
|
|
95
|
+
|
|
89
96
|
## General
|
|
90
97
|
|
|
91
98
|
Almost there.
|
|
@@ -109,7 +116,7 @@ No, env is enough.
|
|
|
109
116
|
Make the host configurable too.
|
|
110
117
|
````
|
|
111
118
|
|
|
112
|
-
Each comment carries the code it refers to, so line numbers drifting doesn't lose the context. Cancelling the tool call closes the review page.
|
|
119
|
+
The result starts with what was reviewed (each repo, its files and base), so the agent knows what the verdict covers. Each comment carries the code it refers to, so line numbers drifting doesn't lose the context. Cancelling the tool call closes the review page.
|
|
113
120
|
|
|
114
121
|
## How it works
|
|
115
122
|
|
|
@@ -126,7 +133,8 @@ feedbacker ──markdown (tool result / stdout)──▶ agent
|
|
|
126
133
|
| `src/ui/` | The review page in React 19 (JSX): diff table, inline comments, questions, submit; highlight.js for syntax, `styles.css` for the look (light and dark, colour tokens at the top). |
|
|
127
134
|
| `src/dist/` | The committed bundle (JS, CSS and fonts), built from `src/ui/` by `build.mjs` (esbuild). |
|
|
128
135
|
| `src/mcp.mjs` | Minimal MCP server over stdio (JSON-RPC), one tool. |
|
|
129
|
-
| `
|
|
136
|
+
| `src/baseline.mjs`, `hooks/hooks.json` | Session baseline: `feedbacker hook` records repos (SessionStart, PreToolUse on edits) and fills in bases for `request_review` (PreToolUse, via `updatedInput`). Stored in `$XDG_STATE_HOME/feedbacker/sessions/`, forgotten after 30 days. |
|
|
137
|
+
| `bin/feedbacker.mjs` | CLI: review, `list`, `mcp`, `hook`. |
|
|
130
138
|
| `skills/review/SKILL.md`, `.mcp.json`, `.claude-plugin/` | The Claude Code plugin and its marketplace entry. |
|
|
131
139
|
|
|
132
140
|
Open reviews write a small file (URL, repos, pid) to `$XDG_STATE_HOME/feedbacker/` (default `~/.local/state/feedbacker/`, mode 0600) and remove it when they finish; `list` reads them.
|
|
@@ -149,7 +157,7 @@ CI runs the tests on Node 22 and 24 for every push and pull request (`.github/wo
|
|
|
149
157
|
|
|
150
158
|
Publishing to npm is done by GitHub Actions (`.github/workflows/publish.yml`) with npm trusted publishing, so no npm token is stored anywhere and each version gets a provenance statement.
|
|
151
159
|
|
|
152
|
-
1. Bump `version` in `package.json`
|
|
160
|
+
1. Bump `version` in `package.json` and `.claude-plugin/plugin.json` (the MCP server reads it from `package.json`), commit, push.
|
|
153
161
|
2. Create a GitHub release with the tag `vX.Y.Z` matching that version: `gh release create v0.2.0 --generate-notes`.
|
|
154
162
|
3. The workflow checks the tag, runs the tests and publishes.
|
|
155
163
|
|
package/bin/feedbacker.mjs
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { buildReview, hasChanges, listReviews, startReview, toMarkdown } from '../src/core.mjs';
|
|
6
6
|
import { runMcp } from '../src/mcp.mjs';
|
|
7
|
+
import { runHook } from '../src/baseline.mjs';
|
|
7
8
|
|
|
8
9
|
const USAGE = `Usage: feedbacker [options] [repo[@base] ...]
|
|
9
10
|
feedbacker list
|
|
10
11
|
feedbacker mcp
|
|
12
|
+
feedbacker hook
|
|
11
13
|
|
|
12
14
|
Opens a review page for the changes in each repo (default: current directory)
|
|
13
15
|
and prints the feedback to stdout when the user submits.
|
|
@@ -24,7 +26,10 @@ and prints the feedback to stdout when the user submits.
|
|
|
24
26
|
-h, --help Show this help.
|
|
25
27
|
|
|
26
28
|
list Show the URLs of reviews that are waiting for you.
|
|
27
|
-
mcp Run as an MCP server on stdio (tool: request_review)
|
|
29
|
+
mcp Run as an MCP server on stdio (tool: request_review).
|
|
30
|
+
hook Claude Code hook (reads the event on stdin): records each
|
|
31
|
+
repo's state when a session first touches it, and fills
|
|
32
|
+
that in as the base of request_review.`;
|
|
28
33
|
|
|
29
34
|
function fail(msg) {
|
|
30
35
|
console.error(`feedbacker: ${msg}`);
|
|
@@ -59,6 +64,8 @@ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => process
|
|
|
59
64
|
const argv = process.argv.slice(2);
|
|
60
65
|
if (argv[0] === 'mcp') {
|
|
61
66
|
runMcp();
|
|
67
|
+
} else if (argv[0] === 'hook') {
|
|
68
|
+
await runHook();
|
|
62
69
|
} else if (argv[0] === 'list') {
|
|
63
70
|
const reviews = listReviews();
|
|
64
71
|
if (!reviews.length) console.log('No reviews waiting.');
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs\" hook", "timeout": 10 }] }
|
|
5
|
+
],
|
|
6
|
+
"PreToolUse": [
|
|
7
|
+
{
|
|
8
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__plugin_feedbacker_feedbacker__request_review",
|
|
9
|
+
"hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs\" hook", "timeout": 10 }]
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-feedbacker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A local merge-request-style review page that coding agents open to collect your line comments and answers, across several git repos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src/*.mjs",
|
|
12
12
|
"src/page.html",
|
|
13
13
|
"src/dist",
|
|
14
|
+
"hooks",
|
|
14
15
|
"skills",
|
|
15
16
|
".claude-plugin",
|
|
16
17
|
".mcp.json"
|
package/skills/review/SKILL.md
CHANGED
|
@@ -10,9 +10,7 @@ Feedbacker opens a review page in the user's browser with the diffs of the repos
|
|
|
10
10
|
## How
|
|
11
11
|
|
|
12
12
|
1. List **every git repo and worktree you changed in this session**, as absolute paths. Include repos outside the current directory. If the work is spread over several worktrees of one repo (e.g. parallel agents), pass one of them with `"worktrees": true` instead of listing each.
|
|
13
|
-
2. Pick a base per repo:
|
|
14
|
-
- Uncommitted changes only: leave `base` out (it defaults to `HEAD`).
|
|
15
|
-
- You committed during the session: use the commit the session started from (`git log` to find it) or the branch you forked from, e.g. `main`.
|
|
13
|
+
2. Pick a base per repo, usually by leaving it out: the plugin's hooks recorded where each repo stood when this session first touched it, and fill that in, so the review shows exactly what you changed in this session, commits included. Only pass `base` for something else, e.g. a whole branch against `main`. The result starts with a "Reviewed" list showing what the user saw.
|
|
16
14
|
3. Call the `request_review` tool of the `feedbacker` MCP server with:
|
|
17
15
|
- `repos`: `[{ "path": "/abs/path", "base": "main", "worktrees": false }, ...]`
|
|
18
16
|
- `summary`: two to five sentences on what you changed and why.
|
package/src/baseline.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Session baseline: where each repo stood when a Claude Code session first
|
|
2
|
+
// touched it, so a review shows only what changed in that session, even after
|
|
3
|
+
// the agent committed. Recorded and applied by the plugin's hooks
|
|
4
|
+
// (`feedbacker hook`, see hooks/hooks.json).
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { hasChanges, buildReview, stateDir } from './core.mjs';
|
|
10
|
+
|
|
11
|
+
const TOOL = 'mcp__plugin_feedbacker_feedbacker__request_review';
|
|
12
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
13
|
+
const KEEP_DAYS = 30;
|
|
14
|
+
|
|
15
|
+
function git(cwd, args) {
|
|
16
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sessionFile(id) {
|
|
20
|
+
return path.join(stateDir(), 'sessions', `${String(id).replace(/[^\w-]/g, '')}.json`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadSession(id) {
|
|
24
|
+
try { return JSON.parse(fs.readFileSync(sessionFile(id), 'utf8')); } catch { return { repos: {} }; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveSession(id, session) {
|
|
28
|
+
const file = sessionFile(id);
|
|
29
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
30
|
+
fs.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
31
|
+
// Forget sessions nobody has touched for a month.
|
|
32
|
+
const cutoff = Date.now() - KEEP_DAYS * 86_400_000;
|
|
33
|
+
for (const name of fs.readdirSync(path.dirname(file))) {
|
|
34
|
+
const f = path.join(path.dirname(file), name);
|
|
35
|
+
try { if (fs.statSync(f).mtimeMs < cutoff) fs.rmSync(f); } catch {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Record the repo containing `dir` unless this session already has it.
|
|
40
|
+
// The baseline is HEAD, or a snapshot commit of uncommitted work
|
|
41
|
+
// (`git stash create`: nothing in the working tree or stash list changes).
|
|
42
|
+
export function record(id, dir) {
|
|
43
|
+
let top;
|
|
44
|
+
try {
|
|
45
|
+
let d = path.resolve(dir);
|
|
46
|
+
while (!fs.existsSync(d)) d = path.dirname(d); // a file that's about to be created
|
|
47
|
+
if (!fs.statSync(d).isDirectory()) d = path.dirname(d);
|
|
48
|
+
top = git(d, ['rev-parse', '--show-toplevel']);
|
|
49
|
+
} catch {
|
|
50
|
+
return null; // not in a git repo
|
|
51
|
+
}
|
|
52
|
+
const session = loadSession(id);
|
|
53
|
+
if (session.repos[top]) return session.repos[top];
|
|
54
|
+
const head = git(top, ['rev-parse', 'HEAD']);
|
|
55
|
+
let snapshot = '';
|
|
56
|
+
try { snapshot = git(top, ['stash', 'create']); } catch {}
|
|
57
|
+
session.started ||= new Date().toISOString();
|
|
58
|
+
session.repos[top] = { base: snapshot || head, head, recorded: new Date().toISOString() };
|
|
59
|
+
saveSession(id, session);
|
|
60
|
+
return session.repos[top];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fill in the session baseline for repos passed without a base, and add
|
|
64
|
+
// recorded repos that have changes but weren't passed. Returns the new input,
|
|
65
|
+
// or null when nothing changes.
|
|
66
|
+
export function applyBaseline(id, input) {
|
|
67
|
+
const session = loadSession(id);
|
|
68
|
+
const known = Object.keys(session.repos);
|
|
69
|
+
if (!known.length || !Array.isArray(input?.repos)) return null;
|
|
70
|
+
let changed = false;
|
|
71
|
+
const topOf = (p) => { try { return git(path.resolve(p), ['rev-parse', '--show-toplevel']); } catch { return null; } };
|
|
72
|
+
const repos = input.repos.map((r) => {
|
|
73
|
+
const spec = typeof r === 'string' ? { path: r } : { ...r };
|
|
74
|
+
const top = topOf(spec.path);
|
|
75
|
+
if (!spec.base && top && session.repos[top]) {
|
|
76
|
+
spec.base = session.repos[top].base;
|
|
77
|
+
spec.exact = true;
|
|
78
|
+
spec.session = true;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
return spec;
|
|
82
|
+
});
|
|
83
|
+
const passed = new Set(repos.map((r) => topOf(r.path)).filter(Boolean));
|
|
84
|
+
for (const top of known) {
|
|
85
|
+
if (passed.has(top) || !fs.existsSync(top)) continue;
|
|
86
|
+
const spec = { path: top, base: session.repos[top].base, exact: true, session: true };
|
|
87
|
+
if (hasChanges(buildReview({ repos: [spec] }))) { repos.push(spec); changed = true; }
|
|
88
|
+
}
|
|
89
|
+
return changed ? { ...input, repos } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Entry point for `feedbacker hook`: reads the hook event JSON on stdin.
|
|
93
|
+
// Never fails the tool call: any error just means no baseline.
|
|
94
|
+
export async function runHook() {
|
|
95
|
+
let event;
|
|
96
|
+
try {
|
|
97
|
+
let text = '';
|
|
98
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
99
|
+
event = JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const id = event.session_id;
|
|
104
|
+
if (!id) return;
|
|
105
|
+
try {
|
|
106
|
+
if (event.hook_event_name === 'SessionStart' && event.cwd) {
|
|
107
|
+
record(id, event.cwd);
|
|
108
|
+
} else if (event.hook_event_name === 'PreToolUse') {
|
|
109
|
+
if (EDIT_TOOLS.has(event.tool_name)) {
|
|
110
|
+
const file = event.tool_input?.file_path || event.tool_input?.notebook_path;
|
|
111
|
+
if (file) record(id, path.resolve(event.cwd || '.', file));
|
|
112
|
+
} else if (event.tool_name === TOOL) {
|
|
113
|
+
const updated = applyBaseline(id, event.tool_input);
|
|
114
|
+
if (updated) {
|
|
115
|
+
process.stdout.write(JSON.stringify({
|
|
116
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput: updated },
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
package/src/core.mjs
CHANGED
|
@@ -75,9 +75,11 @@ export function parseDiff(text) {
|
|
|
75
75
|
return files;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// "repo", "repo@base" or { path, base, worktrees }.
|
|
78
|
+
// "repo", "repo@base" or { path, base, worktrees, exact }. `exact` diffs
|
|
79
|
+
// against base itself instead of the fork point (session baselines, which can
|
|
80
|
+
// be snapshot commits that HEAD never contains).
|
|
79
81
|
export function parseSpec(spec) {
|
|
80
|
-
if (typeof spec !== 'string') return { path: spec.path, base: spec.base || 'HEAD', worktrees: !!spec.worktrees };
|
|
82
|
+
if (typeof spec !== 'string') return { path: spec.path, base: spec.base || 'HEAD', worktrees: !!spec.worktrees, exact: !!spec.exact, session: !!spec.session };
|
|
81
83
|
const at = spec.lastIndexOf('@');
|
|
82
84
|
if (at > 0 && !fs.existsSync(spec)) return { path: spec.slice(0, at), base: spec.slice(at + 1) || 'HEAD', worktrees: false };
|
|
83
85
|
return { path: spec, base: 'HEAD', worktrees: false };
|
|
@@ -99,13 +101,13 @@ export function listWorktrees(dir) {
|
|
|
99
101
|
// point where HEAD forked from base (like a merge request), or base itself
|
|
100
102
|
// when they share no history.
|
|
101
103
|
export function loadRepo(spec) {
|
|
102
|
-
const { path: p, base } = parseSpec(spec);
|
|
103
|
-
const repo = { path: path.resolve(p), base, branch: '', files: [], error: null };
|
|
104
|
+
const { path: p, base, exact, session } = parseSpec(spec);
|
|
105
|
+
const repo = { path: path.resolve(p), base, session, branch: '', files: [], error: null };
|
|
104
106
|
try {
|
|
105
107
|
repo.path = git(repo.path, ['rev-parse', '--show-toplevel']).trim();
|
|
106
108
|
repo.branch = git(repo.path, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
107
109
|
let from = git(repo.path, ['rev-parse', '--verify', '--end-of-options', `${base}^{commit}`]).trim();
|
|
108
|
-
try { from = git(repo.path, ['merge-base', from, 'HEAD']).trim(); } catch {}
|
|
110
|
+
if (!exact) try { from = git(repo.path, ['merge-base', from, 'HEAD']).trim(); } catch {}
|
|
109
111
|
let text = git(repo.path, ['diff', ...DIFF_FLAGS, from, '--']);
|
|
110
112
|
const untracked = git(repo.path, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean);
|
|
111
113
|
for (const f of untracked) {
|
|
@@ -128,7 +130,7 @@ export function buildReview({ repos = ['.'], summary = '', questions = [], workt
|
|
|
128
130
|
if (spec.worktrees || worktrees) {
|
|
129
131
|
try { paths = listWorktrees(spec.path); } catch {} // not a repo: loadRepo reports it
|
|
130
132
|
}
|
|
131
|
-
for (const p of paths) specs.push({ path: p, base: spec.base });
|
|
133
|
+
for (const p of paths) specs.push({ path: p, base: spec.base, exact: spec.exact, session: spec.session });
|
|
132
134
|
}
|
|
133
135
|
const seen = new Set();
|
|
134
136
|
const loaded = specs.map(loadRepo).filter((r) => !seen.has(r.path) && seen.add(r.path));
|
|
@@ -148,6 +150,16 @@ export function hasChanges(review) {
|
|
|
148
150
|
|
|
149
151
|
export function toMarkdown(review, fb) {
|
|
150
152
|
const out = ['# Review feedback', '', `Verdict: **${fb.verdict === 'approve' ? 'approved' : 'changes requested'}**`, ''];
|
|
153
|
+
// What the user actually saw, so the agent knows what the verdict covers
|
|
154
|
+
// (a hook may have filled in bases or added repos).
|
|
155
|
+
out.push('Reviewed:', '');
|
|
156
|
+
for (const r of review.repos) {
|
|
157
|
+
const names = r.files.slice(0, 10).map((f) => f.path).filter(Boolean).join(', ') + (r.files.length > 10 ? ', …' : '');
|
|
158
|
+
const what = r.error ? `could not be read (${r.error})`
|
|
159
|
+
: `${r.files.length} file${r.files.length === 1 ? '' : 's'} changed${names ? ` (${names})` : ''}`;
|
|
160
|
+
out.push(`- ${r.path}${r.branch && r.branch !== 'HEAD' ? ` (${r.branch})` : ''}: ${what}, ${describeBase(r)}`);
|
|
161
|
+
}
|
|
162
|
+
out.push('');
|
|
151
163
|
if (fb.general.trim()) out.push('## General', '', fb.general.trim(), '');
|
|
152
164
|
if (fb.answers.length) {
|
|
153
165
|
out.push('## Answers', '');
|
|
@@ -171,6 +183,12 @@ export function toMarkdown(review, fb) {
|
|
|
171
183
|
return out.join('\n');
|
|
172
184
|
}
|
|
173
185
|
|
|
186
|
+
export function describeBase(r) {
|
|
187
|
+
if (r.session) return 'changes since the session started';
|
|
188
|
+
if (r.base === 'HEAD') return 'uncommitted changes';
|
|
189
|
+
return `compared with ${/^[0-9a-f]{40}$/.test(r.base) ? r.base.slice(0, 10) : r.base}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
// Keep only the fields we expect from the browser, with safe defaults.
|
|
175
193
|
export function normalizeFeedback(review, fb) {
|
|
176
194
|
const str = (v) => (typeof v === 'string' ? v : '');
|
package/src/dist/app.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-ext-wght-normal-2N3YVG5Z.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-wght-normal-VX3WN42K.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-ext-wght-normal-BUWC6ZS3.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-wght-normal-ENSFVLK4.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-greek-wght-normal-Y3BRZMJV.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-vietnamese-wght-normal-SP6EBKAH.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-wght-normal-VBS632QH.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper: #fbfbfc;--panel: #f2f3f6;--sunk: #e9ebf0;--ink: #1b1f2a;--muted: #5d6475;--faint: #8a90a0;--line: #dde0e7;--add: #e4f4ee;--add-gutter: #c8e9dc;--add-ink: #13795b;--del: #fbeaed;--del-gutter: #f4d1d8;--del-ink: #b4233c;--hunk: #eef0f6;--marker: #ffe766;--marker-soft: #fff6bf;--marker-ink: #5c4700;--logo-ink: #1b1f2a;--on-marker: #3d2f00;--focus: #3355d6;--approve: #13795b;--approve-ink: #ffffff;--reject: #b4233c;--reject-ink: #ffffff;--syn-keyword: #8a2a9c;--syn-string: #1c6b4a;--syn-number: #a5500a;--syn-comment: #7c8293;--syn-title: #2347b8;--syn-type: #9a3412;--syn-attr: #6d4c00;--sans: "Atkinson Hyperlegible Next Variable", system-ui, sans-serif;--mono: "JetBrains Mono Variable", ui-monospace, monospace;--bar: 56px;color-scheme:light}@media(prefers-color-scheme:dark){:root{--paper: #14171e;--panel: #1a1e27;--sunk: #222733;--ink: #e4e7ee;--muted: #9aa1b2;--faint: #6f7689;--line: #2a303c;--add: #10261f;--add-gutter: #173a2f;--add-ink: #5cc9a0;--del: #2a1820;--del-gutter: #45212d;--del-ink: #f0778b;--hunk: #1c2130;--marker: #f2d44c;--marker-soft: rgba(242, 212, 76, .16);--marker-ink: #f2d44c;--focus: #7d97ff;--approve: #2f9e78;--reject: #c53a52;--syn-keyword: #d49bf0;--syn-string: #8fd3a8;--syn-number: #f0b072;--syn-comment: #737a8d;--syn-title: #8fb0ff;--syn-type: #f2a37f;--syn-attr: #e6c977;color-scheme:dark}}*{box-sizing:border-box}html{scroll-padding-top:calc(var(--bar) + 12px)}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased}button,textarea{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.bar{position:sticky;top:0;z-index:10;height:var(--bar);display:flex;align-items:center;gap:16px;padding:0 20px;background:color-mix(in srgb,var(--paper) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.brand .logo{flex:none;display:block}.brand .wordmark{font-weight:800;font-size:17px;letter-spacing:-.015em}.status .brand{margin-bottom:28px}.bar .stats{display:flex;gap:12px;color:var(--muted);font-size:14px;white-space:nowrap;overflow:hidden}.bar .spacer{flex:1}.plus{color:var(--add-ink);font-family:var(--mono);font-size:13px}.minus{color:var(--del-ink);font-family:var(--mono);font-size:13px}.marks{background:var(--marker);color:var(--on-marker);border-radius:999px;padding:0 9px;font-weight:700;font-size:13px;white-space:nowrap}.btn{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:6px 14px;cursor:pointer;font-weight:600;font-size:14px;white-space:nowrap}.btn:hover{background:var(--sunk)}.btn.approve{background:var(--approve);border-color:var(--approve);color:var(--approve-ink)}.btn.reject{background:transparent;border-color:var(--reject);color:var(--reject)}.btn.approve:hover{filter:brightness(1.08)}.btn.reject:hover{background:var(--del)}.btn.small{padding:3px 10px;font-size:13px;border-radius:6px}.btn.primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}.btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}.btn.quiet:hover{color:var(--ink);background:var(--sunk)}.picker{position:relative;display:inline-flex;min-width:0}.picker select{appearance:none;min-width:0;max-width:320px;text-overflow:ellipsis;font:inherit;font-size:14px;font-weight:600;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 30px 5px 12px;cursor:pointer}.picker select:hover{background:var(--sunk)}.picker:after{content:"\25be";position:absolute;right:11px;top:50%;transform:translateY(-52%);pointer-events:none;color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:280px minmax(0,1fr)}.tree{position:sticky;top:var(--bar);align-self:start;height:calc(100vh - var(--bar));overflow:auto;padding:20px 12px 40px 16px;border-right:1px solid var(--line);font-size:14px}.tree .repo{display:flex;align-items:baseline;gap:8px;margin:0 0 6px 6px;font-weight:700}.tree>div+div{margin-top:22px}.branch{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--muted);background:var(--sunk);border-radius:5px;padding:1px 6px;white-space:nowrap}.tree a{display:grid;grid-template-columns:14px minmax(0,1fr) auto;gap:8px;align-items:baseline;padding:3px 6px;border-radius:6px;color:var(--ink);text-decoration:none}.tree a:hover{background:var(--panel)}.tree .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.tree .count{font-size:12px}.tree .count .marks{padding:0 6px;font-size:12px}.st{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--faint)}.st.added{color:var(--add-ink)}.st.deleted{color:var(--del-ink)}.st.renamed{color:var(--syn-title)}.review{padding:28px 32px 120px;min-width:0}.handoff{max-width:76ch;margin-bottom:36px}.handoff .from{color:var(--muted);font-size:14px;margin:0 0 6px}.summary{font-size:21px;line-height:1.5;margin:0;white-space:pre-wrap;text-wrap:pretty}.summary.empty{color:var(--muted);font-size:17px}.questions{margin-top:24px;display:grid;gap:14px}.question{border-left:3px solid var(--line);padding:2px 0 2px 16px}.question.answered{border-left-color:var(--approve)}.question p{margin:0 0 8px;font-weight:700;white-space:pre-wrap}textarea{display:block;width:100%;min-height:64px;resize:vertical;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);line-height:1.5}textarea:focus{border-color:var(--focus);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--focus) 22%,transparent)}.repo-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;margin:32px 0 12px}.repo-head h2{margin:0;font-size:19px}.repo-head .where{color:var(--muted);font-size:13px;font-family:var(--mono);overflow-wrap:anywhere}.empty-note{color:var(--muted);margin:0 0 8px}.error{color:var(--del-ink)}.file{border:1px solid var(--line);border-radius:10px;margin-bottom:18px;background:var(--paper)}.fhead{position:sticky;top:var(--bar);z-index:5;display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--panel);border-bottom:1px solid var(--line);border-radius:10px 10px 0 0;font-family:var(--mono);font-size:13px}.file.collapsed .fhead{border-bottom:none;border-radius:10px}.fhead .name{flex:1;min-width:0;overflow-wrap:anywhere}.fhead .old{color:var(--muted)}.fold{border:none;background:none;cursor:pointer;color:var(--muted);padding:0 2px;font-size:13px;width:18px}.fold:hover{color:var(--ink)}.fnote{padding:12px 16px;color:var(--muted);font-size:14px}.scroll{overflow-x:auto;border-radius:0 0 10px 10px}table{border-collapse:collapse;width:100%;font:12.5px/21px var(--mono);font-variant-ligatures:none}td.ln{width:1%;min-width:48px;padding:0 10px;text-align:right;color:var(--faint);user-select:none;cursor:pointer;vertical-align:top;position:relative}tr:not(.hunk):hover td.ln:nth-child(2):before{content:"+";position:absolute;left:-2px;top:1px;width:18px;height:18px;line-height:18px;text-align:center;background:var(--marker);color:var(--on-marker);border-radius:5px;font-weight:700}td.ln:hover{color:var(--ink)}td.code{white-space:pre;padding:0 12px 0 4px}td.code .sign{display:inline-block;width:1.4em;color:var(--faint);user-select:none}tr.add td{background:var(--add)}tr.add td.ln{background:var(--add-gutter)}tr.add .sign{color:var(--add-ink)}tr.del td{background:var(--del)}tr.del td.ln{background:var(--del-gutter)}tr.del .sign{color:var(--del-ink)}tr.hunk td{background:var(--hunk);color:var(--muted);padding-top:3px;padding-bottom:3px}tr.sel td{background:var(--marker-soft)!important}tr.sel td.ln{color:var(--ink)}tr.sel td.ln:first-child{box-shadow:inset 3px 0 0 var(--marker)}tr.comment>td{padding:8px 14px 10px 62px;background:var(--panel);font:15px/1.55 var(--sans);white-space:normal;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.note-card{border-left:3px solid var(--marker);padding:4px 0 4px 12px;max-width:80ch}.note-card .meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;margin-bottom:2px}.note-card .meta .where{font-family:var(--mono);font-size:12px}.note-card .meta .actions{margin-left:auto;display:flex;gap:2px}.note-card p{margin:0;white-space:pre-wrap}.form{max-width:80ch}.form .row{display:flex;align-items:center;gap:8px;margin-top:8px}.form .hint{color:var(--faint);font-size:13px;margin-right:auto}.finish{max-width:76ch;margin-top:44px;padding-top:24px;border-top:1px solid var(--line)}.finish h2{margin:0 0 4px;font-size:19px}.finish p{margin:0 0 12px;color:var(--muted)}.finish .row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-literal{color:var(--syn-keyword)}.hljs-string,.hljs-regexp,.hljs-template-tag,.hljs-addition{color:var(--syn-string)}.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:var(--syn-number)}.hljs-comment,.hljs-quote,.hljs-meta{color:var(--syn-comment);font-style:italic}.hljs-title,.hljs-title.function_,.hljs-section,.hljs-name{color:var(--syn-title)}.hljs-type,.hljs-title.class_,.hljs-selector-class{color:var(--syn-type)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-params{color:var(--syn-attr)}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.status{max-width:46ch;margin:22vh auto 0;padding:0 16px}.status h1{font-size:30px;margin:0 0 8px}.status p{color:var(--muted);margin:0;font-size:17px}.status .stamp{display:table;margin-bottom:18px;padding:2px 12px;border-radius:999px;font-weight:700;font-size:14px;animation:stamp .42s cubic-bezier(.2,.9,.3,1.3)}.status .stamp.approve{background:var(--add);color:var(--add-ink)}.status .stamp.request_changes{background:var(--del);color:var(--del-ink)}@keyframes stamp{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}@media(prefers-reduced-motion:reduce){.status .stamp{animation:none}}@media(max-width:900px){.layout{display:block}.tree{display:none}.review{padding:20px 16px 100px}.bar{padding:0 16px;gap:10px}.bar .stats,.bar .marks,.bar .wordmark{display:none}.picker select{max-width:34vw}.bar .btn{padding:6px 10px}.summary{font-size:18px}tr.comment>td{padding-left:14px}}
|
|
1
|
+
@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-ext-wght-normal-2N3YVG5Z.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-wght-normal-VX3WN42K.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-ext-wght-normal-BUWC6ZS3.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-wght-normal-ENSFVLK4.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-greek-wght-normal-Y3BRZMJV.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-vietnamese-wght-normal-SP6EBKAH.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-wght-normal-VBS632QH.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper: #fbfbfc;--panel: #f2f3f6;--sunk: #e9ebf0;--ink: #1b1f2a;--muted: #5d6475;--faint: #8a90a0;--line: #dde0e7;--add: #e4f4ee;--add-gutter: #c8e9dc;--add-ink: #13795b;--del: #fbeaed;--del-gutter: #f4d1d8;--del-ink: #b4233c;--add-word: #a9dfc8;--del-word: #f0b6c1;--hunk: #eef0f6;--marker: #ffe766;--marker-soft: #fff6bf;--marker-ink: #5c4700;--logo-ink: #1b1f2a;--on-marker: #3d2f00;--focus: #3355d6;--approve: #13795b;--approve-ink: #ffffff;--reject: #b4233c;--reject-ink: #ffffff;--syn-keyword: #8a2a9c;--syn-string: #1c6b4a;--syn-number: #a5500a;--syn-comment: #7c8293;--syn-title: #2347b8;--syn-type: #9a3412;--syn-attr: #6d4c00;--sans: "Atkinson Hyperlegible Next Variable", system-ui, sans-serif;--mono: "JetBrains Mono Variable", ui-monospace, monospace;--bar: 56px;color-scheme:light}@media(prefers-color-scheme:dark){:root{--paper: #14171e;--panel: #1a1e27;--sunk: #222733;--ink: #e4e7ee;--muted: #9aa1b2;--faint: #6f7689;--line: #2a303c;--add: #10261f;--add-gutter: #173a2f;--add-ink: #5cc9a0;--del: #2a1820;--del-gutter: #45212d;--del-ink: #f0778b;--add-word: #1f5a45;--del-word: #6a2a3b;--hunk: #1c2130;--marker: #f2d44c;--marker-soft: rgba(242, 212, 76, .16);--marker-ink: #f2d44c;--focus: #7d97ff;--approve: #2f9e78;--reject: #c53a52;--syn-keyword: #d49bf0;--syn-string: #8fd3a8;--syn-number: #f0b072;--syn-comment: #737a8d;--syn-title: #8fb0ff;--syn-type: #f2a37f;--syn-attr: #e6c977;color-scheme:dark}}*{box-sizing:border-box}html{scroll-padding-top:calc(var(--bar) + 12px)}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased}button,textarea{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.bar{position:sticky;top:0;z-index:10;height:var(--bar);display:flex;align-items:center;gap:16px;padding:0 20px;background:color-mix(in srgb,var(--paper) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.brand .logo{flex:none;display:block}.brand .wordmark{font-weight:800;font-size:17px;letter-spacing:-.015em}.status .brand{margin-bottom:28px}.bar .stats{display:flex;gap:12px;color:var(--muted);font-size:14px;white-space:nowrap;overflow:hidden}.bar .spacer{flex:1}.plus{color:var(--add-ink);font-family:var(--mono);font-size:13px}.minus{color:var(--del-ink);font-family:var(--mono);font-size:13px}.marks{background:var(--marker);color:var(--on-marker);border-radius:999px;padding:0 9px;font-weight:700;font-size:13px;white-space:nowrap}.btn{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:6px 14px;cursor:pointer;font-weight:600;font-size:14px;white-space:nowrap}.btn:hover{background:var(--sunk)}.btn.approve{background:var(--approve);border-color:var(--approve);color:var(--approve-ink)}.btn.reject{background:transparent;border-color:var(--reject);color:var(--reject)}.btn.approve:hover{filter:brightness(1.08)}.btn.reject:hover{background:var(--del)}.btn.small{padding:3px 10px;font-size:13px;border-radius:6px}.btn.primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}.btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}.btn.quiet:hover{color:var(--ink);background:var(--sunk)}.picker{position:relative;display:inline-flex;min-width:0}.picker select{appearance:none;min-width:0;max-width:320px;text-overflow:ellipsis;font:inherit;font-size:14px;font-weight:600;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 30px 5px 12px;cursor:pointer}.picker select:hover{background:var(--sunk)}.picker:after{content:"\25be";position:absolute;right:11px;top:50%;transform:translateY(-52%);pointer-events:none;color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:280px minmax(0,1fr)}.tree{position:sticky;top:var(--bar);align-self:start;height:calc(100vh - var(--bar));overflow:auto;padding:20px 12px 40px 16px;border-right:1px solid var(--line);font-size:14px}.tree .repo{display:flex;align-items:baseline;gap:8px;margin:0 0 6px 6px;font-weight:700}.tree>div+div{margin-top:22px}.branch{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--muted);background:var(--sunk);border-radius:5px;padding:1px 6px;white-space:nowrap}.tree a{display:grid;grid-template-columns:14px minmax(0,1fr) auto;gap:8px;align-items:baseline;padding:3px 6px;border-radius:6px;color:var(--ink);text-decoration:none}.tree a:hover{background:var(--panel)}.tree .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.tree .count{font-size:12px}.tree .count .marks{padding:0 6px;font-size:12px}.st{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--faint)}.st.added{color:var(--add-ink)}.st.deleted{color:var(--del-ink)}.st.renamed{color:var(--syn-title)}.review{padding:28px 32px 120px;min-width:0}.handoff{max-width:76ch;margin-bottom:36px}.handoff .from{color:var(--muted);font-size:14px;margin:0 0 6px}.summary{font-size:21px;line-height:1.5;margin:0;white-space:pre-wrap;text-wrap:pretty}.summary.empty{color:var(--muted);font-size:17px}.questions{margin-top:24px;display:grid;gap:14px}.question{border-left:3px solid var(--line);padding:2px 0 2px 16px}.question.answered{border-left-color:var(--approve)}.question p{margin:0 0 8px;font-weight:700;white-space:pre-wrap}textarea{display:block;width:100%;min-height:64px;resize:vertical;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);line-height:1.5}textarea:focus{border-color:var(--focus);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--focus) 22%,transparent)}.repo-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;margin:32px 0 12px}.repo-head h2{margin:0;font-size:19px}.repo-head .where{color:var(--muted);font-size:13px;font-family:var(--mono);overflow-wrap:anywhere}.empty-note{color:var(--muted);margin:0 0 8px}.error{color:var(--del-ink)}.file{border:1px solid var(--line);border-radius:10px;margin-bottom:18px;background:var(--paper)}.fhead{position:sticky;top:var(--bar);z-index:5;display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--panel);border-bottom:1px solid var(--line);border-radius:10px 10px 0 0;font-family:var(--mono);font-size:13px}.file.collapsed .fhead{border-bottom:none;border-radius:10px}.fhead .name{flex:1;min-width:0;overflow-wrap:anywhere}.fhead .old{color:var(--muted)}.viewed-box{display:inline-flex;align-items:center;gap:6px;font:13px var(--sans);color:var(--muted);cursor:pointer;white-space:nowrap}.viewed-box input{accent-color:var(--approve);margin:0}.file.viewed .fhead .name{color:var(--muted)}.tree a.viewed .path{color:var(--faint)}.tree .tick{color:var(--approve);font-weight:700}.fold{border:none;background:none;cursor:pointer;color:var(--muted);padding:0 2px;font-size:13px;width:18px}.fold:hover{color:var(--ink)}.fnote{padding:12px 16px;color:var(--muted);font-size:14px}.scroll{overflow-x:auto;border-radius:0 0 10px 10px}table{border-collapse:collapse;width:100%;font:12.5px/21px var(--mono);font-variant-ligatures:none}td.ln{width:1%;min-width:48px;padding:0 10px;text-align:right;color:var(--faint);user-select:none;cursor:pointer;vertical-align:top;position:relative}tr:not(.hunk):hover td.ln:nth-child(2):before{content:"+";position:absolute;left:-2px;top:1px;width:18px;height:18px;line-height:18px;text-align:center;background:var(--marker);color:var(--on-marker);border-radius:5px;font-weight:700}td.ln:hover{color:var(--ink)}td.code{white-space:pre;padding:0 12px 0 4px}td.code .sign{display:inline-block;width:1.4em;color:var(--faint);user-select:none}tr.add td{background:var(--add)}tr.add td.ln{background:var(--add-gutter)}tr.add .sign{color:var(--add-ink)}tr.del td{background:var(--del)}tr.del td.ln{background:var(--del-gutter)}tr.del .sign{color:var(--del-ink)}mark.wd{color:inherit;padding:0;border-radius:0}tr.add mark.wd{background:var(--add-word)}tr.del mark.wd{background:var(--del-word)}tr.sel mark.wd{background:var(--marker);color:var(--on-marker, #3d2f00)}tr.hunk td{background:var(--hunk);color:var(--muted);padding-top:3px;padding-bottom:3px}tr.sel td{background:var(--marker-soft)!important}tr.sel td.ln{color:var(--ink)}tr.sel td.ln:first-child{box-shadow:inset 3px 0 0 var(--marker)}tr.comment>td{padding:8px 14px 10px 62px;background:var(--panel);font:15px/1.55 var(--sans);white-space:normal;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.note-card{border-left:3px solid var(--marker);padding:4px 0 4px 12px;max-width:80ch}.note-card .meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;margin-bottom:2px}.note-card .meta .where{font-family:var(--mono);font-size:12px}.note-card .meta .actions{margin-left:auto;display:flex;gap:2px}.note-card p{margin:0;white-space:pre-wrap}.form{max-width:80ch}.form .row{display:flex;align-items:center;gap:8px;margin-top:8px}.form .hint{color:var(--faint);font-size:13px;margin-right:auto}.finish{max-width:76ch;margin-top:44px;padding-top:24px;border-top:1px solid var(--line)}.finish h2{margin:0 0 4px;font-size:19px}.finish p{margin:0 0 12px;color:var(--muted)}.finish .row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-literal{color:var(--syn-keyword)}.hljs-string,.hljs-regexp,.hljs-template-tag,.hljs-addition{color:var(--syn-string)}.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:var(--syn-number)}.hljs-comment,.hljs-quote,.hljs-meta{color:var(--syn-comment);font-style:italic}.hljs-title,.hljs-title.function_,.hljs-section,.hljs-name{color:var(--syn-title)}.hljs-type,.hljs-title.class_,.hljs-selector-class{color:var(--syn-type)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-params{color:var(--syn-attr)}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.status{max-width:46ch;margin:22vh auto 0;padding:0 16px}.status h1{font-size:30px;margin:0 0 8px}.status p{color:var(--muted);margin:0;font-size:17px}.status .stamp{display:table;margin-bottom:18px;padding:2px 12px;border-radius:999px;font-weight:700;font-size:14px;animation:stamp .42s cubic-bezier(.2,.9,.3,1.3)}.status .stamp.approve{background:var(--add);color:var(--add-ink)}.status .stamp.request_changes{background:var(--del);color:var(--del-ink)}@keyframes stamp{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}@media(prefers-reduced-motion:reduce){.status .stamp{animation:none}}@media(max-width:900px){.layout{display:block}.tree{display:none}.review{padding:20px 16px 100px}.bar{padding:0 16px;gap:10px}.bar .stats,.bar .marks,.bar .wordmark{display:none}.picker select{max-width:34vw}.bar .btn{padding:6px 10px}.summary{font-size:18px}tr.comment>td{padding-left:14px}}
|