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.
- saga_cli-0.3.1/.claude/settings.local.json +8 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/PKG-INFO +36 -16
- {saga_cli-0.2.0 → saga_cli-0.3.1}/README.md +35 -15
- {saga_cli-0.2.0 → saga_cli-0.3.1}/docs/example.html +125 -36
- {saga_cli-0.2.0 → saga_cli-0.3.1}/pyproject.toml +1 -1
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/base.css +6 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/saga.css +43 -16
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/saga.js +138 -39
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/assets/tokens.css +6 -8
- saga_cli-0.3.1/saga/cli.py +222 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/comments.py +81 -36
- saga_cli-0.3.1/saga/diff.py +169 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/generate.py +102 -58
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/model.py +11 -5
- saga_cli-0.3.1/saga/prompts/saga.md +59 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/render.py +41 -13
- saga_cli-0.3.1/tests/cassettes/generate_anthropic.yaml +162 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_cli.py +116 -5
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_comments.py +53 -1
- saga_cli-0.3.1/tests/test_diff.py +217 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_generate.py +107 -8
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_model.py +5 -3
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/test_render.py +41 -5
- {saga_cli-0.2.0 → saga_cli-0.3.1}/uv.lock +1 -1
- saga_cli-0.2.0/saga/cli.py +0 -106
- saga_cli-0.2.0/saga/diff.py +0 -79
- saga_cli-0.2.0/saga/prompts/saga.md +0 -50
- saga_cli-0.2.0/skills/read_saga_comments/SKILL.md +0 -62
- saga_cli-0.2.0/tests/cassettes/generate_anthropic.yaml +0 -168
- saga_cli-0.2.0/tests/test_diff.py +0 -95
- {saga_cli-0.2.0 → saga_cli-0.3.1}/.github/CODEOWNERS +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/.github/workflows/ci.yml +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/.gitignore +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/CONTRIBUTING.md +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/LICENSE +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/RELEASING.md +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/__init__.py +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/saga/__main__.py +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/skills/generate_saga/SKILL.md +0 -0
- {saga_cli-0.2.0 → saga_cli-0.3.1}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: saga-cli
|
|
3
|
-
Version: 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` |
|
|
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
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
184
|
-
saga comments push --
|
|
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
|
-
#
|
|
187
|
-
saga comments read --
|
|
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
|
-
|
|
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` |
|
|
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
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
#
|
|
157
|
-
saga comments push --
|
|
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
|
-
#
|
|
160
|
-
saga comments read --
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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'
|
|
819
|
-
if (v.deviations > 0) rail.classList.add('saga-rail-
|
|
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"
|
|
831
|
-
if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">
|
|
832
|
-
if (ch.qa
|
|
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-
|
|
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
|
|
871
|
-
if (
|
|
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
|
|
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">
|
|
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
|
|
892
|
-
? '<div class="saga-qanote"
|
|
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.
|
|
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:
|
|
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
|
-
|
|
20
|
-
.
|
|
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
|
-
|
|
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
|
-
/*
|
|
92
|
-
.
|
|
93
|
-
|
|
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
|
-
/*
|
|
145
|
+
/* Differs-from-plan banner (amber attention, same family as low-confidence). */
|
|
124
146
|
.saga-deviation {
|
|
125
|
-
background: var(--
|
|
126
|
-
color: var(--
|
|
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(--
|
|
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) {
|