saga-cli 0.2.0__tar.gz → 0.3.1__tar.gz

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.
Files changed (40) hide show
  1. saga_cli-0.3.1/.claude/settings.local.json +8 -0
  2. {saga_cli-0.2.0 → saga_cli-0.3.1}/PKG-INFO +36 -16
  3. {saga_cli-0.2.0 → saga_cli-0.3.1}/README.md +35 -15
  4. {saga_cli-0.2.0 → saga_cli-0.3.1}/docs/example.html +125 -36
  5. {saga_cli-0.2.0 → saga_cli-0.3.1}/pyproject.toml +1 -1
  6. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/base.css +6 -0
  7. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/saga.css +43 -16
  8. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/saga.js +138 -39
  9. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/tokens.css +6 -8
  10. saga_cli-0.3.1/saga/cli.py +222 -0
  11. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/comments.py +81 -36
  12. saga_cli-0.3.1/saga/diff.py +169 -0
  13. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/generate.py +102 -58
  14. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/model.py +11 -5
  15. saga_cli-0.3.1/saga/prompts/saga.md +59 -0
  16. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/render.py +41 -13
  17. saga_cli-0.3.1/tests/cassettes/generate_anthropic.yaml +162 -0
  18. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_cli.py +116 -5
  19. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_comments.py +53 -1
  20. saga_cli-0.3.1/tests/test_diff.py +217 -0
  21. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_generate.py +107 -8
  22. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_model.py +5 -3
  23. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_render.py +41 -5
  24. {saga_cli-0.2.0 → saga_cli-0.3.1}/uv.lock +1 -1
  25. saga_cli-0.2.0/saga/cli.py +0 -106
  26. saga_cli-0.2.0/saga/diff.py +0 -79
  27. saga_cli-0.2.0/saga/prompts/saga.md +0 -50
  28. saga_cli-0.2.0/skills/read_saga_comments/SKILL.md +0 -62
  29. saga_cli-0.2.0/tests/cassettes/generate_anthropic.yaml +0 -168
  30. saga_cli-0.2.0/tests/test_diff.py +0 -95
  31. {saga_cli-0.2.0 → saga_cli-0.3.1}/.github/CODEOWNERS +0 -0
  32. {saga_cli-0.2.0 → saga_cli-0.3.1}/.github/workflows/ci.yml +0 -0
  33. {saga_cli-0.2.0 → saga_cli-0.3.1}/.gitignore +0 -0
  34. {saga_cli-0.2.0 → saga_cli-0.3.1}/CONTRIBUTING.md +0 -0
  35. {saga_cli-0.2.0 → saga_cli-0.3.1}/LICENSE +0 -0
  36. {saga_cli-0.2.0 → saga_cli-0.3.1}/RELEASING.md +0 -0
  37. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/__init__.py +0 -0
  38. {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/__main__.py +0 -0
  39. {saga_cli-0.2.0 → saga_cli-0.3.1}/skills/generate_saga/SKILL.md +0 -0
  40. {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/conftest.py +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run *)",
5
+ "Bash(.venv/bin/python -m pytest tests/test_comments.py -q)"
6
+ ]
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: saga-cli
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Generate a self-contained static HTML saga of a git diff — a chapter-by-chapter guided tour of a change.
5
5
  Project-URL: Homepage, https://github.com/JakeBeresford/saga
6
6
  Project-URL: Repository, https://github.com/JakeBeresford/saga
@@ -42,6 +42,8 @@ the data). Open it offline, email it, commit it, or drop it on any static host.
42
42
 
43
43
  - Python 3.11+
44
44
  - `git`
