glassbox 0.20.0-beta.4 → 1.0.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/README.md +19 -4
- package/dist/channel.js +78 -50
- package/dist/cli.js +582 -467
- package/dist/client/app.global.js +44 -44
- package/dist/client/history.global.js +12 -12
- package/dist/client/styles.css +1 -1
- package/dist/plugins/graphviz/index.js +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ Then you run `glassbox` again. Your previous annotations carry forward — match
|
|
|
87
87
|
|
|
88
88
|
- **Split and unified diffs** with syntax-colored add/remove/context lines
|
|
89
89
|
- **Line-level annotations** — click any line to add feedback with a category
|
|
90
|
-
- **Image and SVG diffs** — binary images and SVGs render in a **side-by-side** layout (with a left/right or over/under toggle), a **difference overlay**, a **swipeable slice tool**, and pixel-precision synced zoom/pan. SVGs offer a *Code* / *Rendered* toggle so you can review either the source or the
|
|
90
|
+
- **Image and SVG diffs** — binary images and SVGs render in a **side-by-side** layout (with a left/right or over/under toggle), a **difference overlay**, a **swipeable slice tool**, single-side **A / B focus** modes, and pixel-precision synced zoom/pan. SVGs offer a *Code* / *Rendered* toggle so you can review either the source or the live-rendered output.
|
|
91
91
|
- **Comment on images** — leave a general comment on any image, or **draw a rectangle region** to anchor feedback to a specific spot (per A/B side), just like a line annotation.
|
|
92
92
|
- **Ground-truth image comparison** — `glassbox --ground-truth <manifest>` compares an **actual** render against an **expected** one (a design spec, a reference, or a previous baseline — even outside the repo), scores the perceptual difference, sorts most-changed-first, and lets you **promote** a new actual to the baseline. Great for catching visual regressions.
|
|
93
93
|
- **Attach files to any comment** — drag, paste, or browse to attach screenshots, logs, or any file to a line comment, image comment, or note reply. Image attachments show a thumbnail and open in a lightbox; the export lists each file's path so your AI tool can read it.
|
|
@@ -99,12 +99,13 @@ Then you run `glassbox` again. Your previous annotations carry forward — match
|
|
|
99
99
|
- **Resizable sidebar** and word wrap toggle
|
|
100
100
|
- **Keyboard navigation** — `j`/`k` to move between files, `Cmd+Enter` to save
|
|
101
101
|
- **Go-to-definition and a nav stack** — click any symbol to jump to its definition, with back/forward through your trail
|
|
102
|
-
- **Themes** —
|
|
102
|
+
- **Themes** — thirteen built-in themes (Dark, Light, High Contrast Dark/Light, Dracula, Tokyo Night, One Dark Pro, Solarized Dark/Light, Monokai, Nord, Gruvbox Dark/Light), plus a custom theme editor with live preview
|
|
103
103
|
- **Session persistence** — reviews survive restarts, pick up where you left off
|
|
104
104
|
- **Smart review reuse** — re-running `glassbox` on the same commit updates diffs in place and migrates annotations to their new line positions
|
|
105
105
|
- **Stale annotation detection** — comments that can't be matched to the updated diff are flagged with a visual indicator
|
|
106
106
|
- **Review history** — browse, reopen, or delete past reviews
|
|
107
107
|
- **Structured export** — markdown output with file paths, line numbers, categories, and instructions for AI consumption
|
|
108
|
+
- **Content plugins** — opt-in renderers turn diagram source (Graphviz, PlantUML, Mermaid) into pictures: diagram files diff like images, and diagram artifacts on AI review notes render inline
|
|
108
109
|
- **Automatic .gitignore** — keeps `.glassbox/` out of version control for you at launch, while leaving the per-project `.glassbox/settings.json` tracked
|
|
109
110
|
- **Auto port selection** — if the default port is busy, it finds an open one
|
|
110
111
|
- **Fully local** — no network calls (unless you opt into AI features), no accounts, no telemetry. Your code stays on your machine.
|
|
@@ -123,6 +124,18 @@ And when "correct" means "matches the design," **ground-truth comparison** (`gla
|
|
|
123
124
|
|
|
124
125
|
<img src="assets/demo-ground-truth.png" alt="Ground-truth comparison: expected vs actual with a perceptual difference score" width="720">
|
|
125
126
|
|
|
127
|
+
### Diagrams too, via content plugins
|
|
128
|
+
|
|
129
|
+
Reviewing a diagram's source text tells you what changed; seeing the diagram tells you what it _means_. **Content plugins** render specialized formats server-side — install one and a `.dot`, `.puml`, or `.mmd` file gets a **Code | Rendered** toggle: _Code_ is the ordinary text diff, _Rendered_ treats the diagram **like an image**, with the full image-review toolkit — side-by-side, difference overlay, slice, synced zoom.
|
|
130
|
+
|
|
131
|
+
<img src="assets/demo-plugin-rendered.png" alt="A Graphviz .dot file diffed as an image: old and new pipeline diagrams side by side" width="720">
|
|
132
|
+
|
|
133
|
+
The same plugins render diagram-source **proof artifacts** attached to [AI review notes](#ai-review-notes) — so an AI's sequence-diagram evidence shows up as an actual diagram, inline with the diff:
|
|
134
|
+
|
|
135
|
+
<img src="assets/demo-plugin-mermaid-note.png" alt="A Mermaid sequence-diagram proof artifact rendered inline in an AI review note" width="720">
|
|
136
|
+
|
|
137
|
+
Graphviz ships built-in (WASM, no dependencies). PlantUML and Mermaid are one click away in **Settings → Plugins** (they use a local Java / headless-Chromium render, so they're opt-in), alongside an image-codecs plugin that adds WebP/AVIF support to ground-truth scoring. Everything stays local, and everything fails soft — without a plugin you simply see the source text. Want to render your own format? See the [plugin development guide](docs/plugin-development-guide.md).
|
|
138
|
+
|
|
126
139
|
---
|
|
127
140
|
|
|
128
141
|
## AI-Powered Review Intelligence
|
|
@@ -192,7 +205,7 @@ When an AI writes or modifies code, it knows _why_ each non-obvious change is th
|
|
|
192
205
|
**AI Review Notes** give the generating AI a structured, line-anchored channel to record that reasoning. The AI emits notes with the `glassbox note` command; they're stored as committed SARIF under `.pr-notes/` — tool-neutral and travelling with the repo — and Glassbox renders them review-comment-style at the exact line they apply to:
|
|
193
206
|
|
|
194
207
|
- **Rationale** — why this change, and what alternatives were rejected
|
|
195
|
-
- **Proof** — the test that passed, a measurement, or a before/after screenshot (notes can carry text and
|
|
208
|
+
- **Proof** — the test that passed, a measurement, or a before/after screenshot (notes can carry text, image, and diagram-source artifacts — diagrams render inline via [content plugins](#diagrams-too-via-content-plugins))
|
|
196
209
|
- **Risk / assumption** — what a reviewer should double-check
|
|
197
210
|
|
|
198
211
|
<img src="assets/demo-review-notes.png" alt="AI-authored review notes rendered inline with the diff, with a threaded reply" width="720">
|
|
@@ -293,8 +306,10 @@ glassbox --resume
|
|
|
293
306
|
| `--diff <a> <b>` | Compare two arbitrary files or folders by path — no git repo required |
|
|
294
307
|
| `--ground-truth <m>` | Compare actual vs expected images from a manifest — no git repo required |
|
|
295
308
|
| `--port <number>` | Port to run on (default: 4183) |
|
|
309
|
+
| `--data-dir <path>` | Store review data somewhere other than `.glassbox/` |
|
|
296
310
|
| `--resume` | Resume the latest in-progress review for this mode |
|
|
297
|
-
| `--
|
|
311
|
+
| `--no-open` | Don't open the browser automatically |
|
|
312
|
+
| `--on-complete <cmd>` | Run a local command when a review is completed |
|
|
298
313
|
| `--check-for-updates` | Check for a newer version on npm |
|
|
299
314
|
| `--debug` | Show build timestamp and debug info |
|
|
300
315
|
| `--help` | Show help |
|
package/dist/channel.js
CHANGED
|
@@ -4,11 +4,62 @@ import { createRequire as __createRequire } from 'module';
|
|
|
4
4
|
const require = __createRequire(import.meta.url);
|
|
5
5
|
|
|
6
6
|
// src/channel.ts
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
7
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
10
|
import { unlinkSync, writeFileSync } from "fs";
|
|
10
11
|
import { createServer } from "http";
|
|
11
12
|
import { join } from "path";
|
|
13
|
+
|
|
14
|
+
// src/channel-server.ts
|
|
15
|
+
import { timingSafeEqual } from "crypto";
|
|
16
|
+
var MAX_BODY_BYTES = 1048576;
|
|
17
|
+
function secretMatches(provided, secret2) {
|
|
18
|
+
if (typeof provided !== "string") return false;
|
|
19
|
+
const a = Buffer.from(provided);
|
|
20
|
+
const b = Buffer.from(secret2);
|
|
21
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
22
|
+
}
|
|
23
|
+
function createChannelHandler(deps) {
|
|
24
|
+
return async (req, res) => {
|
|
25
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
26
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
27
|
+
res.end(JSON.stringify({ ok: true }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (req.method === "POST" && req.url === "/trigger") {
|
|
31
|
+
if (!secretMatches(req.headers["x-glassbox-secret"], deps.secret)) {
|
|
32
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
33
|
+
res.end(JSON.stringify({ error: "Missing or invalid X-Glassbox-Secret" }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let body = "";
|
|
37
|
+
let bodySize = 0;
|
|
38
|
+
for await (const chunk of req) {
|
|
39
|
+
bodySize += chunk.length;
|
|
40
|
+
if (bodySize > MAX_BODY_BYTES) {
|
|
41
|
+
res.writeHead(413);
|
|
42
|
+
res.end("Payload too large");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
body += String(chunk);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await deps.notify(body || "Read .glassbox/latest-review.md and apply the feedback.");
|
|
49
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
50
|
+
res.end(JSON.stringify({ ok: true }));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end("not found");
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/channel.ts
|
|
12
63
|
var dataDir = ".glassbox";
|
|
13
64
|
var args = process.argv.slice(2);
|
|
14
65
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -18,6 +69,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
18
69
|
}
|
|
19
70
|
}
|
|
20
71
|
var portFile = join(dataDir, "channel-port");
|
|
72
|
+
var secretFile = join(dataDir, "channel-secret");
|
|
73
|
+
var secret = randomBytes(32).toString("hex");
|
|
21
74
|
var mcp = new Server(
|
|
22
75
|
{ name: "glassbox-channel", version: "0.1.0" },
|
|
23
76
|
{
|
|
@@ -35,69 +88,47 @@ var mcp = new Server(
|
|
|
35
88
|
].join(" ")
|
|
36
89
|
}
|
|
37
90
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
if (req.method === "POST" && req.url === "/trigger") {
|
|
54
|
-
let body = "";
|
|
55
|
-
let bodySize = 0;
|
|
56
|
-
for await (const chunk of req) {
|
|
57
|
-
bodySize += chunk.length;
|
|
58
|
-
if (bodySize > 1048576) {
|
|
59
|
-
res.writeHead(413);
|
|
60
|
-
res.end("Payload too large");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
body += String(chunk);
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
await mcp.notification({
|
|
67
|
-
method: "notifications/claude/channel",
|
|
68
|
-
params: {
|
|
69
|
-
content: body || "Read .glassbox/latest-review.md and apply the feedback.",
|
|
70
|
-
meta: { type: "review-feedback" }
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
74
|
-
res.end(JSON.stringify({ ok: true }));
|
|
75
|
-
} catch (err) {
|
|
76
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
77
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
91
|
+
try {
|
|
92
|
+
await mcp.connect(new StdioServerTransport());
|
|
93
|
+
} catch (err) {
|
|
94
|
+
process.stderr.write(`glassbox-channel: failed to connect MCP stdio transport: ${String(err)}
|
|
95
|
+
`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
var httpServer = createServer(createChannelHandler({
|
|
99
|
+
secret,
|
|
100
|
+
notify: async (content) => {
|
|
101
|
+
await mcp.notification({
|
|
102
|
+
method: "notifications/claude/channel",
|
|
103
|
+
params: { content, meta: { type: "review-feedback" } }
|
|
104
|
+
});
|
|
80
105
|
}
|
|
81
|
-
|
|
82
|
-
res.end("not found");
|
|
83
|
-
});
|
|
106
|
+
}));
|
|
84
107
|
httpServer.listen(0, "127.0.0.1", () => {
|
|
85
108
|
const addr = httpServer.address();
|
|
86
109
|
if (addr !== null && typeof addr !== "string") {
|
|
87
110
|
const port = addr.port;
|
|
88
111
|
try {
|
|
89
112
|
writeFileSync(portFile, String(port), "utf-8");
|
|
113
|
+
writeFileSync(secretFile, secret, { encoding: "utf-8", mode: 384 });
|
|
90
114
|
} catch {
|
|
91
115
|
}
|
|
92
116
|
process.stderr.write(`glassbox-channel listening on port ${port}
|
|
93
117
|
`);
|
|
94
118
|
}
|
|
95
119
|
});
|
|
96
|
-
function
|
|
120
|
+
function removeStateFiles() {
|
|
97
121
|
try {
|
|
98
122
|
unlinkSync(portFile);
|
|
99
123
|
} catch {
|
|
100
124
|
}
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(secretFile);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function cleanup() {
|
|
131
|
+
removeStateFiles();
|
|
101
132
|
process.exit(0);
|
|
102
133
|
}
|
|
103
134
|
process.on("SIGTERM", () => {
|
|
@@ -107,9 +138,6 @@ process.on("SIGINT", () => {
|
|
|
107
138
|
cleanup();
|
|
108
139
|
});
|
|
109
140
|
process.on("exit", () => {
|
|
110
|
-
|
|
111
|
-
unlinkSync(portFile);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
141
|
+
removeStateFiles();
|
|
114
142
|
});
|
|
115
143
|
//# sourceMappingURL=channel.js.map
|