ai-agentmanager 1.6.0 → 1.7.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/CHANGELOG.md +5 -0
- package/README.md +5 -3
- package/package.json +1 -1
- package/public/app.js +37 -2
- package/public/index.html +46 -28
- package/public/review.js +85 -2
- package/public/side.js +191 -0
- package/public/style.css +36 -0
- package/public/walkthrough.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Split view:** **◫ Split** in the terminal bar: the terminal (or a shell, or a browser tab) on the left, and Code, Docs (Diagrams by default, or any other document) or Changes on the right. Resizable and remembered.
|
|
6
|
+
- **Review: changes explained one by one:** **Explain changes** in Review changes puts an explanation above each hunk (what, why, risk, what to check) and **◀ / ▶** or **[ / ]** step through them in reading order, skipping hunks that moved since. A notice and **Explain again** when the code changed after.
|
|
7
|
+
|
|
3
8
|
## 1.6.0 (2026-10-31)
|
|
4
9
|
|
|
5
10
|
- **Changes explained one by one:** `agentmgr doc <agent> walkthrough` explains each hunk of an agent's changes (what, why, risk, what to check) in the order a reviewer should read them, from the same diff the review screen shows. The review screen and a split view for it come next.
|
package/README.md
CHANGED
|
@@ -84,13 +84,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
84
84
|
|
|
85
85
|
```sh
|
|
86
86
|
npm install -g ai-agentmanager # the latest release
|
|
87
|
-
npm install -g ai-agentmanager@1.
|
|
87
|
+
npm install -g ai-agentmanager@1.7.0 # or a specific one
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
Or from the repository, at a release tag:
|
|
91
91
|
|
|
92
92
|
```sh
|
|
93
|
-
git clone --branch v1.
|
|
93
|
+
git clone --branch v1.7.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
94
94
|
cd ai-agentmanager-core
|
|
95
95
|
npm install
|
|
96
96
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -110,7 +110,7 @@ npm link
|
|
|
110
110
|
| Installed with | Update |
|
|
111
111
|
| --- | --- |
|
|
112
112
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
113
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.
|
|
113
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.7.0 && npm install` (the tag you want) |
|
|
114
114
|
| A clone of `main` | `git pull && npm install` |
|
|
115
115
|
|
|
116
116
|
**Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
|
|
@@ -384,6 +384,7 @@ Each card has at most **three points of about 12 words each**, so they're easy t
|
|
|
384
384
|
|
|
385
385
|
- **Files and diff.** The changed files on the left with lines added and removed, and the diff file by file on the right. Click a file header to fold it; very large files start folded.
|
|
386
386
|
- **Line comments.** Click **+** next to a line (or its line number) to comment on it, and add an overall comment if you like. **Send to agent** types them into the agent as one message, each with its file, line and the code it is about, and asks it to address them. Comments not sent yet are kept in this browser.
|
|
387
|
+
- **Explain changes** explains each change one by one: a title, what it changes, why (from the request and the conversation), the risk and what to check, in the order a reviewer should read them. Each explanation sits above its hunk, and **◀ / ▶** (or **[** and **]**) step through them, opening folded files on the way. When the agent changes the code afterwards, the page says so and **Explain again** brings it up to date. From the CLI: `agentmgr doc <agent> walkthrough`.
|
|
387
388
|
- **Commit all** commits everything in the worktree, new files included, with your git name and email.
|
|
388
389
|
- **Push & open PR** pushes the branch and opens a pull request with `gh`, against the branch the agent started from (title from your text, or from the commits; **Draft** if you like). With a pull request already open, it pushes to it. The pull request is linked to the agent.
|
|
389
390
|
|
|
@@ -909,6 +910,7 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
|
|
|
909
910
|
## Studio: code and terminals
|
|
910
911
|
|
|
911
912
|
- **Code** (a tab next to **Claude** in the terminal pane) opens the agent's folder: a file tree, tabs, and Monaco, the editor inside VS Code, with syntax highlighting and search within a file. **Save** (Ctrl/Cmd+S) writes into the agent's folder. If the agent changed the file since you opened it, you're asked before anything is overwritten. Files you haven't edited pick up the agent's changes as it makes them. **Search** finds text across the folder (git grep, so ignored files are skipped). **Diff** compares the file with where the agent started, side by side, and you can edit on the right. **+ / ▣ / ✎ / 🗑** create, rename and delete. Every path stays inside the agent's folder: `..`, and links that point outside, are refused.
|
|
913
|
+
- **◫ Split** (in the terminal bar) keeps the terminal, a shell or a browser tab on the left and opens a pane on the right with **Code**, **Docs** (Diagrams by default; Design, Flow, Analysis, Proposal or Explain from the menu, written there if missing) or **Changes** (the change-by-change explanations, each with its diff). Drag the divider to resize; the pane, its tab and its width are remembered in this browser. Desktop only.
|
|
912
914
|
- **VS Code / Cursor** opens the agent's folder (and the open file) in the editor installed on this computer. The buttons show when its `code` or `cursor` command is found.
|
|
913
915
|
- Monaco loads from the jsDelivr CDN the first time you open **Code**. Offline, a plain text editor takes its place.
|
|
914
916
|
- **Terminal** in the top bar opens a shell on this computer in your home folder, not tied to any agent. It works from any device you're signed in on, so you can run any command from your phone. **+ Terminal** opens more. They keep running when hidden; **×** ends one. Plugins can refuse it with the `terminal.open` policy (`kind: "machine"`).
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import DOMPurify from '/vendor/dompurify/purify.es.mjs';
|
|
|
5
5
|
import { touchScroll } from '/touchscroll.js';
|
|
6
6
|
import { openStream, post, REMOTE } from '/transport.js';
|
|
7
7
|
import { attachBox } from '/attach.js';
|
|
8
|
+
import { initSide } from '/side.js';
|
|
8
9
|
import { initReview } from '/review.js';
|
|
9
10
|
import { initLand, prChips } from '/land.js';
|
|
10
11
|
import { initTriggers } from '/triggers.js';
|
|
@@ -111,7 +112,8 @@ initSetup({ api, esc, $ });
|
|
|
111
112
|
const updateUi = initUpdate({ api, esc, toast, $ });
|
|
112
113
|
initMenu({ api, esc, toast, $ });
|
|
113
114
|
initMachine({ api, esc, toast, $, Terminal, FitAddon });
|
|
114
|
-
|
|
115
|
+
let sidePane = null; // the split view's right pane (set up once the terminal is)
|
|
116
|
+
const codeUi = initCode({ api, esc, toast, $, agents, isOpen: () => term.kind === 'code' || Boolean(sidePane?.showing('code')) });
|
|
115
117
|
const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
|
|
116
118
|
$('#new-playbook').onclick = () => {
|
|
117
119
|
const f = $('#new-form');
|
|
@@ -1899,6 +1901,7 @@ const term = {
|
|
|
1899
1901
|
const a = this.agentId && agents.get(this.agentId);
|
|
1900
1902
|
// The agent's own session tab is named after its engine (Codex, Muse Code, …).
|
|
1901
1903
|
$('.term-tab[data-kind="claude"]').textContent = ENGINE_LABEL[a?.engine] ?? 'Claude';
|
|
1904
|
+
sidePane?.follow(this.agentId);
|
|
1902
1905
|
const nums = new Set(a?.runtime.shells ?? []);
|
|
1903
1906
|
if (isShell(this.kind)) nums.add(shellNum(this.kind));
|
|
1904
1907
|
$('#shell-tabs').innerHTML = [...nums]
|
|
@@ -1930,6 +1933,7 @@ const term = {
|
|
|
1930
1933
|
$('#term-wrap').hidden = false;
|
|
1931
1934
|
$('#detail').classList.add('with-term');
|
|
1932
1935
|
$('#term-wrap').classList.remove('browsing');
|
|
1936
|
+
sidePane?.mainShowsCode();
|
|
1933
1937
|
$('#term-wrap').classList.add('coding');
|
|
1934
1938
|
this.ensure();
|
|
1935
1939
|
this.disconnect();
|
|
@@ -2095,6 +2099,20 @@ $('#term-fit').onclick = () => term.refit();
|
|
|
2095
2099
|
|
|
2096
2100
|
// ------------------------------------------------------------------ resizing, expanding, full screen
|
|
2097
2101
|
|
|
2102
|
+
sidePane = initSide({
|
|
2103
|
+
api,
|
|
2104
|
+
esc,
|
|
2105
|
+
toast,
|
|
2106
|
+
$,
|
|
2107
|
+
agents,
|
|
2108
|
+
codeUi,
|
|
2109
|
+
docHtml,
|
|
2110
|
+
renderDiagrams,
|
|
2111
|
+
mainKind: () => term.kind,
|
|
2112
|
+
setMain: (kind) => term.agentId && term.open(term.agentId, kind),
|
|
2113
|
+
refit: () => requestAnimationFrame(() => term.refit()),
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2098
2116
|
const layoutPref = {
|
|
2099
2117
|
get(key) {
|
|
2100
2118
|
try {
|
|
@@ -2114,6 +2132,8 @@ function applyLayout() {
|
|
|
2114
2132
|
const side = layoutPref.get('sideWidth');
|
|
2115
2133
|
const brief = layoutPref.get('briefHeight');
|
|
2116
2134
|
document.documentElement.style.setProperty('--side-w', side ? `${side}px` : '340px');
|
|
2135
|
+
const pane = layoutPref.get('paneWidth');
|
|
2136
|
+
document.documentElement.style.setProperty('--pane-w', pane ? `${pane}%` : '45%');
|
|
2117
2137
|
if (brief) document.documentElement.style.setProperty('--brief-h', `${brief}px`);
|
|
2118
2138
|
else document.documentElement.style.removeProperty('--brief-h');
|
|
2119
2139
|
$('#detail').classList.toggle('brief-sized', Boolean(brief));
|
|
@@ -2156,6 +2176,14 @@ dragHandle($('#split-side'), 'cols', (ev) => {
|
|
|
2156
2176
|
return w;
|
|
2157
2177
|
}, 'sideWidth');
|
|
2158
2178
|
|
|
2179
|
+
// The split view's divider: the right pane's share of the panel, in percent.
|
|
2180
|
+
dragHandle($('#split-pane'), 'cols', (ev) => {
|
|
2181
|
+
const r = $('#term-split').getBoundingClientRect();
|
|
2182
|
+
const pct = Math.min(Math.max(((r.right - ev.clientX) / r.width) * 100, 20), 75);
|
|
2183
|
+
layoutPref.set('paneWidth', Math.round(pct));
|
|
2184
|
+
return pct;
|
|
2185
|
+
}, 'paneWidth');
|
|
2186
|
+
|
|
2159
2187
|
dragHandle($('#split-term'), 'rows', (ev) => {
|
|
2160
2188
|
const top = $('#detail').getBoundingClientRect().top;
|
|
2161
2189
|
const h = Math.min(Math.max(ev.clientY - top, 60), $('#detail').clientHeight - 160);
|
|
@@ -2863,7 +2891,14 @@ $('#term-file').onchange = (e) => {
|
|
|
2863
2891
|
function upsert(a) {
|
|
2864
2892
|
agents.set(a.id, a);
|
|
2865
2893
|
render();
|
|
2894
|
+
agentChanged(a);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
/** Views that show one agent's details outside the main render: keep them current. */
|
|
2898
|
+
function agentChanged(a) {
|
|
2866
2899
|
if (a.id === term.agentId) term.renderTabs();
|
|
2900
|
+
reviewUi.agentChanged(a);
|
|
2901
|
+
sidePane?.agentChanged(a);
|
|
2867
2902
|
}
|
|
2868
2903
|
|
|
2869
2904
|
$('#counts').addEventListener('click', (e) => {
|
|
@@ -3498,7 +3533,7 @@ function handleEvent(msg) {
|
|
|
3498
3533
|
} else if (msg.t === 'agent') {
|
|
3499
3534
|
agents.set(msg.agent.id, msg.agent);
|
|
3500
3535
|
watchDocs(msg.agent);
|
|
3501
|
-
|
|
3536
|
+
agentChanged(msg.agent);
|
|
3502
3537
|
} else if (msg.t === 'removed') {
|
|
3503
3538
|
agents.delete(msg.id);
|
|
3504
3539
|
if (selectedId === msg.id) select(null);
|
package/public/index.html
CHANGED
|
@@ -96,41 +96,59 @@
|
|
|
96
96
|
<button id="term-attach" class="ghost small" title="Attach files or images (you can also drop or paste them into the terminal)">📎 Attach</button>
|
|
97
97
|
<input type="file" id="term-file" multiple hidden>
|
|
98
98
|
<button id="term-fit" class="ghost small" title="Refit the terminal to the panel">Fit</button>
|
|
99
|
+
<button id="term-split-btn" class="ghost small desk-only" title="Split: the terminal on the left, and Code, Docs (diagrams) or Changes on the right">◫ Split</button>
|
|
99
100
|
<button id="term-max" class="ghost small desk-only" title="Expand to fill the window (Ctrl+Shift+E)">⤢ Expand</button>
|
|
100
101
|
<button id="term-full" class="ghost small desk-only" title="Full screen (Esc to leave)">⛶ Full screen</button>
|
|
101
102
|
<button id="term-close" class="ghost small" title="Hide the terminal (the agent keeps running)">Hide terminal</button>
|
|
102
103
|
</div>
|
|
103
104
|
<div id="signin-bar" class="signin-bar" hidden></div>
|
|
104
|
-
<div
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
105
|
+
<div class="term-split" id="term-split">
|
|
106
|
+
<div class="term-main" id="term-main">
|
|
107
|
+
<div id="terminal" class="terminal"></div>
|
|
108
|
+
<div id="code" class="code"></div>
|
|
109
|
+
<div id="browser" class="browser">
|
|
110
|
+
<form id="browser-bar" class="browser-bar" autocomplete="off">
|
|
111
|
+
<button type="button" id="browser-back" class="ghost small live-only" title="Back">◀</button>
|
|
112
|
+
<button type="button" id="browser-fwd" class="ghost small live-only" title="Forward">▶</button>
|
|
113
|
+
<button type="button" id="browser-reload" class="ghost small" title="Reload the page">⟳</button>
|
|
114
|
+
<input id="browser-url" placeholder="localhost:5173, a port, or any address" spellcheck="false" autocapitalize="off" autocorrect="off" enterkeyhint="go">
|
|
115
|
+
<button class="small">Go</button>
|
|
116
|
+
<button type="button" id="browser-sound" class="ghost small live-only" title="Hear the page: sound from videos and audio playing on that machine">🔇</button>
|
|
117
|
+
<button type="button" id="browser-keys" class="ghost small live-only touch-only" title="Show the keyboard to type into the page">⌨</button>
|
|
118
|
+
<button type="button" id="browser-mode" class="ghost small" hidden></button>
|
|
119
|
+
<a id="browser-pop" class="ghost small button" target="_blank" rel="noopener" title="Open in a new browser tab on this device" hidden>↗</a>
|
|
120
|
+
</form>
|
|
121
|
+
<div id="browser-hint" class="browser-hint" hidden>Blank? Many sites refuse to show inside another page. Open it with ↗ instead.</div>
|
|
122
|
+
<div id="browser-cert" class="browser-start" hidden>
|
|
123
|
+
<p><b>One more step on this device.</b> Each browser tab goes through its own port, and this device hasn't accepted AI Agent Manager's certificate for this one yet.</p>
|
|
124
|
+
<p>Open it, accept the warning the same way you did for AI Agent Manager (Safari: Show Details → visit this website), then come back here. You only do this once per site; it keeps its port.</p>
|
|
125
|
+
<div><a id="browser-cert-open" class="button primary small" target="_blank" rel="noopener">Open and accept</a> <button type="button" id="browser-cert-retry" class="small">Done, load it</button></div>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="browser-start" class="browser-start" hidden>
|
|
128
|
+
<p>Open a page as this machine sees it: the agent's dev server, a site on its network or VPN, or any address. It works from your phone or laptop too: AI Agent Manager relays it over its own secure connection, for signed-in devices only.</p>
|
|
129
|
+
<div id="browser-suggest"></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div id="browser-frames" class="browser-frames"></div>
|
|
132
|
+
<div id="browser-live" class="browser-live" hidden>
|
|
133
|
+
<img id="browser-live-img" alt="" draggable="false">
|
|
134
|
+
<textarea id="browser-live-keys" class="browser-live-keys" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" aria-label="Type into the page"></textarea>
|
|
135
|
+
<div id="browser-live-msg" class="browser-live-msg" hidden></div>
|
|
136
|
+
</div>
|
|
123
137
|
</div>
|
|
124
|
-
<div id="browser-start" class="browser-start" hidden>
|
|
125
|
-
<p>Open a page as this machine sees it: the agent's dev server, a site on its network or VPN, or any address. It works from your phone or laptop too: AI Agent Manager relays it over its own secure connection, for signed-in devices only.</p>
|
|
126
|
-
<div id="browser-suggest"></div>
|
|
127
|
-
</div>
|
|
128
|
-
<div id="browser-frames" class="browser-frames"></div>
|
|
129
|
-
<div id="browser-live" class="browser-live" hidden>
|
|
130
|
-
<img id="browser-live-img" alt="" draggable="false">
|
|
131
|
-
<textarea id="browser-live-keys" class="browser-live-keys" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" aria-label="Type into the page"></textarea>
|
|
132
|
-
<div id="browser-live-msg" class="browser-live-msg" hidden></div>
|
|
133
138
|
</div>
|
|
139
|
+
<div class="splitter split-pane" id="split-pane" role="separator" aria-orientation="vertical" title="Drag to resize; double-click to reset" hidden></div>
|
|
140
|
+
<aside class="side-pane" id="side-pane" hidden>
|
|
141
|
+
<div class="side-bar">
|
|
142
|
+
<div class="term-tabs" role="tablist">
|
|
143
|
+
<button class="term-tab" data-side="code" role="tab" title="The agent's files">Code</button>
|
|
144
|
+
<button class="term-tab" data-side="docs" role="tab" title="Diagrams and the other documents">Docs</button>
|
|
145
|
+
<button class="term-tab" data-side="changes" role="tab" title="Its changes, explained one by one">Changes</button>
|
|
146
|
+
</div>
|
|
147
|
+
<span class="spacer"></span>
|
|
148
|
+
<button type="button" class="ghost small" id="side-close" title="Close the split">✕</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="side-body" id="side-body"></div>
|
|
151
|
+
</aside>
|
|
134
152
|
</div>
|
|
135
153
|
<div id="term-drop" class="term-drop" hidden>Drop files to attach</div>
|
|
136
154
|
<div class="keybar" id="keybar" aria-label="Terminal keys">
|
package/public/review.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Review screen: an agent's changes file by file, line comments sent back to the agent as one
|
|
2
2
|
// message, and Commit / Push & open PR. Comments not sent yet are kept per agent in this browser.
|
|
3
|
+
// With a walkthrough, each hunk shows its explanation and ◀ / ▶ step through them in reading order.
|
|
4
|
+
import { cardHtml, explainButton, stepOf, walkthroughOf } from './walkthrough.js';
|
|
3
5
|
|
|
4
6
|
/** Unified diff -> [{ path, status, binary, hunks: [{ head, lines: [{ t, old, new, text }] }] }] */
|
|
5
7
|
export function parseDiff(text) {
|
|
@@ -56,7 +58,8 @@ const BIG = 600; // files with more changed lines start collapsed
|
|
|
56
58
|
export function initReview({ api, esc, toast, agents, $ }) {
|
|
57
59
|
const dlg = $('#review-dialog');
|
|
58
60
|
const body = $('#review-body');
|
|
59
|
-
const st = { id: null, data: null, files: [], comments: [], collapsed: new Set(), editing: null, loading: false };
|
|
61
|
+
const st = { id: null, data: null, files: [], comments: [], collapsed: new Set(), editing: null, loading: false, step: null };
|
|
62
|
+
const walk = () => walkthroughOf(agents.get(st.id), st.data?.diffHash);
|
|
60
63
|
|
|
61
64
|
const key = (id) => `agentmgr.review.${id}`;
|
|
62
65
|
const loadDrafts = (id) => {
|
|
@@ -80,6 +83,7 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
80
83
|
st.collapsed = new Set();
|
|
81
84
|
st.editing = null;
|
|
82
85
|
st.data = null;
|
|
86
|
+
st.step = null;
|
|
83
87
|
$('#review-title').textContent = `Review: ${agents.get(id)?.name ?? ''}`;
|
|
84
88
|
body.innerHTML = '<p class="hint">Loading changes…</p>';
|
|
85
89
|
if (!dlg.open) dlg.showModal();
|
|
@@ -129,7 +133,10 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
129
133
|
if (collapsed) return `<div class="rv-file" id="rv-${encodeURIComponent(f.path)}">${head}</div>`;
|
|
130
134
|
if (f.binary) return `<div class="rv-file" id="rv-${encodeURIComponent(f.path)}">${head}<p class="hint rv-pad">Binary file</p></div>`;
|
|
131
135
|
const rows = [];
|
|
136
|
+
const w = walk();
|
|
132
137
|
for (const h of f.hunks) {
|
|
138
|
+
const i = w.doc ? stepOf(w.doc, f.path, h.head) : -1;
|
|
139
|
+
if (i >= 0) rows.push(`<tr class="rv-explain ${st.step === i ? 'current' : ''}" id="rv-step-${i}"><td colspan="3">${cardHtml(w.doc.data.changes[i], i, w.doc.data.changes.length, esc)}</td></tr>`);
|
|
133
140
|
rows.push(`<tr class="rv-hunk"><td colspan="3">${esc(h.head)}</td></tr>`);
|
|
134
141
|
for (const l of h.lines) {
|
|
135
142
|
const side = l.t === 'del' ? 'old' : 'new';
|
|
@@ -149,6 +156,48 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
149
156
|
return `<div class="rv-file" id="rv-${encodeURIComponent(f.path)}">${head}<table class="rv-table">${rows.join('')}</table></div>`;
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
/** Explain changes, then ◀ n of N ▶ once there is a walkthrough. */
|
|
160
|
+
function walkNav() {
|
|
161
|
+
const w = walk();
|
|
162
|
+
const n = w.doc?.data.changes.length ?? 0;
|
|
163
|
+
const nav = n
|
|
164
|
+
? `<span class="rv-walk"><button type="button" class="small ghost" data-rv-step="-1" title="Previous change ([)">◀</button><span class="hint">${st.step == null ? `${n} changes` : `${st.step + 1} of ${n}`}</span><button type="button" class="small ghost" data-rv-step="1" title="Next change (])">▶</button></span>`
|
|
165
|
+
: '';
|
|
166
|
+
return `${nav}${explainButton(w, 'data-rv-explain')}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function walkNote() {
|
|
170
|
+
const w = walk();
|
|
171
|
+
if (w.error && !w.pending) return `<p class="error rv-pad">Couldn't explain the changes: ${esc(w.error)}</p>`;
|
|
172
|
+
if (!w.doc) return '';
|
|
173
|
+
const notes = [esc(w.doc.data.summary)];
|
|
174
|
+
if (w.stale) notes.push('<b>The changes moved since this was written</b>; explanations for hunks that changed are missing. Explain again for the latest.');
|
|
175
|
+
if (w.doc.cut) notes.push('The diff was too long to explain all of it; the rest has no explanation.');
|
|
176
|
+
return `<p class="hint rv-walk-note">${notes.join(' ')}</p>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Explanations whose hunk is still in the diff (after more edits, some may be gone). */
|
|
180
|
+
const present = (w) => (w.doc?.data.changes ?? []).map((c, i) => (st.files.some((f) => f.path === c.file && f.hunks.some((h) => h.head === c.head)) ? i : -1)).filter((i) => i >= 0);
|
|
181
|
+
|
|
182
|
+
/** Step `dir` (+1 / -1) from the current explanation: open its file, mark it and scroll to it. */
|
|
183
|
+
function step(dir) {
|
|
184
|
+
const w = walk();
|
|
185
|
+
const ok = present(w);
|
|
186
|
+
if (!ok.length) return toast('These explanations no longer match the changes; Explain again');
|
|
187
|
+
const at = st.step == null ? -1 : ok.indexOf(st.step);
|
|
188
|
+
const next = at < 0 ? (dir > 0 ? ok[0] : ok.at(-1)) : ok[(at + dir + ok.length) % ok.length];
|
|
189
|
+
gotoStep(next);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function gotoStep(i) {
|
|
193
|
+
const w = walk();
|
|
194
|
+
st.step = i;
|
|
195
|
+
st.collapsed.delete(w.doc.data.changes[st.step].file);
|
|
196
|
+
keepInputs();
|
|
197
|
+
render();
|
|
198
|
+
body.querySelector(`#rv-step-${st.step}`)?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
199
|
+
}
|
|
200
|
+
|
|
152
201
|
function render() {
|
|
153
202
|
const d = st.data;
|
|
154
203
|
if (!d) return;
|
|
@@ -168,8 +217,10 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
168
217
|
${d.unpushed ? `<span class="tag">${d.unpushed} not pushed</span>` : ''}
|
|
169
218
|
${openPr ? `<a href="${esc(openPr.url)}" target="_blank" rel="noopener">PR #${esc(openPr.number ?? '')}</a>` : ''}
|
|
170
219
|
<span class="spacer"></span>
|
|
220
|
+
${d.files.length ? walkNav() : ''}
|
|
171
221
|
<button type="button" class="small ghost" data-rv-reload>${st.loading ? 'Loading…' : 'Reload'}</button>
|
|
172
222
|
</div>
|
|
223
|
+
${walkNote()}
|
|
173
224
|
${d.busy ? '<p class="hint rv-warn">The agent is working, so the changes may still move. Reload when it is idle.</p>' : ''}
|
|
174
225
|
${d.truncated ? '<p class="hint rv-warn">The diff is large and was cut short; open a shell for the rest.</p>' : ''}
|
|
175
226
|
<div class="rv-main">
|
|
@@ -235,6 +286,20 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
235
286
|
body.addEventListener('click', async (e) => {
|
|
236
287
|
const t = e.target;
|
|
237
288
|
if (t.closest('[data-rv-reload]')) return keepInputs(), reload();
|
|
289
|
+
const stepBtn = t.closest('[data-rv-step]');
|
|
290
|
+
if (stepBtn) return step(Number(stepBtn.dataset.rvStep));
|
|
291
|
+
if (t.closest('[data-rv-explain]')) {
|
|
292
|
+
keepInputs();
|
|
293
|
+
try {
|
|
294
|
+
// In the background: the agent's record says when it's done (agentChanged re-renders).
|
|
295
|
+
agents.set(st.id, await api('POST', `/agents/${st.id}/docs/walkthrough?background=1`));
|
|
296
|
+
st.step = null;
|
|
297
|
+
render();
|
|
298
|
+
} catch (err) {
|
|
299
|
+
toast(`Error: ${err.message}`, 5000);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
238
303
|
const go = t.closest('[data-rv-goto]');
|
|
239
304
|
if (go) {
|
|
240
305
|
e.preventDefault();
|
|
@@ -339,6 +404,13 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
339
404
|
}
|
|
340
405
|
});
|
|
341
406
|
|
|
407
|
+
// [ and ] step through the explained changes (not while typing).
|
|
408
|
+
dlg.addEventListener('keydown', (e) => {
|
|
409
|
+
if ((e.key !== '[' && e.key !== ']') || e.target.closest?.('textarea, input') || !walk().doc) return;
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
step(e.key === ']' ? 1 : -1);
|
|
412
|
+
});
|
|
413
|
+
|
|
342
414
|
body.addEventListener('keydown', (e) => {
|
|
343
415
|
const form = e.target.closest?.('.rv-form');
|
|
344
416
|
if (form && e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
@@ -364,5 +436,16 @@ export function initReview({ api, esc, toast, agents, $ }) {
|
|
|
364
436
|
st.note = st.msg = st.prTitle = '';
|
|
365
437
|
});
|
|
366
438
|
|
|
367
|
-
|
|
439
|
+
/** The agent changed (its walkthrough finished, say): redraw if it's the one under review. */
|
|
440
|
+
function agentChanged(a) {
|
|
441
|
+
if (!dlg.open || a.id !== st.id || !st.data) return;
|
|
442
|
+
const w = a.docs?.walkthrough;
|
|
443
|
+
const sig = `${w?.at}|${w?.pending}|${w?.error}`;
|
|
444
|
+
if (sig === st.walkSig) return;
|
|
445
|
+
st.walkSig = sig;
|
|
446
|
+
keepInputs();
|
|
447
|
+
render();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { open, agentChanged, get openId() { return dlg.open ? st.id : null; } };
|
|
368
451
|
}
|
package/public/side.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// The split view: the terminal (Claude, a shell or a browser tab) stays on the left, and the right
|
|
2
|
+
// pane shows the same agent's Code, its Docs (Diagrams by default) or its Changes explained one by
|
|
3
|
+
// one. On, the tab and the document are remembered in this browser.
|
|
4
|
+
import { parseDiff } from '/review.js';
|
|
5
|
+
import { cardHtml, explainButton, walkthroughOf } from '/walkthrough.js';
|
|
6
|
+
|
|
7
|
+
const DOCS = [
|
|
8
|
+
['diagrams', 'Diagrams'],
|
|
9
|
+
['design', 'Design'],
|
|
10
|
+
['flow', 'Flow'],
|
|
11
|
+
['analysis', 'Analysis'],
|
|
12
|
+
['proposal', 'Proposal'],
|
|
13
|
+
['explain', 'Explain'],
|
|
14
|
+
];
|
|
15
|
+
const pref = {
|
|
16
|
+
get: (k, d) => {
|
|
17
|
+
try {
|
|
18
|
+
return localStorage.getItem(`agentmgr.side.${k}`) ?? d;
|
|
19
|
+
} catch {
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
set: (k, v) => {
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(`agentmgr.side.${k}`, v);
|
|
26
|
+
} catch {}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function initSide({ api, esc, toast, $, agents, codeUi, docHtml, renderDiagrams, mainKind, setMain, refit }) {
|
|
31
|
+
const pane = $('#side-pane');
|
|
32
|
+
const body = $('#side-body');
|
|
33
|
+
const code = $('#code');
|
|
34
|
+
const codeHome = $('#term-main');
|
|
35
|
+
const st = { on: pref.get('on', '0') === '1', tab: pref.get('tab', 'docs'), doc: pref.get('doc', 'diagrams'), agentId: null, changes: null, sig: '', loading: false };
|
|
36
|
+
|
|
37
|
+
const docOf = (a, kind) => (kind === 'explain' ? a?.explanation : a?.docs?.[kind]);
|
|
38
|
+
|
|
39
|
+
function show() {
|
|
40
|
+
const on = st.on && Boolean(st.agentId);
|
|
41
|
+
pane.hidden = !on;
|
|
42
|
+
$('#split-pane').hidden = !on;
|
|
43
|
+
$('#term-split-btn').classList.toggle('on', on);
|
|
44
|
+
for (const t of pane.querySelectorAll('[data-side]')) t.classList.toggle('active', t.dataset.side === st.tab);
|
|
45
|
+
// Code lives in one place at a time: here when this tab shows it, else back in the main panel.
|
|
46
|
+
const want = on && st.tab === 'code' ? body : codeHome;
|
|
47
|
+
if (code.parentElement !== want) want === body ? body.replaceChildren(code) : codeHome.insertBefore(code, $('#browser'));
|
|
48
|
+
pane.classList.toggle('with-code', on && st.tab === 'code');
|
|
49
|
+
if (!on) return refit();
|
|
50
|
+
if (st.tab === 'code') codeUi.open(st.agentId);
|
|
51
|
+
else if (st.tab === 'docs') renderDocs(true);
|
|
52
|
+
else renderChanges(true);
|
|
53
|
+
refit();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------------------ Docs
|
|
57
|
+
function renderDocs(force) {
|
|
58
|
+
const a = agents.get(st.agentId);
|
|
59
|
+
const d = docOf(a, st.doc);
|
|
60
|
+
const sig = `docs|${st.agentId}|${st.doc}|${d?.at}|${d?.pending}|${d?.error}`;
|
|
61
|
+
if (!force && sig === st.sig) return;
|
|
62
|
+
st.sig = sig;
|
|
63
|
+
const label = DOCS.find(([k]) => k === st.doc)?.[1] ?? st.doc;
|
|
64
|
+
const picker = `<select data-side-doc>${DOCS.map(([k, l]) => `<option value="${k}" ${k === st.doc ? 'selected' : ''}>${l}${docOf(a, k)?.markdown ? ' ✓' : ''}</option>`).join('')}</select>`;
|
|
65
|
+
const stale = d?.markdown && a?.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
66
|
+
const action = d?.pending
|
|
67
|
+
? `<span class="hint">Writing…</span>`
|
|
68
|
+
: `<button type="button" class="small ${d?.markdown ? 'ghost' : 'primary'}" data-side-write>${d?.markdown ? 'Again' : st.doc === 'diagrams' ? 'Draw' : 'Write'}</button>`;
|
|
69
|
+
let content;
|
|
70
|
+
if (d?.pending && !d?.markdown) content = `<p class="hint">Writing the ${esc(label.toLowerCase())} from the agent's work so far…</p>`;
|
|
71
|
+
else if (d?.error && !d?.markdown) content = `<p class="error">Couldn't write it: ${esc(d.error)}</p>`;
|
|
72
|
+
else if (d?.markdown) content = `<div class="md ${st.doc === 'diagrams' ? 'diagrams-view' : ''}">${docHtml(d.markdown)}</div>`;
|
|
73
|
+
else content = `<p class="hint">No ${esc(label.toLowerCase())} for this agent yet.</p>`;
|
|
74
|
+
body.innerHTML = `<div class="side-tools">${picker}<span class="spacer"></span>${stale ? '<span class="hint">brief changed since</span>' : ''}${action}</div><div class="side-scroll">${content}</div>`;
|
|
75
|
+
renderDiagrams(body);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ------------------------------------------------------------ Changes
|
|
79
|
+
async function loadChanges() {
|
|
80
|
+
st.loading = true;
|
|
81
|
+
try {
|
|
82
|
+
const data = await api('GET', `/agents/${st.agentId}/changes`);
|
|
83
|
+
st.changes = { id: st.agentId, diffHash: data.diffHash, files: parseDiff(data.diff) };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
st.changes = { id: st.agentId, error: err.message };
|
|
86
|
+
}
|
|
87
|
+
st.loading = false;
|
|
88
|
+
renderChanges(true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hunkHtml(file, head) {
|
|
92
|
+
const h = st.changes?.files?.find((f) => f.path === file)?.hunks.find((x) => x.head === head);
|
|
93
|
+
if (!h) return '<p class="hint">This hunk has changed since; Explain again for the latest.</p>';
|
|
94
|
+
const lines = h.lines.slice(0, 40).map((l) => `<div class="sd-${l.t}">${l.t === 'add' ? '+' : l.t === 'del' ? '−' : ' '}${esc(l.text)}</div>`);
|
|
95
|
+
if (h.lines.length > 40) lines.push(`<div class="hint">… ${h.lines.length - 40} more lines (Review changes shows all)</div>`);
|
|
96
|
+
return `<div class="sd-hunk"><div class="sd-head">${esc(head)}</div>${lines.join('')}</div>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderChanges(force) {
|
|
100
|
+
if (!st.changes || st.changes.id !== st.agentId) {
|
|
101
|
+
body.innerHTML = '<p class="hint side-pad">Loading the changes…</p>';
|
|
102
|
+
if (!st.loading) loadChanges();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const a = agents.get(st.agentId);
|
|
106
|
+
const w = walkthroughOf(a, st.changes.diffHash);
|
|
107
|
+
const sig = `changes|${st.agentId}|${a?.docs?.walkthrough?.at}|${w.pending}|${w.error}|${st.changes.diffHash}`;
|
|
108
|
+
if (!force && sig === st.sig) return;
|
|
109
|
+
st.sig = sig;
|
|
110
|
+
const tools = `<div class="side-tools">${explainButton(w, 'data-side-explain')}<span class="spacer"></span><button type="button" class="small ghost" data-side-reload title="Load the changes again">⟳</button></div>`;
|
|
111
|
+
let content;
|
|
112
|
+
if (st.changes.error) content = `<p class="error">${esc(st.changes.error)}</p>`;
|
|
113
|
+
else if (!st.changes.files.length) content = '<p class="hint">No changes yet.</p>';
|
|
114
|
+
else if (!w.doc) content = `${w.error && !w.pending ? `<p class="error">Couldn't explain them: ${esc(w.error)}</p>` : ''}<p class="hint">${st.changes.files.length} file${st.changes.files.length === 1 ? '' : 's'} changed. <b>Explain changes</b> goes through them one by one: what, why, the risk and what to check.</p>`;
|
|
115
|
+
else {
|
|
116
|
+
const list = w.doc.data.changes;
|
|
117
|
+
content = `<p class="hint">${esc(w.doc.data.summary)}${w.stale ? ' <b>The changes moved since this was written.</b>' : ''}</p>${list
|
|
118
|
+
.map((c, i) => `<section class="sd-step"><div class="sd-file"><code>${esc(c.file)}</code></div>${cardHtml(c, i, list.length, esc)}${hunkHtml(c.file, c.head)}</section>`)
|
|
119
|
+
.join('')}`;
|
|
120
|
+
}
|
|
121
|
+
body.innerHTML = `${tools}<div class="side-scroll">${content}</div>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ------------------------------------------------------------ events
|
|
125
|
+
pane.addEventListener('click', async (e) => {
|
|
126
|
+
const tab = e.target.closest('[data-side]');
|
|
127
|
+
if (tab) return setTab(tab.dataset.side);
|
|
128
|
+
if (e.target.closest('#side-close')) return toggle(false);
|
|
129
|
+
if (e.target.closest('[data-side-reload]')) return loadChanges();
|
|
130
|
+
try {
|
|
131
|
+
if (e.target.closest('[data-side-explain]')) {
|
|
132
|
+
agents.set(st.agentId, await api('POST', `/agents/${st.agentId}/docs/walkthrough?background=1`));
|
|
133
|
+
return renderChanges(true);
|
|
134
|
+
}
|
|
135
|
+
if (e.target.closest('[data-side-write]')) {
|
|
136
|
+
const route = st.doc === 'explain' ? `/agents/${st.agentId}/explain?background=1` : `/agents/${st.agentId}/docs/${st.doc}?background=1`;
|
|
137
|
+
agents.set(st.agentId, await api('POST', route));
|
|
138
|
+
return renderDocs(true);
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
toast(`Error: ${err.message}`, 5000);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
pane.addEventListener('change', (e) => {
|
|
145
|
+
if (!e.target.matches('[data-side-doc]')) return;
|
|
146
|
+
st.doc = e.target.value;
|
|
147
|
+
pref.set('doc', st.doc);
|
|
148
|
+
renderDocs(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function setTab(tab) {
|
|
152
|
+
st.tab = tab;
|
|
153
|
+
pref.set('tab', tab);
|
|
154
|
+
// Code can't be on both sides: the main panel goes back to the agent's session.
|
|
155
|
+
if (tab === 'code' && mainKind() === 'code') setMain('claude');
|
|
156
|
+
show();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toggle(on = !st.on) {
|
|
160
|
+
st.on = on;
|
|
161
|
+
pref.set('on', on ? '1' : '0');
|
|
162
|
+
if (on && st.tab === 'code' && mainKind() === 'code') st.tab = 'docs';
|
|
163
|
+
show();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$('#term-split-btn').onclick = () => toggle();
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
/** The terminal now shows this agent (or none): follow it. */
|
|
170
|
+
follow(id) {
|
|
171
|
+
if (id === st.agentId) return;
|
|
172
|
+
st.agentId = id ?? null;
|
|
173
|
+
st.changes = null;
|
|
174
|
+
st.sig = '';
|
|
175
|
+
show();
|
|
176
|
+
},
|
|
177
|
+
/** The main panel is about to show Code: move this pane off it. */
|
|
178
|
+
mainShowsCode() {
|
|
179
|
+
if (st.on && st.tab === 'code') {
|
|
180
|
+
st.tab = 'docs';
|
|
181
|
+
show();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
showing: (tab) => st.on && Boolean(st.agentId) && st.tab === tab,
|
|
185
|
+
agentChanged(a) {
|
|
186
|
+
if (!st.on || a.id !== st.agentId) return;
|
|
187
|
+
if (st.tab === 'docs') renderDocs(false);
|
|
188
|
+
else if (st.tab === 'changes') renderChanges(false);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
package/public/style.css
CHANGED
|
@@ -837,3 +837,39 @@ input.inline { display: inline-block; width: 80px; padding: 1px 4px; }
|
|
|
837
837
|
.attach-chip.fail { border-color: var(--red); }
|
|
838
838
|
.attach-chip .link { padding: 0 4px; }
|
|
839
839
|
form.dropping { outline: 2px dashed var(--accent, #6b8cff); outline-offset: 4px; }
|
|
840
|
+
|
|
841
|
+
/* Walkthrough: an explanation card above each hunk; the current one is marked. */
|
|
842
|
+
.wt-card { display: flex; flex-direction: column; gap: 3px; padding: 8px 10px; font: 13px/1.45 var(--sans, system-ui); white-space: normal; color: var(--text); }
|
|
843
|
+
.wt-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
844
|
+
.wt-n { font: 11px var(--mono, ui-monospace); color: var(--muted); }
|
|
845
|
+
.wt-k { display: inline-block; min-width: 44px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
846
|
+
.rv-explain td { background: var(--panel); border-top: 1px solid var(--border); }
|
|
847
|
+
.rv-explain.current td { box-shadow: inset 3px 0 0 var(--accent, #6b8cff); background: var(--panel-2); }
|
|
848
|
+
.rv-walk { display: inline-flex; align-items: center; gap: 4px; }
|
|
849
|
+
.rv-walk-note { margin: 4px 12px 8px; }
|
|
850
|
+
|
|
851
|
+
/* Split view: the terminal (or a browser tab, or Code) on the left, a pane on the right. */
|
|
852
|
+
.term-split { flex: 1; display: flex; min-height: 0; min-width: 0; }
|
|
853
|
+
.term-main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
854
|
+
.split-pane { flex: none; width: 0; }
|
|
855
|
+
.split-pane::after { top: 0; bottom: 0; left: -4px; width: 8px; cursor: col-resize; }
|
|
856
|
+
.split-pane[hidden], .side-pane[hidden] { display: none !important; }
|
|
857
|
+
.side-pane { flex: none; width: var(--pane-w, 45%); display: flex; flex-direction: column; min-height: 0; border-left: 1px solid #30363d; background: var(--bg); color: var(--text); }
|
|
858
|
+
.side-bar { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: #161a22; color: #c9d1d9; font-size: 12.5px; }
|
|
859
|
+
.side-bar button { color: #c9d1d9; border-color: #30363d; }
|
|
860
|
+
.side-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
861
|
+
.side-pane .code { display: flex; flex: 1; }
|
|
862
|
+
.side-tools { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
863
|
+
.side-tools select { width: auto; }
|
|
864
|
+
.side-scroll { flex: 1; overflow: auto; padding: 10px 12px; }
|
|
865
|
+
.side-pad { padding: 10px 12px; }
|
|
866
|
+
.side-scroll .diagrams-view .diagram, .side-scroll .diagram { display: flex; justify-content: center; overflow-x: auto; }
|
|
867
|
+
.side-scroll .diagram svg { max-width: 100%; height: auto; }
|
|
868
|
+
.sd-step { margin-bottom: 14px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
869
|
+
.sd-file { padding: 4px 10px; background: var(--panel-2); font-size: 12px; }
|
|
870
|
+
.sd-hunk { font: 12px/1.45 var(--mono, ui-monospace, monospace); white-space: pre; overflow-x: auto; border-top: 1px solid var(--border); }
|
|
871
|
+
.sd-hunk > div { padding: 0 10px; }
|
|
872
|
+
.sd-head { color: var(--muted); background: var(--panel); }
|
|
873
|
+
.sd-add { background: rgba(46, 160, 67, .15); }
|
|
874
|
+
.sd-del { background: rgba(248, 81, 73, .15); }
|
|
875
|
+
#term-split-btn.on { background: #262c38; }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// An agent's changes explained one by one (the "walkthrough" document): shared by the review
|
|
2
|
+
// screen, which shows each explanation above its hunk, and the Changes pane of the split view.
|
|
3
|
+
|
|
4
|
+
const RISK = { high: 'fail', medium: '', low: 'open' };
|
|
5
|
+
|
|
6
|
+
/** The walkthrough of an agent and whether it still matches the changes (diffHash from /changes). */
|
|
7
|
+
export function walkthroughOf(agent, diffHash) {
|
|
8
|
+
const w = agent?.docs?.walkthrough;
|
|
9
|
+
return {
|
|
10
|
+
doc: w?.data ? w : null,
|
|
11
|
+
pending: Boolean(w?.pending),
|
|
12
|
+
error: w?.error ?? null,
|
|
13
|
+
stale: Boolean(w?.data && diffHash && w.diffHash !== diffHash),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Index of the explanation for a hunk (file + header), or -1. */
|
|
18
|
+
export const stepOf = (doc, file, head) => doc?.data.changes.findIndex((c) => c.file === file && c.head === head) ?? -1;
|
|
19
|
+
|
|
20
|
+
/** One explanation: title, risk, what / why / check. */
|
|
21
|
+
export function cardHtml(c, i, total, esc) {
|
|
22
|
+
if (c.unexplained) return `<div class="wt-card"><div class="wt-head"><span class="wt-n">${i + 1}/${total}</span><span class="hint">Not explained (the model skipped it)</span></div></div>`;
|
|
23
|
+
return `<div class="wt-card">
|
|
24
|
+
<div class="wt-head"><span class="wt-n">${i + 1}/${total}</span><b>${esc(c.title)}</b><span class="tag ${RISK[c.risk] ?? ''}">${esc(c.risk)} risk</span></div>
|
|
25
|
+
${c.what ? `<div><span class="wt-k">What</span> ${esc(c.what)}</div>` : ''}
|
|
26
|
+
${c.why ? `<div><span class="wt-k">Why</span> ${esc(c.why)}</div>` : ''}
|
|
27
|
+
${c.check ? `<div><span class="wt-k">Check</span> ${esc(c.check)}</div>` : ''}
|
|
28
|
+
</div>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The button that writes (or rewrites) the walkthrough, with its state. */
|
|
32
|
+
export function explainButton(w, attrs = 'data-wt-explain') {
|
|
33
|
+
const label = w.pending ? 'Explaining…' : w.doc ? (w.stale ? 'Explain again (changed)' : 'Explain again') : '✨ Explain changes';
|
|
34
|
+
return `<button type="button" class="small ${w.doc && !w.stale ? 'ghost' : 'primary'}" ${attrs} ${w.pending ? 'disabled' : ''}
|
|
35
|
+
title="Explain each change one by one: what, why, the risk and what to check, in the order to read them (a separate one-off call; the agent isn't interrupted)">${label}</button>`;
|
|
36
|
+
}
|