45
+ - The [`gh`](https://cli.github.com) CLI, authenticated — only for reviewing a PR
46
+ by URL (`saga <pr-url>`) or pushing review comments
45
47
  - An API key for your chosen provider, in the standard environment variable:
46
48
  `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY` — or a running
47
49
  local server (Ollama / LM Studio) and no key, with a `local/` model
@@ -83,14 +85,32 @@ Run it from anywhere with `--repo`:
83
85
  saga --repo ~/src/some-project --base main --head my-feature -o out.html
84
86
  ```
85
87
 
88
+ ### From a GitHub PR URL
89
+
90
+ Point saga straight at a pull request by passing its URL as the first argument.
91
+ No checkout is needed and it works from any directory:
92
+
93
+ ```sh
94
+ saga https://github.com/owner/repo/pull/5
95
+ saga https://github.com/owner/repo/pull/5 --model openai/gpt-4o -o pr5.html
96
+ ```
97
+
98
+ This fetches the PR's diff, commits, and branch names with the
99
+ [`gh`](https://cli.github.com) CLI (so `gh` must be installed and authenticated),
100
+ then builds the saga exactly as it would for a local branch. In this mode the PR
101
+ defines the change set, so `--base`, `--head`, and `--repo` are ignored.
102
+
103
+ The optional positional argument is a GitHub PR URL (as above); all flags below
104
+ apply to both modes.
105
+
86
106
  | Flag | Default | Meaning |
87
107
  | ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
88
- | `--base` | `main` | Base ref to diff against |
89
- | `--head` | current branch | Head ref to walk through |
108
+ | `--base` | auto-detected | Base ref to diff against (defaults to the repo's default branch, e.g. `origin/main`); local mode only |
109
+ | `--head` | current branch | Head ref to walk through; local mode only |
90
110
  | `--intent PATH` | — | Optional plan/spec describing the change's intent, for plan-aware narration and deviation flagging |
91
111
  | `--model` | `anthropic/claude-opus-4-8` | `provider/model` string (see [Providers](#providers)); also `$SAGA_MODEL` |
92
112
  | `-o, --output` | `saga.html` | Output file |
93
- | `--repo` | cwd | A path inside the target git repo |
113
+ | `--repo` | cwd | A path inside the target git repo; local mode only |
94
114
  | `--open` / `--no-open` | on | Open the result in a browser (on by default; `--no-open` to disable) |
95
115
 
96
116
  ## Providers
@@ -156,7 +176,7 @@ boots an agent per run) and subject to your subscription's usage limits.
156
176
 
157
177
  ## As a Claude Code skill
158
178
 
159
- The `skills/` directory contains two Claude Code skills. To install both:
179
+ The `skills/` directory contains a Claude Code skill. To install it:
160
180
 
161
181
  ```sh
162
182
  cp -R "$(pwd)/skills" ~/.claude/skills/saga
@@ -164,8 +184,6 @@ cp -R "$(pwd)/skills" ~/.claude/skills/saga
164
184
 
165
185
  - **`saga`** — say **"/saga"** (or "give me a walkthrough of this branch") to generate a
166
186
  saga. It resolves the base/head refs and runs the tool for you.
167
- - **`saga-comments`** — say **"/saga-comments"** (or "address the saga comments") to read an
168
- exported `saga.comments.json` and act on the reviewer's feedback in code.
169
187
 
170
188
  ## Reviewing: comments
171
189
 
@@ -175,26 +193,28 @@ kinds of comments — **inline** (click a line's number in any chapter's diff),
175
193
  (the box at the top of the Chapters list). Comments are drafted in your browser's
176
194
  `localStorage`, so they survive a reload.
177
195
 
178
- When you're done, click **Export comments** to download a `saga.comments.json` sidecar next
179
- to the HTML. (Export is disabled in the [hosted example](https://jakeberesford.github.io/saga/example.html)
180
- so it never writes a file — every other part of the review UX works.) Two commands consume it:
196
+ When you're done, click one of the copy buttons. Each puts a ready-to-run command on your
197
+ clipboard with the comments encoded inline (`--data`), so there's no file to find on disk —
198
+ paste it wherever you need it:
181
199
 
182
200
  ```sh
183
- # Post everything as a single PENDING review on the PR (you submit it on GitHub).
184
- saga comments push --comments saga.comments.json
201
+ # "Copy for GitHub" → post everything as a single PENDING review (you submit it on GitHub).
202
+ saga comments push --data <base64>
185
203
 
186
- # Emit the comments as JSON on stdout for a coding agent to act on.
187
- saga comments read --comments saga.comments.json
204
+ # "Copy for agent" → emit the comments as JSON on stdout for a coding agent to act on.
205
+ saga comments read --data <base64>
188
206
  ```
189
207
 
190
- `push` uses the `gh` CLI: it finds the open PR for the sidecar's branch and creates one
208
+ Prefer a file? Both commands also accept `--comments <path>` for a `saga.comments.json` you
209
+ write yourself (handy for scripting). `push` uses the `gh` CLI: it finds the open PR for the branch and creates one
191
210
  pending review (inline → line comments, per-file → a note anchored to the file's first
192
211
  changed line, overall → the review body). Nothing is submitted until you review and submit
193
212
  it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
194
213
 
195
214
  ## How it works
196
215
 
197
- 1. `diff.py` computes `git diff base...head` (no checkout) and the commit list.
216
+ 1. `diff.py` computes `git diff base...head` (no checkout) and the commit list
217
+ or, given a PR URL, fetches the same diff and metadata from GitHub via `gh`.
198
218
  2. `model.py` splits the diff into stable-id hunks (`h0, h1, …`).
199
219
  3. `generate.py` sends the labeled diff + commits (+ optional intent) to the chosen
200
220
  model via `instructor`, which returns chapters as schema-validated JSON. Coverage
@@ -15,6 +15,8 @@ the data). Open it offline, email it, commit it, or drop it on any static host.
15
15
 
16
16
  - Python 3.11+
17
17
  - `git`
18
+ - The [`gh`](https://cli.github.com) CLI, authenticated — only for reviewing a PR
19
+ by URL (`saga <pr-url>`) or pushing review comments
18
20
  - An API key for your chosen provider, in the standard environment variable:
19
21
  `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY` — or a running
20
22
  local server (Ollama / LM Studio) and no key, with a `local/` model
@@ -56,14 +58,32 @@ Run it from anywhere with `--repo`:
56
58
  saga --repo ~/src/some-project --base main --head my-feature -o out.html
57
59
  ```
58
60
 
61
+ ### From a GitHub PR URL
62
+
63
+ Point saga straight at a pull request by passing its URL as the first argument.
64
+ No checkout is needed and it works from any directory:
65
+
66
+ ```sh
67
+ saga https://github.com/owner/repo/pull/5
68
+ saga https://github.com/owner/repo/pull/5 --model openai/gpt-4o -o pr5.html
69
+ ```
70
+
71
+ This fetches the PR's diff, commits, and branch names with the
72
+ [`gh`](https://cli.github.com) CLI (so `gh` must be installed and authenticated),
73
+ then builds the saga exactly as it would for a local branch. In this mode the PR
74
+ defines the change set, so `--base`, `--head`, and `--repo` are ignored.
75
+
76
+ The optional positional argument is a GitHub PR URL (as above); all flags below
77
+ apply to both modes.
78
+
59
79
  | Flag | Default | Meaning |
60
80
  | ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
61
- | `--base` | `main` | Base ref to diff against |
62
- | `--head` | current branch | Head ref to walk through |
81
+ | `--base` | auto-detected | Base ref to diff against (defaults to the repo's default branch, e.g. `origin/main`); local mode only |
82
+ | `--head` | current branch | Head ref to walk through; local mode only |
63
83
  | `--intent PATH` | — | Optional plan/spec describing the change's intent, for plan-aware narration and deviation flagging |
64
84
  | `--model` | `anthropic/claude-opus-4-8` | `provider/model` string (see [Providers](#providers)); also `$SAGA_MODEL` |
65
85
  | `-o, --output` | `saga.html` | Output file |
66
- | `--repo` | cwd | A path inside the target git repo |
86
+ | `--repo` | cwd | A path inside the target git repo; local mode only |
67
87
  | `--open` / `--no-open` | on | Open the result in a browser (on by default; `--no-open` to disable) |
68
88
 
69
89
  ## Providers
@@ -129,7 +149,7 @@ boots an agent per run) and subject to your subscription's usage limits.
129
149
 
130
150
  ## As a Claude Code skill
131
151
 
132
- The `skills/` directory contains two Claude Code skills. To install both:
152
+ The `skills/` directory contains a Claude Code skill. To install it:
133
153
 
134
154
  ```sh
135
155
  cp -R "$(pwd)/skills" ~/.claude/skills/saga
@@ -137,8 +157,6 @@ cp -R "$(pwd)/skills" ~/.claude/skills/saga
137
157
 
138
158
  - **`saga`** — say **"/saga"** (or "give me a walkthrough of this branch") to generate a
139
159
  saga. It resolves the base/head refs and runs the tool for you.
140
- - **`saga-comments`** — say **"/saga-comments"** (or "address the saga comments") to read an
141
- exported `saga.comments.json` and act on the reviewer's feedback in code.
142
160
 
143
161
  ## Reviewing: comments
144
162
 
@@ -148,26 +166,28 @@ kinds of comments — **inline** (click a line's number in any chapter's diff),
148
166
  (the box at the top of the Chapters list). Comments are drafted in your browser's
149
167
  `localStorage`, so they survive a reload.
150
168
 
151
- When you're done, click **Export comments** to download a `saga.comments.json` sidecar next
152
- to the HTML. (Export is disabled in the [hosted example](https://jakeberesford.github.io/saga/example.html)
153
- so it never writes a file — every other part of the review UX works.) Two commands consume it:
169
+ When you're done, click one of the copy buttons. Each puts a ready-to-run command on your
170
+ clipboard with the comments encoded inline (`--data`), so there's no file to find on disk —
171
+ paste it wherever you need it:
154
172
 
155
173
  ```sh
156
- # Post everything as a single PENDING review on the PR (you submit it on GitHub).
157
- saga comments push --comments saga.comments.json
174
+ # "Copy for GitHub" → post everything as a single PENDING review (you submit it on GitHub).
175
+ saga comments push --data <base64>
158
176
 
159
- # Emit the comments as JSON on stdout for a coding agent to act on.
160
- saga comments read --comments saga.comments.json
177
+ # "Copy for agent" → emit the comments as JSON on stdout for a coding agent to act on.
178
+ saga comments read --data <base64>
161
179
  ```
162
180
 
163
- `push` uses the `gh` CLI: it finds the open PR for the sidecar's branch and creates one
181
+ Prefer a file? Both commands also accept `--comments <path>` for a `saga.comments.json` you
182
+ write yourself (handy for scripting). `push` uses the `gh` CLI: it finds the open PR for the branch and creates one
164
183
  pending review (inline → line comments, per-file → a note anchored to the file's first
165
184
  changed line, overall → the review body). Nothing is submitted until you review and submit
166
185
  it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
167
186
 
168
187
  ## How it works
169
188
 
170
- 1. `diff.py` computes `git diff base...head` (no checkout) and the commit list.
189
+ 1. `diff.py` computes `git diff base...head` (no checkout) and the commit list
190
+ or, given a PR URL, fetches the same diff and metadata from GitHub via `gh`.
171
191
  2. `model.py` splits the diff into stable-id hunks (`h0, h1, …`).
172
192
  3. `generate.py` sends the labeled diff + commits (+ optional intent) to the chosen
173
193
  model via `instructor`, which returns chapters as schema-validated JSON. Coverage
@@ -520,8 +520,8 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
520
520
  // diff). This file renders it — a table of contents (the entry point) and a
521
521
  // chapter reader — tracks per-chapter mark-as-read in localStorage, and lets a
522
522
  // reviewer leave inline / per-file / overall comments (also drafted in
523
- // localStorage) that Export downloads as a saga.comments.json sidecar. No
524
- // server, no fetch.
523
+ // localStorage) that the Copy buttons hand off to the `saga comments` CLI as a
524
+ // base64 `--data` payload. No server, no fetch.
525
525
 
526
526
  (function () {
527
527
  const data = window.__sagaData || {chapters: [], verdict: null, branch: ''};
@@ -588,7 +588,7 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
588
588
  localStorage.setItem(readKey(), JSON.stringify([...s]));
589
589
  }
590
590
 
591
- // --- comments (localStorage draft, exported as a sidecar) ----------
591
+ // --- comments (localStorage draft, copied to the CLI as --data) ----
592
592
 
593
593
  let comments = {files: {}, overall: ''};
594
594
 
@@ -779,15 +779,64 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
779
779
  });
780
780
  }
781
781
 
782
- function exportComments() {
783
- // Hosted demo: export is disabled so the example never writes a
784
- // saga.comments.json sidecar. The button still confirms visually.
785
- const btn = $('saga-export');
786
- if (!btn) return;
787
- const label = btn.textContent;
788
- btn.textContent = 'Exported ';
789
- btn.disabled = true;
790
- setTimeout(() => { btn.textContent = label; btn.disabled = false; }, 1500);
782
+ // Assemble the comments payload — the same shape the CLI validates.
783
+ function buildPayload() {
784
+ const files = {};
785
+ Object.keys(comments.files).forEach((p) => {
786
+ const e = comments.files[p];
787
+ const inline = (e.inline || []).filter((c) => c.body && c.body.trim());
788
+ const fc = (e.file_comment || '').trim();
789
+ if (!inline.length && !fc) return;
790
+ const out = {};
791
+ if (fc) {
792
+ out.file_comment = fc;
793
+ if (e.file_anchor) out.file_anchor = e.file_anchor;
794
+ }
795
+ if (inline.length) out.inline = inline;
796
+ files[p] = out;
797
+ });
798
+ return {
799
+ branch: data.branch || '',
800
+ base: data.base || '',
801
+ overall: (comments.overall || '').trim(),
802
+ files: files,
803
+ };
804
+ }
805
+
806
+ // base64(UTF-8 JSON) — the payload rides inside the copied command as
807
+ // `--data`, so the reviewer never has to find a file on disk.
808
+ function encodePayload() {
809
+ const bytes = new TextEncoder().encode(JSON.stringify(buildPayload()));
810
+ let bin = '';
811
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
812
+ return btoa(bin);
813
+ }
814
+
815
+ function copyText(text) {
816
+ if (navigator.clipboard && navigator.clipboard.writeText) {
817
+ return navigator.clipboard.writeText(text);
818
+ }
819
+ // file:// fallback where the async clipboard API is unavailable.
820
+ const ta = document.createElement('textarea');
821
+ ta.value = text;
822
+ ta.style.position = 'fixed';
823
+ ta.style.opacity = '0';
824
+ document.body.appendChild(ta);
825
+ ta.select();
826
+ try { document.execCommand('copy'); } finally { ta.remove(); }
827
+ return Promise.resolve();
828
+ }
829
+
830
+ // Copy a ready-to-run `saga comments <sub> --data …` command. The reviewer
831
+ // pastes it into their terminal (push) or coding agent (read).
832
+ function copyCommand(sub, btn) {
833
+ const cmd = 'saga comments ' + sub + ' --data ' + encodePayload();
834
+ copyText(cmd).then(() => {
835
+ const label = btn.dataset.label;
836
+ btn.textContent = 'Copied ✓';
837
+ btn.disabled = true;
838
+ setTimeout(() => { btn.textContent = label; btn.disabled = false; }, 1500);
839
+ });
791
840
  }
792
841
 
793
842
  // --- entry ----------------------------------------------------------
@@ -796,28 +845,65 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
796
845
  chapters = data.chapters || [];
797
846
  comments = loadComments();
798
847
  initTheme();
799
- renderVerdict(data.verdict);
848
+ renderHead();
849
+ renderVerdict(data.verdict, data.stats);
800
850
  renderTOC();
801
851
  }
802
852
 
853
+ // --- header (title, summary, provenance) ----------------------------
854
+
855
+ function renderHead() {
856
+ if (data.title) $('saga-title').textContent = data.title;
857
+ const sum = $('saga-summary');
858
+ if (sum && data.summary) { sum.textContent = data.summary; sum.hidden = false; }
859
+ const meta = $('saga-meta');
860
+ if (meta) {
861
+ const parts = [];
862
+ if (data.commit_sha) parts.push(data.commit_sha.slice(0, 7));
863
+ const rel = relativeTime(data.generated_at);
864
+ if (rel) parts.push('generated ' + rel);
865
+ meta.textContent = parts.join(' · ');
866
+ }
867
+ }
868
+
869
+ function relativeTime(iso) {
870
+ if (!iso) return '';
871
+ const then = new Date(iso).getTime();
872
+ if (isNaN(then)) return '';
873
+ const secs = Math.round((Date.now() - then) / 1000);
874
+ if (secs < 60) return 'just now';
875
+ const units = [['year', 31536000], ['month', 2592000], ['week', 604800],
876
+ ['day', 86400], ['hour', 3600], ['minute', 60]];
877
+ for (const [name, size] of units) {
878
+ const n = Math.floor(secs / size);
879
+ if (n >= 1) return n + ' ' + name + (n === 1 ? '' : 's') + ' ago';
880
+ }
881
+ return 'just now';
882
+ }
883
+
803
884
  // --- verdict line --------------------------------------------------
804
885
 
805
- function renderVerdict(v) {
886
+ function renderVerdict(v, stats) {
806
887
  if (!v) return;
807
- const parts = [
808
- v.chapters + (v.chapters === 1 ? ' chapter' : ' chapters'),
809
- v.deviations + (v.deviations === 1 ? ' deviation' : ' deviations'),
810
- v.low_confidence + ' low-confidence',
811
- ];
812
- if (v.qa && v.qa !== 'n/a') parts.push('QA ' + v.qa);
813
- $('saga-verdict').textContent = parts.join(' · ');
814
- // Shift the top status rail to the loudest state present:
815
- // deviation (red) > attention (amber) > clear (green).
888
+ // Scope segments read neutral; attention flags echo the amber rail so a
889
+ // reviewer can spot what needs a look. All text is generated (no user
890
+ // content), so building innerHTML here is injection-safe.
891
+ const seg = [{ t: v.chapters + (v.chapters === 1 ? ' chapter' : ' chapters') }];
892
+ if (stats && stats.files) seg.push({ t: stats.files + (stats.files === 1 ? ' file' : ' files') });
893
+ if (stats && (stats.added || stats.removed)) {
894
+ seg.push({ html: '<span class="saga-add">+' + (stats.added || 0) + '</span> ' +
895
+ '<span class="saga-del">−' + (stats.removed || 0) + '</span>' });
896
+ }
897
+ if (v.deviations > 0) seg.push({ t: v.deviations + (v.deviations === 1 ? ' differs from plan' : ' differ from plan'), flag: true });
898
+ if (v.low_confidence > 0) seg.push({ t: v.low_confidence + (v.low_confidence === 1 ? ' needs a closer look' : ' need a closer look'), flag: true });
899
+ $('saga-verdict').innerHTML = seg
900
+ .map((s) => (s.html ? s.html : s.flag ? '<span class="saga-flag">' + s.t + '</span>' : s.t))
901
+ .join(' · ');
902
+ // Two-tier status rail: amber when anything is flagged, else green.
816
903
  const rail = $('saga-rail');
817
904
  if (rail) {
818
- rail.classList.remove('saga-rail-ok', 'saga-rail-attn', 'saga-rail-dev');
819
- if (v.deviations > 0) rail.classList.add('saga-rail-dev');
820
- else if (v.low_confidence > 0 || v.qa === 'attention') rail.classList.add('saga-rail-attn');
905
+ rail.classList.remove('saga-rail-ok', 'saga-rail-attn');
906
+ if (v.deviations > 0 || v.low_confidence > 0) rail.classList.add('saga-rail-attn');
821
907
  else rail.classList.add('saga-rail-ok');
822
908
  }
823
909
  }
@@ -827,9 +913,9 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
827
913
  function badges(ch, read) {
828
914
  const out = [];
829
915
  if (ch.plan_step) out.push('<span class="saga-badge saga-plan">' + esc(ch.plan_step) + '</span>');
830
- if (ch.deviation) out.push('<span class="saga-badge saga-dev">⚠ Deviation</span>');
831
- if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">Low confidence</span>');
832
- if (ch.qa && ch.qa.status === 'green') out.push('<span class="saga-badge saga-qa">✓ QA</span>');
916
+ if (ch.deviation) out.push('<span class="saga-badge saga-dev">Differs from plan</span>');
917
+ if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">Needs a closer look</span>');
918
+ if (ch.qa) out.push('<span class="saga-badge saga-qa">⚠ QA</span>');
833
919
  if (read) out.push('<span class="saga-badge saga-read">✓ Read</span>');
834
920
  return out.join('');
835
921
  }
@@ -845,7 +931,8 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
845
931
  '<textarea id="saga-overall" class="saga-cmt-input saga-overall" placeholder="Overall review comment…"></textarea>' +
846
932
  '<div class="saga-review-actions">' +
847
933
  '<span class="saga-cmt-count" id="saga-cmt-count"></span>' +
848
- '<button class="saga-btn" id="saga-export">Export comments</button>' +
934
+ '<button class="saga-btn" id="saga-copy-agent" data-label="Copy for agent">Copy for agent</button>' +
935
+ '<button class="saga-btn" id="saga-copy-gh" data-label="Copy for GitHub">Copy for GitHub</button>' +
849
936
  '</div>' +
850
937
  '</div>' +
851
938
  '<h2 class="saga-toc-title">Chapters</h2>';
@@ -867,8 +954,10 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
867
954
  overall.value = comments.overall || '';
868
955
  overall.addEventListener('input', () => { comments.overall = overall.value; persist(); });
869
956
  }
870
- const exp = $('saga-export');
871
- if (exp) exp.addEventListener('click', exportComments);
957
+ const agentBtn = $('saga-copy-agent');
958
+ if (agentBtn) agentBtn.addEventListener('click', () => copyCommand('read', agentBtn));
959
+ const ghBtn = $('saga-copy-gh');
960
+ if (ghBtn) ghBtn.addEventListener('click', () => copyCommand('push', ghBtn));
872
961
  updateCount();
873
962
  }
874
963
 
@@ -883,13 +972,13 @@ window.__sagaData = {"branch": "tui-gui", "base": "main", "verdict": {"chapters"
883
972
  reader.hidden = false;
884
973
 
885
974
  const devBanner = ch.deviation
886
- ? '<div class="saga-deviation"><strong>⚠ Deviation from plan.</strong> ' + esc(ch.deviation) + '</div>'
975
+ ? '<div class="saga-deviation"><strong>Differs from the plan.</strong> ' + esc(ch.deviation) + ' Worth confirming this was intentional.</div>'
887
976
  : '';
888
977
  const lowBanner = ch.confidence === 'low'
889
- ? '<div class="saga-lowconf">Low confidencethis chapter needs close review.</div>'
978
+ ? '<div class="saga-lowconf"><strong>Needs a closer look.</strong> The walkthrough is unsure here read the diff rather than just trusting the summary.</div>'
890
979
  : '';
891
- const qaLine = ch.qa && ch.qa.note
892
- ? '<div class="saga-qanote">' + (ch.qa.status === 'green' ? '✓ ' : '') + esc(ch.qa.note) + '</div>'
980
+ const qaLine = ch.qa
981
+ ? '<div class="saga-qanote">⚠ Manual QA: ' + esc(ch.qa) + '</div>'
893
982
  : '';
894
983
 
895
984
  // Class-based (not id-based) so the same nav can render at top and bottom.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "saga-cli"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "Generate a self-contained static HTML saga of a git diff — a chapter-by-chapter guided tour of a change."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -17,6 +17,12 @@ body { font-family: var(--font-sans); background: var(--bg); color: var(--text);
17
17
  .d2h-file-diff { background: var(--bg); }
18
18
  .d2h-code-line-ctn, .d2h-code-line { background: var(--bg) !important; color: var(--text) !important; }
19
19
  .d2h-code-linenumber { background: var(--surface) !important; color: var(--text-faint) !important; border-color: var(--line) !important; }
20
+ /* Wrap long lines instead of overflowing the file wrapper (its overflow:hidden
21
+ was clipping them). diff2html defaults to a fixed-width nowrap/pre line; here
22
+ the code line fills its cell and wraps, hang-indented past the
23
+ absolutely-positioned line-number gutter (~7.5em wide). */
24
+ .d2h-code-line { display: block; width: auto; padding: 0 1em 0 8em; white-space: normal; }
25
+ .d2h-code-line-ctn { display: inline; width: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
20
26
  .d2h-del { background: var(--diff-del-bg) !important; }
21
27
  .d2h-ins { background: var(--diff-add-bg) !important; }
22
28
  .d2h-del .d2h-code-line-ctn { background: var(--diff-del-bg) !important; }
@@ -13,16 +13,42 @@
13
13
  }
14
14
  .saga-theme-toggle:hover { border-color: var(--accent); color: var(--text); }
15
15
 
16
- .saga-crumbs { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: 10px; }
16
+ .saga-crumbs { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: 8px; }
17
17
  .saga-crumbs a { color: var(--accent); text-decoration: none; }
18
18
  .saga-crumbs a:hover { color: var(--accent-bright); text-decoration: underline; }
19
- .saga-crumb-sep { margin: 0 8px; color: var(--text-faint); }
20
- .saga-crumb-current { color: var(--text); }
19
+
20
+ .header h1 { line-height: 1.2; }
21
+
22
+ .saga-summary {
23
+ color: var(--text-dim);
24
+ font-size: var(--fs-md);
25
+ line-height: 1.45;
26
+ margin-top: 4px;
27
+ }
28
+
29
+ .saga-statusline {
30
+ display: flex;
31
+ justify-content: space-between;
32
+ align-items: baseline;
33
+ gap: 8px 24px;
34
+ flex-wrap: wrap;
35
+ margin-top: 10px;
36
+ }
21
37
 
22
38
  .saga-verdict {
23
39
  color: var(--text-dim);
24
40
  font-size: var(--fs-sm);
25
- margin-top: 6px;
41
+ font-family: var(--font-mono);
42
+ letter-spacing: var(--tracking-mono);
43
+ }
44
+ .saga-verdict .saga-flag { color: var(--warn); }
45
+ /* diff add/remove hues, matching the diff body's green/red */
46
+ .saga-verdict .saga-add { color: var(--ok); }
47
+ .saga-verdict .saga-del { color: var(--danger); }
48
+
49
+ .saga-meta {
50
+ color: var(--text-faint);
51
+ font-size: var(--fs-xs);
26
52
  font-family: var(--font-mono);
27
53
  letter-spacing: var(--tracking-mono);
28
54
  }
@@ -86,15 +112,11 @@
86
112
  border: 1px solid var(--line); color: var(--text-dim); background: var(--surface-2);
87
113
  }
88
114
  .saga-plan { color: var(--text-dim); }
89
- .saga-qa { color: var(--ok); border-color: var(--ok-solid); background: var(--ok-tint); }
90
115
  .saga-read { color: var(--accent); border-color: var(--accent-solid); background: var(--accent-tint); }
91
- /* Deviations must be loud. */
92
- .saga-dev {
93
- color: var(--danger-ink); background: var(--danger-loud); border-color: var(--danger); font-weight: 600;
94
- }
95
- /* Low confidence flagged for close review (amber). */
96
- .saga-low {
97
- color: var(--warn-ink); background: var(--warn); border-color: var(--warn-solid); font-weight: 600;
116
+ /* Agent attention flags (QA, differs-from-plan, closer-look) share one calm
117
+ amber; the label carries the meaning, not the colour. */
118
+ .saga-qa, .saga-dev, .saga-low {
119
+ color: var(--warn); border-color: var(--warn-solid); background: var(--warn-tint);
98
120
  }
99
121
 
100
122
  /* --- chapter reader --- */
@@ -120,10 +142,10 @@
120
142
  .saga-chapter-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
121
143
  .saga-chapter-title { font-size: var(--fs-xl); font-weight: 600; margin: 0; }
122
144
 
123
- /* Loud deviation banner. */
145
+ /* Differs-from-plan banner (amber attention, same family as low-confidence). */
124
146
  .saga-deviation {
125
- background: var(--danger-loud-bg); border: 1px solid var(--danger); border-left: 4px solid var(--danger);
126
- color: var(--danger-ink); border-radius: var(--radius); padding: 12px 16px; margin: 14px 0; font-size: var(--fs-base);
147
+ background: var(--warn-tint); border: 1px solid var(--warn-solid); border-left: 4px solid var(--warn);
148
+ color: var(--warn); border-radius: var(--radius); padding: 12px 16px; margin: 14px 0; font-size: var(--fs-base);
127
149
  }
128
150
  .saga-lowconf {
129
151
  background: var(--warn-tint); border: 1px solid var(--warn-solid); border-left: 4px solid var(--warn);
@@ -139,7 +161,7 @@
139
161
  font-family: var(--font-mono); font-size: var(--fs-sm);
140
162
  }
141
163
 
142
- .saga-qanote { color: var(--ok); font-size: var(--fs-sm); margin: 8px 0; }
164
+ .saga-qanote { color: var(--warn); font-size: var(--fs-sm); margin: 8px 0; }
143
165
 
144
166
  .saga-readmark {
145
167
  display: inline-flex; align-items: center; gap: 6px;
@@ -215,6 +237,11 @@
215
237
  .saga-file-cmt-panel[hidden] { display: none; }
216
238
  .saga-file-cmt-panel .saga-cmt-input { flex: 1 1 240px; min-height: 44px; }
217
239
 
240
+ /* The clickable file path in a diff header inherits diff2html's file-name colour;
241
+ only the hover underline distinguishes it, keeping the header uncluttered. */
242
+ .saga-file-link { color: inherit; text-decoration: none; }
243
+ .saga-file-link:hover { text-decoration: underline; }
244
+
218
245
  /* --- narrow screens: tighten the reader gutter (the 1000px max-width already
219
246
  goes fluid below that width) and shrink the chapter title. --- */
220
247
  @media (max-width: 640px) {