nandi/oripublic Fork 0
76d62ac
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme

- Workspace panel: file tree, Monaco preview/editor, PTY terminal, rendered
  Markdown/AsciiDoc, image and draw.io previews (internal/files, internal/terminal,
  ui/src/components/*).
- Composer: @ file mentions (GET /api/files/search, ACP resource_link attachments)
  and / skills + ACP commands (internal/skills, GET /api/skills).
- Resizable file tree column (ticket 0015): ARIA separator, drag / arrows /
  Home-End / double-click reset, width remembered in localStorage.
- Light/dark theme, light by default (ticket 0013): header toggle, data-theme
  palettes, Monaco follows; per-browser localStorage.
- ori-desktop: Wails v2 app embedding the ori SPA (ticket 0005); main.js is an ES module.
- Sandbox packaging: template/Dockerfile + build.sh, kits/ori mixin, quickstart.md.
- Docs EN + FR (Diátaxis), .memory/ project record, tickets 0005–0015, quality
  reports; qlty gate PASS (run #16: 0 errors, 0 warnings, 0 smells).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
k33g committed 2026-09-18T01:21:03+02:00 Browse files
76d62ac parent: 2434cc7
added .claude/skills/slide-diagram/SKILL.md +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+---
2+name: slide-diagram
3+description: Draw a diagram for the talk's slides — boxes-and-arrows explaining a mechanism (function calling, tool detection, MCP, an agent loop…) — in this deck's house style, as an SVG that draw.io can reopen and edit. Use whenever someone asks for a diagram, a schema, "un schéma", a drawing of a flow, or wants to change an existing `.assets/*.drawio.svg`.
4+---
5+
6+# Slide diagram
7+
8+Diagrams in this repository are **generated from a small JSON spec**, never hand-drawn:
9+
10+```
11+<chapter>/.assets/<name>.json the spec — the source of truth
12+<chapter>/.assets/<name>.drawio.svg the output — embeddable in Marp, editable in draw.io
13+```
14+
15+The renderer is `scripts/make-diagram.mjs` (in this skill). The SVG it writes carries the
16+mxfile in its `content` attribute, so draw.io opens it, and Marp/GitHub/browsers show it as a
17+plain SVG.
18+
19+## Workflow
20+
21+1. **Turn the explanation into a flow.** Name the actors (boxes) and what travels between them
22+ (arrows). Number the steps in the edge labels (`1.`, `2.`, …) whenever the order matters —
23+ the audience reads a slide in two seconds, the numbers do that job.
24+2. **Write the spec** as `<chapter>/.assets/<name>.json` (schema below). Start from
25+ `references/example.json`.
26+3. **Render**: `node <skill>/scripts/make-diagram.mjs <chapter>/.assets/<name>.json`
27+4. **Look at it — always.** Text overflow is the failure mode and it is invisible in the source:
28+
29+ ```bash
30+ command -v rsvg-convert || sudo apt-get install -y -qq librsvg2-bin
31+ rsvg-convert -z 1 <name>.drawio.svg -o /tmp/check.png
32+ ```
33+
34+ Then read `/tmp/check.png` with the Read tool and check: no text crossing a box border, no
35+ label sitting on top of another, every arrow head visible, nothing clipped by the canvas.
36+5. **Fix and re-render** until it is clean. Widen boxes, shorten wording, move a column right.
37+6. Report the file path and describe the flow in a few lines.
38+
39+Grep the code the slide is about (`tools.go`, `main.go`, …) before naming things: use the real
40+tool names, the real model, the real arguments. A diagram that matches the demo output is worth
41+three that are merely plausible.
42+
43+## Spec format
44+
45+```json
46+{
47+ "name": "Tool detection", // the draw.io page name
48+ "width": 1220, "height": 540, // canvas, in px
49+ "nodes": [
50+ { "id": "llm", "x": 370, "y": 250, "w": 280, "h": 110,
51+ "label": "LLM (tool support)\\nDocker Model Runner", "kind": "dark" }
52+ ],
53+ "edges": [
54+ { "from": "user", "to": "llm", "label": "1. prompt +\\ntools[]" }
55+ ]
56+}
57+```
58+
59+`\\n` in a JSON string (a literal backslash-n, *not* a real newline) is a line break.
60+
61+**Node `kind`** — pick by role, not by taste:
62+
63+| kind | look | use for |
64+|---|---|---|
65+| `box` | white, dark border, regular | the human, the calling program |
66+| `dark` | dark fill, white bold text | the LLM — always exactly one, it is the centre of gravity |
67+| `accent` | sage fill, bold | data or a tool the model is given: the catalog, a tool, a store |
68+| `intent` | white, blue border, blue bold | what the model *emits*: `tool_calls`, a JSON payload |
69+| `note` | dashed border, smaller regular | the one sentence the audience must leave with |
70+
71+**Edge fields**: `from`, `to`, `label`, `dashed` (blue + dashed — reserve it for the tool /
72+model machinery, keep plain dark for the human-facing flow), `waypoints`
73+(`[{"x":…,"y":…}]`, absolute, for a return path routed around the boxes), and
74+`fromSide`/`toSide` (`"top" | "bottom" | "left" | "right"`) with optional `fromAt`/`toAt`
75+(absolute coordinate along that side). Use the sides when two boxes are stacked and linked
76+**twice** — otherwise both arrows leave from the same centre and cross.
77+
78+Anything else about the palette, the sizing arithmetic and the standard layouts:
79+`references/style.md`. Read it before choosing box widths.
80+
81+## Rules that keep the deck coherent
82+
83+- **One spec, one SVG, same basename.** Never edit the `.drawio.svg` by hand.
84+- **Never regenerate a diagram someone has re-exported from draw.io** without saying so: their
85+ export replaces the file and the JSON no longer describes it (a draw.io-exported file starts
86+ with `<svg host="…"` and has a compressed `content`; ours starts with `<svg xmlns=…` and a
87+ readable one). Ask, or port the edits back into the JSON first.
88+- **English on the diagrams**, whatever language the conversation is in.
89+- The renderer is byte-stable: same spec → same SVG. Regenerating a chapter's diagrams should
90+ produce no diff.
91+
92+## Embedding in a Marp slide
93+
94+```markdown
95+![w:1000 center](.assets/tool-detection.drawio.svg)
96+```
97+
98+Keep the canvas around 1000–1250 px wide: it lands at a readable size on a 1280×720 slide
99+without scaling the text into mush.
new file mode 100644
@@ -0,0 +1,99 @@
1+---
2+name: slide-diagram
3+description: Draw a diagram for the talk's slides — boxes-and-arrows explaining a mechanism (function calling, tool detection, MCP, an agent loop…) — in this deck's house style, as an SVG that draw.io can reopen and edit. Use whenever someone asks for a diagram, a schema, "un schéma", a drawing of a flow, or wants to change an existing `.assets/*.drawio.svg`.
4+---
5+
6+# Slide diagram
7+
8+Diagrams in this repository are **generated from a small JSON spec**, never hand-drawn:
9+
10+```
11+<chapter>/.assets/<name>.json the spec — the source of truth
12+<chapter>/.assets/<name>.drawio.svg the output — embeddable in Marp, editable in draw.io
13+```
14+
15+The renderer is `scripts/make-diagram.mjs` (in this skill). The SVG it writes carries the
16+mxfile in its `content` attribute, so draw.io opens it, and Marp/GitHub/browsers show it as a
17+plain SVG.
18+
19+## Workflow
20+
21+1. **Turn the explanation into a flow.** Name the actors (boxes) and what travels between them
22+ (arrows). Number the steps in the edge labels (`1.`, `2.`, …) whenever the order matters —
23+ the audience reads a slide in two seconds, the numbers do that job.
24+2. **Write the spec** as `<chapter>/.assets/<name>.json` (schema below). Start from
25+ `references/example.json`.
26+3. **Render**: `node <skill>/scripts/make-diagram.mjs <chapter>/.assets/<name>.json`
27+4. **Look at it — always.** Text overflow is the failure mode and it is invisible in the source:
28+
29+ ```bash
30+ command -v rsvg-convert || sudo apt-get install -y -qq librsvg2-bin
31+ rsvg-convert -z 1 <name>.drawio.svg -o /tmp/check.png
32+ ```
33+
34+ Then read `/tmp/check.png` with the Read tool and check: no text crossing a box border, no
35+ label sitting on top of another, every arrow head visible, nothing clipped by the canvas.
36+5. **Fix and re-render** until it is clean. Widen boxes, shorten wording, move a column right.
37+6. Report the file path and describe the flow in a few lines.
38+
39+Grep the code the slide is about (`tools.go`, `main.go`, …) before naming things: use the real
40+tool names, the real model, the real arguments. A diagram that matches the demo output is worth
41+three that are merely plausible.
42+
43+## Spec format
44+
45+```json
46+{
47+ "name": "Tool detection", // the draw.io page name
48+ "width": 1220, "height": 540, // canvas, in px
49+ "nodes": [
50+ { "id": "llm", "x": 370, "y": 250, "w": 280, "h": 110,
51+ "label": "LLM (tool support)\\nDocker Model Runner", "kind": "dark" }
52+ ],
53+ "edges": [
54+ { "from": "user", "to": "llm", "label": "1. prompt +\\ntools[]" }
55+ ]
56+}
57+```
58+
59+`\\n` in a JSON string (a literal backslash-n, *not* a real newline) is a line break.
60+
61+**Node `kind`** — pick by role, not by taste:
62+
63+| kind | look | use for |
64+|---|---|---|
65+| `box` | white, dark border, regular | the human, the calling program |
66+| `dark` | dark fill, white bold text | the LLM — always exactly one, it is the centre of gravity |
67+| `accent` | sage fill, bold | data or a tool the model is given: the catalog, a tool, a store |
68+| `intent` | white, blue border, blue bold | what the model *emits*: `tool_calls`, a JSON payload |
69+| `note` | dashed border, smaller regular | the one sentence the audience must leave with |
70+
71+**Edge fields**: `from`, `to`, `label`, `dashed` (blue + dashed — reserve it for the tool /
72+model machinery, keep plain dark for the human-facing flow), `waypoints`
73+(`[{"x":…,"y":…}]`, absolute, for a return path routed around the boxes), and
74+`fromSide`/`toSide` (`"top" | "bottom" | "left" | "right"`) with optional `fromAt`/`toAt`
75+(absolute coordinate along that side). Use the sides when two boxes are stacked and linked
76+**twice** — otherwise both arrows leave from the same centre and cross.
77+
78+Anything else about the palette, the sizing arithmetic and the standard layouts:
79+`references/style.md`. Read it before choosing box widths.
80+
81+## Rules that keep the deck coherent
82+
83+- **One spec, one SVG, same basename.** Never edit the `.drawio.svg` by hand.
84+- **Never regenerate a diagram someone has re-exported from draw.io** without saying so: their
85+ export replaces the file and the JSON no longer describes it (a draw.io-exported file starts
86+ with `<svg host="…"` and has a compressed `content`; ours starts with `<svg xmlns=…` and a
87+ readable one). Ask, or port the edits back into the JSON first.
88+- **English on the diagrams**, whatever language the conversation is in.
89+- The renderer is byte-stable: same spec → same SVG. Regenerating a chapter's diagrams should
90+ produce no diff.
91+
92+## Embedding in a Marp slide
93+
94+```markdown
95+![w:1000 center](.assets/tool-detection.drawio.svg)
96+```
97+
98+Keep the canvas around 1000–1250 px wide: it lands at a readable size on a 1280×720 slide
99+without scaling the text into mush.
added .claude/skills/slide-diagram/references/example.json +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "name": "Tool detection",
3+ "width": 1220,
4+ "height": 540,
5+ "nodes": [
6+ { "id": "catalog", "x": 310, "y": 20, "w": 400, "h": 145, "label": "Tool catalog — tools[]\\nadd · multiply\\nlist_products · find_price\\nname + description + JSON Schema", "kind": "accent" },
7+ { "id": "user", "x": 20, "y": 250, "w": 220, "h": 110, "label": "User\\n\"How much is a\\nmechanical keyboard?\"", "kind": "box" },
8+ { "id": "llm", "x": 370, "y": 250, "w": 280, "h": 110, "label": "LLM (tool support)\\nDocker Model Runner", "kind": "dark" },
9+ { "id": "intent", "x": 820, "y": 250, "w": 380, "h": 110, "label": "tool_calls — an intention\\nfind_price(\\nproduct=\"mechanical keyboard\")", "kind": "intent" },
10+ { "id": "note", "x": 20, "y": 460, "w": 1180, "h": 60, "label": "The LLM detects and names the tool, it never runs it: executing the call and feeding the result back is the program's job.", "kind": "note" }
11+ ],
12+ "edges": [
13+ { "from": "user", "to": "llm", "label": "1. prompt +\\ntools[]" },
14+ { "from": "catalog", "to": "llm", "label": "2. catalog sent\\nwith the prompt", "fromSide": "bottom", "fromAt": 600, "toSide": "top", "toAt": 600 },
15+ { "from": "llm", "to": "catalog", "label": "3. intent compared\\nto each description", "dashed": true, "fromSide": "top", "fromAt": 430, "toSide": "bottom", "toAt": 430 },
16+ { "from": "llm", "to": "intent", "label": "4. match:\\ntool + arguments", "dashed": true },
17+ { "from": "llm", "to": "user", "label": "no match → plain text answer", "waypoints": [{ "x": 510, "y": 410 }, { "x": 130, "y": 410 }] }
18+ ]
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "name": "Tool detection",
3+ "width": 1220,
4+ "height": 540,
5+ "nodes": [
6+ { "id": "catalog", "x": 310, "y": 20, "w": 400, "h": 145, "label": "Tool catalog — tools[]\\nadd · multiply\\nlist_products · find_price\\nname + description + JSON Schema", "kind": "accent" },
7+ { "id": "user", "x": 20, "y": 250, "w": 220, "h": 110, "label": "User\\n\"How much is a\\nmechanical keyboard?\"", "kind": "box" },
8+ { "id": "llm", "x": 370, "y": 250, "w": 280, "h": 110, "label": "LLM (tool support)\\nDocker Model Runner", "kind": "dark" },
9+ { "id": "intent", "x": 820, "y": 250, "w": 380, "h": 110, "label": "tool_calls — an intention\\nfind_price(\\nproduct=\"mechanical keyboard\")", "kind": "intent" },
10+ { "id": "note", "x": 20, "y": 460, "w": 1180, "h": 60, "label": "The LLM detects and names the tool, it never runs it: executing the call and feeding the result back is the program's job.", "kind": "note" }
11+ ],
12+ "edges": [
13+ { "from": "user", "to": "llm", "label": "1. prompt +\\ntools[]" },
14+ { "from": "catalog", "to": "llm", "label": "2. catalog sent\\nwith the prompt", "fromSide": "bottom", "fromAt": 600, "toSide": "top", "toAt": 600 },
15+ { "from": "llm", "to": "catalog", "label": "3. intent compared\\nto each description", "dashed": true, "fromSide": "top", "fromAt": 430, "toSide": "bottom", "toAt": 430 },
16+ { "from": "llm", "to": "intent", "label": "4. match:\\ntool + arguments", "dashed": true },
17+ { "from": "llm", "to": "user", "label": "no match → plain text answer", "waypoints": [{ "x": 510, "y": 410 }, { "x": 130, "y": 410 }] }
18+ ]
19+}
added .claude/skills/slide-diagram/references/style.md +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+# House style
2+
3+## Palette
4+
5+The one from the Marp theme in the slide headers — do not introduce a colour.
6+
7+| token | value | where |
8+|---|---|---|
9+| ink | `#15202b` | dark boxes, borders, human-flow arrows and their labels |
10+| paper | `#fdfdfb` | white fills, text on dark, label backgrounds (`opacity 0.92`) |
11+| sage | `#e8ebda` | `accent` fills — the things handed to the model |
12+| blue | `#0062FF` | `intent` boxes, dashed arrows, their labels |
13+
14+Boxes: `rx=10`, `stroke-width=2`. Node text 17 px (15 px for `note`), edge labels 14 px,
15+Helvetica Neue / Helvetica / Arial. Every box except `box` and `note` is bold.
16+
17+## Sizing arithmetic
18+
19+The renderer does not measure text, so **you** size the boxes. Rough advance width per
20+character at 17 px: **9.4 px bold**, **8.6 px regular**. Rule of thumb:
21+
22+```
23+box width >= longest_line_chars * 9.4 + 40 (20 px of air on each side)
24+box height = lines * 21 + 45
25+```
26+
27+Edge labels get an opaque background rect of `chars * 7.6 + 14` wide and `lines * 17 + 8` high,
28+centred on the longest segment of the route. So:
29+
30+- leave a **gap of at least `label_width + 40`** between two boxes an edge links, or the label
31+ swallows the whole arrow;
32+- two labels on parallel edges must not overlap horizontally — that is what `fromAt` / `toAt`
33+ are for (put one vertical edge at 1/3 of the width, the other at 2/3).
34+
35+When a line still overflows after widening, rewrite it shorter. Three lines of six words each
36+beat one line of eighteen on a slide seen from row twelve.
37+
38+## Standard layouts
39+
40+**The flow line** (`function-calling.json`): User → LLM → tool, left to right at one height,
41+return paths routed above (tool → LLM, `y` above the boxes) and below (LLM → user), both via
42+`waypoints`. Best when the story is a loop.
43+
44+**The T** (`tool-detection.json`): the same left-to-right line, with what the model *consults*
45+sitting above the LLM and linked by two short vertical edges — one down ("sent with the
46+prompt"), one dashed up ("compared against"). Best when the point is *what the model reads*
47+before it answers. Keep the two vertical edges at different `x`, and reserve ~90 px of vertical
48+gap for their labels.
49+
50+Common to both: a `note` band at the bottom, full width, holding the single sentence the slide
51+exists for. Canvas margin 20 px all round.
52+
53+## Geometry cheat-sheet
54+
55+- Vertical gap between two rows of boxes: 85–95 px (two-line edge label + air).
56+- Horizontal gap between two linked boxes: 150–190 px.
57+- A return path routed around the boxes sits 50 px outside them (`y = box_bottom + 50`).
58+- Arrows stop 6 px short of the target and start 2 px outside the source: the renderer does it,
59+ do not compensate in the spec.
new file mode 100644
@@ -0,0 +1,59 @@
1+# House style
2+
3+## Palette
4+
5+The one from the Marp theme in the slide headers — do not introduce a colour.
6+
7+| token | value | where |
8+|---|---|---|
9+| ink | `#15202b` | dark boxes, borders, human-flow arrows and their labels |
10+| paper | `#fdfdfb` | white fills, text on dark, label backgrounds (`opacity 0.92`) |
11+| sage | `#e8ebda` | `accent` fills — the things handed to the model |
12+| blue | `#0062FF` | `intent` boxes, dashed arrows, their labels |
13+
14+Boxes: `rx=10`, `stroke-width=2`. Node text 17 px (15 px for `note`), edge labels 14 px,
15+Helvetica Neue / Helvetica / Arial. Every box except `box` and `note` is bold.
16+
17+## Sizing arithmetic
18+
19+The renderer does not measure text, so **you** size the boxes. Rough advance width per
20+character at 17 px: **9.4 px bold**, **8.6 px regular**. Rule of thumb:
21+
22+```
23+box width >= longest_line_chars * 9.4 + 40 (20 px of air on each side)
24+box height = lines * 21 + 45
25+```
26+
27+Edge labels get an opaque background rect of `chars * 7.6 + 14` wide and `lines * 17 + 8` high,
28+centred on the longest segment of the route. So:
29+
30+- leave a **gap of at least `label_width + 40`** between two boxes an edge links, or the label
31+ swallows the whole arrow;
32+- two labels on parallel edges must not overlap horizontally — that is what `fromAt` / `toAt`
33+ are for (put one vertical edge at 1/3 of the width, the other at 2/3).
34+
35+When a line still overflows after widening, rewrite it shorter. Three lines of six words each
36+beat one line of eighteen on a slide seen from row twelve.
37+
38+## Standard layouts
39+
40+**The flow line** (`function-calling.json`): User → LLM → tool, left to right at one height,
41+return paths routed above (tool → LLM, `y` above the boxes) and below (LLM → user), both via
42+`waypoints`. Best when the story is a loop.
43+
44+**The T** (`tool-detection.json`): the same left-to-right line, with what the model *consults*
45+sitting above the LLM and linked by two short vertical edges — one down ("sent with the
46+prompt"), one dashed up ("compared against"). Best when the point is *what the model reads*
47+before it answers. Keep the two vertical edges at different `x`, and reserve ~90 px of vertical
48+gap for their labels.
49+
50+Common to both: a `note` band at the bottom, full width, holding the single sentence the slide
51+exists for. Canvas margin 20 px all round.
52+
53+## Geometry cheat-sheet
54+
55+- Vertical gap between two rows of boxes: 85–95 px (two-line edge label + air).
56+- Horizontal gap between two linked boxes: 150–190 px.
57+- A return path routed around the boxes sits 50 px outside them (`y = box_bottom + 50`).
58+- Arrows stop 6 px short of the target and start 2 px outside the source: the renderer does it,
59+ do not compensate in the spec.
added .claude/skills/slide-diagram/scripts/make-diagram.mjs +237 -0
new file mode 100644
@@ -0,0 +1,237 @@
1+#!/usr/bin/env node
2+// Renders a diagram spec (JSON) into a draw.io-editable SVG.
3+//
4+// node make-diagram.mjs tool-detection.json
5+//
6+// The output is a plain SVG (readable anywhere, embeddable in Marp) whose
7+// `content` attribute carries the mxfile source, so draw.io can reopen and
8+// edit it.
9+import { readFileSync, writeFileSync } from "node:fs";
10+
11+const FONT = "Helvetica Neue, Helvetica, Arial, sans-serif";
12+const NODE_FONT = 17;
13+const EDGE_FONT = 14;
14+
15+// The palette is the one from the slide theme.
16+const KINDS = {
17+ box: { fill: "#fdfdfb", stroke: "#15202b", font: "#15202b", bold: false },
18+ dark: { fill: "#15202b", stroke: "#15202b", font: "#fdfdfb", bold: true },
19+ accent: { fill: "#e8ebda", stroke: "#15202b", font: "#15202b", bold: true },
20+ intent: { fill: "#fdfdfb", stroke: "#0062FF", font: "#0062FF", bold: true },
21+ note: { fill: "#fdfdfb", stroke: "#15202b", font: "#15202b", bold: false, dashed: true, fontSize: 15 },
22+};
23+
24+const spec = JSON.parse(readFileSync(process.argv[2], "utf8"));
25+const byId = Object.fromEntries(spec.nodes.map((n) => [n.id, n]));
26+const lines = (label) => label.split("\\n");
27+
28+// --- geometry ----------------------------------------------------------------
29+
30+const center = (n) => ({ x: n.x + n.w / 2, y: n.y + n.h / 2 });
31+
32+// An explicit side ("top" | "bottom" | "left" | "right") with an optional
33+// absolute coordinate along it, when the automatic anchor picks the wrong edge
34+// (two boxes stacked and linked twice, for instance).
35+function sideAnchor(node, side, at, out) {
36+ switch (side) {
37+ case "top": return { x: at ?? node.x + node.w / 2, y: node.y - out };
38+ case "bottom": return { x: at ?? node.x + node.w / 2, y: node.y + node.h + out };
39+ case "left": return { x: node.x - out, y: at ?? node.y + node.h / 2 };
40+ default: return { x: node.x + node.w + out, y: at ?? node.y + node.h / 2 };
41+ }
42+}
43+
44+// Where an edge leaves/enters a box. `out` is the outward gap: 2px on the
45+// source side, 6px on the target side so the arrow head does not touch.
46+function anchor(node, toward, out) {
47+ const c = center(node);
48+ const dx = toward.x - c.x;
49+ const dy = toward.y - c.y;
50+ if (Math.abs(dx) >= Math.abs(dy)) {
51+ return dx >= 0
52+ ? { x: node.x + node.w + out, y: c.y }
53+ : { x: node.x - out, y: c.y };
54+ }
55+ return dy >= 0
56+ ? { x: c.x, y: node.y + node.h + out }
57+ : { x: c.x, y: node.y - out };
58+}
59+
60+function route(edge) {
61+ const src = byId[edge.from];
62+ const tgt = byId[edge.to];
63+ const wps = edge.waypoints ?? [];
64+ const first = wps[0] ?? center(tgt);
65+ const last = wps[wps.length - 1] ?? center(src);
66+ const start = edge.fromSide
67+ ? sideAnchor(src, edge.fromSide, edge.fromAt, 2)
68+ : anchor(src, first, 2);
69+ const end = edge.toSide
70+ ? sideAnchor(tgt, edge.toSide, edge.toAt, 6)
71+ : anchor(tgt, last, 6);
72+ // Keep the orthogonal legs square when a waypoint dictates the axis.
73+ const pts = [start, ...wps.map((p) => ({ ...p })), end];
74+ if (wps.length === 0 && !edge.fromSide && !edge.toSide) {
75+ if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) end.y = start.y;
76+ else end.x = start.x;
77+ }
78+ return pts;
79+}
80+
81+// The label sits on the longest segment of the route.
82+function labelPoint(pts) {
83+ let best = null;
84+ let bestLen = -1;
85+ for (let i = 0; i < pts.length - 1; i++) {
86+ const len = Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y);
87+ if (len > bestLen) {
88+ bestLen = len;
89+ best = { x: (pts[i].x + pts[i + 1].x) / 2, y: (pts[i].y + pts[i + 1].y) / 2 };
90+ }
91+ }
92+ return best;
93+}
94+
95+// --- escaping ----------------------------------------------------------------
96+
97+const xmlText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
98+const xmlAttr = (s) =>
99+ s
100+ .replace(/&/g, "&amp;")
101+ .replace(/</g, "&lt;")
102+ .replace(/>/g, "&gt;")
103+ .replace(/"/g, "&quot;")
104+ // Line breaks travel as an entity draw.io still sees after the SVG
105+ // `content` attribute has been decoded once, hence the extra level.
106+ .replace(/\\n/g, "&amp;#10;");
107+
108+// --- mxfile (what draw.io reopens) -------------------------------------------
109+
110+// The same side hint, expressed the way draw.io stores it.
111+function sideStyle(prefix, node, side, at) {
112+ if (!side) return "";
113+ const along = { top: [null, 0], bottom: [null, 1], left: [0, null], right: [1, null] }[side];
114+ const x = along[0] ?? (at === undefined ? 0.5 : (at - node.x) / node.w);
115+ const y = along[1] ?? (at === undefined ? 0.5 : (at - node.y) / node.h);
116+ return `${prefix}X=${x};${prefix}Y=${y};${prefix}Dx=0;${prefix}Dy=0;`;
117+}
118+
119+function mxfile() {
120+ const cells = [];
121+ let id = 3;
122+ const ids = {};
123+
124+ for (const n of spec.nodes) {
125+ const k = KINDS[n.kind];
126+ ids[n.id] = id;
127+ const style =
128+ `rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=${k.fill};strokeColor=${k.stroke};` +
129+ `fontColor=${k.font};fontSize=${k.fontSize ?? NODE_FONT};fontStyle=${k.bold ? 1 : 0};` +
130+ `strokeWidth=2;${k.dashed ? "dashed=1;" : ""}`;
131+ cells.push(
132+ `<mxCell id="${id}" value="${xmlAttr(n.label)}" style="${xmlAttr(style)}" vertex="1" parent="1">` +
133+ `<mxGeometry x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" as="geometry"/></mxCell>`
134+ );
135+ id++;
136+ }
137+
138+ for (const e of spec.edges) {
139+ const color = e.dashed ? "#0062FF" : "#15202b";
140+ const style =
141+ `edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=${color};` +
142+ `fontSize=${EDGE_FONT};fontColor=${color};${e.dashed ? "dashed=1;" : ""}` +
143+ sideStyle("exit", byId[e.from], e.fromSide, e.fromAt) +
144+ sideStyle("entry", byId[e.to], e.toSide, e.toAt);
145+ const pts = (e.waypoints ?? [])
146+ .map((p) => `<mxPoint x="${p.x}" y="${p.y}"/>`)
147+ .join("");
148+ cells.push(
149+ `<mxCell id="${id}" value="${xmlAttr(e.label ?? "")}" style="${xmlAttr(style)}" edge="1" parent="1" ` +
150+ `source="${ids[e.from]}" target="${ids[e.to]}"><mxGeometry relative="1" as="geometry">` +
151+ (pts ? `<Array as="points">${pts}</Array>` : "") +
152+ `</mxGeometry></mxCell>`
153+ );
154+ id++;
155+ }
156+
157+ return (
158+ `<mxfile host="Electron" agent="make-diagram.mjs" type="device">` +
159+ `<diagram name="${xmlAttr(spec.name)}" id="diagram-1">` +
160+ `<mxGraphModel dx="1000" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" ` +
161+ `arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">` +
162+ `<root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells.join("")}</root>` +
163+ `</mxGraphModel></diagram></mxfile>`
164+ );
165+}
166+
167+// --- svg ---------------------------------------------------------------------
168+
169+const out = [];
170+out.push(
171+ `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ` +
172+ `width="${spec.width}" height="${spec.height}" viewBox="0 0 ${spec.width} ${spec.height}" ` +
173+ `content="${xmlAttr(mxfile())}">`
174+);
175+out.push(
176+ ` <defs>` +
177+ `<marker id="arrow-dark" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">` +
178+ `<path d="M 0 0 L 10 5 L 0 10 z" fill="#15202b"/></marker>` +
179+ `<marker id="arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">` +
180+ `<path d="M 0 0 L 10 5 L 0 10 z" fill="#0062FF"/></marker>` +
181+ `</defs>`
182+);
183+
184+// Edges first, so the boxes paint over their tails.
185+for (const e of spec.edges) {
186+ const pts = route(e);
187+ const color = e.dashed ? "#0062FF" : "#15202b";
188+ const marker = e.dashed ? "arrow-blue" : "arrow-dark";
189+ const d = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
190+ out.push(
191+ ` <path d="${d}" fill="none" stroke="${color}" stroke-width="2"` +
192+ (e.dashed ? ` stroke-dasharray="6 5"` : "") +
193+ ` marker-end="url(#${marker})"/>`
194+ );
195+ if (!e.label) continue;
196+ const ls = lines(e.label);
197+ const p = labelPoint(pts);
198+ const w = Math.max(...ls.map((l) => l.length)) * 7.6 + 14;
199+ const h = ls.length * 17 + 8;
200+ out.push(
201+ ` <rect x="${p.x - w / 2}" y="${p.y - h / 2}" width="${w}" height="${h}" rx="4" fill="#fdfdfb" opacity="0.92"/>`
202+ );
203+ ls.forEach((l, i) => {
204+ const y = p.y + (i - (ls.length - 1) / 2) * 17;
205+ out.push(
206+ ` <text x="${p.x}" y="${y}" text-anchor="middle" dominant-baseline="central" ` +
207+ `font-family="${FONT}" font-size="${EDGE_FONT}" fill="${color}">${xmlText(l)}</text>`
208+ );
209+ });
210+}
211+
212+for (const n of spec.nodes) {
213+ const k = KINDS[n.kind];
214+ const size = k.fontSize ?? NODE_FONT;
215+ out.push(
216+ ` <rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="10" ry="10" ` +
217+ `fill="${k.fill}" stroke="${k.stroke}" stroke-width="2"` +
218+ (k.dashed ? ` stroke-dasharray="6 5"` : "") +
219+ `/>`
220+ );
221+ const ls = lines(n.label);
222+ const c = center(n);
223+ ls.forEach((l, i) => {
224+ const y = c.y + (i - (ls.length - 1) / 2) * (size + 4);
225+ out.push(
226+ ` <text x="${c.x}" y="${y}" text-anchor="middle" dominant-baseline="central" ` +
227+ `font-family="${FONT}" font-size="${size}" font-weight="${k.bold ? "bold" : "normal"}" ` +
228+ `fill="${k.font}">${xmlText(l)}</text>`
229+ );
230+ });
231+}
232+
233+out.push(`</svg>`);
234+
235+const target = process.argv[2].replace(/\.json$/, ".drawio.svg");
236+writeFileSync(target, out.join("\n") + "\n");
237+console.log(`wrote ${target}`);
new file mode 100644
@@ -0,0 +1,237 @@
1+#!/usr/bin/env node
2+// Renders a diagram spec (JSON) into a draw.io-editable SVG.
3+//
4+// node make-diagram.mjs tool-detection.json
5+//
6+// The output is a plain SVG (readable anywhere, embeddable in Marp) whose
7+// `content` attribute carries the mxfile source, so draw.io can reopen and
8+// edit it.
9+import { readFileSync, writeFileSync } from "node:fs";
10+
11+const FONT = "Helvetica Neue, Helvetica, Arial, sans-serif";
12+const NODE_FONT = 17;
13+const EDGE_FONT = 14;
14+
15+// The palette is the one from the slide theme.
16+const KINDS = {
17+ box: { fill: "#fdfdfb", stroke: "#15202b", font: "#15202b", bold: false },
18+ dark: { fill: "#15202b", stroke: "#15202b", font: "#fdfdfb", bold: true },
19+ accent: { fill: "#e8ebda", stroke: "#15202b", font: "#15202b", bold: true },
20+ intent: { fill: "#fdfdfb", stroke: "#0062FF", font: "#0062FF", bold: true },
21+ note: { fill: "#fdfdfb", stroke: "#15202b", font: "#15202b", bold: false, dashed: true, fontSize: 15 },
22+};
23+
24+const spec = JSON.parse(readFileSync(process.argv[2], "utf8"));
25+const byId = Object.fromEntries(spec.nodes.map((n) => [n.id, n]));
26+const lines = (label) => label.split("\\n");
27+
28+// --- geometry ----------------------------------------------------------------
29+
30+const center = (n) => ({ x: n.x + n.w / 2, y: n.y + n.h / 2 });
31+
32+// An explicit side ("top" | "bottom" | "left" | "right") with an optional
33+// absolute coordinate along it, when the automatic anchor picks the wrong edge
34+// (two boxes stacked and linked twice, for instance).
35+function sideAnchor(node, side, at, out) {
36+ switch (side) {
37+ case "top": return { x: at ?? node.x + node.w / 2, y: node.y - out };
38+ case "bottom": return { x: at ?? node.x + node.w / 2, y: node.y + node.h + out };
39+ case "left": return { x: node.x - out, y: at ?? node.y + node.h / 2 };
40+ default: return { x: node.x + node.w + out, y: at ?? node.y + node.h / 2 };
41+ }
42+}
43+
44+// Where an edge leaves/enters a box. `out` is the outward gap: 2px on the
45+// source side, 6px on the target side so the arrow head does not touch.
46+function anchor(node, toward, out) {
47+ const c = center(node);
48+ const dx = toward.x - c.x;
49+ const dy = toward.y - c.y;
50+ if (Math.abs(dx) >= Math.abs(dy)) {
51+ return dx >= 0
52+ ? { x: node.x + node.w + out, y: c.y }
53+ : { x: node.x - out, y: c.y };
54+ }
55+ return dy >= 0
56+ ? { x: c.x, y: node.y + node.h + out }
57+ : { x: c.x, y: node.y - out };
58+}
59+
60+function route(edge) {
61+ const src = byId[edge.from];
62+ const tgt = byId[edge.to];
63+ const wps = edge.waypoints ?? [];
64+ const first = wps[0] ?? center(tgt);
65+ const last = wps[wps.length - 1] ?? center(src);
66+ const start = edge.fromSide
67+ ? sideAnchor(src, edge.fromSide, edge.fromAt, 2)
68+ : anchor(src, first, 2);
69+ const end = edge.toSide
70+ ? sideAnchor(tgt, edge.toSide, edge.toAt, 6)
71+ : anchor(tgt, last, 6);
72+ // Keep the orthogonal legs square when a waypoint dictates the axis.
73+ const pts = [start, ...wps.map((p) => ({ ...p })), end];
74+ if (wps.length === 0 && !edge.fromSide && !edge.toSide) {
75+ if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) end.y = start.y;
76+ else end.x = start.x;
77+ }
78+ return pts;
79+}
80+
81+// The label sits on the longest segment of the route.
82+function labelPoint(pts) {
83+ let best = null;
84+ let bestLen = -1;
85+ for (let i = 0; i < pts.length - 1; i++) {
86+ const len = Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y);
87+ if (len > bestLen) {
88+ bestLen = len;
89+ best = { x: (pts[i].x + pts[i + 1].x) / 2, y: (pts[i].y + pts[i + 1].y) / 2 };
90+ }
91+ }
92+ return best;
93+}
94+
95+// --- escaping ----------------------------------------------------------------
96+
97+const xmlText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
98+const xmlAttr = (s) =>
99+ s
100+ .replace(/&/g, "&amp;")
101+ .replace(/</g, "&lt;")
102+ .replace(/>/g, "&gt;")
103+ .replace(/"/g, "&quot;")
104+ // Line breaks travel as an entity draw.io still sees after the SVG
105+ // `content` attribute has been decoded once, hence the extra level.
106+ .replace(/\\n/g, "&amp;#10;");
107+
108+// --- mxfile (what draw.io reopens) -------------------------------------------
109+
110+// The same side hint, expressed the way draw.io stores it.
111+function sideStyle(prefix, node, side, at) {
112+ if (!side) return "";
113+ const along = { top: [null, 0], bottom: [null, 1], left: [0, null], right: [1, null] }[side];
114+ const x = along[0] ?? (at === undefined ? 0.5 : (at - node.x) / node.w);
115+ const y = along[1] ?? (at === undefined ? 0.5 : (at - node.y) / node.h);
116+ return `${prefix}X=${x};${prefix}Y=${y};${prefix}Dx=0;${prefix}Dy=0;`;
117+}
118+
119+function mxfile() {
120+ const cells = [];
121+ let id = 3;
122+ const ids = {};
123+
124+ for (const n of spec.nodes) {
125+ const k = KINDS[n.kind];
126+ ids[n.id] = id;
127+ const style =
128+ `rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=${k.fill};strokeColor=${k.stroke};` +
129+ `fontColor=${k.font};fontSize=${k.fontSize ?? NODE_FONT};fontStyle=${k.bold ? 1 : 0};` +
130+ `strokeWidth=2;${k.dashed ? "dashed=1;" : ""}`;
131+ cells.push(
132+ `<mxCell id="${id}" value="${xmlAttr(n.label)}" style="${xmlAttr(style)}" vertex="1" parent="1">` +
133+ `<mxGeometry x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" as="geometry"/></mxCell>`
134+ );
135+ id++;
136+ }
137+
138+ for (const e of spec.edges) {
139+ const color = e.dashed ? "#0062FF" : "#15202b";
140+ const style =
141+ `edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=${color};` +
142+ `fontSize=${EDGE_FONT};fontColor=${color};${e.dashed ? "dashed=1;" : ""}` +
143+ sideStyle("exit", byId[e.from], e.fromSide, e.fromAt) +
144+ sideStyle("entry", byId[e.to], e.toSide, e.toAt);
145+ const pts = (e.waypoints ?? [])
146+ .map((p) => `<mxPoint x="${p.x}" y="${p.y}"/>`)
147+ .join("");
148+ cells.push(
149+ `<mxCell id="${id}" value="${xmlAttr(e.label ?? "")}" style="${xmlAttr(style)}" edge="1" parent="1" ` +
150+ `source="${ids[e.from]}" target="${ids[e.to]}"><mxGeometry relative="1" as="geometry">` +
151+ (pts ? `<Array as="points">${pts}</Array>` : "") +
152+ `</mxGeometry></mxCell>`
153+ );
154+ id++;
155+ }
156+
157+ return (
158+ `<mxfile host="Electron" agent="make-diagram.mjs" type="device">` +
159+ `<diagram name="${xmlAttr(spec.name)}" id="diagram-1">` +
160+ `<mxGraphModel dx="1000" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" ` +
161+ `arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">` +
162+ `<root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells.join("")}</root>` +
163+ `</mxGraphModel></diagram></mxfile>`
164+ );
165+}
166+
167+// --- svg ---------------------------------------------------------------------
168+
169+const out = [];
170+out.push(
171+ `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ` +
172+ `width="${spec.width}" height="${spec.height}" viewBox="0 0 ${spec.width} ${spec.height}" ` +
173+ `content="${xmlAttr(mxfile())}">`
174+);
175+out.push(
176+ ` <defs>` +
177+ `<marker id="arrow-dark" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">` +
178+ `<path d="M 0 0 L 10 5 L 0 10 z" fill="#15202b"/></marker>` +
179+ `<marker id="arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">` +
180+ `<path d="M 0 0 L 10 5 L 0 10 z" fill="#0062FF"/></marker>` +
181+ `</defs>`
182+);
183+
184+// Edges first, so the boxes paint over their tails.
185+for (const e of spec.edges) {
186+ const pts = route(e);
187+ const color = e.dashed ? "#0062FF" : "#15202b";
188+ const marker = e.dashed ? "arrow-blue" : "arrow-dark";
189+ const d = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
190+ out.push(
191+ ` <path d="${d}" fill="none" stroke="${color}" stroke-width="2"` +
192+ (e.dashed ? ` stroke-dasharray="6 5"` : "") +
193+ ` marker-end="url(#${marker})"/>`
194+ );
195+ if (!e.label) continue;
196+ const ls = lines(e.label);
197+ const p = labelPoint(pts);
198+ const w = Math.max(...ls.map((l) => l.length)) * 7.6 + 14;
199+ const h = ls.length * 17 + 8;
200+ out.push(
201+ ` <rect x="${p.x - w / 2}" y="${p.y - h / 2}" width="${w}" height="${h}" rx="4" fill="#fdfdfb" opacity="0.92"/>`
202+ );
203+ ls.forEach((l, i) => {
204+ const y = p.y + (i - (ls.length - 1) / 2) * 17;
205+ out.push(
206+ ` <text x="${p.x}" y="${y}" text-anchor="middle" dominant-baseline="central" ` +
207+ `font-family="${FONT}" font-size="${EDGE_FONT}" fill="${color}">${xmlText(l)}</text>`
208+ );
209+ });
210+}
211+
212+for (const n of spec.nodes) {
213+ const k = KINDS[n.kind];
214+ const size = k.fontSize ?? NODE_FONT;
215+ out.push(
216+ ` <rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="10" ry="10" ` +
217+ `fill="${k.fill}" stroke="${k.stroke}" stroke-width="2"` +
218+ (k.dashed ? ` stroke-dasharray="6 5"` : "") +
219+ `/>`
220+ );
221+ const ls = lines(n.label);
222+ const c = center(n);
223+ ls.forEach((l, i) => {
224+ const y = c.y + (i - (ls.length - 1) / 2) * (size + 4);
225+ out.push(
226+ ` <text x="${c.x}" y="${y}" text-anchor="middle" dominant-baseline="central" ` +
227+ `font-family="${FONT}" font-size="${size}" font-weight="${k.bold ? "bold" : "normal"}" ` +
228+ `fill="${k.font}">${xmlText(l)}</text>`
229+ );
230+ });
231+}
232+
233+out.push(`</svg>`);
234+
235+const target = process.argv[2].replace(/\.json$/, ".drawio.svg");
236+writeFileSync(target, out.join("\n") + "\n");
237+console.log(`wrote ${target}`);
added .dockerignore +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+# Keep the template build context small: only sources needed by the build.
2+sandboxes/
3+.git/
4+ui/node_modules/
5+ui/dist/
6+ui/public/material-icons/
7+bin/
8+tmp/
9+.memory/
10+.quality/
11+.qlty/
12+docs/
13+kits/
14+.tickets/
15+.claude/
16+.vscode/
17+*.log
new file mode 100644
@@ -0,0 +1,17 @@
1+# Keep the template build context small: only sources needed by the build.
2+sandboxes/
3+.git/
4+ui/node_modules/
5+ui/dist/
6+ui/public/material-icons/
7+bin/
8+tmp/
9+.memory/
10+.quality/
11+.qlty/
12+docs/
13+kits/
14+.tickets/
15+.claude/
16+.vscode/
17+*.log
modified .gitignore +15 -0
@@ -1,12 +1,27 @@
11 # Go
22 /bin/
33 *.test
4+bin
45
56 # Node / Vite
67 node_modules/
78 ui/dist/*
89 !ui/dist/.gitkeep
10+# copied from vscode-material-icons by ui/scripts/copy-icons.mjs
11+ui/public/material-icons/
912
1013 # Editors & OS
1114 .DS_Store
1215 *.log
16+sandboxes
17+
18+# Tools
19+.qlty
20+
21+# ori-desktop (Wails app, separate Go module)
22+ori-desktop/build/bin/
23+ori-desktop/build/windows/installer/wails_tools.nsh
24+ori-desktop/build/windows/installer/tmp/
25+ori-desktop/frontend/wailsjs/
26+ori-desktop/frontend/node_modules/
27+ori-desktop/frontend/dist/
@@ -1,12 +1,27 @@
1 # Go1 # Go
2 /bin/2 /bin/
3 *.test3 *.test
4+bin
4 5
5 # Node / Vite6 # Node / Vite
6 node_modules/7 node_modules/
7 ui/dist/*8 ui/dist/*
8 !ui/dist/.gitkeep9 !ui/dist/.gitkeep
10+# copied from vscode-material-icons by ui/scripts/copy-icons.mjs
11+ui/public/material-icons/
9 12
10 # Editors & OS13 # Editors & OS
11 .DS_Store14 .DS_Store
12 *.log15 *.log
16+sandboxes
17+
18+# Tools
19+.qlty
20+
21+# ori-desktop (Wails app, separate Go module)
22+ori-desktop/build/bin/
23+ori-desktop/build/windows/installer/wails_tools.nsh
24+ori-desktop/build/windows/installer/tmp/
25+ori-desktop/frontend/wailsjs/
26+ori-desktop/frontend/node_modules/
27+ori-desktop/frontend/dist/
added .memory/README.md +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+# Project memory
2+
3+This folder is the durable record of the ori project for whoever continues building it (human or agent). It is committed to the repository and maintained at the end of every working session.
4+
5+| File | Nature | Rule |
6+| --- | --- | --- |
7+| `summary.md` | Living snapshot of the project's current state | Edited in place; only verified facts; never regenerated wholesale |
8+| `history.md` | Chronological log of sessions | Append-only; past entries are never rewritten |
9+| `handoffs/` | One note per session/topic: where work stopped, traps, next steps | New file per topic; never overwrite another session's handoff |
10+
11+`.memory/` is for whoever continues building the project; `docs/` is for whoever uses it.
new file mode 100644
@@ -0,0 +1,11 @@
1+# Project memory
2+
3+This folder is the durable record of the ori project for whoever continues building it (human or agent). It is committed to the repository and maintained at the end of every working session.
4+
5+| File | Nature | Rule |
6+| --- | --- | --- |
7+| `summary.md` | Living snapshot of the project's current state | Edited in place; only verified facts; never regenerated wholesale |
8+| `history.md` | Chronological log of sessions | Append-only; past entries are never rewritten |
9+| `handoffs/` | One note per session/topic: where work stopped, traps, next steps | New file per topic; never overwrite another session's handoff |
10+
11+`.memory/` is for whoever continues building the project; `docs/` is for whoever uses it.
added .memory/handoffs/2026-09-17-acp-webapp.md +56 -0
new file mode 100644
@@ -0,0 +1,56 @@
1+# Handoff — 2026-09-17 — ACP web client (ticket 0004)
2+
3+## State
4+
5+The full v1 works and is verified: `make run-mock` serves the panel on :8888 with the bundled demo agent (smoke-tested live over a real WebSocket: hello → prompt → thought/plan/tool calls → permission → turn_ended). All test suites green (`make test`, plus `go test -race`), quality gate PASS, bilingual docs written, `.memory/` created. Two commits on branch `feat/acp-webapp` (`main` holds only the initial repo state); a final commit with docs/memory/module-rename is proposed but not yet made — the user decides.
6+
7+## In flight — READ THIS FIRST (updated at session end)
8+
9+**Open bug: the UI shows "offline" in the user's real sbx sandbox** (`sbx create claude . --template k33g/ori:0.0.0 --kit ./kits/ori -p 5555:8888`; page loads, so ori runs and HTTP forwards — the /ws WebSocket is what fails or drops). Already established:
10+
11+- ori's WS origin handling is CORRECT: curl handshakes against the image give 101 for `Origin: http://localhost:5555` (and any localhost:*), 403 for foreign origins. Node-based smoke tests send no Origin header, so they never exercised this.
12+- sbx port publishing is raw L4 (gvisor MapPorts in `sandboxes/sandboxd/pkg/server/backend_dockernext.go`), so the upgrade should traverse.
13+- Three discriminating diagnostics were given to the user, ANSWERS PENDING: (1) `sbx exec ori tail -20 /home/agent/.ori.log` (an origin rejection logs "websocket accept failed"); (2) curl WS handshake from INSIDE the sandbox to :8888/ws; (3) same from the host through :5555. 101/101 → look at the browser console; inside-101/forward-not-101 → the forward blocks upgrades; inside-403 → origin, ask the exact browser URL. If (1) shows nothing at all, check `/var/log/sbx-kit-startup.log`.
14+- Possible follow-up angle if 101/101: default publish protocol is tcp4 (IPv4-only host listener) while browsers try ::1 first for localhost — page fallback works, some WS stacks may not; retry publishing with explicit `/tcp` (dual-stack) or have the browser use http://127.0.0.1:5555.
15+
16+## In flight (rest)
17+
18+Nothing half-written. The final commit of docs + `.memory/` + the module rename is the only pending mechanical action.
19+
20+## Workspace panel (added later in the session)
21+
22+The user requested and approved a workspace evolution, delivered complete: `internal/files` + `internal/terminal` on the backend; file tree, Monaco preview/editor, Markdown/AsciiDoc rendering, xterm.js terminal on the front; `execute` tool output in monospace. Gate PASS again, 46 Go + 66 front tests. Server redeployed live with it. New traps recorded in "Watch out for".
23+
24+## Next steps
25+
26+1. Let the user try the UI in a real browser (in the sandbox this needs `sbx ports <sandbox> --publish 8888:8888/tcp` on the host; the server already binds `0.0.0.0`).
27+2. Try against real Claude Code (`make run`) — needs claude auth in the environment; never exercised here.
28+3. Tickets 0002 (architecture diagram — largely satisfied by `docs/diagrams/packages.drawio`, consider closing) and 0003 (how to run outside the sandbox — the docs' how-to pages are most of the answer).
29+4. Natural evolutions, in design order: render non-text content blocks (images/resources), session modes (`current_mode_update` is currently ignored), multi-session/tabs via the `bridge.Prompter` seam, file preview / file list panes (the ticket names them as future needs).
30+
31+## Open questions / blockers
32+
33+- No git remote is configured; where this repo is hosted (rickub.com?) is unknown here.
34+- Whether `make run` works with the user's Claude auth has not been verified in this sandbox.
35+
36+## Verified against real Claude Code (later in the session)
37+
38+`make run` failed at first inside this sandbox: **Claude Code refuses to launch inside another Claude Code session** (the `CLAUDECODE` env var is inherited from the sandbox's own session). Workaround that works: `env -u CLAUDECODE ./bin/ori`. With that, the full chain was verified live: real session created (UUID id), prompt over WebSocket, streamed answer, `turn_ended end_turn`. Decision pending: whether to strip `CLAUDECODE` in `spawnProcess`, in the Makefile, or only document it. Also noted: the adapter package was renamed `@zed-industries/claude-code-acp``@agentclientprotocol/claude-agent-acp` (old name still works, deprecated) — consider updating `config.DefaultAgentCommand`.
39+
40+## Watch out for
41+
42+- **The adapter inherits the user's `~/.claude/settings.json` default model.** A `/model` toggle in another session saved `"claude-fable-5[1m]"` (the 1M-context variant), which the adapter's session cannot access → every prompt failed with -32603 "issue with the selected model (fable[1m])". Fix in force: launch ori with `ANTHROPIC_MODEL=claude-fable-5` (plain id) so the adapter is decoupled from the UI's saved default.
43+
44+- **`~/.npm` is a full 488MB dedicated volume** — npm installs of big packages fail with ENOSPC; the cache now lives at `~/.npm-big` via `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`.
45+- **monaco-editor 0.56 exports map**: import workers as `monaco-editor/editor/editor.worker.js?worker` (no `esm/vs/` prefix — the map adds it); `?worker` imports need `"vite/client"` in tsconfig types.
46+- **biome 1.9 suppressions**: `lint/a11y/noStaticElementInteractions` does not exist in 1.9 (suppression = parse error); a suppression for a JSX attribute must sit on the line directly above that attribute, inside the opening tag.
47+- **@asciidoctor/core v4 API**: `convert` is a NAMED export and returns a **Promise** — there is no default export and no `asciidoctor()` factory (that was v3). Never mock this module in the only test covering it: the real-render test in `AsciiDocView.test.tsx` exists precisely because a mocked test let a production breakage through.
48+- **Material icons**: served from `/material-icons` (copied from `vscode-material-icons` into `ui/public/` by `ui/scripts/copy-icons.mjs`, hooked on npm `predev`/`prebuild`, gitignored). A fresh checkout gets them on the first `make build`/`make dev`.
49+
50+- **Module path is `rickub.com/bots-garden/ori`** — do not "fix" it back to github; the user explicitly corrected this mid-session.
51+- `~/.qlty` is a dedicated 488MB volume; the qlty tool cache lives at `~/.qlty-cache` on the main disk behind a symlink. If that symlink disappears (sandbox recreation), qlty installs will fail with ENOSPC again; recreate it before running the quality skill.
52+- The coder acp-go-sdk does NOT auto-inject the connection into an agent implementation: call `SetAgentConnection` manually (bit us once; the mock in `internal/agent/agent_test.go` and `mockagent.Run` both do it).
53+- qlty's `return-statements` smell counts returns inside nested closures as part of the enclosing function — extract named functions, don't fight it with inline funcs.
54+- `qlty fmt` (biome) reformats TS/CSS/JSON to tabs; run it rather than hand-formatting.
55+- Frontend tests: jsdom lacks `scrollIntoView` (call it optionally) and store dispatches outside React need `act()`.
56+- Vitest is pinned to major 3 in `ui/package.json` (`^3.0.0`) — fine today, but remember it's a floor choice made for install determinism, not a hard requirement.
new file mode 100644
@@ -0,0 +1,56 @@
1+# Handoff — 2026-09-17 — ACP web client (ticket 0004)
2+
3+## State
4+
5+The full v1 works and is verified: `make run-mock` serves the panel on :8888 with the bundled demo agent (smoke-tested live over a real WebSocket: hello → prompt → thought/plan/tool calls → permission → turn_ended). All test suites green (`make test`, plus `go test -race`), quality gate PASS, bilingual docs written, `.memory/` created. Two commits on branch `feat/acp-webapp` (`main` holds only the initial repo state); a final commit with docs/memory/module-rename is proposed but not yet made — the user decides.
6+
7+## In flight — READ THIS FIRST (updated at session end)
8+
9+**Open bug: the UI shows "offline" in the user's real sbx sandbox** (`sbx create claude . --template k33g/ori:0.0.0 --kit ./kits/ori -p 5555:8888`; page loads, so ori runs and HTTP forwards — the /ws WebSocket is what fails or drops). Already established:
10+
11+- ori's WS origin handling is CORRECT: curl handshakes against the image give 101 for `Origin: http://localhost:5555` (and any localhost:*), 403 for foreign origins. Node-based smoke tests send no Origin header, so they never exercised this.
12+- sbx port publishing is raw L4 (gvisor MapPorts in `sandboxes/sandboxd/pkg/server/backend_dockernext.go`), so the upgrade should traverse.
13+- Three discriminating diagnostics were given to the user, ANSWERS PENDING: (1) `sbx exec ori tail -20 /home/agent/.ori.log` (an origin rejection logs "websocket accept failed"); (2) curl WS handshake from INSIDE the sandbox to :8888/ws; (3) same from the host through :5555. 101/101 → look at the browser console; inside-101/forward-not-101 → the forward blocks upgrades; inside-403 → origin, ask the exact browser URL. If (1) shows nothing at all, check `/var/log/sbx-kit-startup.log`.
14+- Possible follow-up angle if 101/101: default publish protocol is tcp4 (IPv4-only host listener) while browsers try ::1 first for localhost — page fallback works, some WS stacks may not; retry publishing with explicit `/tcp` (dual-stack) or have the browser use http://127.0.0.1:5555.
15+
16+## In flight (rest)
17+
18+Nothing half-written. The final commit of docs + `.memory/` + the module rename is the only pending mechanical action.
19+
20+## Workspace panel (added later in the session)
21+
22+The user requested and approved a workspace evolution, delivered complete: `internal/files` + `internal/terminal` on the backend; file tree, Monaco preview/editor, Markdown/AsciiDoc rendering, xterm.js terminal on the front; `execute` tool output in monospace. Gate PASS again, 46 Go + 66 front tests. Server redeployed live with it. New traps recorded in "Watch out for".
23+
24+## Next steps
25+
26+1. Let the user try the UI in a real browser (in the sandbox this needs `sbx ports <sandbox> --publish 8888:8888/tcp` on the host; the server already binds `0.0.0.0`).
27+2. Try against real Claude Code (`make run`) — needs claude auth in the environment; never exercised here.
28+3. Tickets 0002 (architecture diagram — largely satisfied by `docs/diagrams/packages.drawio`, consider closing) and 0003 (how to run outside the sandbox — the docs' how-to pages are most of the answer).
29+4. Natural evolutions, in design order: render non-text content blocks (images/resources), session modes (`current_mode_update` is currently ignored), multi-session/tabs via the `bridge.Prompter` seam, file preview / file list panes (the ticket names them as future needs).
30+
31+## Open questions / blockers
32+
33+- No git remote is configured; where this repo is hosted (rickub.com?) is unknown here.
34+- Whether `make run` works with the user's Claude auth has not been verified in this sandbox.
35+
36+## Verified against real Claude Code (later in the session)
37+
38+`make run` failed at first inside this sandbox: **Claude Code refuses to launch inside another Claude Code session** (the `CLAUDECODE` env var is inherited from the sandbox's own session). Workaround that works: `env -u CLAUDECODE ./bin/ori`. With that, the full chain was verified live: real session created (UUID id), prompt over WebSocket, streamed answer, `turn_ended end_turn`. Decision pending: whether to strip `CLAUDECODE` in `spawnProcess`, in the Makefile, or only document it. Also noted: the adapter package was renamed `@zed-industries/claude-code-acp``@agentclientprotocol/claude-agent-acp` (old name still works, deprecated) — consider updating `config.DefaultAgentCommand`.
39+
40+## Watch out for
41+
42+- **The adapter inherits the user's `~/.claude/settings.json` default model.** A `/model` toggle in another session saved `"claude-fable-5[1m]"` (the 1M-context variant), which the adapter's session cannot access → every prompt failed with -32603 "issue with the selected model (fable[1m])". Fix in force: launch ori with `ANTHROPIC_MODEL=claude-fable-5` (plain id) so the adapter is decoupled from the UI's saved default.
43+
44+- **`~/.npm` is a full 488MB dedicated volume** — npm installs of big packages fail with ENOSPC; the cache now lives at `~/.npm-big` via `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`.
45+- **monaco-editor 0.56 exports map**: import workers as `monaco-editor/editor/editor.worker.js?worker` (no `esm/vs/` prefix — the map adds it); `?worker` imports need `"vite/client"` in tsconfig types.
46+- **biome 1.9 suppressions**: `lint/a11y/noStaticElementInteractions` does not exist in 1.9 (suppression = parse error); a suppression for a JSX attribute must sit on the line directly above that attribute, inside the opening tag.
47+- **@asciidoctor/core v4 API**: `convert` is a NAMED export and returns a **Promise** — there is no default export and no `asciidoctor()` factory (that was v3). Never mock this module in the only test covering it: the real-render test in `AsciiDocView.test.tsx` exists precisely because a mocked test let a production breakage through.
48+- **Material icons**: served from `/material-icons` (copied from `vscode-material-icons` into `ui/public/` by `ui/scripts/copy-icons.mjs`, hooked on npm `predev`/`prebuild`, gitignored). A fresh checkout gets them on the first `make build`/`make dev`.
49+
50+- **Module path is `rickub.com/bots-garden/ori`** — do not "fix" it back to github; the user explicitly corrected this mid-session.
51+- `~/.qlty` is a dedicated 488MB volume; the qlty tool cache lives at `~/.qlty-cache` on the main disk behind a symlink. If that symlink disappears (sandbox recreation), qlty installs will fail with ENOSPC again; recreate it before running the quality skill.
52+- The coder acp-go-sdk does NOT auto-inject the connection into an agent implementation: call `SetAgentConnection` manually (bit us once; the mock in `internal/agent/agent_test.go` and `mockagent.Run` both do it).
53+- qlty's `return-statements` smell counts returns inside nested closures as part of the enclosing function — extract named functions, don't fight it with inline funcs.
54+- `qlty fmt` (biome) reformats TS/CSS/JSON to tabs; run it rather than hand-formatting.
55+- Frontend tests: jsdom lacks `scrollIntoView` (call it optionally) and store dispatches outside React need `act()`.
56+- Vitest is pinned to major 3 in `ui/package.json` (`^3.0.0`) — fine today, but remember it's a floor choice made for install determinism, not a hard requirement.
added .memory/handoffs/2026-09-17-desktop-selectors-previews.md +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+# Handoff — 2026-09-17 — ori-desktop (Wails), @/skill selectors, draw.io + image previews
2+
3+## Where things stand
4+
5+- **ori-desktop** (`ori-desktop/`, own Go module, Wails v2.16.0): builds and launches on the user's Mac. Check reported "ori is reachable (status: ok)"; Connect appeared to do nothing → root cause was CSS specificity (`#connect-screen` beat `.screen[hidden]`), fixed in `frontend/src/main.css` + `index.html`. **The user has not yet rebuilt and retested.** Next unknown: does the `<iframe>` to `http://localhost:8888` render inside WKWebView from the `wails://` origin? If blank: `wails build -debug`, right-click → Inspect Element, read the console; candidates are ATS (`NSAllowsLocalNetworking` is already in `build/darwin/Info.plist`) or a mixed-content/frame policy. Fallback already wired: "Open in browser" (`runtime.BrowserOpenURL`).
6+- **Selectors + previews** in ori: implemented, tested (102 vitest, 9 Go pkgs, -race clean), documented EN+FR. Smoke-tested by riker with the real binary + mock agent (`make run-mock`: type `@` and `/` in the composer; the mock announces commands `review`/`compact` and echoes `[attached: name]`). Not yet tried against the real Claude adapter (v0.16.2 reportedly converts `resource_link` into a file reference — verified by reading its source, not by running it).
7+- **Quality gate not run**: qlty is not installed in this sandbox. Rerun `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` in a sandbox with the dev-toolkit kit before considering this work closed; expect possible findings in the new files (`internal/files/search.go`, `raw.go`, `internal/skills/`, `ui/src/mentions.ts`, `useCompletion.ts`, `CompletionPopup.tsx`, `PromptInput.tsx`, `ImageView.tsx`, `DrawioView.tsx`).
8+
9+## Traps learned
10+
11+- Wails CLI `go install` needs `CGO_ENABLED=0` where there is no gcc; Windows cross-build needs no C toolchain; macOS/Linux builds need Xcode / webkit2gtk. `wails build` writes `frontend/wailsjs/` and `build/bin/` — both gitignored.
12+- Wails "plain" frontend: never style a screen by `#id` when hiding relies on `[hidden]` — id specificity wins.
13+- biome `organizeImports` fires on many pre-existing files → evidently not part of the project gate; `useExhaustiveDependencies` rejects synthetic deps; `role="listbox"/"option"` on divs needs `tabIndex={-1}` plus a `useSemanticElements` suppression.
14+- `sandboxes/` (untracked sbx source checkout at the repo root) is walked by `/api/files/search`; consider gitignoring/removing it.
15+- The IssueSpec task-item schema is unknown here (no tool installed); tickets were filled with `body` text only (bob used `{text, done}` tasks in 0005 — unverified shape).
16+
17+## Next steps (user's decisions)
18+
19+1. Rebuild ori-desktop (`cd ori-desktop && wails build && open build/bin/ori-desktop.app`) and report whether ori shows in the frame.
20+2. Run the qlty gate; fix findings without weakening checks.
21+3. Optional follow-ups: render `user_message.attachments` as chips; refresh the skills list on demand; offline draw.io via vendored `viewer-static.min.js`; ori's own app icon for the desktop; mermaid preview (ticket 0010), light/dark themes (0013), resizable explorer (0015) are open tickets not touched.
22+4. Commit — everything since 2434cc7 is still uncommitted.
new file mode 100644
@@ -0,0 +1,22 @@
1+# Handoff — 2026-09-17 — ori-desktop (Wails), @/skill selectors, draw.io + image previews
2+
3+## Where things stand
4+
5+- **ori-desktop** (`ori-desktop/`, own Go module, Wails v2.16.0): builds and launches on the user's Mac. Check reported "ori is reachable (status: ok)"; Connect appeared to do nothing → root cause was CSS specificity (`#connect-screen` beat `.screen[hidden]`), fixed in `frontend/src/main.css` + `index.html`. **The user has not yet rebuilt and retested.** Next unknown: does the `<iframe>` to `http://localhost:8888` render inside WKWebView from the `wails://` origin? If blank: `wails build -debug`, right-click → Inspect Element, read the console; candidates are ATS (`NSAllowsLocalNetworking` is already in `build/darwin/Info.plist`) or a mixed-content/frame policy. Fallback already wired: "Open in browser" (`runtime.BrowserOpenURL`).
6+- **Selectors + previews** in ori: implemented, tested (102 vitest, 9 Go pkgs, -race clean), documented EN+FR. Smoke-tested by riker with the real binary + mock agent (`make run-mock`: type `@` and `/` in the composer; the mock announces commands `review`/`compact` and echoes `[attached: name]`). Not yet tried against the real Claude adapter (v0.16.2 reportedly converts `resource_link` into a file reference — verified by reading its source, not by running it).
7+- **Quality gate not run**: qlty is not installed in this sandbox. Rerun `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` in a sandbox with the dev-toolkit kit before considering this work closed; expect possible findings in the new files (`internal/files/search.go`, `raw.go`, `internal/skills/`, `ui/src/mentions.ts`, `useCompletion.ts`, `CompletionPopup.tsx`, `PromptInput.tsx`, `ImageView.tsx`, `DrawioView.tsx`).
8+
9+## Traps learned
10+
11+- Wails CLI `go install` needs `CGO_ENABLED=0` where there is no gcc; Windows cross-build needs no C toolchain; macOS/Linux builds need Xcode / webkit2gtk. `wails build` writes `frontend/wailsjs/` and `build/bin/` — both gitignored.
12+- Wails "plain" frontend: never style a screen by `#id` when hiding relies on `[hidden]` — id specificity wins.
13+- biome `organizeImports` fires on many pre-existing files → evidently not part of the project gate; `useExhaustiveDependencies` rejects synthetic deps; `role="listbox"/"option"` on divs needs `tabIndex={-1}` plus a `useSemanticElements` suppression.
14+- `sandboxes/` (untracked sbx source checkout at the repo root) is walked by `/api/files/search`; consider gitignoring/removing it.
15+- The IssueSpec task-item schema is unknown here (no tool installed); tickets were filled with `body` text only (bob used `{text, done}` tasks in 0005 — unverified shape).
16+
17+## Next steps (user's decisions)
18+
19+1. Rebuild ori-desktop (`cd ori-desktop && wails build && open build/bin/ori-desktop.app`) and report whether ori shows in the frame.
20+2. Run the qlty gate; fix findings without weakening checks.
21+3. Optional follow-ups: render `user_message.attachments` as chips; refresh the skills list on demand; offline draw.io via vendored `viewer-static.min.js`; ori's own app icon for the desktop; mermaid preview (ticket 0010), light/dark themes (0013), resizable explorer (0015) are open tickets not touched.
22+4. Commit — everything since 2434cc7 is still uncommitted.
added .memory/handoffs/2026-09-17-launch-for-testing.md +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+# Handoff — 2026-09-17 — ori launched for manual testing
2+
3+- ori runs in the `claude-ori` sandbox: `env -u CLAUDECODE CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275 ./bin/ori --addr 0.0.0.0:8888` (find it with `pgrep -f bin/ori`), log at `/tmp/ori.log`, real Claude Code adapter. Stop with `pkill -f 'bin/ori '` (mind the pattern: it must not match the shell running pkill).
4+- The user is testing the uncommitted work since 2434cc7 (@ file selector, / skill+command selector, image and draw.io previews, terminal, editor). Their feedback is the input for the next session.
5+- Traps: bind `0.0.0.0` so the `sbx ports` forward reaches the server; `env -u CLAUDECODE` remains mandatory; a curl WS probe against `/ws` hangs on success.
6+- Note for later: `@zed-industries/claude-code-acp` is deprecated in favour of `@agentclientprotocol/claude-agent-acp` — the default `--agent-cmd`, `template/Dockerfile` and the docs may want migrating (not done, user's call).
7+- **Trap (blocking, hit this session)**: the Zed adapter's bundled Claude Code CLI is 2.1.44 and rejects `model: "fable[1m]"` from `~/.claude/settings.json` (`-32603 … issue with the selected model`). Workaround: `CLAUDE_CODE_EXECUTABLE=<path to current claude binary>` in ori's environment (adapter env passthrough → SDK `pathToClaudeCodeExecutable`). Candidate permanent fixes, user's call: set it in `kits/ori/spec.yaml` startup + `template/Dockerfile` (the image has the claude CLI at `~/.local/bin/claude`), document it in the run how-to, and/or migrate to `@agentclientprotocol/claude-agent-acp` whose newer SDK may bundle a Fable-aware CLI (unverified).
new file mode 100644
@@ -0,0 +1,7 @@
1+# Handoff — 2026-09-17 — ori launched for manual testing
2+
3+- ori runs in the `claude-ori` sandbox: `env -u CLAUDECODE CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275 ./bin/ori --addr 0.0.0.0:8888` (find it with `pgrep -f bin/ori`), log at `/tmp/ori.log`, real Claude Code adapter. Stop with `pkill -f 'bin/ori '` (mind the pattern: it must not match the shell running pkill).
4+- The user is testing the uncommitted work since 2434cc7 (@ file selector, / skill+command selector, image and draw.io previews, terminal, editor). Their feedback is the input for the next session.
5+- Traps: bind `0.0.0.0` so the `sbx ports` forward reaches the server; `env -u CLAUDECODE` remains mandatory; a curl WS probe against `/ws` hangs on success.
6+- Note for later: `@zed-industries/claude-code-acp` is deprecated in favour of `@agentclientprotocol/claude-agent-acp` — the default `--agent-cmd`, `template/Dockerfile` and the docs may want migrating (not done, user's call).
7+- **Trap (blocking, hit this session)**: the Zed adapter's bundled Claude Code CLI is 2.1.44 and rejects `model: "fable[1m]"` from `~/.claude/settings.json` (`-32603 … issue with the selected model`). Workaround: `CLAUDE_CODE_EXECUTABLE=<path to current claude binary>` in ori's environment (adapter env passthrough → SDK `pathToClaudeCodeExecutable`). Candidate permanent fixes, user's call: set it in `kits/ori/spec.yaml` startup + `template/Dockerfile` (the image has the claude CLI at `~/.local/bin/claude`), document it in the run how-to, and/or migrate to `@agentclientprotocol/claude-agent-acp` whose newer SDK may bundle a Fable-aware CLI (unverified).
added .memory/handoffs/2026-09-17-light-dark-theme.md +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+# Handoff — 2026-09-17 — Light / dark theme (ticket 0013)
2+
3+## State
4+- Implemented, tested (124 vitest), gate PASS (#16), documented EN + FR, ticket closed. ori is running on :8888 with this build for the user's single end-of-work review — **their feedback has not been received yet** at the time of writing.
5+
6+## In flight
7+- Waiting for the user's review of both 0015 (resizable file tree) and 0013 (theme). Any adjustment they ask for is the next step.
8+
9+## Next steps
10+1. Act on the review feedback, if any.
11+2. On the Mac: rebuild ori-desktop (`wails build`) — its `main.js` became an ES module during the quality cleanup; also check the ori page inside the desktop iframe picks the theme correctly (it is per-browser localStorage, so WKWebView has its own).
12+3. Commit — everything since 2434cc7 is still uncommitted (user's call).
13+4. Open tickets untouched: 0010 mermaid preview, 0014 architecture diagram update, and whatever else `ls .tickets/issues` shows as open.
14+
15+## Watch out for
16+- The dark palette is no longer applied by the OS preference; if someone reports "ori ignores my dark mode", that is by design (see the how-to and the architecture explanation), not a regression.
17+- `theme.ts` stamps `<html>` at import time; tests that render `App` must call `resetTheme()` in `beforeEach` or they inherit the previous test's theme.
new file mode 100644
@@ -0,0 +1,17 @@
1+# Handoff — 2026-09-17 — Light / dark theme (ticket 0013)
2+
3+## State
4+- Implemented, tested (124 vitest), gate PASS (#16), documented EN + FR, ticket closed. ori is running on :8888 with this build for the user's single end-of-work review — **their feedback has not been received yet** at the time of writing.
5+
6+## In flight
7+- Waiting for the user's review of both 0015 (resizable file tree) and 0013 (theme). Any adjustment they ask for is the next step.
8+
9+## Next steps
10+1. Act on the review feedback, if any.
11+2. On the Mac: rebuild ori-desktop (`wails build`) — its `main.js` became an ES module during the quality cleanup; also check the ori page inside the desktop iframe picks the theme correctly (it is per-browser localStorage, so WKWebView has its own).
12+3. Commit — everything since 2434cc7 is still uncommitted (user's call).
13+4. Open tickets untouched: 0010 mermaid preview, 0014 architecture diagram update, and whatever else `ls .tickets/issues` shows as open.
14+
15+## Watch out for
16+- The dark palette is no longer applied by the OS preference; if someone reports "ori ignores my dark mode", that is by design (see the how-to and the architecture explanation), not a regression.
17+- `theme.ts` stamps `<html>` at import time; tests that render `App` must call `resetTheme()` in `beforeEach` or they inherit the previous test's theme.
added .memory/handoffs/2026-09-17-resizable-file-tree.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Handoff — 2026-09-17 — Resizable file tree column (ticket 0015)
2+
3+## State
4+- Feature complete and user-tested in the browser: drag the divider between the file tree and the tabs, ←/→, Home/End, double-click reset, width remembered in localStorage (`ori.fileTreeWidth`). Ticket 0015 closed.
5+- Quality gate PASS (run #15) after cleaning up pre-existing findings in ori-desktop `main.js`, `raw.go`, `search.go`, `PreviewPane.tsx`.
6+- ori is running in the sandbox on :8888 with the current build (`env -u CLAUDECODE CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275 ./bin/ori --addr 0.0.0.0:8888`, log `/tmp/ori.log`).
7+
8+## In flight
9+- Nothing on this topic. The user's next request, received mid-session: **light/dark theme selector, light by default (ticket 0013)**. Not started. Note `ui/src/app.css` already defines a dark palette under `@media (prefers-color-scheme: dark)` — the feature is mostly a `data-theme` attribute + a toggle + persistence, same localStorage pattern as the width.
10+
11+## Next steps
12+1. Ticket 0013 (themes) through methodical-dev.
13+2. On the Mac: `cd ori-desktop && wails build`, check the connect screen still works now that `main.js` is an ES module.
14+3. Commit — everything since 2434cc7 is still uncommitted (user's call).
15+
16+## Watch out for
17+- `~/.qlty` is a fresh 488 MB volume after a sandbox recreation: recreate `~/.qlty/cache -> ~/.qlty-cache` before the first qlty run or `qlty check` dies with ENOSPC while unpacking Go.
18+- jsdom 26 lacks `PointerEvent` and `setPointerCapture`; `ResizeHandle.test.tsx` polyfills the former, the component guards the latter with `?.()`.
19+- `pkill -f 'bin/ori'` matches the shell running it; use `pkill -f '^\./bin/ori '`.
20+- biome for ad-hoc checks: `npx -y @biomejs/biome@1.9.4` (project pin from `biome.json`); a bare `npx biome` installs an unrelated package.
new file mode 100644
@@ -0,0 +1,20 @@
1+# Handoff — 2026-09-17 — Resizable file tree column (ticket 0015)
2+
3+## State
4+- Feature complete and user-tested in the browser: drag the divider between the file tree and the tabs, ←/→, Home/End, double-click reset, width remembered in localStorage (`ori.fileTreeWidth`). Ticket 0015 closed.
5+- Quality gate PASS (run #15) after cleaning up pre-existing findings in ori-desktop `main.js`, `raw.go`, `search.go`, `PreviewPane.tsx`.
6+- ori is running in the sandbox on :8888 with the current build (`env -u CLAUDECODE CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275 ./bin/ori --addr 0.0.0.0:8888`, log `/tmp/ori.log`).
7+
8+## In flight
9+- Nothing on this topic. The user's next request, received mid-session: **light/dark theme selector, light by default (ticket 0013)**. Not started. Note `ui/src/app.css` already defines a dark palette under `@media (prefers-color-scheme: dark)` — the feature is mostly a `data-theme` attribute + a toggle + persistence, same localStorage pattern as the width.
10+
11+## Next steps
12+1. Ticket 0013 (themes) through methodical-dev.
13+2. On the Mac: `cd ori-desktop && wails build`, check the connect screen still works now that `main.js` is an ES module.
14+3. Commit — everything since 2434cc7 is still uncommitted (user's call).
15+
16+## Watch out for
17+- `~/.qlty` is a fresh 488 MB volume after a sandbox recreation: recreate `~/.qlty/cache -> ~/.qlty-cache` before the first qlty run or `qlty check` dies with ENOSPC while unpacking Go.
18+- jsdom 26 lacks `PointerEvent` and `setPointerCapture`; `ResizeHandle.test.tsx` polyfills the former, the component guards the latter with `?.()`.
19+- `pkill -f 'bin/ori'` matches the shell running it; use `pkill -f '^\./bin/ori '`.
20+- biome for ad-hoc checks: `npx -y @biomejs/biome@1.9.4` (project pin from `biome.json`); a bare `npx biome` installs an unrelated package.
added .memory/handoffs/2026-09-17-sbx-auto-stop.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Handoff — 2026-09-17 — sbx auto-stop: why the ori sandbox goes `stopped` alone
2+
3+## Conclusion (verified)
4+
5+sandboxd auto-stops a sandbox 30 s after its **last CLI sentinel session** closes. `sbx create` holds one only while it runs, so with the documented `sbx create … -p 8888:8888` command the webapp dies 30 s after the prompt comes back. Nothing the browser does keeps it up. Daemon log proving it: `~/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/daemon.log` (`sbx daemon status` prints the path).
6+
7+Source anchors (in `sandboxes/`): `sandboxd/pkg/server/backend_dockernext.go``sessionTracker`, `holdSession`, `WithAutoStopDelay(30*time.Second)` in `setupDockerNextBackend`; `cli-plugin/commands/sentinel.go` (`openSentinelConn`, `holdSentinel`); `cli-plugin/commands/create.go` `executeCreateAndReleaseSession`; `sandboxapi/openapi.yaml` `sessionHold` + `detached` field.
8+
9+## The only switch: `detached: true`, create-time, via `sbx run -d`
10+
11+- `sbx run -d claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888` creates the sandbox with `Spec.Detached=true``holdSession` logs "detached; not auto-stopping" and never stops it. `sbx run` accepts the same flags as `sbx create` (checked `docs/yml/sbx_run.yaml`).
12+- `sbx create` has no `--detached` flag; `Detached` cannot be changed afterwards (no PATCH). An existing sandbox must be `sbx rm`'d first.
13+- `sbx run -d --name ori` on an existing NON-detached sandbox restarts it without holding a sentinel, so it stays up — until the next `sbx exec`/`sbx run` session ends, which re-arms the 30 s stop. Fragile; prefer recreating.
14+- The 30 s delay is hard-coded; no `sbx settings` key, no env var (grep for `autoStop` outside the backend file finds only an e2e feature name).
15+
16+## Next steps (user's decision)
17+
18+1. The user was recreating with `sbx run -d` and testing when the session ended — result not yet reported. Expected in the daemon log: `session disconnected (detached; not auto-stopping)`. This likely also explains the "offline" WebSocket symptom of the earlier handoff (the container was already stopping).
19+2. DONE: `quickstart.md`, `kits/ori/README.md`, `docs/en|fr/how-to/run-in-a-sandbox.md` switched to `sbx run -d` with the explanation. If the test shows `sbx run -d` behaves differently from expected (e.g. `--name` + workspace positional interplay), fix the docs accordingly.
20+3. Still uncommitted since 2434cc7 — the user has deferred the commit repeatedly.
new file mode 100644
@@ -0,0 +1,20 @@
1+# Handoff — 2026-09-17 — sbx auto-stop: why the ori sandbox goes `stopped` alone
2+
3+## Conclusion (verified)
4+
5+sandboxd auto-stops a sandbox 30 s after its **last CLI sentinel session** closes. `sbx create` holds one only while it runs, so with the documented `sbx create … -p 8888:8888` command the webapp dies 30 s after the prompt comes back. Nothing the browser does keeps it up. Daemon log proving it: `~/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/daemon.log` (`sbx daemon status` prints the path).
6+
7+Source anchors (in `sandboxes/`): `sandboxd/pkg/server/backend_dockernext.go``sessionTracker`, `holdSession`, `WithAutoStopDelay(30*time.Second)` in `setupDockerNextBackend`; `cli-plugin/commands/sentinel.go` (`openSentinelConn`, `holdSentinel`); `cli-plugin/commands/create.go` `executeCreateAndReleaseSession`; `sandboxapi/openapi.yaml` `sessionHold` + `detached` field.
8+
9+## The only switch: `detached: true`, create-time, via `sbx run -d`
10+
11+- `sbx run -d claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888` creates the sandbox with `Spec.Detached=true``holdSession` logs "detached; not auto-stopping" and never stops it. `sbx run` accepts the same flags as `sbx create` (checked `docs/yml/sbx_run.yaml`).
12+- `sbx create` has no `--detached` flag; `Detached` cannot be changed afterwards (no PATCH). An existing sandbox must be `sbx rm`'d first.
13+- `sbx run -d --name ori` on an existing NON-detached sandbox restarts it without holding a sentinel, so it stays up — until the next `sbx exec`/`sbx run` session ends, which re-arms the 30 s stop. Fragile; prefer recreating.
14+- The 30 s delay is hard-coded; no `sbx settings` key, no env var (grep for `autoStop` outside the backend file finds only an e2e feature name).
15+
16+## Next steps (user's decision)
17+
18+1. The user was recreating with `sbx run -d` and testing when the session ended — result not yet reported. Expected in the daemon log: `session disconnected (detached; not auto-stopping)`. This likely also explains the "offline" WebSocket symptom of the earlier handoff (the container was already stopping).
19+2. DONE: `quickstart.md`, `kits/ori/README.md`, `docs/en|fr/how-to/run-in-a-sandbox.md` switched to `sbx run -d` with the explanation. If the test shows `sbx run -d` behaves differently from expected (e.g. `--name` + workspace positional interplay), fix the docs accordingly.
20+3. Still uncommitted since 2434cc7 — the user has deferred the commit repeatedly.
added .memory/handoffs/2026-09-17-source-code-analysis.md +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Handoff — 2026-09-17 — Source code analysis document
2+
3+## State
4+
5+Documentation-only session. Created `docs/source-code-analysis.adoc`: an AsciiDoc summary of the whole codebase (architecture, Go packages, frontend modules/components, endpoints, WebSocket protocol, tests, Makefile, design decisions, limitations). Verified against the sources via an Explore pass, not just copied from memory. No code, tests or config touched; quality gate not re-run (nothing to measure).
6+
7+## Notes
8+
9+- The document sits at `docs/source-code-analysis.adoc`, outside the Diátaxis en/fr quadrants — it is a one-off analysis artifact, not part of the bilingual user docs. If it is later promoted into the doc set, it belongs in `explanation/` and would need a French counterpart (see `ebook-sync`-style rule the docs follow).
10+- Counts recorded in it: ~3,064 Go LOC / ~3,252 TS+TSX LOC; 55 Go tests + 1 e2e; ~215 vitest cases. These will drift — treat as a 2026-09-17 snapshot (`:revdate:` is set).
11+
12+## Next steps
13+
14+- Unchanged from the previous handoff (`2026-09-17-acp-webapp.md`): pending final commit of docs/memory/module-rename, real-browser trial, tickets 0002/0003, evolution list. This new .adoc is also uncommitted.
new file mode 100644
@@ -0,0 +1,14 @@
1+# Handoff — 2026-09-17 — Source code analysis document
2+
3+## State
4+
5+Documentation-only session. Created `docs/source-code-analysis.adoc`: an AsciiDoc summary of the whole codebase (architecture, Go packages, frontend modules/components, endpoints, WebSocket protocol, tests, Makefile, design decisions, limitations). Verified against the sources via an Explore pass, not just copied from memory. No code, tests or config touched; quality gate not re-run (nothing to measure).
6+
7+## Notes
8+
9+- The document sits at `docs/source-code-analysis.adoc`, outside the Diátaxis en/fr quadrants — it is a one-off analysis artifact, not part of the bilingual user docs. If it is later promoted into the doc set, it belongs in `explanation/` and would need a French counterpart (see `ebook-sync`-style rule the docs follow).
10+- Counts recorded in it: ~3,064 Go LOC / ~3,252 TS+TSX LOC; 55 Go tests + 1 e2e; ~215 vitest cases. These will drift — treat as a 2026-09-17 snapshot (`:revdate:` is set).
11+
12+## Next steps
13+
14+- Unchanged from the previous handoff (`2026-09-17-acp-webapp.md`): pending final commit of docs/memory/module-rename, real-browser trial, tickets 0002/0003, evolution list. This new .adoc is also uncommitted.
added .memory/history.md +128 -0
new file mode 100644
@@ -0,0 +1,128 @@
1+# History
2+
3+## 2026-09-17 — Ticket 0004: ACP web client (initial implementation)
4+
5+- **Goal**: implement `.tickets/issues/0004-specifications.yaml` — a webapp (Go backend + SPA) acting as an ACP client for a code agent (Claude Code first), UI following the principles of Zed's agent panel, designed to evolve. Driven with the `methodical-dev` skill, all checkpoints user-approved.
6+- **Changes**: whole codebase created — `cmd/ori`, `cmd/ori-mock-agent`, `internal/{config,httpserver,agent,bridge,mockagent}`, `ui/` (Vite + React 19 + TS SPA with reducer/store/ws-client and panel components), `Makefile`, `biome.json`, `.qlty/qlty.toml`, bilingual `docs/` (en+fr, Diátaxis, 7 pages each), `docs/diagrams/packages.drawio`, root `README.md`.
7+- **Decisions**: `coder/acp-go-sdk` for ACP (no official Go lib exists; the claude CLI has no native ACP mode — the user initially chose "native CLI", reversed after verification, and approved the `claude-code-acp` adapter); ACP payloads relayed verbatim to the browser; server as single source of truth with history replay + `user_message` echo; one shared session in v1. Mid-session the user corrected the Go module path from `github.com/bots-garden/ori` to `rickub.com/bots-garden/ori` (repo not on GitHub) — renamed everywhere, tests green.
8+- **Tests**: 7 config + 6 httpserver + 10 agent (in-memory mock ACP agent) + 15 bridge (incl. 4 real-WebSocket integration) + 2 mockagent + 1 full e2e (compiled mock binary, real subprocess, real WS, permission round-trip, late-joiner replay) — all green incl. `-race`; 28 frontend tests (reducer, ws client with fake WebSocket, components) — green. One command: `make test`.
9+- **Quality**: gate **PASS** (0 errors / 0 warnings / 0 smells, total complexity 145; started at 13/4/5/158). Config adjustments user-approved (see summary). Two environment fights: the 488MB `~/.qlty` volume (cache symlinked to main disk) and a partial JDK extraction.
10+- **Docs**: `docs/en/` + `docs/fr/` complete four-quadrant sets; package diagram in draw.io; root README quick start.
11+
12+## 2026-09-17 — Live thinking block + working spinner (same session, follow-up)
13+
14+- **Goal**: after testing against real Claude Code, the user asked for (1) the agent's thinking content visible live while it streams, (2) a spinner with rotating phrases while the agent works, Claude-style.
15+- **Changes**: new `ui/src/components/WorkingIndicator.tsx` (spinner + `workingPhrases` rotating every 2.5s), `ChatThread` refactored with a `ThoughtEntry` component (live open block `💭 Thinking…` while streaming, collapsible `💭 Thought for a moment` once the turn ends), CSS spinner/thought-live, tutorials updated in both languages.
16+- **Decisions**: live-vs-collapsed decided by the existing `closed` flag on thought items (no reducer change needed); phrases exported for deterministic tests.
17+- **Tests**: +3 WorkingIndicator (fake timers: first phrase, rotation, wrap-around) and +2 App (thought open→collapsed across turn end; spinner present during a turn, gone after) — 33 frontend tests green, Go suite untouched.
18+- **Quality**: gate PASS again (a first re-run flagged `ThreadEntry` at 6 returns; fixed by extracting `ThoughtEntry`).
19+- **Also verified this session**: real Claude Code works end to end via `env -u CLAUDECODE ./bin/ori` (nested-session guard); adapter package renamed upstream to `@agentclientprotocol/claude-agent-acp` (default not changed yet).
20+
21+## 2026-09-17 — Workspace panel: preview, editor, terminal, monospace command output (same session, follow-up 2)
22+
23+- **Goal**: user-requested evolutions — a preview panel (syntax-highlighted code + rendered Markdown/AsciiDoc), a terminal panel, a file editor panel (Monaco), and monospace rendering for system-command output in the chat.
24+- **Changes**: backend `internal/files` (list/read/write file API, unrestricted access per user's explicit choice) and `internal/terminal` (PTY shell per WebSocket, creack/pty); frontend workspace layout (chat left + collapsible right panel: file tree with lazy directories, Preview/Editor/Terminal tabs kept mounted when inactive), `MonacoViewer` shared by read-only preview and editor (bundled workers, lazy chunk), AsciiDoc rendering via lazy `@asciidoctor/core`, xterm.js terminal, `tool-output` monospace block for `execute` tool calls. New deps: creack/pty, monaco-editor + @monaco-editor/react, @asciidoctor/core, @xterm/xterm + addon-fit.
25+- **Decisions**: layout chat+workspace (user), interactive user terminal only in v1 (ACP terminals later, user), free filesystem/terminal access (user — sandbox assumed isolated), Monaco over CodeMirror (user); panes hidden not unmounted so the shell survives tab switches; monaco 0.56 import specifiers go through its exports map (no `esm/vs/` prefix).
26+- **Tests**: +9 files API, +5 terminal PTY integration (real shell over real WS: command output, cwd, resize via stty, malformed frames, exit closes socket) → 46 Go tests incl. e2e; front +5 workspace store, +4 FileTree, +8 lang detection, +6 PreviewPane, +6 EditorPane, +4 TerminalPane (xterm/WS mocked), +2 ToolCallCard mono/markdown → 66 front tests. All green, `-race` clean.
27+- **Quality**: gate PASS (0/0/0, total complexity 281). Iterations: biome 1.9 rejects the `noStaticElementInteractions` suppression (rule doesn't exist there — removed), the dangerouslySetInnerHTML suppression must sit between JSX attributes, EditorPane/ToolContent refactored (extracted EditorHeader/editorPlaceholder/TextToolContent) to clear return/complexity smells.
28+- **Docs**: new how-to `use-the-workspace` + reference `workspace-api` in both languages; architecture pages and indexes updated; drawio diagram extended (files, terminal, pty, SPA runtime edges); root README.
29+
30+## 2026-09-17 — Source code analysis document (new session)
31+
32+- **Goal**: analyse the source code and produce a summary as an AsciiDoc document.
33+- **Changes**: new `docs/source-code-analysis.adoc` — overview, backend package table, frontend module/component inventory, HTTP/WS endpoints, bridge protocol summary, test counts (55 Go tests + 1 e2e, ~215 vitest cases), Makefile targets, design decisions, known limitations. No code changed.
34+- **Method**: read `.memory/` first, then an Explore agent verified the memory picture against the sources (LOC: ~3,064 Go / ~3,252 TS-TSX).
35+- **Tests / quality**: not run — documentation-only session, no code touched.
36+
37+## 2026-09-17 — Workspace polish: AsciiDoc fix, VS Code-style tree with Material icons, dark terminal (same session, follow-up 3)
38+
39+- **Goal**: user feedback after trying the workspace — AsciiDoc preview broken in production ("m is not a function"), file tree should look like VS Code's with Material icons, terminal should always be dark.
40+- **Changes**: AsciiDocView now uses @asciidoctor/core v4's real API — `convert` is a **named export and asynchronous** (the v3 `asciidoctor()` factory no longer exists; my CJS-default cast called `undefined()`); added a **non-mocked** AsciiDocView test rendering with the real converter — the mocked-only tests were exactly why the breakage reached production. FileTree restyled VS Code-like: `vscode-material-icons` (910 SVGs copied to `ui/public/material-icons/` by `ui/scripts/copy-icons.mjs`, npm pre-hooks on dev/build, gitignored), chevrons, indent guides, open-folder icons, active-file highlight. Terminal always dark (`#1e1e1e`) whatever the app theme.
41+- **Tests**: +2 real AsciiDoc render, +2 FileTree (icons present, active highlight) → 70 front tests, all green; Go untouched (46).
42+- **Quality**: gate PASS.
43+
44+## 2026-09-17 — Inline monospace for backticked commands (same session, follow-up 4)
45+
46+- **Goal**: user feedback — tool titles like ``Run `ls .memory/` `` showed literal backticks in a proportional font.
47+- **Changes**: new `InlineText` component (backtick spans → `<code class="inline-code">`, no markdown pipeline), applied to tool call titles, permission titles and plan entries; `.inline-code` CSS.
48+- **Tests**: +4 InlineText (spans, multiples, plain, unpaired backtick) and +1 ToolCallCard title assertion → 75 front tests green.
49+
50+## 2026-09-17 — sbx template + kit for ori (same session, follow-up 5)
51+
52+- **Goal**: package ori as a Docker Sandboxes template (image `k33g/ori`, the user builds it under their handle) and add a kit to launch it easily from sbx.
53+- **Changes**: `template/Dockerfile` (multi-stage: node builds the SPA, go builds the binaries, final layer = `docker/sandbox-templates:claude-code` + globally installed `@zed-industries/claude-code-acp` + `/usr/local/bin/{ori,ori-mock-agent}`, EXPOSE 8888, standalone CMD), `.dockerignore` (excludes the 635MB `sandboxes/` checkout, node_modules, .git…), `kits/ori/spec.yaml` (schemaVersion 2 mixin: idempotent `setup.startup` launching ori as the agent user on :8888, agentInstructions), `kits/ori/README.md`, `make template`, how-to `run-in-a-sandbox` in docs en+fr.
54+- **Decisions**: no `sbx template build` exists — a template IS a container image passed via `sbx create claude --template k33g/ori`, and `--kit` accepts a mixin directory (verified in `sandboxes/docs/yml/sbx_create_claude.yaml`); the ori kit adds no network permissions (Anthropic hosts come from the claude agent kit, the adapter is baked into the image); kit startup guarded by `pgrep -x ori` because startup commands re-run on every container start.
55+- **Tests/validation**: image built locally (2.81GB) and smoke-tested standalone with the mock agent (healthz + session OK); the kit's exact startup command exec'd in a sleeping container of the image → adapter session created, healthz OK; idempotency check OK; kit YAML parse-validated.
56+
57+## 2026-09-17 — Kit refinements from the sbx source deep-dive (same session, follow-up 5b)
58+
59+- A thorough exploration of the sbx source confirmed the template/kit design and yielded three refinements applied to `kits/ori/spec.yaml`: `requires: {agent: claude}` (mixin-only field, enforces composition with the claude agent kit), a `ports:` block (container 8888 — kits auto-publish an ephemeral localhost port at create; Dockerfile EXPOSE is read by nothing), and dropping `user: "agent"` from the startup step (StartupCommand.user defaults to "1000", the agent uid; "agent" is non-standard).
60+- Also learned and documented: sandboxd has its own image store — a never-pushed local image reaches it via `docker save` + `sbx template load`; kit references resolve fresh each create (no local kit registry); `sbx kit validate/pack/push` exist for distribution; an alternative shape (kind:sandbox + `extends: claude` + `sandbox.image: k33g/ori`) would allow `sbx create ./kits/ori .` in one argument — not adopted, the mixin matches the dev-toolkit pattern already in use.
61+
62+## 2026-09-17 — build.sh, Hub publication at 0.0.0, parameterized kit port, quickstart (same session, follow-up 5c)
63+
64+- **Goal**: user wants to push `k33g/ori` to Docker Hub — a `template/build.sh` they run themselves, image tagged `0.0.0`; the kit's listen port made configurable; a `quickstart.md` with the sandbox-creation commands.
65+- **Changes**: `template/build.sh` (multi-arch buildx --push, tags 0.0.0 + latest, VERSION as $1, PLATFORMS overridable); `kits/ori/spec.yaml` gained `args.port` (default "8888", pattern-validated) substituted into `ports.container` (unquoted `${{ kit.args.port }}` — the source's expand.go explicitly re-infers it as an integer), the startup `--addr`, and the agentInstructions; `quickstart.md` at the repo root; kit README and both run-in-a-sandbox pages now reference `k33g/ori:0.0.0` and `--kit-arg ori.port=`.
66+- **Validation**: spec YAML parses with the arg refs; build.sh is the push path, `make template` stays the local-only path. The actual Hub push is the user's action (their Docker login).
67+
68+## 2026-09-17 — Multi-arch build fix: no qemu in the template build (same session, follow-up 5d)
69+
70+- **Problem**: the user's `./template/build.sh` failed on the linux/amd64 half — on an arm64 host that stage runs under qemu, where Vite bundling Monaco exhausts Node's 2GB heap ("FATAL ERROR: Ineffective mark-compacts near heap limit", "qemu: uncaught target signal 6").
71+- **Fix**: every build stage now runs on `--platform=$BUILDPLATFORM`: the SPA build is platform-independent (built once, natively), Go cross-compiles with `GOOS=$TARGETOS GOARCH=$TARGETARCH`, and the ACP adapter is npm-installed in a native stage then COPY'd into the template's npm prefix (`/usr/local/share/npm-global`, agent-owned, on PATH). The final stage contains no RUN at all, so a multi-arch build never invokes qemu.
72+- **Validation**: native rebuild green; smoke tests pass (healthz with mock agent; `claude-code-acp` resolves from the copied prefix and initializes a real ACP session).
73+
74+## 2026-09-17 — Kit startup fix: su resets PATH (same session, follow-up 5e)
75+
76+- **Problem**: the user's real `sbx create … --kit ./kits/ori -p 5555:8888` came up with nothing listening. Root cause read from the sbx source (`sandboxlib/kit/durable_startup.go`): startup commands are rendered as `exec su -s /bin/sh -c '<script>' agent`, and util-linux `su` always resets PATH to the login.defs default — which lacks `/usr/local/share/npm-global/bin`, so ori died instantly with "claude-code-acp: executable file not found". Reproduced in the image (`su -c 'command -v claude-code-acp' agent` → missing); my earlier validation had used `docker exec`, which preserves image ENV — that's why it passed.
77+- **Fix**: the kit's startup script now exports `PATH="/usr/local/share/npm-global/bin:/home/agent/.local/bin:$PATH"` first (comment marks it load-bearing). Re-validated through the exact `su` path: healthz OK, ACP session created.
78+- **No image rebuild needed** — kit-side only; the user must `sbx rm` and recreate the sandbox for the new kit to apply. Debug window for kit startup: `/var/log/sbx-kit-startup.log` (dispatcher log) + `/home/agent/.ori.log`.
79+
80+## 2026-09-17 — Session end inside the sandbox: WS-offline investigation in flight (follow-up 5f)
81+
82+- After the su-PATH fix, the user's sandbox serves the SPA on localhost:5555 but the UI shows "offline" (the WebSocket to /ws does not stay up). Established before stopping: ori's WS handshake is correct against browser-like Origin headers (101 for localhost:* origins, 403 for foreign — tested with curl against the image); sbx port publishing goes through gvisor's port mapper (raw L4, so the upgrade should pass). Three discriminating diagnostics were handed to the user (server log; WS handshake from inside the sandbox; WS handshake through the forward) — answers not yet received. The session ends here: the user is relaunching the assistant outside sbx.
83+- Everything since commit 2434cc7 is still uncommitted (user's call, repeatedly deferred).
84+
85+## 2026-09-17 — Why the ori sandbox stops on its own: sbx auto-stop investigation (follow-up 6, read-only)
86+
87+- **Asked**: the sandbox created with `sbx create claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888` serves the webapp then goes `stopped` by itself; find the auto-stop mode in the sbx source and a way to disable it.
88+- **Found** (in `sandboxes/`, the vendored sbx checkout, v0.43.0 daemon on this Mac): sandboxd counts "sentinel" long-poll sessions per sandbox (`GET /sandbox/{name}/session`, `holdSession` in `sandboxd/pkg/server/backend_dockernext.go`); when the count hits zero it stops the container after a hard-coded 30 s grace (`WithAutoStopDelay(30*time.Second)` in `setupDockerNextBackend`). `sbx create` holds a sentinel only for its own lifetime (`executeCreateAndReleaseSession` in `cli-plugin/commands/create.go`), so the stop fires 30 s after the command returns — confirmed by the daemon log (`session disconnected, deferring auto-stop``auto-stopped runtime` 30 s later for `ori` and `ori2`). Port publishing and browser traffic hold no session.
89+- **Escape hatch**: the create-time spec field `detached: true` (openapi `SandboxCreateRequest.detached`) makes `holdSession` never auto-stop. Only `sbx run -d/--detached` sets it (`create_daemon.go: Detached: opts.detached`); `sbx create` has no such flag; there is no update API, no `sbx settings` key and no env var for the delay.
90+- **Recommendation given**: `sbx rm ori` then `sbx run -d claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888`.
91+- **Docs switched (same session, at the user's request while they tested)**: `quickstart.md`, `kits/ori/README.md`, `docs/en|fr/how-to/run-in-a-sandbox.md` now use `sbx run -d`, explain the 30 s auto-stop and why detached is the fix, and add a restart line (`sbx run -d --name <name>`) plus a daemon-log diagnostic hint. No `sbx create` invocation remains in project docs. No code change.
92+- **Also observed**: the WS-"offline" symptom from follow-up 5f is very plausibly this same stop (the page was already loaded when the container died), still to be confirmed by the user.
93+
94+## 2026-09-17 — Three parallel agents: ori-desktop (Wails), @/skill selectors + draw.io/image previews, ACP command inventory
95+
96+- **Asked**: (bob) create `ori-desktop/`, a Wails app connecting to the ori webapp; (riker) add an `@` file selector, a skill selector, draw.io and image previews; (milo, read-only) list the ACP commands exposed. Then the user built ori-desktop on their Mac: Check OK, Connect "did nothing".
97+- **bob**: new module `ori-desktop/` (Wails v2.16.0, plain template, iframe embed of the ori SPA, settings JSON, bound methods, `internal/settings` + `internal/health` with 12 tests, EN+FR READMEs, `make desktop`/`desktop-test`, .gitignore, ticket 0005 filled). Verified: tests, vet, gofmt, Windows cross-build, root `make build`/`make test` unaffected. Not verifiable in the sandbox: running the window (no gcc/webkit2gtk).
98+- **riker**: `GET /api/files/search`, `GET /api/raw`, new `internal/skills` + `GET /api/skills`; `prompt.attachments` → ACP `resource_link` blocks (`Prompter.Prompt(blocks)` replaces `PromptText`, `Bridge.SetWorkspaceRoot`); reducer stores `available_commands_update` commands; `mentions.ts`, `useCompletion.ts`, `CompletionPopup`, rewritten `PromptInput`, `ImageView`, `DrawioView` (embed.diagrams.net + Source fallback), `lang.ts` kinds `image`/`drawio`; mock agent emits commands and echoes attachments; Vite dev proxy gained `/api`; `biome format --write` reformatted 11 files. Docs EN+FR: workspace-api, websocket-protocol, use-the-workspace, new how-to `mention-files-and-skills`, architecture sections, READMEs. Tickets 0008/0009/0011/0012 filled.
99+- **milo**: ori calls `initialize`, `session/new`, `session/prompt`, `session/cancel`; serves `session/update`, `session/request_permission`, `fs/read_text_file`, `fs/write_text_file`; refuses `terminal/*`; unused stable methods: `authenticate`, `logout`, `session/load|resume|list|close|set_mode|set_config_option`. "Agent commands" are not ACP methods: `available_commands_update``/name` text in the prompt. Relayed to riker, who implemented it (ticket 0008).
100+- **Bug fixed after the user's test**: `ori-desktop/frontend/src/main.css``#connect-screen {display:grid}` outranked `.screen[hidden]`, so Connect (and the startup auto-connect) rendered the viewer below the fold; now class `.connect` + `.screen[hidden] {display:none !important}`. Rebuild needed (`wails build`); iframe content in WKWebView still unconfirmed.
101+- **Tests**: `make test` → 9 Go packages ok, vitest 16 files / 102 tests (was 66); `go test -race ./...` ok (riker installed gcc + libc6-dev via apt in the sandbox for it).
102+- **Quality gate**: NOT run — qlty absent from this sandbox (`~/.qlty` missing). gofmt / go vet / biome check clean.
103+- Nothing committed (still everything since 2434cc7).
104+
105+## 2026-09-17 — Launch ori for manual testing of the new features (evening session)
106+
107+- **Asked**: "lance ori que je puisse tester les nouveautés". No code change: `make build` (SPA + binaries), then `env -u CLAUDECODE nohup ./bin/ori --addr 0.0.0.0:8888 > /tmp/ori.log 2>&1 &` inside the `claude-ori` sandbox. Adapter `@zed-industries/claude-code-acp@0.16.2` resolved via npx (npm now flags it deprecated → renamed `@agentclientprotocol/claude-agent-acp`; still works). ACP session ready, `/healthz` ok, `/api/skills` lists the skills. Port 8888 must be published on the host with `sbx ports claude-ori --publish 8888:8888/tcp` (not doable from inside the sandbox).
108+- Trap hit: a `curl` WebSocket handshake against `/ws` never returns on success (101 keeps the socket open) — use `-m` or check the log instead.
109+- **Follow-up (same session)**: first real prompt failed with `-32603 There's an issue with the selected model (fable[1m])`. Root cause: `@zed-industries/claude-code-acp@0.16.2` bundles `@anthropic-ai/claude-agent-sdk@0.2.44`, whose embedded Claude Code CLI is **2.1.44** (zero mentions of Fable), while `~/.claude/settings.json` sets `"model": "fable[1m]"` and the adapter forwards it verbatim. The system CLI (2.1.275) accepts `fable[1m]` fine. Fix without touching the user's settings: the adapter honours `CLAUDE_CODE_EXECUTABLE``pathToClaudeCodeExecutable`; ori relaunched with `CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275`; a WS prompt round-trip returned `pong` (`turn_ended end_turn`). No code change.
110+
111+## 2026-09-17 — Resizable file explorer column (ticket 0015), via methodical-dev
112+
113+- **Goal**: make the workspace file tree column resizable with the mouse; user chose localStorage persistence and the file-tree divider only (not the chat/workspace split).
114+- **Changes**: `ui/src/workspace.ts` (`fileTreeWidth`, `setFileTreeWidth`, `resetFileTreeWidth`, `clampFileTreeWidth`, constants, guarded localStorage), new `ui/src/components/ResizeHandle.tsx` (+ test), `Workspace.tsx` (handle between tree and tabs, `--filetree-width` on the aside; new `Workspace.test.tsx`), `app.css` (`.filetree` width from the variable, `.resize-handle` styles). Ticket 0015 closed.
115+- **Decisions**: width in px (drag produces px; comparable with bounds); FileTree untouched — width reaches CSS via a custom property; handle owns no width, the store clamps. jsdom 26 has no PointerEvent → test-local MouseEvent polyfill.
116+- **Quality gate** (first run of qlty on the tree since the parallel-agents session): 12 errors / 1 warning / 2 smells, all pre-existing — fixed without weakening anything: `ori-desktop/frontend/src/main.js` became an ES module (`type="module"`, IIFE and `"use strict"` dropped, optional chains, template literals, `reloadFrame()` instead of `src = src`), `internal/files/raw.go` explicit `_ = file.Close()` with rationale, `internal/files/search.go` walk callback → `searchWalker.visit/visitDir/collect`, `PreviewPane.tsx` loading effect → `usePreviewFile` hook; `qlty fmt` on main.css / main.js / wails.json. Run #15 PASS (0/0/0, complex 507).
117+- **Tests**: 118 vitest (18 files), 9 Go packages, ori-desktop 2 packages — green. `make test`.
118+- **Docs**: how-to `use-the-workspace` (step 5 + keyboard/persistence variant) and architecture explanation, EN + FR; root README; ori-desktop READMEs (ES module).
119+- **Not verifiable here**: the ori-desktop frontend change (ES module) needs a `wails build` + launch on the user's Mac.
120+
121+## 2026-09-17 — Light / dark theme selector, light by default (ticket 0013)
122+
123+- **Goal**: user request received mid-session; choices made via AskUserQuestion: two themes only (no "system" option), single review at the end.
124+- **Changes**: new `ui/src/theme.ts` (+ `theme.test.ts`), `app.css` (dark palette on `:root[data-theme="dark"]`, `color-scheme` per theme, `.topbar-theme`), `App.tsx` (☾/☀ button, aria-label "Switch to dark/light theme"), `MonacoViewer.tsx` (store instead of `matchMedia`), `main.tsx` (side-effect import so the theme is stamped before the first render), `App.test.tsx` (+1). Ticket 0013 closed.
125+- **Decisions**: dropping `prefers-color-scheme` is a behaviour change (dark-OS users now get light until they toggle) — accepted by the user ("light par défaut"); documented in the architecture explanation. Terminal keeps its dark background (pre-existing decision).
126+- **Tests**: 124 vitest / 19 files green; Go untouched. **Quality**: run #16 PASS (0/0/0, complex 514).
127+- **Docs**: new how-to `switch-the-theme.md` EN + FR, both indexes, architecture explanation paragraph EN + FR, root README.
128+- ori rebuilt and restarted on :8888 for the user's review.
new file mode 100644
@@ -0,0 +1,128 @@
1+# History
2+
3+## 2026-09-17 — Ticket 0004: ACP web client (initial implementation)
4+
5+- **Goal**: implement `.tickets/issues/0004-specifications.yaml` — a webapp (Go backend + SPA) acting as an ACP client for a code agent (Claude Code first), UI following the principles of Zed's agent panel, designed to evolve. Driven with the `methodical-dev` skill, all checkpoints user-approved.
6+- **Changes**: whole codebase created — `cmd/ori`, `cmd/ori-mock-agent`, `internal/{config,httpserver,agent,bridge,mockagent}`, `ui/` (Vite + React 19 + TS SPA with reducer/store/ws-client and panel components), `Makefile`, `biome.json`, `.qlty/qlty.toml`, bilingual `docs/` (en+fr, Diátaxis, 7 pages each), `docs/diagrams/packages.drawio`, root `README.md`.
7+- **Decisions**: `coder/acp-go-sdk` for ACP (no official Go lib exists; the claude CLI has no native ACP mode — the user initially chose "native CLI", reversed after verification, and approved the `claude-code-acp` adapter); ACP payloads relayed verbatim to the browser; server as single source of truth with history replay + `user_message` echo; one shared session in v1. Mid-session the user corrected the Go module path from `github.com/bots-garden/ori` to `rickub.com/bots-garden/ori` (repo not on GitHub) — renamed everywhere, tests green.
8+- **Tests**: 7 config + 6 httpserver + 10 agent (in-memory mock ACP agent) + 15 bridge (incl. 4 real-WebSocket integration) + 2 mockagent + 1 full e2e (compiled mock binary, real subprocess, real WS, permission round-trip, late-joiner replay) — all green incl. `-race`; 28 frontend tests (reducer, ws client with fake WebSocket, components) — green. One command: `make test`.
9+- **Quality**: gate **PASS** (0 errors / 0 warnings / 0 smells, total complexity 145; started at 13/4/5/158). Config adjustments user-approved (see summary). Two environment fights: the 488MB `~/.qlty` volume (cache symlinked to main disk) and a partial JDK extraction.
10+- **Docs**: `docs/en/` + `docs/fr/` complete four-quadrant sets; package diagram in draw.io; root README quick start.
11+
12+## 2026-09-17 — Live thinking block + working spinner (same session, follow-up)
13+
14+- **Goal**: after testing against real Claude Code, the user asked for (1) the agent's thinking content visible live while it streams, (2) a spinner with rotating phrases while the agent works, Claude-style.
15+- **Changes**: new `ui/src/components/WorkingIndicator.tsx` (spinner + `workingPhrases` rotating every 2.5s), `ChatThread` refactored with a `ThoughtEntry` component (live open block `💭 Thinking…` while streaming, collapsible `💭 Thought for a moment` once the turn ends), CSS spinner/thought-live, tutorials updated in both languages.
16+- **Decisions**: live-vs-collapsed decided by the existing `closed` flag on thought items (no reducer change needed); phrases exported for deterministic tests.
17+- **Tests**: +3 WorkingIndicator (fake timers: first phrase, rotation, wrap-around) and +2 App (thought open→collapsed across turn end; spinner present during a turn, gone after) — 33 frontend tests green, Go suite untouched.
18+- **Quality**: gate PASS again (a first re-run flagged `ThreadEntry` at 6 returns; fixed by extracting `ThoughtEntry`).
19+- **Also verified this session**: real Claude Code works end to end via `env -u CLAUDECODE ./bin/ori` (nested-session guard); adapter package renamed upstream to `@agentclientprotocol/claude-agent-acp` (default not changed yet).
20+
21+## 2026-09-17 — Workspace panel: preview, editor, terminal, monospace command output (same session, follow-up 2)
22+
23+- **Goal**: user-requested evolutions — a preview panel (syntax-highlighted code + rendered Markdown/AsciiDoc), a terminal panel, a file editor panel (Monaco), and monospace rendering for system-command output in the chat.
24+- **Changes**: backend `internal/files` (list/read/write file API, unrestricted access per user's explicit choice) and `internal/terminal` (PTY shell per WebSocket, creack/pty); frontend workspace layout (chat left + collapsible right panel: file tree with lazy directories, Preview/Editor/Terminal tabs kept mounted when inactive), `MonacoViewer` shared by read-only preview and editor (bundled workers, lazy chunk), AsciiDoc rendering via lazy `@asciidoctor/core`, xterm.js terminal, `tool-output` monospace block for `execute` tool calls. New deps: creack/pty, monaco-editor + @monaco-editor/react, @asciidoctor/core, @xterm/xterm + addon-fit.
25+- **Decisions**: layout chat+workspace (user), interactive user terminal only in v1 (ACP terminals later, user), free filesystem/terminal access (user — sandbox assumed isolated), Monaco over CodeMirror (user); panes hidden not unmounted so the shell survives tab switches; monaco 0.56 import specifiers go through its exports map (no `esm/vs/` prefix).
26+- **Tests**: +9 files API, +5 terminal PTY integration (real shell over real WS: command output, cwd, resize via stty, malformed frames, exit closes socket) → 46 Go tests incl. e2e; front +5 workspace store, +4 FileTree, +8 lang detection, +6 PreviewPane, +6 EditorPane, +4 TerminalPane (xterm/WS mocked), +2 ToolCallCard mono/markdown → 66 front tests. All green, `-race` clean.
27+- **Quality**: gate PASS (0/0/0, total complexity 281). Iterations: biome 1.9 rejects the `noStaticElementInteractions` suppression (rule doesn't exist there — removed), the dangerouslySetInnerHTML suppression must sit between JSX attributes, EditorPane/ToolContent refactored (extracted EditorHeader/editorPlaceholder/TextToolContent) to clear return/complexity smells.
28+- **Docs**: new how-to `use-the-workspace` + reference `workspace-api` in both languages; architecture pages and indexes updated; drawio diagram extended (files, terminal, pty, SPA runtime edges); root README.
29+
30+## 2026-09-17 — Source code analysis document (new session)
31+
32+- **Goal**: analyse the source code and produce a summary as an AsciiDoc document.
33+- **Changes**: new `docs/source-code-analysis.adoc` — overview, backend package table, frontend module/component inventory, HTTP/WS endpoints, bridge protocol summary, test counts (55 Go tests + 1 e2e, ~215 vitest cases), Makefile targets, design decisions, known limitations. No code changed.
34+- **Method**: read `.memory/` first, then an Explore agent verified the memory picture against the sources (LOC: ~3,064 Go / ~3,252 TS-TSX).
35+- **Tests / quality**: not run — documentation-only session, no code touched.
36+
37+## 2026-09-17 — Workspace polish: AsciiDoc fix, VS Code-style tree with Material icons, dark terminal (same session, follow-up 3)
38+
39+- **Goal**: user feedback after trying the workspace — AsciiDoc preview broken in production ("m is not a function"), file tree should look like VS Code's with Material icons, terminal should always be dark.
40+- **Changes**: AsciiDocView now uses @asciidoctor/core v4's real API — `convert` is a **named export and asynchronous** (the v3 `asciidoctor()` factory no longer exists; my CJS-default cast called `undefined()`); added a **non-mocked** AsciiDocView test rendering with the real converter — the mocked-only tests were exactly why the breakage reached production. FileTree restyled VS Code-like: `vscode-material-icons` (910 SVGs copied to `ui/public/material-icons/` by `ui/scripts/copy-icons.mjs`, npm pre-hooks on dev/build, gitignored), chevrons, indent guides, open-folder icons, active-file highlight. Terminal always dark (`#1e1e1e`) whatever the app theme.
41+- **Tests**: +2 real AsciiDoc render, +2 FileTree (icons present, active highlight) → 70 front tests, all green; Go untouched (46).
42+- **Quality**: gate PASS.
43+
44+## 2026-09-17 — Inline monospace for backticked commands (same session, follow-up 4)
45+
46+- **Goal**: user feedback — tool titles like ``Run `ls .memory/` `` showed literal backticks in a proportional font.
47+- **Changes**: new `InlineText` component (backtick spans → `<code class="inline-code">`, no markdown pipeline), applied to tool call titles, permission titles and plan entries; `.inline-code` CSS.
48+- **Tests**: +4 InlineText (spans, multiples, plain, unpaired backtick) and +1 ToolCallCard title assertion → 75 front tests green.
49+
50+## 2026-09-17 — sbx template + kit for ori (same session, follow-up 5)
51+
52+- **Goal**: package ori as a Docker Sandboxes template (image `k33g/ori`, the user builds it under their handle) and add a kit to launch it easily from sbx.
53+- **Changes**: `template/Dockerfile` (multi-stage: node builds the SPA, go builds the binaries, final layer = `docker/sandbox-templates:claude-code` + globally installed `@zed-industries/claude-code-acp` + `/usr/local/bin/{ori,ori-mock-agent}`, EXPOSE 8888, standalone CMD), `.dockerignore` (excludes the 635MB `sandboxes/` checkout, node_modules, .git…), `kits/ori/spec.yaml` (schemaVersion 2 mixin: idempotent `setup.startup` launching ori as the agent user on :8888, agentInstructions), `kits/ori/README.md`, `make template`, how-to `run-in-a-sandbox` in docs en+fr.
54+- **Decisions**: no `sbx template build` exists — a template IS a container image passed via `sbx create claude --template k33g/ori`, and `--kit` accepts a mixin directory (verified in `sandboxes/docs/yml/sbx_create_claude.yaml`); the ori kit adds no network permissions (Anthropic hosts come from the claude agent kit, the adapter is baked into the image); kit startup guarded by `pgrep -x ori` because startup commands re-run on every container start.
55+- **Tests/validation**: image built locally (2.81GB) and smoke-tested standalone with the mock agent (healthz + session OK); the kit's exact startup command exec'd in a sleeping container of the image → adapter session created, healthz OK; idempotency check OK; kit YAML parse-validated.
56+
57+## 2026-09-17 — Kit refinements from the sbx source deep-dive (same session, follow-up 5b)
58+
59+- A thorough exploration of the sbx source confirmed the template/kit design and yielded three refinements applied to `kits/ori/spec.yaml`: `requires: {agent: claude}` (mixin-only field, enforces composition with the claude agent kit), a `ports:` block (container 8888 — kits auto-publish an ephemeral localhost port at create; Dockerfile EXPOSE is read by nothing), and dropping `user: "agent"` from the startup step (StartupCommand.user defaults to "1000", the agent uid; "agent" is non-standard).
60+- Also learned and documented: sandboxd has its own image store — a never-pushed local image reaches it via `docker save` + `sbx template load`; kit references resolve fresh each create (no local kit registry); `sbx kit validate/pack/push` exist for distribution; an alternative shape (kind:sandbox + `extends: claude` + `sandbox.image: k33g/ori`) would allow `sbx create ./kits/ori .` in one argument — not adopted, the mixin matches the dev-toolkit pattern already in use.
61+
62+## 2026-09-17 — build.sh, Hub publication at 0.0.0, parameterized kit port, quickstart (same session, follow-up 5c)
63+
64+- **Goal**: user wants to push `k33g/ori` to Docker Hub — a `template/build.sh` they run themselves, image tagged `0.0.0`; the kit's listen port made configurable; a `quickstart.md` with the sandbox-creation commands.
65+- **Changes**: `template/build.sh` (multi-arch buildx --push, tags 0.0.0 + latest, VERSION as $1, PLATFORMS overridable); `kits/ori/spec.yaml` gained `args.port` (default "8888", pattern-validated) substituted into `ports.container` (unquoted `${{ kit.args.port }}` — the source's expand.go explicitly re-infers it as an integer), the startup `--addr`, and the agentInstructions; `quickstart.md` at the repo root; kit README and both run-in-a-sandbox pages now reference `k33g/ori:0.0.0` and `--kit-arg ori.port=`.
66+- **Validation**: spec YAML parses with the arg refs; build.sh is the push path, `make template` stays the local-only path. The actual Hub push is the user's action (their Docker login).
67+
68+## 2026-09-17 — Multi-arch build fix: no qemu in the template build (same session, follow-up 5d)
69+
70+- **Problem**: the user's `./template/build.sh` failed on the linux/amd64 half — on an arm64 host that stage runs under qemu, where Vite bundling Monaco exhausts Node's 2GB heap ("FATAL ERROR: Ineffective mark-compacts near heap limit", "qemu: uncaught target signal 6").
71+- **Fix**: every build stage now runs on `--platform=$BUILDPLATFORM`: the SPA build is platform-independent (built once, natively), Go cross-compiles with `GOOS=$TARGETOS GOARCH=$TARGETARCH`, and the ACP adapter is npm-installed in a native stage then COPY'd into the template's npm prefix (`/usr/local/share/npm-global`, agent-owned, on PATH). The final stage contains no RUN at all, so a multi-arch build never invokes qemu.
72+- **Validation**: native rebuild green; smoke tests pass (healthz with mock agent; `claude-code-acp` resolves from the copied prefix and initializes a real ACP session).
73+
74+## 2026-09-17 — Kit startup fix: su resets PATH (same session, follow-up 5e)
75+
76+- **Problem**: the user's real `sbx create … --kit ./kits/ori -p 5555:8888` came up with nothing listening. Root cause read from the sbx source (`sandboxlib/kit/durable_startup.go`): startup commands are rendered as `exec su -s /bin/sh -c '<script>' agent`, and util-linux `su` always resets PATH to the login.defs default — which lacks `/usr/local/share/npm-global/bin`, so ori died instantly with "claude-code-acp: executable file not found". Reproduced in the image (`su -c 'command -v claude-code-acp' agent` → missing); my earlier validation had used `docker exec`, which preserves image ENV — that's why it passed.
77+- **Fix**: the kit's startup script now exports `PATH="/usr/local/share/npm-global/bin:/home/agent/.local/bin:$PATH"` first (comment marks it load-bearing). Re-validated through the exact `su` path: healthz OK, ACP session created.
78+- **No image rebuild needed** — kit-side only; the user must `sbx rm` and recreate the sandbox for the new kit to apply. Debug window for kit startup: `/var/log/sbx-kit-startup.log` (dispatcher log) + `/home/agent/.ori.log`.
79+
80+## 2026-09-17 — Session end inside the sandbox: WS-offline investigation in flight (follow-up 5f)
81+
82+- After the su-PATH fix, the user's sandbox serves the SPA on localhost:5555 but the UI shows "offline" (the WebSocket to /ws does not stay up). Established before stopping: ori's WS handshake is correct against browser-like Origin headers (101 for localhost:* origins, 403 for foreign — tested with curl against the image); sbx port publishing goes through gvisor's port mapper (raw L4, so the upgrade should pass). Three discriminating diagnostics were handed to the user (server log; WS handshake from inside the sandbox; WS handshake through the forward) — answers not yet received. The session ends here: the user is relaunching the assistant outside sbx.
83+- Everything since commit 2434cc7 is still uncommitted (user's call, repeatedly deferred).
84+
85+## 2026-09-17 — Why the ori sandbox stops on its own: sbx auto-stop investigation (follow-up 6, read-only)
86+
87+- **Asked**: the sandbox created with `sbx create claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888` serves the webapp then goes `stopped` by itself; find the auto-stop mode in the sbx source and a way to disable it.
88+- **Found** (in `sandboxes/`, the vendored sbx checkout, v0.43.0 daemon on this Mac): sandboxd counts "sentinel" long-poll sessions per sandbox (`GET /sandbox/{name}/session`, `holdSession` in `sandboxd/pkg/server/backend_dockernext.go`); when the count hits zero it stops the container after a hard-coded 30 s grace (`WithAutoStopDelay(30*time.Second)` in `setupDockerNextBackend`). `sbx create` holds a sentinel only for its own lifetime (`executeCreateAndReleaseSession` in `cli-plugin/commands/create.go`), so the stop fires 30 s after the command returns — confirmed by the daemon log (`session disconnected, deferring auto-stop``auto-stopped runtime` 30 s later for `ori` and `ori2`). Port publishing and browser traffic hold no session.
89+- **Escape hatch**: the create-time spec field `detached: true` (openapi `SandboxCreateRequest.detached`) makes `holdSession` never auto-stop. Only `sbx run -d/--detached` sets it (`create_daemon.go: Detached: opts.detached`); `sbx create` has no such flag; there is no update API, no `sbx settings` key and no env var for the delay.
90+- **Recommendation given**: `sbx rm ori` then `sbx run -d claude . --template k33g/ori:0.0.0 --kit ./kits/ori --name ori -p 8888:8888`.
91+- **Docs switched (same session, at the user's request while they tested)**: `quickstart.md`, `kits/ori/README.md`, `docs/en|fr/how-to/run-in-a-sandbox.md` now use `sbx run -d`, explain the 30 s auto-stop and why detached is the fix, and add a restart line (`sbx run -d --name <name>`) plus a daemon-log diagnostic hint. No `sbx create` invocation remains in project docs. No code change.
92+- **Also observed**: the WS-"offline" symptom from follow-up 5f is very plausibly this same stop (the page was already loaded when the container died), still to be confirmed by the user.
93+
94+## 2026-09-17 — Three parallel agents: ori-desktop (Wails), @/skill selectors + draw.io/image previews, ACP command inventory
95+
96+- **Asked**: (bob) create `ori-desktop/`, a Wails app connecting to the ori webapp; (riker) add an `@` file selector, a skill selector, draw.io and image previews; (milo, read-only) list the ACP commands exposed. Then the user built ori-desktop on their Mac: Check OK, Connect "did nothing".
97+- **bob**: new module `ori-desktop/` (Wails v2.16.0, plain template, iframe embed of the ori SPA, settings JSON, bound methods, `internal/settings` + `internal/health` with 12 tests, EN+FR READMEs, `make desktop`/`desktop-test`, .gitignore, ticket 0005 filled). Verified: tests, vet, gofmt, Windows cross-build, root `make build`/`make test` unaffected. Not verifiable in the sandbox: running the window (no gcc/webkit2gtk).
98+- **riker**: `GET /api/files/search`, `GET /api/raw`, new `internal/skills` + `GET /api/skills`; `prompt.attachments` → ACP `resource_link` blocks (`Prompter.Prompt(blocks)` replaces `PromptText`, `Bridge.SetWorkspaceRoot`); reducer stores `available_commands_update` commands; `mentions.ts`, `useCompletion.ts`, `CompletionPopup`, rewritten `PromptInput`, `ImageView`, `DrawioView` (embed.diagrams.net + Source fallback), `lang.ts` kinds `image`/`drawio`; mock agent emits commands and echoes attachments; Vite dev proxy gained `/api`; `biome format --write` reformatted 11 files. Docs EN+FR: workspace-api, websocket-protocol, use-the-workspace, new how-to `mention-files-and-skills`, architecture sections, READMEs. Tickets 0008/0009/0011/0012 filled.
99+- **milo**: ori calls `initialize`, `session/new`, `session/prompt`, `session/cancel`; serves `session/update`, `session/request_permission`, `fs/read_text_file`, `fs/write_text_file`; refuses `terminal/*`; unused stable methods: `authenticate`, `logout`, `session/load|resume|list|close|set_mode|set_config_option`. "Agent commands" are not ACP methods: `available_commands_update``/name` text in the prompt. Relayed to riker, who implemented it (ticket 0008).
100+- **Bug fixed after the user's test**: `ori-desktop/frontend/src/main.css``#connect-screen {display:grid}` outranked `.screen[hidden]`, so Connect (and the startup auto-connect) rendered the viewer below the fold; now class `.connect` + `.screen[hidden] {display:none !important}`. Rebuild needed (`wails build`); iframe content in WKWebView still unconfirmed.
101+- **Tests**: `make test` → 9 Go packages ok, vitest 16 files / 102 tests (was 66); `go test -race ./...` ok (riker installed gcc + libc6-dev via apt in the sandbox for it).
102+- **Quality gate**: NOT run — qlty absent from this sandbox (`~/.qlty` missing). gofmt / go vet / biome check clean.
103+- Nothing committed (still everything since 2434cc7).
104+
105+## 2026-09-17 — Launch ori for manual testing of the new features (evening session)
106+
107+- **Asked**: "lance ori que je puisse tester les nouveautés". No code change: `make build` (SPA + binaries), then `env -u CLAUDECODE nohup ./bin/ori --addr 0.0.0.0:8888 > /tmp/ori.log 2>&1 &` inside the `claude-ori` sandbox. Adapter `@zed-industries/claude-code-acp@0.16.2` resolved via npx (npm now flags it deprecated → renamed `@agentclientprotocol/claude-agent-acp`; still works). ACP session ready, `/healthz` ok, `/api/skills` lists the skills. Port 8888 must be published on the host with `sbx ports claude-ori --publish 8888:8888/tcp` (not doable from inside the sandbox).
108+- Trap hit: a `curl` WebSocket handshake against `/ws` never returns on success (101 keeps the socket open) — use `-m` or check the log instead.
109+- **Follow-up (same session)**: first real prompt failed with `-32603 There's an issue with the selected model (fable[1m])`. Root cause: `@zed-industries/claude-code-acp@0.16.2` bundles `@anthropic-ai/claude-agent-sdk@0.2.44`, whose embedded Claude Code CLI is **2.1.44** (zero mentions of Fable), while `~/.claude/settings.json` sets `"model": "fable[1m]"` and the adapter forwards it verbatim. The system CLI (2.1.275) accepts `fable[1m]` fine. Fix without touching the user's settings: the adapter honours `CLAUDE_CODE_EXECUTABLE``pathToClaudeCodeExecutable`; ori relaunched with `CLAUDE_CODE_EXECUTABLE=/home/agent/.local/share/claude/versions/2.1.275`; a WS prompt round-trip returned `pong` (`turn_ended end_turn`). No code change.
110+
111+## 2026-09-17 — Resizable file explorer column (ticket 0015), via methodical-dev
112+
113+- **Goal**: make the workspace file tree column resizable with the mouse; user chose localStorage persistence and the file-tree divider only (not the chat/workspace split).
114+- **Changes**: `ui/src/workspace.ts` (`fileTreeWidth`, `setFileTreeWidth`, `resetFileTreeWidth`, `clampFileTreeWidth`, constants, guarded localStorage), new `ui/src/components/ResizeHandle.tsx` (+ test), `Workspace.tsx` (handle between tree and tabs, `--filetree-width` on the aside; new `Workspace.test.tsx`), `app.css` (`.filetree` width from the variable, `.resize-handle` styles). Ticket 0015 closed.
115+- **Decisions**: width in px (drag produces px; comparable with bounds); FileTree untouched — width reaches CSS via a custom property; handle owns no width, the store clamps. jsdom 26 has no PointerEvent → test-local MouseEvent polyfill.
116+- **Quality gate** (first run of qlty on the tree since the parallel-agents session): 12 errors / 1 warning / 2 smells, all pre-existing — fixed without weakening anything: `ori-desktop/frontend/src/main.js` became an ES module (`type="module"`, IIFE and `"use strict"` dropped, optional chains, template literals, `reloadFrame()` instead of `src = src`), `internal/files/raw.go` explicit `_ = file.Close()` with rationale, `internal/files/search.go` walk callback → `searchWalker.visit/visitDir/collect`, `PreviewPane.tsx` loading effect → `usePreviewFile` hook; `qlty fmt` on main.css / main.js / wails.json. Run #15 PASS (0/0/0, complex 507).
117+- **Tests**: 118 vitest (18 files), 9 Go packages, ori-desktop 2 packages — green. `make test`.
118+- **Docs**: how-to `use-the-workspace` (step 5 + keyboard/persistence variant) and architecture explanation, EN + FR; root README; ori-desktop READMEs (ES module).
119+- **Not verifiable here**: the ori-desktop frontend change (ES module) needs a `wails build` + launch on the user's Mac.
120+
121+## 2026-09-17 — Light / dark theme selector, light by default (ticket 0013)
122+
123+- **Goal**: user request received mid-session; choices made via AskUserQuestion: two themes only (no "system" option), single review at the end.
124+- **Changes**: new `ui/src/theme.ts` (+ `theme.test.ts`), `app.css` (dark palette on `:root[data-theme="dark"]`, `color-scheme` per theme, `.topbar-theme`), `App.tsx` (☾/☀ button, aria-label "Switch to dark/light theme"), `MonacoViewer.tsx` (store instead of `matchMedia`), `main.tsx` (side-effect import so the theme is stamped before the first render), `App.test.tsx` (+1). Ticket 0013 closed.
125+- **Decisions**: dropping `prefers-color-scheme` is a behaviour change (dark-OS users now get light until they toggle) — accepted by the user ("light par défaut"); documented in the architecture explanation. Terminal keeps its dark background (pre-existing decision).
126+- **Tests**: 124 vitest / 19 files green; Go untouched. **Quality**: run #16 PASS (0/0/0, complex 514).
127+- **Docs**: new how-to `switch-the-theme.md` EN + FR, both indexes, architecture explanation paragraph EN + FR, root README.
128+- ori rebuilt and restarted on :8888 for the user's review.
added .memory/summary.md +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+# Ori — project summary
2+
3+## What it is
4+
5+Ori is a web client for ACP (Agent Client Protocol, https://agentclientprotocol.com) code agents: a Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent subprocess — Claude Code via the `@zed-industries/claude-code-acp` adapter by default. The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts, tool call cards (with statuses, locations and diffs), the agent's plan, and permission prompts answered from the UI. Implements ticket `.tickets/issues/0004-specifications.yaml`.
6+
7+## Architecture
8+
9+```
10+browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess
11+```
12+
13+- Go module: `rickub.com/bots-garden/ori` (NOT github — the repo is not hosted on GitHub; the user corrected this explicitly).
14+- `cmd/ori` — server binary; flags `--addr` (default `:8888`), `--cwd`, `--agent-cmd` (whitespace-split).
15+- `cmd/ori-mock-agent` — deterministic demo ACP agent on stdio (thought → plan → read tool call → permission → edit tool call with diff → answer); used by `make run-mock` and the e2e test.
16+- `internal/config` — flag parsing/validation.
17+- `internal/httpserver` — embedded SPA (via `ui/embed.go`, `go:embed all:dist`), `/healthz`, SPA fallback, extra-route injection point for `/ws`.
18+- `internal/agent` — spawns the agent, drives the ACP lifecycle through `github.com/coder/acp-go-sdk` v0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); generic `Handler` interface (session updates + blocking permission requests); real fs read/write; terminals refused (capability not announced).
19+- `internal/files` — workspace file API (`GET /api/files`, `GET /api/file`, `PUT /api/file`, plus `GET /api/files/search?q=&limit=` — recursive, case-insensitive, skips `.git`/`node_modules`, default 50 / max 500 results, walk capped at 50 000 entries — and `GET /api/raw?path=` streaming bytes with pinned image Content-Types, Range support, no size cap): list/read/write, relative paths against `--cwd`, absolute allowed; access deliberately unrestricted (user's choice: ori targets already-isolated sandboxes); binary → 415, >5MiB → 413.
20+- `internal/terminal` — one shell per WebSocket on `GET /ws/terminal` in a PTY (`creack/pty` v1.1.24); binary frames out, JSON `input`/`resize` frames in; shell from `$SHELL`→bash→sh.
21+- `internal/skills``GET /api/skills`: discovers `<cwd>/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md` (frontmatter `name`/`description`, directory-name fallback, project shadows user, sorted).
22+- `internal/bridge` — hub between agent and browsers: JSON protocol (see `docs/*/reference/websocket-protocol.md`), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection, `user_message` echo so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). The `prompt` message carries optional `attachments: [{path,name}]`, turned server-side into ACP `resource_link` blocks (`file://<abs>`) next to the text block — `Prompter.Prompt(ctx, []acp.ContentBlock)` replaced `PromptText`; relative paths are joined to the root given by `Bridge.SetWorkspaceRoot` (wired in `cmd/ori/main.go`); `user_message` echoes the attachments. Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost.
23+- `ui/` — Vite + React 19 + TS strict; zustand store around a pure reducer (`ui/src/reducer.ts`, handler-table style); WS client with backoff reconnect; thread resets on `hello` so replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand store `ui/src/workspace.ts`): file tree (click=preview, double-click=edit; column resizable via `ResizeHandle` — ARIA separator, pointer drag / arrows / Home-End / double-click reset — width `fileTreeWidth` in the store, bounded 120–800 px, default 224, applied through the `--filetree-width` CSS custom property, persisted in localStorage `ori.fileTreeWidth`). Colour theme (`ui/src/theme.ts`, own zustand store): `light` (default) | `dark`, stamped as `data-theme` on `<html>` at module load and on change, remembered in localStorage `ori.theme`, toggled by the header ☾/☀ button; `app.css` keys the dark palette on `:root[data-theme="dark"]` (the former `prefers-color-scheme` media query is gone — decision: explicit choice, light default), `MonacoViewer` reads the store (`vs-dark`/`light`), the terminal stays dark. Preview tab (Monaco read-only for code — bundled workers, no CDN, lazy chunk ~1MB gzip; rendered Markdown; rendered AsciiDoc via lazily-imported `@asciidoctor/core`, Rendered/Source toggle), Editor tab (Monaco, dirty ●, Save/Ctrl+S via PUT /api/file), Terminal tab (xterm.js on /ws/terminal, starts on first activation); inactive panes are hidden not unmounted so shell/edit state survives tab switches; `execute` tool-call output renders in a monospace `<pre>`, never markdown. Composer completions (`ui/src/mentions.ts` pure helpers, `ui/src/useCompletion.ts`, `CompletionPopup`): `@` opens a workspace file search (→ attachments), `/` opens a merged list of local skills (`/api/skills`) and ACP `available_commands_update` commands (stored as `commands` in the reducer, cleared on `hello`; deduped by name, skill wins) and inserts `/<name> ` as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kinds `image` (`ImageView`, `<img>` over checkerboard via `/api/raw`, natural size shown) and `drawio` (`DrawioView`: iframe on `embed.diagrams.net` fed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback; `.drawio.svg/.png` are images). The mock agent emits `available_commands_update` (`review`, `compact`) on session start and echoes `[attached: name]`.
24+- `ori-desktop/` — separate Go module `rickub.com/bots-garden/ori-desktop` (Wails v2.16.0, Go ≥ 1.25, "plain" template, `//go:embed all:frontend/src`, no npm; `frontend/src/main.js` is loaded as an ES module — `type="module"` — since the 2026-09-17 lint cleanup): connection screen (URL remembered in `<UserConfigDir>/ori-desktop/settings.json`, key `serverUrl`; auto-connect at startup), `/healthz` probe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an `<iframe>` (Wails v2 cannot navigate the main webview to an external URL; `runtime.BrowserOpenURL` is the fallback). Ticket 0005. `make desktop` / `make desktop-test`; root `go test ./...` unaffected (nested module).
25+
26+## Key decisions in force
27+
28+- Claude Code is reached through the official Zed adapter (`npx -y @zed-industries/claude-code-acp`) — the `claude` CLI (v2.1.274) has no native ACP mode; verified.
29+- draw.io diagrams render online via `embed.diagrams.net` (no bundlable offline renderer exists: `viewer.min.js` is not on npm, `mxgraph` is archived and lacks draw.io's shapes, `drawio2svg` is GPL); future offline path = vendoring `viewer-static.min.js` (~3 MB, Apache-2.0). Documented in the architecture explanations.
30+- ACP payloads cross the WebSocket verbatim (raw `SessionUpdate` / `RequestPermissionRequest`), keeping the front forward-compatible.
31+- One agent session shared by all browsers in v1; multi-session is a planned evolution seam (`bridge.Prompter` interface, per-connection subscriptions).
32+- The server owns the thread (history + user_message echo); the SPA rebuilds from replay on every (re)connection.
33+- Quality config decisions (all user-approved): `.claude/**` excluded from qlty (vendored kit sources, the measurement instrument itself); `radarlint-python` plugin removed (its ~300MB JDK cannot fit the dedicated 488MB `~/.qlty` volume — diagnosis in `.qlty/qlty.toml` comments); `golangci-lint` + `biome` added manually (init missed both languages); `biome.json` at root parses `.vscode/**` as JSONC.
34+
35+## Build / test / run
36+
37+- `make deps` — npm install in `ui/`.
38+- `make build` — SPA build then Go binaries (`bin/ori`, `bin/ori-mock-agent`). Order matters: the Go build embeds `ui/dist`.
39+- `make test``go test ./...` + `vitest run`. Also useful: `go test -race ./...`, `go test -short ./...` (skips e2e).
40+- `make run` — with Claude Code adapter; `make run-mock` — with the demo agent (no Claude/network needed).
41+- `make template` — builds the `k33g/ori` sandbox template image (`template/Dockerfile`, FROM `docker/sandbox-templates:claude-code`); launch with `sbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori repo>/kits/ori --name ori -p 8888:8888` (detached — see the auto-stop trap below) (see `kits/ori/README.md` and docs how-to `run-in-a-sandbox`).
42+- **sbx auto-stop trap (verified in the sbx source + daemon log, 2026-09-17)**: sandboxd stops a sandbox 30 s (hard-coded `WithAutoStopDelay(30*time.Second)`, `sandboxd/pkg/server/backend_dockernext.go`) after its last CLI "sentinel" session closes; `sbx create`/`sbx exec`/`sbx run` all hold one for their own duration, browser traffic on a published port counts for nothing. Only a sandbox created with the spec field `detached: true` (CLI: `sbx run -d …`, absent from `sbx create`) is exempt; the field is create-time only (no PATCH), so an existing sandbox must be `sbx rm`'d and recreated with `sbx run -d`. No settings/env knob for the delay.
43+- Quality: `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` (gate PASS, run #16, 2026-09-17 late evening: 0 errors, 0 warnings, 0 smells, complex 514 — covers the selectors/previews/desktop work, the resizable file tree and the light/dark theme).
44+- `docs/source-code-analysis.adoc` — AsciiDoc snapshot analysis of the codebase (2026-09-17), outside the bilingual Diátaxis set.
45+
46+## Environment facts (sandbox)
47+
48+- `~/.qlty` is a dedicated 488MB volume, too small for qlty's tool cache; the cache was moved to `~/.qlty-cache` on the main disk with a symlink `~/.qlty/cache -> /home/agent/.qlty-cache`. If qlty reports "No space left on device", check this symlink survived. (It did NOT survive a sandbox recreation on 2026-09-17: the volume came back empty and the first run failed exactly that way; recreating the symlink fixed it.)
49+- `~/.npm` is the same kind of 488MB dedicated volume and filled up when installing Monaco; the npm cache now lives at `~/.npm-big` via `NPM_CONFIG_CACHE` exported in `/etc/sandbox-persistent.sh`.
50+- Running ori inside this Claude-Code-driven sandbox requires `env -u CLAUDECODE ./bin/ori` (Claude refuses nested sessions). Also set `CLAUDE_CODE_EXECUTABLE=<current claude binary>` (e.g. `/home/agent/.local/share/claude/versions/2.1.275`): the Zed adapter 0.16.2 bundles Claude Code 2.1.44, which rejects the `fable[1m]` model from `~/.claude/settings.json`.
51+- npm blocks install scripts by default here; esbuild's postinstall was approved via `npm approve-scripts` (recorded in `ui/package.json` `allowScripts`).
52+
53+## Known limitations
54+
55+- `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
56+- One prompt turn at a time; a second `prompt` while a turn runs is rejected with an error event.
57+- Terminal ACP methods are not supported (declared absent; answered with JSON-RPC method-not-found).
58+- `user_message` / thought / message chunks only render text content blocks; images/audio/resources are not rendered yet; `user_message.attachments` are not rendered as chips (only the `@path` text mention).
59+- Skills list is fetched once per page load; the file search walks whatever sits under `--cwd` (an untracked `sandboxes/` clone at the repo root gets walked).
60+
61+## Not yet established
62+
63+- ori-desktop: the window was launched by the user on their Mac (build OK, Check OK); after the CSS specificity fix (`#connect-screen` outranked `.screen[hidden]`, so the connect screen never hid and the viewer rendered below the fold) it has not yet been confirmed that the iframe actually shows ori inside WKWebView (`wails://` origin loading `http://localhost`; `NSAllowsLocalNetworking` set in `build/darwin/Info.plist`).
64+
65+- Behaviour against a real Claude Code session (auth flow, real tool calls) has not been exercised in this sandbox — only against the mock agent and unit doubles.
66+- Whether the repo has a remote to push to (no git remote configured at the time of writing).
new file mode 100644
@@ -0,0 +1,66 @@
1+# Ori — project summary
2+
3+## What it is
4+
5+Ori is a web client for ACP (Agent Client Protocol, https://agentclientprotocol.com) code agents: a Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent subprocess — Claude Code via the `@zed-industries/claude-code-acp` adapter by default. The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts, tool call cards (with statuses, locations and diffs), the agent's plan, and permission prompts answered from the UI. Implements ticket `.tickets/issues/0004-specifications.yaml`.
6+
7+## Architecture
8+
9+```
10+browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess
11+```
12+
13+- Go module: `rickub.com/bots-garden/ori` (NOT github — the repo is not hosted on GitHub; the user corrected this explicitly).
14+- `cmd/ori` — server binary; flags `--addr` (default `:8888`), `--cwd`, `--agent-cmd` (whitespace-split).
15+- `cmd/ori-mock-agent` — deterministic demo ACP agent on stdio (thought → plan → read tool call → permission → edit tool call with diff → answer); used by `make run-mock` and the e2e test.
16+- `internal/config` — flag parsing/validation.
17+- `internal/httpserver` — embedded SPA (via `ui/embed.go`, `go:embed all:dist`), `/healthz`, SPA fallback, extra-route injection point for `/ws`.
18+- `internal/agent` — spawns the agent, drives the ACP lifecycle through `github.com/coder/acp-go-sdk` v0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); generic `Handler` interface (session updates + blocking permission requests); real fs read/write; terminals refused (capability not announced).
19+- `internal/files` — workspace file API (`GET /api/files`, `GET /api/file`, `PUT /api/file`, plus `GET /api/files/search?q=&limit=` — recursive, case-insensitive, skips `.git`/`node_modules`, default 50 / max 500 results, walk capped at 50 000 entries — and `GET /api/raw?path=` streaming bytes with pinned image Content-Types, Range support, no size cap): list/read/write, relative paths against `--cwd`, absolute allowed; access deliberately unrestricted (user's choice: ori targets already-isolated sandboxes); binary → 415, >5MiB → 413.
20+- `internal/terminal` — one shell per WebSocket on `GET /ws/terminal` in a PTY (`creack/pty` v1.1.24); binary frames out, JSON `input`/`resize` frames in; shell from `$SHELL`→bash→sh.
21+- `internal/skills``GET /api/skills`: discovers `<cwd>/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md` (frontmatter `name`/`description`, directory-name fallback, project shadows user, sorted).
22+- `internal/bridge` — hub between agent and browsers: JSON protocol (see `docs/*/reference/websocket-protocol.md`), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection, `user_message` echo so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). The `prompt` message carries optional `attachments: [{path,name}]`, turned server-side into ACP `resource_link` blocks (`file://<abs>`) next to the text block — `Prompter.Prompt(ctx, []acp.ContentBlock)` replaced `PromptText`; relative paths are joined to the root given by `Bridge.SetWorkspaceRoot` (wired in `cmd/ori/main.go`); `user_message` echoes the attachments. Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost.
23+- `ui/` — Vite + React 19 + TS strict; zustand store around a pure reducer (`ui/src/reducer.ts`, handler-table style); WS client with backoff reconnect; thread resets on `hello` so replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand store `ui/src/workspace.ts`): file tree (click=preview, double-click=edit; column resizable via `ResizeHandle` — ARIA separator, pointer drag / arrows / Home-End / double-click reset — width `fileTreeWidth` in the store, bounded 120–800 px, default 224, applied through the `--filetree-width` CSS custom property, persisted in localStorage `ori.fileTreeWidth`). Colour theme (`ui/src/theme.ts`, own zustand store): `light` (default) | `dark`, stamped as `data-theme` on `<html>` at module load and on change, remembered in localStorage `ori.theme`, toggled by the header ☾/☀ button; `app.css` keys the dark palette on `:root[data-theme="dark"]` (the former `prefers-color-scheme` media query is gone — decision: explicit choice, light default), `MonacoViewer` reads the store (`vs-dark`/`light`), the terminal stays dark. Preview tab (Monaco read-only for code — bundled workers, no CDN, lazy chunk ~1MB gzip; rendered Markdown; rendered AsciiDoc via lazily-imported `@asciidoctor/core`, Rendered/Source toggle), Editor tab (Monaco, dirty ●, Save/Ctrl+S via PUT /api/file), Terminal tab (xterm.js on /ws/terminal, starts on first activation); inactive panes are hidden not unmounted so shell/edit state survives tab switches; `execute` tool-call output renders in a monospace `<pre>`, never markdown. Composer completions (`ui/src/mentions.ts` pure helpers, `ui/src/useCompletion.ts`, `CompletionPopup`): `@` opens a workspace file search (→ attachments), `/` opens a merged list of local skills (`/api/skills`) and ACP `available_commands_update` commands (stored as `commands` in the reducer, cleared on `hello`; deduped by name, skill wins) and inserts `/<name> ` as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kinds `image` (`ImageView`, `<img>` over checkerboard via `/api/raw`, natural size shown) and `drawio` (`DrawioView`: iframe on `embed.diagrams.net` fed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback; `.drawio.svg/.png` are images). The mock agent emits `available_commands_update` (`review`, `compact`) on session start and echoes `[attached: name]`.
24+- `ori-desktop/` — separate Go module `rickub.com/bots-garden/ori-desktop` (Wails v2.16.0, Go ≥ 1.25, "plain" template, `//go:embed all:frontend/src`, no npm; `frontend/src/main.js` is loaded as an ES module — `type="module"` — since the 2026-09-17 lint cleanup): connection screen (URL remembered in `<UserConfigDir>/ori-desktop/settings.json`, key `serverUrl`; auto-connect at startup), `/healthz` probe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an `<iframe>` (Wails v2 cannot navigate the main webview to an external URL; `runtime.BrowserOpenURL` is the fallback). Ticket 0005. `make desktop` / `make desktop-test`; root `go test ./...` unaffected (nested module).
25+
26+## Key decisions in force
27+
28+- Claude Code is reached through the official Zed adapter (`npx -y @zed-industries/claude-code-acp`) — the `claude` CLI (v2.1.274) has no native ACP mode; verified.
29+- draw.io diagrams render online via `embed.diagrams.net` (no bundlable offline renderer exists: `viewer.min.js` is not on npm, `mxgraph` is archived and lacks draw.io's shapes, `drawio2svg` is GPL); future offline path = vendoring `viewer-static.min.js` (~3 MB, Apache-2.0). Documented in the architecture explanations.
30+- ACP payloads cross the WebSocket verbatim (raw `SessionUpdate` / `RequestPermissionRequest`), keeping the front forward-compatible.
31+- One agent session shared by all browsers in v1; multi-session is a planned evolution seam (`bridge.Prompter` interface, per-connection subscriptions).
32+- The server owns the thread (history + user_message echo); the SPA rebuilds from replay on every (re)connection.
33+- Quality config decisions (all user-approved): `.claude/**` excluded from qlty (vendored kit sources, the measurement instrument itself); `radarlint-python` plugin removed (its ~300MB JDK cannot fit the dedicated 488MB `~/.qlty` volume — diagnosis in `.qlty/qlty.toml` comments); `golangci-lint` + `biome` added manually (init missed both languages); `biome.json` at root parses `.vscode/**` as JSONC.
34+
35+## Build / test / run
36+
37+- `make deps` — npm install in `ui/`.
38+- `make build` — SPA build then Go binaries (`bin/ori`, `bin/ori-mock-agent`). Order matters: the Go build embeds `ui/dist`.
39+- `make test``go test ./...` + `vitest run`. Also useful: `go test -race ./...`, `go test -short ./...` (skips e2e).
40+- `make run` — with Claude Code adapter; `make run-mock` — with the demo agent (no Claude/network needed).
41+- `make template` — builds the `k33g/ori` sandbox template image (`template/Dockerfile`, FROM `docker/sandbox-templates:claude-code`); launch with `sbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori repo>/kits/ori --name ori -p 8888:8888` (detached — see the auto-stop trap below) (see `kits/ori/README.md` and docs how-to `run-in-a-sandbox`).
42+- **sbx auto-stop trap (verified in the sbx source + daemon log, 2026-09-17)**: sandboxd stops a sandbox 30 s (hard-coded `WithAutoStopDelay(30*time.Second)`, `sandboxd/pkg/server/backend_dockernext.go`) after its last CLI "sentinel" session closes; `sbx create`/`sbx exec`/`sbx run` all hold one for their own duration, browser traffic on a published port counts for nothing. Only a sandbox created with the spec field `detached: true` (CLI: `sbx run -d …`, absent from `sbx create`) is exempt; the field is create-time only (no PATCH), so an existing sandbox must be `sbx rm`'d and recreated with `sbx run -d`. No settings/env knob for the delay.
43+- Quality: `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` (gate PASS, run #16, 2026-09-17 late evening: 0 errors, 0 warnings, 0 smells, complex 514 — covers the selectors/previews/desktop work, the resizable file tree and the light/dark theme).
44+- `docs/source-code-analysis.adoc` — AsciiDoc snapshot analysis of the codebase (2026-09-17), outside the bilingual Diátaxis set.
45+
46+## Environment facts (sandbox)
47+
48+- `~/.qlty` is a dedicated 488MB volume, too small for qlty's tool cache; the cache was moved to `~/.qlty-cache` on the main disk with a symlink `~/.qlty/cache -> /home/agent/.qlty-cache`. If qlty reports "No space left on device", check this symlink survived. (It did NOT survive a sandbox recreation on 2026-09-17: the volume came back empty and the first run failed exactly that way; recreating the symlink fixed it.)
49+- `~/.npm` is the same kind of 488MB dedicated volume and filled up when installing Monaco; the npm cache now lives at `~/.npm-big` via `NPM_CONFIG_CACHE` exported in `/etc/sandbox-persistent.sh`.
50+- Running ori inside this Claude-Code-driven sandbox requires `env -u CLAUDECODE ./bin/ori` (Claude refuses nested sessions). Also set `CLAUDE_CODE_EXECUTABLE=<current claude binary>` (e.g. `/home/agent/.local/share/claude/versions/2.1.275`): the Zed adapter 0.16.2 bundles Claude Code 2.1.44, which rejects the `fable[1m]` model from `~/.claude/settings.json`.
51+- npm blocks install scripts by default here; esbuild's postinstall was approved via `npm approve-scripts` (recorded in `ui/package.json` `allowScripts`).
52+
53+## Known limitations
54+
55+- `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
56+- One prompt turn at a time; a second `prompt` while a turn runs is rejected with an error event.
57+- Terminal ACP methods are not supported (declared absent; answered with JSON-RPC method-not-found).
58+- `user_message` / thought / message chunks only render text content blocks; images/audio/resources are not rendered yet; `user_message.attachments` are not rendered as chips (only the `@path` text mention).
59+- Skills list is fetched once per page load; the file search walks whatever sits under `--cwd` (an untracked `sandboxes/` clone at the repo root gets walked).
60+
61+## Not yet established
62+
63+- ori-desktop: the window was launched by the user on their Mac (build OK, Check OK); after the CSS specificity fix (`#connect-screen` outranked `.screen[hidden]`, so the connect screen never hid and the viewer rendered below the fold) it has not yet been confirmed that the iframe actually shows ori inside WKWebView (`wails://` origin loading `http://localhost`; `NSAllowsLocalNetworking` set in `build/darwin/Info.plist`).
64+
65+- Behaviour against a real Claude Code session (auth flow, real tool calls) has not been exercised in this sandbox — only against the mock agent and unit doubles.
66+- Whether the repo has a remote to push to (no git remote configured at the time of writing).
modified .quality/history.jsonl +12 -0
@@ -2,3 +2,15 @@
22 {"branch": "feat/acp-webapp", "breaches": ["error-level issues: 13 (max 0)", "warning-level issues: 4 (max 0)", "code smells: 5 (max 0)"], "commit": "4edda86", "counts": {"error": 13, "note": 21, "warning": 4}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 158, "cyclo": 307, "fields": 39, "funcs": 92, "lcom": 0, "lines": 1989, "loc": 1398}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 5, "timestamp": "2026-09-17T19:10:18Z"}
33 {"branch": "feat/acp-webapp", "breaches": ["code smells: 2 (max 0)"], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 146, "cyclo": 294, "fields": 39, "funcs": 106, "lcom": 0, "lines": 2064, "loc": 1453}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 2, "timestamp": "2026-09-17T19:14:34Z"}
44 {"branch": "feat/acp-webapp", "breaches": [], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 27, "complex": 145, "cyclo": 294, "fields": 40, "funcs": 111, "lcom": 0, "lines": 2068, "loc": 1451}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-09-17T19:15:21Z"}
5+{"branch": "feat/acp-webapp", "breaches": ["code smells: 1 (max 0)"], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 147, "cyclo": 298, "fields": 40, "funcs": 112, "lcom": 0, "lines": 2123, "loc": 1488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 5, "smells": 1, "timestamp": "2026-09-17T19:35:29Z"}
6+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 27, "complex": 146, "cyclo": 298, "fields": 40, "funcs": 113, "lcom": 0, "lines": 2130, "loc": 1491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 6, "smells": 0, "timestamp": "2026-09-17T19:36:00Z"}
7+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 2 (max 0)", "warning-level issues: 1 (max 0)", "code smells: 3 (max 0)"], "commit": "2434cc7", "counts": {"error": 2, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 34, "complex": 284, "cyclo": 503, "fields": 53, "funcs": 168, "lcom": 0, "lines": 3387, "loc": 2446}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 7, "smells": 3, "timestamp": "2026-09-17T20:05:32Z"}
8+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 1 (max 0)", "warning-level issues: 1 (max 0)"], "commit": "2434cc7", "counts": {"error": 1, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 34, "complex": 281, "cyclo": 504, "fields": 53, "funcs": 171, "lcom": 0, "lines": 3436, "loc": 2488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 8, "smells": 0, "timestamp": "2026-09-17T20:06:46Z"}
9+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 281, "cyclo": 504, "fields": 53, "funcs": 171, "lcom": 0, "lines": 3436, "loc": 2488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 9, "smells": 0, "timestamp": "2026-09-17T20:08:01Z"}
10+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 286, "cyclo": 518, "fields": 53, "funcs": 172, "lcom": 0, "lines": 3493, "loc": 2534}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 10, "smells": 0, "timestamp": "2026-09-17T20:25:57Z"}
11+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {"note": 2}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 288, "cyclo": 523, "fields": 53, "funcs": 173, "lcom": 0, "lines": 3527, "loc": 2557}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 11, "smells": 0, "timestamp": "2026-09-17T20:55:17Z"}
12+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {"note": 2}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 288, "cyclo": 523, "fields": 53, "funcs": 173, "lcom": 0, "lines": 3527, "loc": 2557}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 12, "smells": 0, "timestamp": "2026-09-17T21:04:25Z"}
13+{"branch": "feat/acp-webapp", "breaches": ["code smells: 2 (max 0)"], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 54, "complex": 517, "cyclo": 888, "fields": 75, "funcs": 255, "lcom": 0, "lines": 5523, "loc": 4005}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 13, "smells": 2, "timestamp": "2026-09-17T23:07:19Z"}
14+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 12 (max 0)", "warning-level issues: 1 (max 0)", "code smells: 2 (max 0)"], "commit": "2434cc7", "counts": {"error": 12, "note": 4, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 54, "complex": 517, "cyclo": 888, "fields": 75, "funcs": 255, "lcom": 0, "lines": 5523, "loc": 4005}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 14, "smells": 2, "timestamp": "2026-09-17T23:07:54Z"}
15+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 55, "complex": 507, "cyclo": 877, "fields": 79, "funcs": 260, "lcom": 0, "lines": 5570, "loc": 4030}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 15, "smells": 0, "timestamp": "2026-09-17T23:10:03Z"}
16+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 56, "complex": 514, "cyclo": 893, "fields": 79, "funcs": 267, "lcom": 0, "lines": 5671, "loc": 4082}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 16, "smells": 0, "timestamp": "2026-09-17T23:15:34Z"}
@@ -2,3 +2,15 @@
2 {"branch": "feat/acp-webapp", "breaches": ["error-level issues: 13 (max 0)", "warning-level issues: 4 (max 0)", "code smells: 5 (max 0)"], "commit": "4edda86", "counts": {"error": 13, "note": 21, "warning": 4}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 158, "cyclo": 307, "fields": 39, "funcs": 92, "lcom": 0, "lines": 1989, "loc": 1398}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 5, "timestamp": "2026-09-17T19:10:18Z"}2 {"branch": "feat/acp-webapp", "breaches": ["error-level issues: 13 (max 0)", "warning-level issues: 4 (max 0)", "code smells: 5 (max 0)"], "commit": "4edda86", "counts": {"error": 13, "note": 21, "warning": 4}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 158, "cyclo": 307, "fields": 39, "funcs": 92, "lcom": 0, "lines": 1989, "loc": 1398}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 5, "timestamp": "2026-09-17T19:10:18Z"}
3 {"branch": "feat/acp-webapp", "breaches": ["code smells: 2 (max 0)"], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 146, "cyclo": 294, "fields": 39, "funcs": 106, "lcom": 0, "lines": 2064, "loc": 1453}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 2, "timestamp": "2026-09-17T19:14:34Z"}3 {"branch": "feat/acp-webapp", "breaches": ["code smells: 2 (max 0)"], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 146, "cyclo": 294, "fields": 39, "funcs": 106, "lcom": 0, "lines": 2064, "loc": 1453}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 2, "timestamp": "2026-09-17T19:14:34Z"}
4 {"branch": "feat/acp-webapp", "breaches": [], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 27, "complex": 145, "cyclo": 294, "fields": 40, "funcs": 111, "lcom": 0, "lines": 2068, "loc": 1451}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-09-17T19:15:21Z"}4 {"branch": "feat/acp-webapp", "breaches": [], "commit": "4edda86", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 27, "complex": 145, "cyclo": 294, "fields": 40, "funcs": 111, "lcom": 0, "lines": 2068, "loc": 1451}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-09-17T19:15:21Z"}
5+{"branch": "feat/acp-webapp", "breaches": ["code smells: 1 (max 0)"], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 27, "complex": 147, "cyclo": 298, "fields": 40, "funcs": 112, "lcom": 0, "lines": 2123, "loc": 1488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 5, "smells": 1, "timestamp": "2026-09-17T19:35:29Z"}
6+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 27, "complex": 146, "cyclo": 298, "fields": 40, "funcs": 113, "lcom": 0, "lines": 2130, "loc": 1491}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 6, "smells": 0, "timestamp": "2026-09-17T19:36:00Z"}
7+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 2 (max 0)", "warning-level issues: 1 (max 0)", "code smells: 3 (max 0)"], "commit": "2434cc7", "counts": {"error": 2, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 34, "complex": 284, "cyclo": 503, "fields": 53, "funcs": 168, "lcom": 0, "lines": 3387, "loc": 2446}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 7, "smells": 3, "timestamp": "2026-09-17T20:05:32Z"}
8+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 1 (max 0)", "warning-level issues: 1 (max 0)"], "commit": "2434cc7", "counts": {"error": 1, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 34, "complex": 281, "cyclo": 504, "fields": 53, "funcs": 171, "lcom": 0, "lines": 3436, "loc": 2488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 8, "smells": 0, "timestamp": "2026-09-17T20:06:46Z"}
9+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 281, "cyclo": 504, "fields": 53, "funcs": 171, "lcom": 0, "lines": 3436, "loc": 2488}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 9, "smells": 0, "timestamp": "2026-09-17T20:08:01Z"}
10+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 286, "cyclo": 518, "fields": 53, "funcs": 172, "lcom": 0, "lines": 3493, "loc": 2534}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 10, "smells": 0, "timestamp": "2026-09-17T20:25:57Z"}
11+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {"note": 2}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 288, "cyclo": 523, "fields": 53, "funcs": 173, "lcom": 0, "lines": 3527, "loc": 2557}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 11, "smells": 0, "timestamp": "2026-09-17T20:55:17Z"}
12+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {"note": 2}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 34, "complex": 288, "cyclo": 523, "fields": 53, "funcs": 173, "lcom": 0, "lines": 3527, "loc": 2557}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 12, "smells": 0, "timestamp": "2026-09-17T21:04:25Z"}
13+{"branch": "feat/acp-webapp", "breaches": ["code smells: 2 (max 0)"], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 54, "complex": 517, "cyclo": 888, "fields": 75, "funcs": 255, "lcom": 0, "lines": 5523, "loc": 4005}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 13, "smells": 2, "timestamp": "2026-09-17T23:07:19Z"}
14+{"branch": "feat/acp-webapp", "breaches": ["error-level issues: 12 (max 0)", "warning-level issues: 1 (max 0)", "code smells: 2 (max 0)"], "commit": "2434cc7", "counts": {"error": 12, "note": 4, "warning": 1}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 54, "complex": 517, "cyclo": 888, "fields": 75, "funcs": 255, "lcom": 0, "lines": 5523, "loc": 4005}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 14, "smells": 2, "timestamp": "2026-09-17T23:07:54Z"}
15+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 55, "complex": 507, "cyclo": 877, "fields": 79, "funcs": 260, "lcom": 0, "lines": 5570, "loc": 4030}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 15, "smells": 0, "timestamp": "2026-09-17T23:10:03Z"}
16+{"branch": "feat/acp-webapp", "breaches": [], "commit": "2434cc7", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 56, "complex": 514, "cyclo": 893, "fields": 79, "funcs": 267, "lcom": 0, "lines": 5671, "loc": 4082}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 16, "smells": 0, "timestamp": "2026-09-17T23:15:34Z"}
added .quality/report-20260917T193529Z.md +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-09-17T19:35:29Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #5 (previous: 2026-09-17T19:15:21Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | ui/src/components/ChatThread.tsx | 46 | Function with many returns (count = 6): ThreadEntry |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 112 | +1 |
37+| classes | 27 | ±0 |
38+| fields | 40 | ±0 |
39+| cyclo | 298 | +4 |
40+| complex | 147 | +2 |
41+| lcom | 0 | ±0 |
42+| lines | 2123 | +55 |
43+| loc | 1488 | +37 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/bridge/bridge.go | 26 | 40 | 201 |
50+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
51+| ui/src/reducer.ts | 17 | 53 | 187 |
52+| internal/agent/agent.go | 15 | 31 | 121 |
53+| internal/bridge/ws.go | 14 | 15 | 61 |
54+| internal/agent/client.go | 11 | 28 | 74 |
55+| ui/src/ws.ts | 10 | 13 | 59 |
56+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
57+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
58+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
59+| ui/src/components/ChatThread.tsx | 4 | 11 | 67 |
60+| cmd/ori/main.go | 3 | 7 | 45 |
61+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
62+| internal/bridge/protocol.go | 1 | 3 | 42 |
63+| ui/embed.go | 1 | 3 | 13 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
70+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
71+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
72+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
73+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-09-17T19:35:29Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #5 (previous: 2026-09-17T19:15:21Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | ui/src/components/ChatThread.tsx | 46 | Function with many returns (count = 6): ThreadEntry |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 112 | +1 |
37+| classes | 27 | ±0 |
38+| fields | 40 | ±0 |
39+| cyclo | 298 | +4 |
40+| complex | 147 | +2 |
41+| lcom | 0 | ±0 |
42+| lines | 2123 | +55 |
43+| loc | 1488 | +37 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/bridge/bridge.go | 26 | 40 | 201 |
50+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
51+| ui/src/reducer.ts | 17 | 53 | 187 |
52+| internal/agent/agent.go | 15 | 31 | 121 |
53+| internal/bridge/ws.go | 14 | 15 | 61 |
54+| internal/agent/client.go | 11 | 28 | 74 |
55+| ui/src/ws.ts | 10 | 13 | 59 |
56+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
57+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
58+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
59+| ui/src/components/ChatThread.tsx | 4 | 11 | 67 |
60+| cmd/ori/main.go | 3 | 7 | 45 |
61+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
62+| internal/bridge/protocol.go | 1 | 3 | 42 |
63+| ui/embed.go | 1 | 3 | 13 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
70+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
71+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
72+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
73+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
added .quality/report-20260917T193600Z.md +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+# Quality report — 2026-09-17T19:36:00Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #6 (previous: 2026-09-17T19:35:29Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 113 | +1 |
31+| classes | 27 | ±0 |
32+| fields | 40 | ±0 |
33+| cyclo | 298 | ±0 |
34+| complex | 146 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2130 | +7 |
37+| loc | 1491 | +3 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/bridge/bridge.go | 26 | 40 | 201 |
44+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
45+| ui/src/reducer.ts | 17 | 53 | 187 |
46+| internal/agent/agent.go | 15 | 31 | 121 |
47+| internal/bridge/ws.go | 14 | 15 | 61 |
48+| internal/agent/client.go | 11 | 28 | 74 |
49+| ui/src/ws.ts | 10 | 13 | 59 |
50+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
51+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
52+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
53+| cmd/ori/main.go | 3 | 7 | 45 |
54+| ui/src/components/ChatThread.tsx | 3 | 11 | 70 |
55+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
56+| internal/bridge/protocol.go | 1 | 3 | 42 |
57+| ui/embed.go | 1 | 3 | 13 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
64+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
65+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
66+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
67+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
68+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
new file mode 100644
@@ -0,0 +1,68 @@
1+# Quality report — 2026-09-17T19:36:00Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #6 (previous: 2026-09-17T19:35:29Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 113 | +1 |
31+| classes | 27 | ±0 |
32+| fields | 40 | ±0 |
33+| cyclo | 298 | ±0 |
34+| complex | 146 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2130 | +7 |
37+| loc | 1491 | +3 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/bridge/bridge.go | 26 | 40 | 201 |
44+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
45+| ui/src/reducer.ts | 17 | 53 | 187 |
46+| internal/agent/agent.go | 15 | 31 | 121 |
47+| internal/bridge/ws.go | 14 | 15 | 61 |
48+| internal/agent/client.go | 11 | 28 | 74 |
49+| ui/src/ws.ts | 10 | 13 | 59 |
50+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
51+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
52+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
53+| cmd/ori/main.go | 3 | 7 | 45 |
54+| ui/src/components/ChatThread.tsx | 3 | 11 | 70 |
55+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
56+| internal/bridge/protocol.go | 1 | 3 | 42 |
57+| ui/embed.go | 1 | 3 | 13 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
64+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
65+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
66+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
67+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
68+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
added .quality/report-20260917T200532Z.md +89 -0
new file mode 100644
@@ -0,0 +1,89 @@
1+# Quality report — 2026-09-17T20:05:32Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #7 (previous: 2026-09-17T19:36:00Z)
7+
8+## Gate violations
9+
10+- error-level issues: 2 (max 0)
11+- warning-level issues: 1 (max 0)
12+- code smells: 3 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 2 | — |
19+| warning | 1 | — |
20+
21+### Top rules
22+
23+| rule | count |
24+|---|---|
25+| biome:suppressions/unused | 1 |
26+| biome:lint/security/noDangerouslySetInnerHtml | 1 |
27+| biome:suppressions/parse | 1 |
28+
29+### Most affected files
30+
31+| file | issues |
32+|---|---|
33+| ui/src/components/AsciiDocView.tsx | 2 |
34+| ui/src/components/EditorPane.tsx | 1 |
35+
36+## Code smells (`qlty smells`)
37+
38+Total: **3** (vs previous: +3)
39+
40+| smell | file | line | detail |
41+|---|---|---|---|
42+| qlty:return-statements | ui/src/components/EditorPane.tsx | 17 | Function with many returns (count = 6): EditorPane |
43+| qlty:function-complexity | ui/src/components/EditorPane.tsx | 17 | Function with high complexity (count = 19): EditorPane |
44+| qlty:return-statements | ui/src/components/ToolCallCard.tsx | 70 | Function with many returns (count = 6): ToolContent |
45+
46+## Metrics (`qlty metrics`)
47+
48+| metric | total | vs previous |
49+|---|---|---|
50+| funcs | 168 | +55 |
51+| classes | 34 | +7 |
52+| fields | 53 | +13 |
53+| cyclo | 503 | +205 |
54+| complex | 284 | +138 |
55+| lcom | 0 | ±0 |
56+| lines | 3387 | +1257 |
57+| loc | 2446 | +955 |
58+
59+### Most complex files
60+
61+| file | complex | cyclo | loc |
62+|---|---|---|---|
63+| internal/terminal/terminal.go | 32 | 33 | 111 |
64+| internal/bridge/bridge.go | 26 | 40 | 201 |
65+| internal/files/files.go | 21 | 35 | 138 |
66+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
67+| ui/src/components/EditorPane.tsx | 19 | 32 | 91 |
68+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
69+| ui/src/reducer.ts | 17 | 53 | 187 |
70+| internal/agent/agent.go | 15 | 31 | 121 |
71+| internal/bridge/ws.go | 14 | 15 | 61 |
72+| internal/agent/client.go | 11 | 28 | 74 |
73+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
74+| ui/src/ws.ts | 10 | 13 | 59 |
75+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
76+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
77+| ui/src/components/ToolCallCard.tsx | 8 | 21 | 91 |
78+
79+## Trend
80+
81+| run | timestamp | error | warning | smells | complex | gate |
82+|---|---|---|---|---|---|---|
83+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
84+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
85+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
86+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
87+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
88+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
89+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
new file mode 100644
@@ -0,0 +1,89 @@
1+# Quality report — 2026-09-17T20:05:32Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #7 (previous: 2026-09-17T19:36:00Z)
7+
8+## Gate violations
9+
10+- error-level issues: 2 (max 0)
11+- warning-level issues: 1 (max 0)
12+- code smells: 3 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 2 | — |
19+| warning | 1 | — |
20+
21+### Top rules
22+
23+| rule | count |
24+|---|---|
25+| biome:suppressions/unused | 1 |
26+| biome:lint/security/noDangerouslySetInnerHtml | 1 |
27+| biome:suppressions/parse | 1 |
28+
29+### Most affected files
30+
31+| file | issues |
32+|---|---|
33+| ui/src/components/AsciiDocView.tsx | 2 |
34+| ui/src/components/EditorPane.tsx | 1 |
35+
36+## Code smells (`qlty smells`)
37+
38+Total: **3** (vs previous: +3)
39+
40+| smell | file | line | detail |
41+|---|---|---|---|
42+| qlty:return-statements | ui/src/components/EditorPane.tsx | 17 | Function with many returns (count = 6): EditorPane |
43+| qlty:function-complexity | ui/src/components/EditorPane.tsx | 17 | Function with high complexity (count = 19): EditorPane |
44+| qlty:return-statements | ui/src/components/ToolCallCard.tsx | 70 | Function with many returns (count = 6): ToolContent |
45+
46+## Metrics (`qlty metrics`)
47+
48+| metric | total | vs previous |
49+|---|---|---|
50+| funcs | 168 | +55 |
51+| classes | 34 | +7 |
52+| fields | 53 | +13 |
53+| cyclo | 503 | +205 |
54+| complex | 284 | +138 |
55+| lcom | 0 | ±0 |
56+| lines | 3387 | +1257 |
57+| loc | 2446 | +955 |
58+
59+### Most complex files
60+
61+| file | complex | cyclo | loc |
62+|---|---|---|---|
63+| internal/terminal/terminal.go | 32 | 33 | 111 |
64+| internal/bridge/bridge.go | 26 | 40 | 201 |
65+| internal/files/files.go | 21 | 35 | 138 |
66+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
67+| ui/src/components/EditorPane.tsx | 19 | 32 | 91 |
68+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
69+| ui/src/reducer.ts | 17 | 53 | 187 |
70+| internal/agent/agent.go | 15 | 31 | 121 |
71+| internal/bridge/ws.go | 14 | 15 | 61 |
72+| internal/agent/client.go | 11 | 28 | 74 |
73+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
74+| ui/src/ws.ts | 10 | 13 | 59 |
75+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
76+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
77+| ui/src/components/ToolCallCard.tsx | 8 | 21 | 91 |
78+
79+## Trend
80+
81+| run | timestamp | error | warning | smells | complex | gate |
82+|---|---|---|---|---|---|---|
83+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
84+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
85+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
86+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
87+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
88+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
89+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
added .quality/report-20260917T200646Z.md +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+# Quality report — 2026-09-17T20:06:46Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #8 (previous: 2026-09-17T20:05:32Z)
7+
8+## Gate violations
9+
10+- error-level issues: 1 (max 0)
11+- warning-level issues: 1 (max 0)
12+
13+## Lint issues (`qlty check`)
14+
15+| level | count | vs previous |
16+|---|---|---|
17+| error | 1 | -1 |
18+| warning | 1 | ±0 |
19+
20+### Top rules
21+
22+| rule | count |
23+|---|---|
24+| biome:suppressions/unused | 1 |
25+| biome:lint/security/noDangerouslySetInnerHtml | 1 |
26+
27+### Most affected files
28+
29+| file | issues |
30+|---|---|
31+| ui/src/components/AsciiDocView.tsx | 2 |
32+
33+## Code smells (`qlty smells`)
34+
35+Total: **0** (vs previous: -3)
36+
37+_none_
38+
39+## Metrics (`qlty metrics`)
40+
41+| metric | total | vs previous |
42+|---|---|---|
43+| funcs | 171 | +3 |
44+| classes | 34 | ±0 |
45+| fields | 53 | ±0 |
46+| cyclo | 504 | +1 |
47+| complex | 281 | -3 |
48+| lcom | 0 | ±0 |
49+| lines | 3436 | +49 |
50+| loc | 2488 | +42 |
51+
52+### Most complex files
53+
54+| file | complex | cyclo | loc |
55+|---|---|---|---|
56+| internal/terminal/terminal.go | 32 | 33 | 111 |
57+| internal/bridge/bridge.go | 26 | 40 | 201 |
58+| internal/files/files.go | 21 | 35 | 138 |
59+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
60+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
61+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
62+| ui/src/reducer.ts | 17 | 53 | 187 |
63+| internal/agent/agent.go | 15 | 31 | 121 |
64+| internal/bridge/ws.go | 14 | 15 | 61 |
65+| internal/agent/client.go | 11 | 28 | 74 |
66+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
67+| ui/src/ws.ts | 10 | 13 | 59 |
68+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
69+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
70+| ui/src/components/Workspace.tsx | 8 | 13 | 67 |
71+
72+## Trend
73+
74+| run | timestamp | error | warning | smells | complex | gate |
75+|---|---|---|---|---|---|---|
76+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
77+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
78+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
79+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
80+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
81+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
82+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
83+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
new file mode 100644
@@ -0,0 +1,83 @@
1+# Quality report — 2026-09-17T20:06:46Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #8 (previous: 2026-09-17T20:05:32Z)
7+
8+## Gate violations
9+
10+- error-level issues: 1 (max 0)
11+- warning-level issues: 1 (max 0)
12+
13+## Lint issues (`qlty check`)
14+
15+| level | count | vs previous |
16+|---|---|---|
17+| error | 1 | -1 |
18+| warning | 1 | ±0 |
19+
20+### Top rules
21+
22+| rule | count |
23+|---|---|
24+| biome:suppressions/unused | 1 |
25+| biome:lint/security/noDangerouslySetInnerHtml | 1 |
26+
27+### Most affected files
28+
29+| file | issues |
30+|---|---|
31+| ui/src/components/AsciiDocView.tsx | 2 |
32+
33+## Code smells (`qlty smells`)
34+
35+Total: **0** (vs previous: -3)
36+
37+_none_
38+
39+## Metrics (`qlty metrics`)
40+
41+| metric | total | vs previous |
42+|---|---|---|
43+| funcs | 171 | +3 |
44+| classes | 34 | ±0 |
45+| fields | 53 | ±0 |
46+| cyclo | 504 | +1 |
47+| complex | 281 | -3 |
48+| lcom | 0 | ±0 |
49+| lines | 3436 | +49 |
50+| loc | 2488 | +42 |
51+
52+### Most complex files
53+
54+| file | complex | cyclo | loc |
55+|---|---|---|---|
56+| internal/terminal/terminal.go | 32 | 33 | 111 |
57+| internal/bridge/bridge.go | 26 | 40 | 201 |
58+| internal/files/files.go | 21 | 35 | 138 |
59+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
60+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
61+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
62+| ui/src/reducer.ts | 17 | 53 | 187 |
63+| internal/agent/agent.go | 15 | 31 | 121 |
64+| internal/bridge/ws.go | 14 | 15 | 61 |
65+| internal/agent/client.go | 11 | 28 | 74 |
66+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
67+| ui/src/ws.ts | 10 | 13 | 59 |
68+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
69+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
70+| ui/src/components/Workspace.tsx | 8 | 13 | 67 |
71+
72+## Trend
73+
74+| run | timestamp | error | warning | smells | complex | gate |
75+|---|---|---|---|---|---|---|
76+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
77+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
78+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
79+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
80+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
81+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
82+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
83+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
added .quality/report-20260917T200801Z.md +74 -0
new file mode 100644
@@ -0,0 +1,74 @@
1+# Quality report — 2026-09-17T20:08:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #9 (previous: 2026-09-17T20:06:46Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| error | 0 | -1 |
13+| warning | 0 | -1 |
14+
15+### Top rules
16+
17+_none_
18+
19+### Most affected files
20+
21+_none_
22+
23+## Code smells (`qlty smells`)
24+
25+Total: **0** (vs previous: ±0)
26+
27+_none_
28+
29+## Metrics (`qlty metrics`)
30+
31+| metric | total | vs previous |
32+|---|---|---|
33+| funcs | 171 | ±0 |
34+| classes | 34 | ±0 |
35+| fields | 53 | ±0 |
36+| cyclo | 504 | ±0 |
37+| complex | 281 | ±0 |
38+| lcom | 0 | ±0 |
39+| lines | 3436 | ±0 |
40+| loc | 2488 | ±0 |
41+
42+### Most complex files
43+
44+| file | complex | cyclo | loc |
45+|---|---|---|---|
46+| internal/terminal/terminal.go | 32 | 33 | 111 |
47+| internal/bridge/bridge.go | 26 | 40 | 201 |
48+| internal/files/files.go | 21 | 35 | 138 |
49+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
50+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
51+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
52+| ui/src/reducer.ts | 17 | 53 | 187 |
53+| internal/agent/agent.go | 15 | 31 | 121 |
54+| internal/bridge/ws.go | 14 | 15 | 61 |
55+| internal/agent/client.go | 11 | 28 | 74 |
56+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
57+| ui/src/ws.ts | 10 | 13 | 59 |
58+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
59+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
60+| ui/src/components/Workspace.tsx | 8 | 13 | 67 |
61+
62+## Trend
63+
64+| run | timestamp | error | warning | smells | complex | gate |
65+|---|---|---|---|---|---|---|
66+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
67+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
68+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
69+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
70+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
71+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
72+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
73+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
74+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
new file mode 100644
@@ -0,0 +1,74 @@
1+# Quality report — 2026-09-17T20:08:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #9 (previous: 2026-09-17T20:06:46Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| error | 0 | -1 |
13+| warning | 0 | -1 |
14+
15+### Top rules
16+
17+_none_
18+
19+### Most affected files
20+
21+_none_
22+
23+## Code smells (`qlty smells`)
24+
25+Total: **0** (vs previous: ±0)
26+
27+_none_
28+
29+## Metrics (`qlty metrics`)
30+
31+| metric | total | vs previous |
32+|---|---|---|
33+| funcs | 171 | ±0 |
34+| classes | 34 | ±0 |
35+| fields | 53 | ±0 |
36+| cyclo | 504 | ±0 |
37+| complex | 281 | ±0 |
38+| lcom | 0 | ±0 |
39+| lines | 3436 | ±0 |
40+| loc | 2488 | ±0 |
41+
42+### Most complex files
43+
44+| file | complex | cyclo | loc |
45+|---|---|---|---|
46+| internal/terminal/terminal.go | 32 | 33 | 111 |
47+| internal/bridge/bridge.go | 26 | 40 | 201 |
48+| internal/files/files.go | 21 | 35 | 138 |
49+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
50+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
51+| ui/src/components/TerminalPane.tsx | 17 | 10 | 79 |
52+| ui/src/reducer.ts | 17 | 53 | 187 |
53+| internal/agent/agent.go | 15 | 31 | 121 |
54+| internal/bridge/ws.go | 14 | 15 | 61 |
55+| internal/agent/client.go | 11 | 28 | 74 |
56+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
57+| ui/src/ws.ts | 10 | 13 | 59 |
58+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
59+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 45 |
60+| ui/src/components/Workspace.tsx | 8 | 13 | 67 |
61+
62+## Trend
63+
64+| run | timestamp | error | warning | smells | complex | gate |
65+|---|---|---|---|---|---|---|
66+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
67+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
68+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
69+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
70+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
71+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
72+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
73+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
74+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
added .quality/report-20260917T202557Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-09-17T20:25:57Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #10 (previous: 2026-09-17T20:08:01Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 172 | +1 |
31+| classes | 34 | ±0 |
32+| fields | 53 | ±0 |
33+| cyclo | 518 | +14 |
34+| complex | 286 | +5 |
35+| lcom | 0 | ±0 |
36+| lines | 3493 | +57 |
37+| loc | 2534 | +46 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/terminal/terminal.go | 32 | 33 | 111 |
44+| internal/bridge/bridge.go | 26 | 40 | 201 |
45+| internal/files/files.go | 21 | 35 | 138 |
46+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
47+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
48+| ui/src/reducer.ts | 17 | 53 | 187 |
49+| internal/agent/agent.go | 15 | 31 | 121 |
50+| internal/bridge/ws.go | 14 | 15 | 61 |
51+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
52+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
53+| internal/agent/client.go | 11 | 28 | 74 |
54+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
55+| ui/src/ws.ts | 10 | 13 | 59 |
56+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
57+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
64+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
65+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
66+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
67+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
68+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
69+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
70+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
71+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
72+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-09-17T20:25:57Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #10 (previous: 2026-09-17T20:08:01Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 172 | +1 |
31+| classes | 34 | ±0 |
32+| fields | 53 | ±0 |
33+| cyclo | 518 | +14 |
34+| complex | 286 | +5 |
35+| lcom | 0 | ±0 |
36+| lines | 3493 | +57 |
37+| loc | 2534 | +46 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/terminal/terminal.go | 32 | 33 | 111 |
44+| internal/bridge/bridge.go | 26 | 40 | 201 |
45+| internal/files/files.go | 21 | 35 | 138 |
46+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
47+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
48+| ui/src/reducer.ts | 17 | 53 | 187 |
49+| internal/agent/agent.go | 15 | 31 | 121 |
50+| internal/bridge/ws.go | 14 | 15 | 61 |
51+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
52+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
53+| internal/agent/client.go | 11 | 28 | 74 |
54+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
55+| ui/src/ws.ts | 10 | 13 | 59 |
56+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
57+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
64+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
65+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
66+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
67+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
68+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
69+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
70+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
71+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
72+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
added .quality/report-20260917T205517Z.md +79 -0
new file mode 100644
@@ -0,0 +1,79 @@
1+# Quality report — 2026-09-17T20:55:17Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #11 (previous: 2026-09-17T20:25:57Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| note | 2 | — |
13+
14+### Top rules
15+
16+| rule | count |
17+|---|---|
18+| biome:fmt | 2 |
19+
20+### Most affected files
21+
22+| file | issues |
23+|---|---|
24+| ui/src/app.css | 1 |
25+| ui/src/components/ToolCallCard.test.tsx | 1 |
26+
27+## Code smells (`qlty smells`)
28+
29+Total: **0** (vs previous: ±0)
30+
31+_none_
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 173 | +1 |
38+| classes | 34 | ±0 |
39+| fields | 53 | ±0 |
40+| cyclo | 523 | +5 |
41+| complex | 288 | +2 |
42+| lcom | 0 | ±0 |
43+| lines | 3527 | +34 |
44+| loc | 2557 | +23 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| internal/terminal/terminal.go | 32 | 33 | 111 |
51+| internal/bridge/bridge.go | 26 | 40 | 201 |
52+| internal/files/files.go | 21 | 35 | 138 |
53+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
54+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
55+| ui/src/reducer.ts | 17 | 53 | 187 |
56+| internal/agent/agent.go | 15 | 31 | 121 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
59+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
60+| internal/agent/client.go | 11 | 28 | 74 |
61+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
62+| ui/src/ws.ts | 10 | 13 | 59 |
63+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
64+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
71+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
72+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
73+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
74+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
75+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
76+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
77+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
78+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
79+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
new file mode 100644
@@ -0,0 +1,79 @@
1+# Quality report — 2026-09-17T20:55:17Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #11 (previous: 2026-09-17T20:25:57Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| note | 2 | — |
13+
14+### Top rules
15+
16+| rule | count |
17+|---|---|
18+| biome:fmt | 2 |
19+
20+### Most affected files
21+
22+| file | issues |
23+|---|---|
24+| ui/src/app.css | 1 |
25+| ui/src/components/ToolCallCard.test.tsx | 1 |
26+
27+## Code smells (`qlty smells`)
28+
29+Total: **0** (vs previous: ±0)
30+
31+_none_
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 173 | +1 |
38+| classes | 34 | ±0 |
39+| fields | 53 | ±0 |
40+| cyclo | 523 | +5 |
41+| complex | 288 | +2 |
42+| lcom | 0 | ±0 |
43+| lines | 3527 | +34 |
44+| loc | 2557 | +23 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| internal/terminal/terminal.go | 32 | 33 | 111 |
51+| internal/bridge/bridge.go | 26 | 40 | 201 |
52+| internal/files/files.go | 21 | 35 | 138 |
53+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
54+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
55+| ui/src/reducer.ts | 17 | 53 | 187 |
56+| internal/agent/agent.go | 15 | 31 | 121 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
59+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
60+| internal/agent/client.go | 11 | 28 | 74 |
61+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
62+| ui/src/ws.ts | 10 | 13 | 59 |
63+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
64+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
71+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
72+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
73+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
74+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
75+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
76+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
77+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
78+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
79+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
added .quality/report-20260917T210425Z.md +79 -0
new file mode 100644
@@ -0,0 +1,79 @@
1+# Quality report — 2026-09-17T21:04:25Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #12 (previous: 2026-09-17T20:55:17Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| note | 2 | ±0 |
13+
14+### Top rules
15+
16+| rule | count |
17+|---|---|
18+| biome:fmt | 2 |
19+
20+### Most affected files
21+
22+| file | issues |
23+|---|---|
24+| ui/src/app.css | 1 |
25+| ui/src/components/ToolCallCard.test.tsx | 1 |
26+
27+## Code smells (`qlty smells`)
28+
29+Total: **0** (vs previous: ±0)
30+
31+_none_
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 173 | ±0 |
38+| classes | 34 | ±0 |
39+| fields | 53 | ±0 |
40+| cyclo | 523 | ±0 |
41+| complex | 288 | ±0 |
42+| lcom | 0 | ±0 |
43+| lines | 3527 | ±0 |
44+| loc | 2557 | ±0 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| internal/terminal/terminal.go | 32 | 33 | 111 |
51+| internal/bridge/bridge.go | 26 | 40 | 201 |
52+| internal/files/files.go | 21 | 35 | 138 |
53+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
54+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
55+| ui/src/reducer.ts | 17 | 53 | 187 |
56+| internal/agent/agent.go | 15 | 31 | 121 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
59+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
60+| internal/agent/client.go | 11 | 28 | 74 |
61+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
62+| ui/src/ws.ts | 10 | 13 | 59 |
63+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
64+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
71+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
72+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
73+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
74+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
75+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
76+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
77+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
78+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
79+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
new file mode 100644
@@ -0,0 +1,79 @@
1+# Quality report — 2026-09-17T21:04:25Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #12 (previous: 2026-09-17T20:55:17Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| note | 2 | ±0 |
13+
14+### Top rules
15+
16+| rule | count |
17+|---|---|
18+| biome:fmt | 2 |
19+
20+### Most affected files
21+
22+| file | issues |
23+|---|---|
24+| ui/src/app.css | 1 |
25+| ui/src/components/ToolCallCard.test.tsx | 1 |
26+
27+## Code smells (`qlty smells`)
28+
29+Total: **0** (vs previous: ±0)
30+
31+_none_
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 173 | ±0 |
38+| classes | 34 | ±0 |
39+| fields | 53 | ±0 |
40+| cyclo | 523 | ±0 |
41+| complex | 288 | ±0 |
42+| lcom | 0 | ±0 |
43+| lines | 3527 | ±0 |
44+| loc | 2557 | ±0 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| internal/terminal/terminal.go | 32 | 33 | 111 |
51+| internal/bridge/bridge.go | 26 | 40 | 201 |
52+| internal/files/files.go | 21 | 35 | 138 |
53+| internal/mockagent/mockagent.go | 20 | 32 | 174 |
54+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
55+| ui/src/reducer.ts | 17 | 53 | 187 |
56+| internal/agent/agent.go | 15 | 31 | 121 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+| ui/src/components/TerminalPane.tsx | 14 | 9 | 70 |
59+| ui/src/components/FileTree.tsx | 13 | 27 | 135 |
60+| internal/agent/client.go | 11 | 28 | 74 |
61+| ui/src/components/PreviewPane.tsx | 11 | 21 | 106 |
62+| ui/src/ws.ts | 10 | 13 | 59 |
63+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
64+| ui/src/components/AsciiDocView.tsx | 8 | 6 | 40 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
71+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
72+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
73+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
74+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
75+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
76+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
77+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
78+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
79+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
added .quality/report-20260917T230719Z.md +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+# Quality report — 2026-09-17T23:07:19Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #13 (previous: 2026-09-17T21:04:25Z)
7+
8+## Gate violations
9+
10+- code smells: 2 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+| level | count | vs previous |
15+|---|---|---|
16+| note | 0 | -2 |
17+
18+### Top rules
19+
20+_none_
21+
22+### Most affected files
23+
24+_none_
25+
26+## Code smells (`qlty smells`)
27+
28+Total: **2** (vs previous: +2)
29+
30+| smell | file | line | detail |
31+|---|---|---|---|
32+| qlty:return-statements | internal/files/search.go | 50 | Function with many returns (count = 8): Search |
33+| qlty:return-statements | ui/src/components/PreviewPane.tsx | 30 | Function with many returns (count = 6): PreviewPane |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 255 | +82 |
40+| classes | 54 | +20 |
41+| fields | 75 | +22 |
42+| cyclo | 888 | +365 |
43+| complex | 517 | +229 |
44+| lcom | 0 | ±0 |
45+| lines | 5523 | +1996 |
46+| loc | 4005 | +1448 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/bridge/bridge.go | 34 | 49 | 228 |
53+| internal/skills/skills.go | 34 | 53 | 129 |
54+| internal/terminal/terminal.go | 32 | 33 | 111 |
55+| ui/src/useCompletion.ts | 29 | 32 | 122 |
56+| internal/files/search.go | 24 | 32 | 93 |
57+| internal/files/files.go | 21 | 35 | 140 |
58+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
59+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
60+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
61+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
62+| ori-desktop/frontend/src/main.js | 17 | 29 | 118 |
63+| ui/src/components/PreviewPane.tsx | 17 | 30 | 127 |
64+| ui/src/reducer.ts | 17 | 54 | 195 |
65+| internal/agent/agent.go | 15 | 31 | 121 |
66+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
67+
68+## Tooling notes
69+
70+- `qlty check` exit 99: ERROR
71+
72+ > Error installing go@1.22.0.
73+ >
74+ >
75+ > Caused by:
76+ > 0: Error extracting tar.gz archive from https://go.dev/dl/go1.22.0.linux-arm64.tar.gz
77+ > 1: failed to unpack `go/src/cmd/internal/obj/objfile.go` into `/home/agent/.qlty/cache/tools/go/1.22.0-c5a26d064299/src/cmd/internal/obj/objfile.go`
78+ > 2: No space left on device (os error 28)
79+
80+## Trend
81+
82+| run | timestamp | error | warning | smells | complex | gate |
83+|---|---|---|---|---|---|---|
84+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
85+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
86+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
87+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
88+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
89+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
90+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
91+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
92+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
93+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
new file mode 100644
@@ -0,0 +1,93 @@
1+# Quality report — 2026-09-17T23:07:19Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #13 (previous: 2026-09-17T21:04:25Z)
7+
8+## Gate violations
9+
10+- code smells: 2 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+| level | count | vs previous |
15+|---|---|---|
16+| note | 0 | -2 |
17+
18+### Top rules
19+
20+_none_
21+
22+### Most affected files
23+
24+_none_
25+
26+## Code smells (`qlty smells`)
27+
28+Total: **2** (vs previous: +2)
29+
30+| smell | file | line | detail |
31+|---|---|---|---|
32+| qlty:return-statements | internal/files/search.go | 50 | Function with many returns (count = 8): Search |
33+| qlty:return-statements | ui/src/components/PreviewPane.tsx | 30 | Function with many returns (count = 6): PreviewPane |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 255 | +82 |
40+| classes | 54 | +20 |
41+| fields | 75 | +22 |
42+| cyclo | 888 | +365 |
43+| complex | 517 | +229 |
44+| lcom | 0 | ±0 |
45+| lines | 5523 | +1996 |
46+| loc | 4005 | +1448 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/bridge/bridge.go | 34 | 49 | 228 |
53+| internal/skills/skills.go | 34 | 53 | 129 |
54+| internal/terminal/terminal.go | 32 | 33 | 111 |
55+| ui/src/useCompletion.ts | 29 | 32 | 122 |
56+| internal/files/search.go | 24 | 32 | 93 |
57+| internal/files/files.go | 21 | 35 | 140 |
58+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
59+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
60+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
61+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
62+| ori-desktop/frontend/src/main.js | 17 | 29 | 118 |
63+| ui/src/components/PreviewPane.tsx | 17 | 30 | 127 |
64+| ui/src/reducer.ts | 17 | 54 | 195 |
65+| internal/agent/agent.go | 15 | 31 | 121 |
66+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
67+
68+## Tooling notes
69+
70+- `qlty check` exit 99: ERROR
71+
72+ > Error installing go@1.22.0.
73+ >
74+ >
75+ > Caused by:
76+ > 0: Error extracting tar.gz archive from https://go.dev/dl/go1.22.0.linux-arm64.tar.gz
77+ > 1: failed to unpack `go/src/cmd/internal/obj/objfile.go` into `/home/agent/.qlty/cache/tools/go/1.22.0-c5a26d064299/src/cmd/internal/obj/objfile.go`
78+ > 2: No space left on device (os error 28)
79+
80+## Trend
81+
82+| run | timestamp | error | warning | smells | complex | gate |
83+|---|---|---|---|---|---|---|
84+| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
85+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
86+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
87+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
88+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
89+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
90+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
91+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
92+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
93+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
added .quality/report-20260917T230754Z.md +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+# Quality report — 2026-09-17T23:07:54Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #14 (previous: 2026-09-17T23:07:19Z)
7+
8+## Gate violations
9+
10+- error-level issues: 12 (max 0)
11+- warning-level issues: 1 (max 0)
12+- code smells: 2 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 12 | — |
19+| warning | 1 | — |
20+| note | 4 | — |
21+
22+### Top rules
23+
24+| rule | count |
25+|---|---|
26+| biome:lint/style/useTemplate | 7 |
27+| biome:fmt | 4 |
28+| biome:lint/complexity/useOptionalChain | 2 |
29+| golangci-lint:errcheck | 1 |
30+| biome:lint/complexity/useArrowFunction | 1 |
31+| biome:lint/suspicious/noRedundantUseStrict | 1 |
32+| biome:lint/correctness/noSelfAssign | 1 |
33+
34+### Most affected files
35+
36+| file | issues |
37+|---|---|
38+| ori-desktop/frontend/src/main.js | 13 |
39+| docs/.assets/ori-architecture.json | 1 |
40+| internal/files/raw.go | 1 |
41+| ori-desktop/frontend/src/main.css | 1 |
42+| ori-desktop/wails.json | 1 |
43+
44+## Code smells (`qlty smells`)
45+
46+Total: **2** (vs previous: ±0)
47+
48+| smell | file | line | detail |
49+|---|---|---|---|
50+| qlty:return-statements | internal/files/search.go | 50 | Function with many returns (count = 8): Search |
51+| qlty:return-statements | ui/src/components/PreviewPane.tsx | 30 | Function with many returns (count = 6): PreviewPane |
52+
53+## Metrics (`qlty metrics`)
54+
55+| metric | total | vs previous |
56+|---|---|---|
57+| funcs | 255 | ±0 |
58+| classes | 54 | ±0 |
59+| fields | 75 | ±0 |
60+| cyclo | 888 | ±0 |
61+| complex | 517 | ±0 |
62+| lcom | 0 | ±0 |
63+| lines | 5523 | ±0 |
64+| loc | 4005 | ±0 |
65+
66+### Most complex files
67+
68+| file | complex | cyclo | loc |
69+|---|---|---|---|
70+| internal/bridge/bridge.go | 34 | 49 | 228 |
71+| internal/skills/skills.go | 34 | 53 | 129 |
72+| internal/terminal/terminal.go | 32 | 33 | 111 |
73+| ui/src/useCompletion.ts | 29 | 32 | 122 |
74+| internal/files/search.go | 24 | 32 | 93 |
75+| internal/files/files.go | 21 | 35 | 140 |
76+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
77+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
78+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
79+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
80+| ori-desktop/frontend/src/main.js | 17 | 29 | 118 |
81+| ui/src/components/PreviewPane.tsx | 17 | 30 | 127 |
82+| ui/src/reducer.ts | 17 | 54 | 195 |
83+| internal/agent/agent.go | 15 | 31 | 121 |
84+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
85+
86+## Trend
87+
88+| run | timestamp | error | warning | smells | complex | gate |
89+|---|---|---|---|---|---|---|
90+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
91+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
92+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
93+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
94+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
95+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
96+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
97+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
98+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
99+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
new file mode 100644
@@ -0,0 +1,99 @@
1+# Quality report — 2026-09-17T23:07:54Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #14 (previous: 2026-09-17T23:07:19Z)
7+
8+## Gate violations
9+
10+- error-level issues: 12 (max 0)
11+- warning-level issues: 1 (max 0)
12+- code smells: 2 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 12 | — |
19+| warning | 1 | — |
20+| note | 4 | — |
21+
22+### Top rules
23+
24+| rule | count |
25+|---|---|
26+| biome:lint/style/useTemplate | 7 |
27+| biome:fmt | 4 |
28+| biome:lint/complexity/useOptionalChain | 2 |
29+| golangci-lint:errcheck | 1 |
30+| biome:lint/complexity/useArrowFunction | 1 |
31+| biome:lint/suspicious/noRedundantUseStrict | 1 |
32+| biome:lint/correctness/noSelfAssign | 1 |
33+
34+### Most affected files
35+
36+| file | issues |
37+|---|---|
38+| ori-desktop/frontend/src/main.js | 13 |
39+| docs/.assets/ori-architecture.json | 1 |
40+| internal/files/raw.go | 1 |
41+| ori-desktop/frontend/src/main.css | 1 |
42+| ori-desktop/wails.json | 1 |
43+
44+## Code smells (`qlty smells`)
45+
46+Total: **2** (vs previous: ±0)
47+
48+| smell | file | line | detail |
49+|---|---|---|---|
50+| qlty:return-statements | internal/files/search.go | 50 | Function with many returns (count = 8): Search |
51+| qlty:return-statements | ui/src/components/PreviewPane.tsx | 30 | Function with many returns (count = 6): PreviewPane |
52+
53+## Metrics (`qlty metrics`)
54+
55+| metric | total | vs previous |
56+|---|---|---|
57+| funcs | 255 | ±0 |
58+| classes | 54 | ±0 |
59+| fields | 75 | ±0 |
60+| cyclo | 888 | ±0 |
61+| complex | 517 | ±0 |
62+| lcom | 0 | ±0 |
63+| lines | 5523 | ±0 |
64+| loc | 4005 | ±0 |
65+
66+### Most complex files
67+
68+| file | complex | cyclo | loc |
69+|---|---|---|---|
70+| internal/bridge/bridge.go | 34 | 49 | 228 |
71+| internal/skills/skills.go | 34 | 53 | 129 |
72+| internal/terminal/terminal.go | 32 | 33 | 111 |
73+| ui/src/useCompletion.ts | 29 | 32 | 122 |
74+| internal/files/search.go | 24 | 32 | 93 |
75+| internal/files/files.go | 21 | 35 | 140 |
76+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
77+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
78+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
79+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
80+| ori-desktop/frontend/src/main.js | 17 | 29 | 118 |
81+| ui/src/components/PreviewPane.tsx | 17 | 30 | 127 |
82+| ui/src/reducer.ts | 17 | 54 | 195 |
83+| internal/agent/agent.go | 15 | 31 | 121 |
84+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
85+
86+## Trend
87+
88+| run | timestamp | error | warning | smells | complex | gate |
89+|---|---|---|---|---|---|---|
90+| 5 | 2026-09-17T19:35:29Z | 0 | 0 | 1 | 147 | FAIL |
91+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
92+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
93+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
94+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
95+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
96+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
97+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
98+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
99+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
added .quality/report-20260917T231003Z.md +76 -0
new file mode 100644
@@ -0,0 +1,76 @@
1+# Quality report — 2026-09-17T23:10:03Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #15 (previous: 2026-09-17T23:07:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| error | 0 | -12 |
13+| warning | 0 | -1 |
14+| note | 0 | -4 |
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **0** (vs previous: -2)
27+
28+_none_
29+
30+## Metrics (`qlty metrics`)
31+
32+| metric | total | vs previous |
33+|---|---|---|
34+| funcs | 260 | +5 |
35+| classes | 55 | +1 |
36+| fields | 79 | +4 |
37+| cyclo | 877 | -11 |
38+| complex | 507 | -10 |
39+| lcom | 0 | ±0 |
40+| lines | 5570 | +47 |
41+| loc | 4030 | +25 |
42+
43+### Most complex files
44+
45+| file | complex | cyclo | loc |
46+|---|---|---|---|
47+| internal/bridge/bridge.go | 34 | 49 | 228 |
48+| internal/skills/skills.go | 34 | 53 | 129 |
49+| internal/terminal/terminal.go | 32 | 33 | 111 |
50+| ui/src/useCompletion.ts | 29 | 32 | 122 |
51+| internal/files/files.go | 21 | 35 | 140 |
52+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
53+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
54+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
55+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
56+| internal/files/search.go | 17 | 32 | 104 |
57+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
58+| ui/src/reducer.ts | 17 | 54 | 195 |
59+| internal/agent/agent.go | 15 | 31 | 121 |
60+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
61+| internal/bridge/ws.go | 14 | 15 | 61 |
62+
63+## Trend
64+
65+| run | timestamp | error | warning | smells | complex | gate |
66+|---|---|---|---|---|---|---|
67+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
68+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
69+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
70+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
71+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
72+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
73+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
74+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
75+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
76+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
new file mode 100644
@@ -0,0 +1,76 @@
1+# Quality report — 2026-09-17T23:10:03Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #15 (previous: 2026-09-17T23:07:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+| level | count | vs previous |
11+|---|---|---|
12+| error | 0 | -12 |
13+| warning | 0 | -1 |
14+| note | 0 | -4 |
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **0** (vs previous: -2)
27+
28+_none_
29+
30+## Metrics (`qlty metrics`)
31+
32+| metric | total | vs previous |
33+|---|---|---|
34+| funcs | 260 | +5 |
35+| classes | 55 | +1 |
36+| fields | 79 | +4 |
37+| cyclo | 877 | -11 |
38+| complex | 507 | -10 |
39+| lcom | 0 | ±0 |
40+| lines | 5570 | +47 |
41+| loc | 4030 | +25 |
42+
43+### Most complex files
44+
45+| file | complex | cyclo | loc |
46+|---|---|---|---|
47+| internal/bridge/bridge.go | 34 | 49 | 228 |
48+| internal/skills/skills.go | 34 | 53 | 129 |
49+| internal/terminal/terminal.go | 32 | 33 | 111 |
50+| ui/src/useCompletion.ts | 29 | 32 | 122 |
51+| internal/files/files.go | 21 | 35 | 140 |
52+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
53+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
54+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
55+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
56+| internal/files/search.go | 17 | 32 | 104 |
57+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
58+| ui/src/reducer.ts | 17 | 54 | 195 |
59+| internal/agent/agent.go | 15 | 31 | 121 |
60+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
61+| internal/bridge/ws.go | 14 | 15 | 61 |
62+
63+## Trend
64+
65+| run | timestamp | error | warning | smells | complex | gate |
66+|---|---|---|---|---|---|---|
67+| 6 | 2026-09-17T19:36:00Z | 0 | 0 | 0 | 146 | PASS |
68+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
69+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
70+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
71+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
72+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
73+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
74+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
75+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
76+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
added .quality/report-20260917T231534Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-09-17T23:15:34Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #16 (previous: 2026-09-17T23:10:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 267 | +7 |
31+| classes | 56 | +1 |
32+| fields | 79 | ±0 |
33+| cyclo | 893 | +16 |
34+| complex | 514 | +7 |
35+| lcom | 0 | ±0 |
36+| lines | 5671 | +101 |
37+| loc | 4082 | +52 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/bridge/bridge.go | 34 | 49 | 228 |
44+| internal/skills/skills.go | 34 | 53 | 129 |
45+| internal/terminal/terminal.go | 32 | 33 | 111 |
46+| ui/src/useCompletion.ts | 29 | 32 | 122 |
47+| internal/files/files.go | 21 | 35 | 140 |
48+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
49+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
50+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
51+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
52+| internal/files/search.go | 17 | 32 | 104 |
53+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
54+| ui/src/reducer.ts | 17 | 54 | 195 |
55+| internal/agent/agent.go | 15 | 31 | 121 |
56+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
64+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
65+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
66+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
67+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
68+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
69+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
70+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
71+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
72+| 16 | 2026-09-17T23:15:34Z | 0 | 0 | 0 | 514 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-09-17T23:15:34Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #16 (previous: 2026-09-17T23:10:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 267 | +7 |
31+| classes | 56 | +1 |
32+| fields | 79 | ±0 |
33+| cyclo | 893 | +16 |
34+| complex | 514 | +7 |
35+| lcom | 0 | ±0 |
36+| lines | 5671 | +101 |
37+| loc | 4082 | +52 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/bridge/bridge.go | 34 | 49 | 228 |
44+| internal/skills/skills.go | 34 | 53 | 129 |
45+| internal/terminal/terminal.go | 32 | 33 | 111 |
46+| ui/src/useCompletion.ts | 29 | 32 | 122 |
47+| internal/files/files.go | 21 | 35 | 140 |
48+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
49+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
50+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
51+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
52+| internal/files/search.go | 17 | 32 | 104 |
53+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
54+| ui/src/reducer.ts | 17 | 54 | 195 |
55+| internal/agent/agent.go | 15 | 31 | 121 |
56+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
57+| internal/bridge/ws.go | 14 | 15 | 61 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
64+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
65+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
66+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
67+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
68+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
69+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
70+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
71+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
72+| 16 | 2026-09-17T23:15:34Z | 0 | 0 | 0 | 514 | PASS |
modified .quality/report-latest.md +34 -28
@@ -1,9 +1,9 @@
1-# Quality report — 2026-09-17T19:15:21Z
1+# Quality report — 2026-09-17T23:15:34Z
22
33 - **Gate**: ✅ **PASS**
4-- **Commit**: `4edda86` on `feat/acp-webapp`
4+- **Commit**: `2434cc7` on `feat/acp-webapp`
55 - **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6-- **Run**: #4 (previous: 2026-09-17T19:14:34Z)
6+- **Run**: #16 (previous: 2026-09-17T23:10:03Z)
77
88 ## Lint issues (`qlty check`)
99
@@ -19,7 +19,7 @@ _none_
1919
2020 ## Code smells (`qlty smells`)
2121
22-Total: **0** (vs previous: -2)
22+Total: **0** (vs previous: ±0)
2323
2424 _none_
2525
@@ -27,40 +27,46 @@ _none_
2727
2828 | metric | total | vs previous |
2929 |---|---|---|
30-| funcs | 111 | +5 |
31-| classes | 27 | ±0 |
32-| fields | 40 | +1 |
33-| cyclo | 294 | ±0 |
34-| complex | 145 | -1 |
30+| funcs | 267 | +7 |
31+| classes | 56 | +1 |
32+| fields | 79 | ±0 |
33+| cyclo | 893 | +16 |
34+| complex | 514 | +7 |
3535 | lcom | 0 | ±0 |
36-| lines | 2068 | +4 |
37-| loc | 1451 | -2 |
36+| lines | 5671 | +101 |
37+| loc | 4082 | +52 |
3838
3939 ### Most complex files
4040
4141 | file | complex | cyclo | loc |
4242 |---|---|---|---|
43-| internal/bridge/bridge.go | 26 | 40 | 201 |
44-| internal/mockagent/mockagent.go | 20 | 32 | 174 |
45-| ui/src/reducer.ts | 17 | 53 | 187 |
43+| internal/bridge/bridge.go | 34 | 49 | 228 |
44+| internal/skills/skills.go | 34 | 53 | 129 |
45+| internal/terminal/terminal.go | 32 | 33 | 111 |
46+| ui/src/useCompletion.ts | 29 | 32 | 122 |
47+| internal/files/files.go | 21 | 35 | 140 |
48+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
49+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
50+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
51+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
52+| internal/files/search.go | 17 | 32 | 104 |
53+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
54+| ui/src/reducer.ts | 17 | 54 | 195 |
4655 | internal/agent/agent.go | 15 | 31 | 121 |
56+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
4757 | internal/bridge/ws.go | 14 | 15 | 61 |
48-| internal/agent/client.go | 11 | 28 | 74 |
49-| ui/src/ws.ts | 10 | 13 | 59 |
50-| internal/httpserver/httpserver.go | 8 | 8 | 33 |
51-| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
52-| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
53-| cmd/ori/main.go | 3 | 7 | 45 |
54-| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
55-| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
56-| internal/bridge/protocol.go | 1 | 3 | 42 |
57-| ui/embed.go | 1 | 3 | 13 |
5858
5959 ## Trend
6060
6161 | run | timestamp | error | warning | smells | complex | gate |
6262 |---|---|---|---|---|---|---|
63-| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
64-| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
65-| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
66-| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |
63+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
64+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
65+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
66+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
67+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
68+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
69+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
70+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
71+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
72+| 16 | 2026-09-17T23:15:34Z | 0 | 0 | 0 | 514 | PASS |
@@ -1,9 +1,9 @@
1-# Quality report — 2026-09-17T19:15:21Z1+# Quality report — 2026-09-17T23:15:34Z
2 2
3 - **Gate**: ✅ **PASS**3 - **Gate**: ✅ **PASS**
4-- **Commit**: `4edda86` on `feat/acp-webapp`4+- **Commit**: `2434cc7` on `feat/acp-webapp`
5 - **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)5 - **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6-- **Run**: #4 (previous: 2026-09-17T19:14:34Z)6+- **Run**: #16 (previous: 2026-09-17T23:10:03Z)
7 7
8 ## Lint issues (`qlty check`)8 ## Lint issues (`qlty check`)
9 9
@@ -19,7 +19,7 @@ _none_
19 19
20 ## Code smells (`qlty smells`)20 ## Code smells (`qlty smells`)
21 21
22-Total: **0** (vs previous: -2)22+Total: **0** (vs previous: ±0)
23 23
24 _none_24 _none_
25 25
@@ -27,40 +27,46 @@ _none_
27 27
28 | metric | total | vs previous |28 | metric | total | vs previous |
29 |---|---|---|29 |---|---|---|
30-| funcs | 111 | +5 |30+| funcs | 267 | +7 |
31-| classes | 27 | ±0 |31+| classes | 56 | +1 |
32-| fields | 40 | +1 |32+| fields | 79 | ±0 |
33-| cyclo | 294 | ±0 |33+| cyclo | 893 | +16 |
34-| complex | 145 | -1 |34+| complex | 514 | +7 |
35 | lcom | 0 | ±0 |35 | lcom | 0 | ±0 |
36-| lines | 2068 | +4 |36+| lines | 5671 | +101 |
37-| loc | 1451 | -2 |37+| loc | 4082 | +52 |
38 38
39 ### Most complex files39 ### Most complex files
40 40
41 | file | complex | cyclo | loc |41 | file | complex | cyclo | loc |
42 |---|---|---|---|42 |---|---|---|---|
43-| internal/bridge/bridge.go | 26 | 40 | 201 |43+| internal/bridge/bridge.go | 34 | 49 | 228 |
44-| internal/mockagent/mockagent.go | 20 | 32 | 174 |44+| internal/skills/skills.go | 34 | 53 | 129 |
45-| ui/src/reducer.ts | 17 | 53 | 187 |45+| internal/terminal/terminal.go | 32 | 33 | 111 |
46+| ui/src/useCompletion.ts | 29 | 32 | 122 |
47+| internal/files/files.go | 21 | 35 | 140 |
48+| internal/mockagent/mockagent.go | 21 | 38 | 190 |
49+| ui/src/components/DrawioView.tsx | 20 | 22 | 76 |
50+| ui/src/components/PromptInput.tsx | 20 | 26 | 153 |
51+| ui/src/components/EditorPane.tsx | 18 | 33 | 125 |
52+| internal/files/search.go | 17 | 32 | 104 |
53+| ui/src/components/PreviewPane.tsx | 17 | 30 | 131 |
54+| ui/src/reducer.ts | 17 | 54 | 195 |
46 | internal/agent/agent.go | 15 | 31 | 121 |55 | internal/agent/agent.go | 15 | 31 | 121 |
56+| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 |
47 | internal/bridge/ws.go | 14 | 15 | 61 |57 | internal/bridge/ws.go | 14 | 15 | 61 |
48-| internal/agent/client.go | 11 | 28 | 74 |
49-| ui/src/ws.ts | 10 | 13 | 59 |
50-| internal/httpserver/httpserver.go | 8 | 8 | 33 |
51-| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
52-| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
53-| cmd/ori/main.go | 3 | 7 | 45 |
54-| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
55-| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
56-| internal/bridge/protocol.go | 1 | 3 | 42 |
57-| ui/embed.go | 1 | 3 | 13 |
58 58
59 ## Trend59 ## Trend
60 60
61 | run | timestamp | error | warning | smells | complex | gate |61 | run | timestamp | error | warning | smells | complex | gate |
62 |---|---|---|---|---|---|---|62 |---|---|---|---|---|---|---|
63-| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |63+| 7 | 2026-09-17T20:05:32Z | 2 | 1 | 3 | 284 | FAIL |
64-| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |64+| 8 | 2026-09-17T20:06:46Z | 1 | 1 | 0 | 281 | FAIL |
65-| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |65+| 9 | 2026-09-17T20:08:01Z | 0 | 0 | 0 | 281 | PASS |
66-| 4 | 2026-09-17T19:15:21Z | 0 | 0 | 0 | 145 | PASS |66+| 10 | 2026-09-17T20:25:57Z | 0 | 0 | 0 | 286 | PASS |
67+| 11 | 2026-09-17T20:55:17Z | 0 | 0 | 0 | 288 | PASS |
68+| 12 | 2026-09-17T21:04:25Z | 0 | 0 | 0 | 288 | PASS |
69+| 13 | 2026-09-17T23:07:19Z | 0 | 0 | 2 | 517 | FAIL |
70+| 14 | 2026-09-17T23:07:54Z | 12 | 1 | 2 | 517 | FAIL |
71+| 15 | 2026-09-17T23:10:03Z | 0 | 0 | 0 | 507 | PASS |
72+| 16 | 2026-09-17T23:15:34Z | 0 | 0 | 0 | 514 | PASS |
modified .tickets/issues/0002-create-an-architecture-diagram.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 2
33 title: Create an architecture diagram
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-17T09:36:53.046Z
9-updatedAt: 2026-09-17T09:36:53.046Z
9+updatedAt: 2026-09-17T22:23:54.249Z
1010 labels: []
1111 body: ""
1212 tasks: []
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 22 id: 2
3 title: Create an architecture diagram3 title: Create an architecture diagram
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-17T09:36:53.046Z8 createdAt: 2026-09-17T09:36:53.046Z
9-updatedAt: 2026-09-17T09:36:53.046Z9+updatedAt: 2026-09-17T22:23:54.249Z
10 labels: []10 labels: []
11 body: ""11 body: ""
12 tasks: []12 tasks: []
modified .tickets/issues/0004-specifications.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 4
33 title: Specifications
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-17T17:58:21.934Z
9-updatedAt: 2026-09-17T18:04:56.702Z
9+updatedAt: 2026-09-17T19:39:19.515Z
1010 labels: []
1111 body: |
1212 Je voudrais faire une webapp (front et back) qui soit un client ACP (Agent Client Protocol) d'un agent de code (on commencera par faire les tests avec claude code)
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 42 id: 4
3 title: Specifications3 title: Specifications
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-17T17:58:21.934Z8 createdAt: 2026-09-17T17:58:21.934Z
9-updatedAt: 2026-09-17T18:04:56.702Z9+updatedAt: 2026-09-17T19:39:19.515Z
10 labels: []10 labels: []
11 body: |11 body: |
12 Je voudrais faire une webapp (front et back) qui soit un client ACP (Agent Client Protocol) d'un agent de code (on commencera par faire les tests avec claude code)12 Je voudrais faire une webapp (front et back) qui soit un client ACP (Agent Client Protocol) d'un agent de code (on commencera par faire les tests avec claude code)
added .tickets/issues/0005-create-a-native-ui-to-connect-the-webapp.yaml +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 5
3+title: Create a "native" UI to connect the webapp
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T19:19:43.179Z
9+updatedAt: 2026-09-17T22:23:39.000Z
10+labels: []
11+body: |
12+ Desktop shell for the ori webapp, built with Wails v2 (Go + platform webview), living in
13+ `ori-desktop/` as its own Go module (`rickub.com/bots-garden/ori-desktop`).
14+
15+ Behaviour: a connection screen asks for the ori server URL (default http://localhost:8888,
16+ remembered in `<user config dir>/ori-desktop/settings.json`), a Go-bound method checks
17+ `GET /healthz`, then the ori SPA is shown inside the window in an iframe (Wails v2 cannot
18+ navigate its main webview to an external URL; `runtime.BrowserOpenURL` is offered as fallback).
19+
20+ Docs: `ori-desktop/README.md` (EN) and `ori-desktop/README.fr.md` (FR).
21+ Root Makefile targets: `make desktop` (wails build), `make desktop-test` (pure-Go tests).
22+tasks:
23+ - text: Own Go module with Wails v2.16 layout (main.go, app.go, wails.json, frontend/, build/)
24+ done: true
25+ - text: Bound Go methods GetConfig / SaveConfig / CheckHealth / Connect / OpenInBrowser / SettingsPath
26+ done: true
27+ - text: Settings persisted as JSON in the user config dir, with unit tests
28+ done: true
29+ - text: /healthz probe with unit tests against httptest
30+ done: true
31+ - text: Plain HTML/CSS/JS frontend embedding ori in an iframe with a thin native bar
32+ done: true
33+ - text: README (EN) + README.fr (FR), root Makefile targets, .gitignore entries
34+ done: true
35+ - text: Verified go test / go vet and a Windows cross-build (wails build -platform windows/amd64)
36+ done: true
37+ - text: Launch the window on a machine with the GUI toolchain (webkit2gtk / Xcode / WebView2) and confirm the iframe embed
38+ done: false
39+comments: []
new file mode 100644
@@ -0,0 +1,39 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 5
3+title: Create a "native" UI to connect the webapp
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T19:19:43.179Z
9+updatedAt: 2026-09-17T22:23:39.000Z
10+labels: []
11+body: |
12+ Desktop shell for the ori webapp, built with Wails v2 (Go + platform webview), living in
13+ `ori-desktop/` as its own Go module (`rickub.com/bots-garden/ori-desktop`).
14+
15+ Behaviour: a connection screen asks for the ori server URL (default http://localhost:8888,
16+ remembered in `<user config dir>/ori-desktop/settings.json`), a Go-bound method checks
17+ `GET /healthz`, then the ori SPA is shown inside the window in an iframe (Wails v2 cannot
18+ navigate its main webview to an external URL; `runtime.BrowserOpenURL` is offered as fallback).
19+
20+ Docs: `ori-desktop/README.md` (EN) and `ori-desktop/README.fr.md` (FR).
21+ Root Makefile targets: `make desktop` (wails build), `make desktop-test` (pure-Go tests).
22+tasks:
23+ - text: Own Go module with Wails v2.16 layout (main.go, app.go, wails.json, frontend/, build/)
24+ done: true
25+ - text: Bound Go methods GetConfig / SaveConfig / CheckHealth / Connect / OpenInBrowser / SettingsPath
26+ done: true
27+ - text: Settings persisted as JSON in the user config dir, with unit tests
28+ done: true
29+ - text: /healthz probe with unit tests against httptest
30+ done: true
31+ - text: Plain HTML/CSS/JS frontend embedding ori in an iframe with a thin native bar
32+ done: true
33+ - text: README (EN) + README.fr (FR), root Makefile targets, .gitignore entries
34+ done: true
35+ - text: Verified go test / go vet and a Windows cross-build (wails build -platform windows/amd64)
36+ done: true
37+ - text: Launch the window on a machine with the GUI toolchain (webkit2gtk / Xcode / WebView2) and confirm the iframe embed
38+ done: false
39+comments: []
added .tickets/issues/0006-other-features.yaml +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 6
3+title: Other features
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T19:33:52.644Z
9+updatedAt: 2026-09-17T22:59:26.003Z
10+labels: []
11+body: |
12+ - avoir un panel preview pour voir le contenu de documents quand c'est du code mais avec la colorisation synthaxique, et aussi avoir la possibilité d'avoir une preview du rendu markdown et ascii doc
13+ - avoir un panel terminal
14+ - avoir un panel d'edition de fichiers (monaco editor par exemple)
15+ - le rendu de commande systeme doit etre fait avec une police non proportionnelle
16+tasks: []
17+comments: []
new file mode 100644
@@ -0,0 +1,17 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 6
3+title: Other features
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T19:33:52.644Z
9+updatedAt: 2026-09-17T22:59:26.003Z
10+labels: []
11+body: |
12+ - avoir un panel preview pour voir le contenu de documents quand c'est du code mais avec la colorisation synthaxique, et aussi avoir la possibilité d'avoir une preview du rendu markdown et ascii doc
13+ - avoir un panel terminal
14+ - avoir un panel d'edition de fichiers (monaco editor par exemple)
15+ - le rendu de commande systeme doit etre fait avec une police non proportionnelle
16+tasks: []
17+comments: []
added .tickets/issues/0007-add-claude-commands.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 7
3+title: Add claude commands
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T20:29:07.778Z
9+updatedAt: 2026-09-17T22:56:32.765Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 7
3+title: Add claude commands
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T20:29:07.778Z
9+updatedAt: 2026-09-17T22:56:32.765Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0008-add-acp-commands.yaml +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 8
3+title: Add ACP commands
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T20:29:17.602Z
9+updatedAt: 2026-09-17T22:57:22.259Z
10+labels: []
11+body: |
12+ The Claude Code ACP adapter announces its slash commands with a
13+ `session/update` of kind `available_commands_update`; the bridge already relayed it
14+ verbatim but the SPA ignored it.
15+
16+ Implemented (2026-09-17):
17+ - Reducer: `available_commands_update` stores `commands` in `ChatState` (typed
18+ `AcpAvailableCommand` in `ui/src/protocol.ts`), cleared on `hello`.
19+ - Composer: the commands are merged into the `/` popup with the skills discovered on
20+ disk (ticket 0012), deduplicated by name and tagged "command"; picking one inserts
21+ `/<name> ` — the text in `session/prompt` is the invocation mechanism.
22+ - Mock agent: emits one `available_commands_update` (`review`, `compact`) at session
23+ start so `make run-mock` exercises the feature; covered by a Go test.
24+tasks: []
25+comments: []
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 8
3+title: Add ACP commands
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T20:29:17.602Z
9+updatedAt: 2026-09-17T22:57:22.259Z
10+labels: []
11+body: |
12+ The Claude Code ACP adapter announces its slash commands with a
13+ `session/update` of kind `available_commands_update`; the bridge already relayed it
14+ verbatim but the SPA ignored it.
15+
16+ Implemented (2026-09-17):
17+ - Reducer: `available_commands_update` stores `commands` in `ChatState` (typed
18+ `AcpAvailableCommand` in `ui/src/protocol.ts`), cleared on `hello`.
19+ - Composer: the commands are merged into the `/` popup with the skills discovered on
20+ disk (ticket 0012), deduplicated by name and tagged "command"; picking one inserts
21+ `/<name> ` — the text in `session/prompt` is the invocation mechanism.
22+ - Mock agent: emits one `available_commands_update` (`review`, `compact`) at session
23+ start so `make run-mock` exercises the feature; covered by a Go test.
24+tasks: []
25+comments: []
added .tickets/issues/0009-preview-drawio.yaml +27 -0
new file mode 100644
@@ -0,0 +1,27 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 9
3+title: Preview drawio
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:05:28.282Z
9+updatedAt: 2026-09-17T22:56:38.766Z
10+labels: []
11+body: |
12+ Preview draw.io diagrams in the workspace Preview tab.
13+
14+ Implemented (2026-09-17):
15+ - `.drawio` / `.dio` (XML): rendered with the diagrams.net embed viewer
16+ (`embed.diagrams.net`, iframe, read-only lightbox) fed the XML locally over its
17+ postMessage protocol (`init` → `load`); the diagram is not uploaded. Rendered/Source
18+ toggle shows the XML. If the viewer does not answer within 8 s (isolated sandbox,
19+ no network), a notice appears and the Source view remains.
20+ - `.drawio.svg` / `.drawio.png`: plain images, shown by the image preview
21+ (`GET /api/raw`).
22+ - Trade-off recorded in docs/*/explanation/architecture.md: no offline renderer
23+ could be bundled (draw.io's viewer is not on npm, `mxgraph` is archived and lacks
24+ draw.io's shapes, `@markdown-viewer/drawio2svg` is GPL-3.0-only). Vendoring
25+ `viewer-static.min.js` (Apache-2.0, ~3 MB) is the future offline path.
26+tasks: []
27+comments: []
new file mode 100644
@@ -0,0 +1,27 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 9
3+title: Preview drawio
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:05:28.282Z
9+updatedAt: 2026-09-17T22:56:38.766Z
10+labels: []
11+body: |
12+ Preview draw.io diagrams in the workspace Preview tab.
13+
14+ Implemented (2026-09-17):
15+ - `.drawio` / `.dio` (XML): rendered with the diagrams.net embed viewer
16+ (`embed.diagrams.net`, iframe, read-only lightbox) fed the XML locally over its
17+ postMessage protocol (`init` → `load`); the diagram is not uploaded. Rendered/Source
18+ toggle shows the XML. If the viewer does not answer within 8 s (isolated sandbox,
19+ no network), a notice appears and the Source view remains.
20+ - `.drawio.svg` / `.drawio.png`: plain images, shown by the image preview
21+ (`GET /api/raw`).
22+ - Trade-off recorded in docs/*/explanation/architecture.md: no offline renderer
23+ could be bundled (draw.io's viewer is not on npm, `mxgraph` is archived and lacks
24+ draw.io's shapes, `@markdown-viewer/drawio2svg` is GPL-3.0-only). Vendoring
25+ `viewer-static.min.js` (Apache-2.0, ~3 MB) is the future offline path.
26+tasks: []
27+comments: []
added .tickets/issues/0010-preview-mermaid.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 10
3+title: Preview mermaid
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:05:38.680Z
9+updatedAt: 2026-09-17T21:05:38.680Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 10
3+title: Preview mermaid
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:05:38.680Z
9+updatedAt: 2026-09-17T21:05:38.680Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0011-add-file-selector.yaml +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 11
3+title: Add file selector (@)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:56:20.311Z
9+updatedAt: 2026-09-17T22:56:44.367Z
10+labels: []
11+body: |
12+ Typing `@` in the composer (at the start or after a space) opens a popup listing the
13+ workspace files, filtered as the user types, with keyboard navigation (↑/↓, Enter/Tab to
14+ pick, Esc to close). Picking inserts `@<relative path>` into the prompt.
15+
16+ Implemented (2026-09-17):
17+ - Backend: `GET /api/files/search?q=&limit=` in `internal/files` (recursive walk from
18+ `--cwd`, skips `.git`/`node_modules`, ranked, capped at 500 hits / 50 000 entries).
19+ - Prompt transport: the `prompt` WebSocket message gained an optional `attachments`
20+ array (`{path, name}`); the bridge converts each one into an ACP `resource_link`
21+ block (`file://` URI) sent next to the text block, and echoes them in `user_message`.
22+ The `@path` mention stays visible in the text. The Claude Code adapter turns the
23+ link into a file reference; the mock agent echoes `[attached: name]`.
24+ - Frontend: `ui/src/mentions.ts` (pure trigger/completion logic), `useCompletion`
25+ hook, `CompletionPopup`, rewired `PromptInput`; attachments whose mention was
26+ deleted before sending are dropped.
27+ - Docs: how-to `mention-files-and-skills.md`, references updated (EN + FR).
28+tasks: []
29+comments: []
new file mode 100644
@@ -0,0 +1,29 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 11
3+title: Add file selector (@)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T21:56:20.311Z
9+updatedAt: 2026-09-17T22:56:44.367Z
10+labels: []
11+body: |
12+ Typing `@` in the composer (at the start or after a space) opens a popup listing the
13+ workspace files, filtered as the user types, with keyboard navigation (↑/↓, Enter/Tab to
14+ pick, Esc to close). Picking inserts `@<relative path>` into the prompt.
15+
16+ Implemented (2026-09-17):
17+ - Backend: `GET /api/files/search?q=&limit=` in `internal/files` (recursive walk from
18+ `--cwd`, skips `.git`/`node_modules`, ranked, capped at 500 hits / 50 000 entries).
19+ - Prompt transport: the `prompt` WebSocket message gained an optional `attachments`
20+ array (`{path, name}`); the bridge converts each one into an ACP `resource_link`
21+ block (`file://` URI) sent next to the text block, and echoes them in `user_message`.
22+ The `@path` mention stays visible in the text. The Claude Code adapter turns the
23+ link into a file reference; the mock agent echoes `[attached: name]`.
24+ - Frontend: `ui/src/mentions.ts` (pure trigger/completion logic), `useCompletion`
25+ hook, `CompletionPopup`, rewired `PromptInput`; attachments whose mention was
26+ deleted before sending are dropped.
27+ - Docs: how-to `mention-files-and-skills.md`, references updated (EN + FR).
28+tasks: []
29+comments: []
added .tickets/issues/0012-add-a-skill-selector.yaml +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 12
3+title: Add a skill selector
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:02:20.346Z
9+updatedAt: 2026-09-17T22:56:49.863Z
10+labels: []
11+body: |
12+ A leading `/` in the composer opens a popup listing the Claude Code skills (name +
13+ description) with filtering and keyboard navigation; picking one inserts `/<name> `,
14+ which is how Claude Code invokes a skill (the text is what gets sent over ACP).
15+
16+ Implemented (2026-09-17):
17+ - Backend: new package `internal/skills`, `GET /api/skills`, discovering
18+ `<cwd>/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md`; frontmatter
19+ `name`/`description` parsed (quoted values and `>`/`|` block scalars), directory
20+ name as fallback, project skills shadow user skills. Tests on temp dirs.
21+ - Frontend: same popup as the `@` selector; skills are merged with the agent's
22+ `available_commands_update` commands (ticket 0008), tagged "skill" / "command".
23+ - Docs: how-to `mention-files-and-skills.md`, workspace API reference (EN + FR).
24+tasks: []
25+comments: []
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 12
3+title: Add a skill selector
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:02:20.346Z
9+updatedAt: 2026-09-17T22:56:49.863Z
10+labels: []
11+body: |
12+ A leading `/` in the composer opens a popup listing the Claude Code skills (name +
13+ description) with filtering and keyboard navigation; picking one inserts `/<name> `,
14+ which is how Claude Code invokes a skill (the text is what gets sent over ACP).
15+
16+ Implemented (2026-09-17):
17+ - Backend: new package `internal/skills`, `GET /api/skills`, discovering
18+ `<cwd>/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md`; frontmatter
19+ `name`/`description` parsed (quoted values and `>`/`|` block scalars), directory
20+ name as fallback, project skills shadow user skills. Tests on temp dirs.
21+ - Frontend: same popup as the `@` selector; skills are merged with the agent's
22+ `available_commands_update` commands (ticket 0008), tagged "skill" / "command".
23+ - Docs: how-to `mention-files-and-skills.md`, workspace API reference (EN + FR).
24+tasks: []
25+comments: []
added .tickets/issues/0013-themes-light-and-dark.yaml +26 -0
new file mode 100644
@@ -0,0 +1,26 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 13
3+title: Themes light and dark
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:15:20.392Z
9+updatedAt: 2026-09-17T23:20:00.000Z
10+labels: []
11+body: |
12+ Let the user choose between a light and a dark theme, light by default.
13+
14+ Implemented (2026-09-17):
15+ - `ui/src/theme.ts`: zustand store (`theme`: light | dark), `setTheme` / `toggleTheme` /
16+ `resetTheme`, `isTheme` guard; remembered in localStorage `ori.theme`; stamps
17+ `data-theme` on `<html>` at module load (before the first render) and on every change.
18+ - `app.css`: the dark palette moved from `@media (prefers-color-scheme: dark)` to
19+ `:root[data-theme="dark"]`; `color-scheme` follows. The OS preference is no longer
20+ followed (user's decision: two themes only, light default).
21+ - Header button ☾ / ☀ (`aria-label` "Switch to dark/light theme") in `App.tsx`;
22+ `MonacoViewer` picks `vs-dark` / `light` from the store; the terminal stays dark.
23+ - Tests: +6 store, +1 App (124 vitest total). Quality gate PASS (run #16).
24+ - Docs: how-to `switch-the-theme` (EN + FR), architecture explanation, indexes, README.
25+tasks: []
26+comments: []
new file mode 100644
@@ -0,0 +1,26 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 13
3+title: Themes light and dark
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:15:20.392Z
9+updatedAt: 2026-09-17T23:20:00.000Z
10+labels: []
11+body: |
12+ Let the user choose between a light and a dark theme, light by default.
13+
14+ Implemented (2026-09-17):
15+ - `ui/src/theme.ts`: zustand store (`theme`: light | dark), `setTheme` / `toggleTheme` /
16+ `resetTheme`, `isTheme` guard; remembered in localStorage `ori.theme`; stamps
17+ `data-theme` on `<html>` at module load (before the first render) and on every change.
18+ - `app.css`: the dark palette moved from `@media (prefers-color-scheme: dark)` to
19+ `:root[data-theme="dark"]`; `color-scheme` follows. The OS preference is no longer
20+ followed (user's decision: two themes only, light default).
21+ - Header button ☾ / ☀ (`aria-label` "Switch to dark/light theme") in `App.tsx`;
22+ `MonacoViewer` picks `vs-dark` / `light` from the store; the terminal stays dark.
23+ - Tests: +6 store, +1 App (124 vitest total). Quality gate PASS (run #16).
24+ - Docs: how-to `switch-the-theme` (EN + FR), architecture explanation, indexes, README.
25+tasks: []
26+comments: []
added .tickets/issues/0014-update-architecture-diagram.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 14
3+title: Update architecture diagram
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:24:19.634Z
9+updatedAt: 2026-09-17T22:24:19.634Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 14
3+title: Update architecture diagram
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:24:19.634Z
9+updatedAt: 2026-09-17T22:24:19.634Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0015-the-files-explorer-panel-needs-a-resizable-panel.yaml +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 15
3+title: The files explorer panel needs a resizable panel
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:35:25.296Z
9+updatedAt: 2026-09-17T23:15:00.000Z
10+labels: []
11+body: |
12+ The file tree column of the workspace panel had a fixed width (14rem). It is now resizable.
13+
14+ Implemented (2026-09-17):
15+ - Store: `fileTreeWidth` (px) in `ui/src/workspace.ts` with `setFileTreeWidth` /
16+ `resetFileTreeWidth` / `clampFileTreeWidth`, bounded 120–800 px (default 224), remembered
17+ in localStorage under `ori.fileTreeWidth` (guarded reads/writes, default on any failure).
18+ - UI: `ui/src/components/ResizeHandle.tsx` — ARIA separator between the tree and the tabs;
19+ pointer drag with capture, ←/→ (16 px), Home/End (bounds), double-click resets.
20+ `Workspace.tsx` applies the width through the `--filetree-width` CSS custom property;
21+ `FileTree` is unchanged.
22+ - Tests: +7 store, +7 ResizeHandle, +2 Workspace (118 vitest total). Quality gate PASS.
23+ - Docs: how-to `use-the-workspace` and architecture explanation (EN + FR).
24+tasks: []
25+comments: []
new file mode 100644
@@ -0,0 +1,25 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 15
3+title: The files explorer panel needs a resizable panel
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-17T22:35:25.296Z
9+updatedAt: 2026-09-17T23:15:00.000Z
10+labels: []
11+body: |
12+ The file tree column of the workspace panel had a fixed width (14rem). It is now resizable.
13+
14+ Implemented (2026-09-17):
15+ - Store: `fileTreeWidth` (px) in `ui/src/workspace.ts` with `setFileTreeWidth` /
16+ `resetFileTreeWidth` / `clampFileTreeWidth`, bounded 120–800 px (default 224), remembered
17+ in localStorage under `ori.fileTreeWidth` (guarded reads/writes, default on any failure).
18+ - UI: `ui/src/components/ResizeHandle.tsx` — ARIA separator between the tree and the tabs;
19+ pointer drag with capture, ←/→ (16 px), Home/End (bounds), double-click resets.
20+ `Workspace.tsx` applies the width through the `--filetree-width` CSS custom property;
21+ `FileTree` is unchanged.
22+ - Tests: +7 store, +7 ResizeHandle, +2 Workspace (118 vitest total). Quality gate PASS.
23+ - Docs: how-to `use-the-workspace` and architecture explanation (EN + FR).
24+tasks: []
25+comments: []
modified .vscode/settings.json +1 -1
@@ -9,7 +9,7 @@
99 "files.autoSave": "afterDelay",
1010 "files.autoSaveDelay": 1000,
1111 "workbench.tree.indent": 20,
12- "window.zoomLevel": 1.0,
12+ "window.zoomLevel": 0.5,
1313 "[markdown]": {
1414 "editor.unicodeHighlight.ambiguousCharacters": false,
1515 "editor.unicodeHighlight.invisibleCharacters": false,
@@ -9,7 +9,7 @@
9 "files.autoSave": "afterDelay",9 "files.autoSave": "afterDelay",
10 "files.autoSaveDelay": 1000,10 "files.autoSaveDelay": 1000,
11 "workbench.tree.indent": 20,11 "workbench.tree.indent": 20,
12- "window.zoomLevel": 1.0,12+ "window.zoomLevel": 0.5,
13 "[markdown]": {13 "[markdown]": {
14 "editor.unicodeHighlight.ambiguousCharacters": false,14 "editor.unicodeHighlight.ambiguousCharacters": false,
15 "editor.unicodeHighlight.invisibleCharacters": false,15 "editor.unicodeHighlight.invisibleCharacters": false,
modified Makefile +10 -1
@@ -1,6 +1,6 @@
11 # Ori — ACP web client. `make help` lists the targets.
22
3-.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev clean
3+.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev template desktop desktop-test clean
44
55 help: ## Show this help
66 @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-12s %s\n", $$1, $$2}'
@@ -34,6 +34,15 @@ run-mock: build ## Build then start the server with the demo mock agent (no Clau
3434 dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal)
3535 cd ui && npm run dev
3636
37+template: ## Build the k33g/ori sandbox template image (see kits/ori/README.md)
38+ docker build -f template/Dockerfile -t k33g/ori .
39+
40+desktop: ## Build the Wails desktop shell into ori-desktop/build/bin (needs the wails CLI, see ori-desktop/README.md)
41+ cd ori-desktop && wails build
42+
43+desktop-test: ## Run the desktop shell's pure-Go tests (no GUI toolchain needed)
44+ cd ori-desktop && go test ./internal/...
45+
3746 clean: ## Remove build artifacts
3847 rm -rf bin ui/dist/*
3948 @touch ui/dist/.gitkeep
@@ -1,6 +1,6 @@
1 # Ori — ACP web client. `make help` lists the targets.1 # Ori — ACP web client. `make help` lists the targets.
2 2
3-.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev clean3+.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev template desktop desktop-test clean
4 4
5 help: ## Show this help5 help: ## Show this help
6 @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-12s %s\n", $$1, $$2}'6 @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-12s %s\n", $$1, $$2}'
@@ -34,6 +34,15 @@ run-mock: build ## Build then start the server with the demo mock agent (no Clau
34 dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal)34 dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal)
35 cd ui && npm run dev35 cd ui && npm run dev
36 36
37+template: ## Build the k33g/ori sandbox template image (see kits/ori/README.md)
38+ docker build -f template/Dockerfile -t k33g/ori .
39+
40+desktop: ## Build the Wails desktop shell into ori-desktop/build/bin (needs the wails CLI, see ori-desktop/README.md)
41+ cd ori-desktop && wails build
42+
43+desktop-test: ## Run the desktop shell's pure-Go tests (no GUI toolchain needed)
44+ cd ori-desktop && go test ./internal/...
45+
37 clean: ## Remove build artifacts46 clean: ## Remove build artifacts
38 rm -rf bin ui/dist/*47 rm -rf bin ui/dist/*
39 @touch ui/dist/.gitkeep48 @touch ui/dist/.gitkeep
modified README.md +17 -0
@@ -1,2 +1,19 @@
11 # Ori
22 > The origin from which many voices are called, and through which they are known
3+
4+Ori is a web client for [ACP (Agent Client Protocol)](https://agentclientprotocol.com) code agents: a Go backend serves a React single-page application and connects, over ACP, to an agent such as Claude Code. The browser gets a Zed-style agent panel — streamed answers, thoughts, tool calls, plans and permission prompts — plus a workspace panel: resizable file tree, syntax-highlighted previews (with rendered Markdown and AsciiDoc), a Monaco editor and an interactive terminal. Light and dark themes, switchable from the header.
5+
6+## Quick start
7+
8+```bash
9+make deps # install frontend dependencies
10+make run-mock # build everything and start on :8888 with the bundled demo agent (no Claude needed)
11+make run # same, but connected to Claude Code via npx @zed-industries/claude-code-acp
12+make test # run the Go and frontend test suites
13+```
14+
15+Then open [http://localhost:8888](http://localhost:8888).
16+
17+## Documentation
18+
19+Full documentation, in English and French, lives in [`docs/`](docs/README.md) — tutorials, how-to guides, reference (CLI, WebSocket protocol) and architecture notes.
@@ -1,2 +1,19 @@
1 # Ori1 # Ori
2 > The origin from which many voices are called, and through which they are known2 > The origin from which many voices are called, and through which they are known
3+
4+Ori is a web client for [ACP (Agent Client Protocol)](https://agentclientprotocol.com) code agents: a Go backend serves a React single-page application and connects, over ACP, to an agent such as Claude Code. The browser gets a Zed-style agent panel — streamed answers, thoughts, tool calls, plans and permission prompts — plus a workspace panel: resizable file tree, syntax-highlighted previews (with rendered Markdown and AsciiDoc), a Monaco editor and an interactive terminal. Light and dark themes, switchable from the header.
5+
6+## Quick start
7+
8+```bash
9+make deps # install frontend dependencies
10+make run-mock # build everything and start on :8888 with the bundled demo agent (no Claude needed)
11+make run # same, but connected to Claude Code via npx @zed-industries/claude-code-acp
12+make test # run the Go and frontend test suites
13+```
14+
15+Then open [http://localhost:8888](http://localhost:8888).
16+
17+## Documentation
18+
19+Full documentation, in English and French, lives in [`docs/`](docs/README.md) — tutorials, how-to guides, reference (CLI, WebSocket protocol) and architecture notes.
modified cmd/ori-mock-agent/main.go +1 -1
@@ -11,7 +11,7 @@ import (
1111 "os"
1212 "time"
1313
14- "github.com/bots-garden/ori/internal/mockagent"
14+ "rickub.com/bots-garden/ori/internal/mockagent"
1515 )
1616
1717 func main() {
@@ -11,7 +11,7 @@ import (
11 "os"11 "os"
12 "time"12 "time"
13 13
14- "github.com/bots-garden/ori/internal/mockagent"14+ "rickub.com/bots-garden/ori/internal/mockagent"
15 )15 )
16 16
17 func main() {17 func main() {
modified cmd/ori/e2e_test.go +4 -4
@@ -14,9 +14,9 @@ import (
1414 "github.com/coder/websocket"
1515 "github.com/coder/websocket/wsjson"
1616
17- "github.com/bots-garden/ori/internal/agent"
18- "github.com/bots-garden/ori/internal/bridge"
19- "github.com/bots-garden/ori/internal/httpserver"
17+ "rickub.com/bots-garden/ori/internal/agent"
18+ "rickub.com/bots-garden/ori/internal/bridge"
19+ "rickub.com/bots-garden/ori/internal/httpserver"
2020 )
2121
2222 // TestEndToEnd exercises the full backend chain exactly as production wires
@@ -28,7 +28,7 @@ func TestEndToEnd(t *testing.T) {
2828 }
2929
3030 mockBinary := filepath.Join(t.TempDir(), "ori-mock-agent")
31- build := exec.Command("go", "build", "-o", mockBinary, "github.com/bots-garden/ori/cmd/ori-mock-agent")
31+ build := exec.Command("go", "build", "-o", mockBinary, "rickub.com/bots-garden/ori/cmd/ori-mock-agent")
3232 if output, err := build.CombinedOutput(); err != nil {
3333 t.Fatalf("building the mock agent failed: %v\n%s", err, output)
3434 }
@@ -14,9 +14,9 @@ import (
14 "github.com/coder/websocket"14 "github.com/coder/websocket"
15 "github.com/coder/websocket/wsjson"15 "github.com/coder/websocket/wsjson"
16 16
17- "github.com/bots-garden/ori/internal/agent"17+ "rickub.com/bots-garden/ori/internal/agent"
18- "github.com/bots-garden/ori/internal/bridge"18+ "rickub.com/bots-garden/ori/internal/bridge"
19- "github.com/bots-garden/ori/internal/httpserver"19+ "rickub.com/bots-garden/ori/internal/httpserver"
20 )20 )
21 21
22 // TestEndToEnd exercises the full backend chain exactly as production wires22 // TestEndToEnd exercises the full backend chain exactly as production wires
@@ -28,7 +28,7 @@ func TestEndToEnd(t *testing.T) {
28 }28 }
29 29
30 mockBinary := filepath.Join(t.TempDir(), "ori-mock-agent")30 mockBinary := filepath.Join(t.TempDir(), "ori-mock-agent")
31- build := exec.Command("go", "build", "-o", mockBinary, "github.com/bots-garden/ori/cmd/ori-mock-agent")31+ build := exec.Command("go", "build", "-o", mockBinary, "rickub.com/bots-garden/ori/cmd/ori-mock-agent")
32 if output, err := build.CombinedOutput(); err != nil {32 if output, err := build.CombinedOutput(); err != nil {
33 t.Fatalf("building the mock agent failed: %v\n%s", err, output)33 t.Fatalf("building the mock agent failed: %v\n%s", err, output)
34 }34 }
modified cmd/ori/main.go +21 -8
@@ -13,11 +13,14 @@ import (
1313 "net/http"
1414 "os"
1515
16- "github.com/bots-garden/ori/internal/agent"
17- "github.com/bots-garden/ori/internal/bridge"
18- "github.com/bots-garden/ori/internal/config"
19- "github.com/bots-garden/ori/internal/httpserver"
20- "github.com/bots-garden/ori/ui"
16+ "rickub.com/bots-garden/ori/internal/agent"
17+ "rickub.com/bots-garden/ori/internal/bridge"
18+ "rickub.com/bots-garden/ori/internal/config"
19+ "rickub.com/bots-garden/ori/internal/files"
20+ "rickub.com/bots-garden/ori/internal/httpserver"
21+ "rickub.com/bots-garden/ori/internal/skills"
22+ "rickub.com/bots-garden/ori/internal/terminal"
23+ "rickub.com/bots-garden/ori/ui"
2124 )
2225
2326 func main() {
@@ -48,11 +51,21 @@ func run(args []string) error {
4851 }
4952 defer func() { _ = session.Close() }()
5053 relay.SetSession(session)
54+ relay.SetWorkspaceRoot(cfg.Cwd)
5155 logger.Info("agent session ready", "sessionId", session.ID())
5256
53- handler := httpserver.Handler(ui.Dist(), map[string]http.Handler{
54- "GET /ws": relay.WebSocketHandler(),
55- })
57+ routes := map[string]http.Handler{
58+ "GET /ws": relay.WebSocketHandler(),
59+ "GET /ws/terminal": terminal.New(cfg.Cwd, logger).WebSocketHandler(),
60+ }
61+ for pattern, handler := range files.New(cfg.Cwd).Routes() {
62+ routes[pattern] = handler
63+ }
64+ home, _ := os.UserHomeDir()
65+ for pattern, handler := range skills.New(cfg.Cwd, home).Routes() {
66+ routes[pattern] = handler
67+ }
68+ handler := httpserver.Handler(ui.Dist(), routes)
5669
5770 logger.Info("ori listening", "addr", cfg.Addr)
5871 return http.ListenAndServe(cfg.Addr, handler)
@@ -13,11 +13,14 @@ import (
13 "net/http"13 "net/http"
14 "os"14 "os"
15 15
16- "github.com/bots-garden/ori/internal/agent"16+ "rickub.com/bots-garden/ori/internal/agent"
17- "github.com/bots-garden/ori/internal/bridge"17+ "rickub.com/bots-garden/ori/internal/bridge"
18- "github.com/bots-garden/ori/internal/config"18+ "rickub.com/bots-garden/ori/internal/config"
19- "github.com/bots-garden/ori/internal/httpserver"19+ "rickub.com/bots-garden/ori/internal/files"
20- "github.com/bots-garden/ori/ui"20+ "rickub.com/bots-garden/ori/internal/httpserver"
21+ "rickub.com/bots-garden/ori/internal/skills"
22+ "rickub.com/bots-garden/ori/internal/terminal"
23+ "rickub.com/bots-garden/ori/ui"
21 )24 )
22 25
23 func main() {26 func main() {
@@ -48,11 +51,21 @@ func run(args []string) error {
48 }51 }
49 defer func() { _ = session.Close() }()52 defer func() { _ = session.Close() }()
50 relay.SetSession(session)53 relay.SetSession(session)
54+ relay.SetWorkspaceRoot(cfg.Cwd)
51 logger.Info("agent session ready", "sessionId", session.ID())55 logger.Info("agent session ready", "sessionId", session.ID())
52 56
53- handler := httpserver.Handler(ui.Dist(), map[string]http.Handler{57+ routes := map[string]http.Handler{
54- "GET /ws": relay.WebSocketHandler(),58+ "GET /ws": relay.WebSocketHandler(),
55- })59+ "GET /ws/terminal": terminal.New(cfg.Cwd, logger).WebSocketHandler(),
60+ }
61+ for pattern, handler := range files.New(cfg.Cwd).Routes() {
62+ routes[pattern] = handler
63+ }
64+ home, _ := os.UserHomeDir()
65+ for pattern, handler := range skills.New(cfg.Cwd, home).Routes() {
66+ routes[pattern] = handler
67+ }
68+ handler := httpserver.Handler(ui.Dist(), routes)
56 69
57 logger.Info("ori listening", "addr", cfg.Addr)70 logger.Info("ori listening", "addr", cfg.Addr)
58 return http.ListenAndServe(cfg.Addr, handler)71 return http.ListenAndServe(cfg.Addr, handler)
added docs/.assets/ori-architecture.drawio.svg +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1220" height="640" viewBox="0 0 1220 640" content="&lt;mxfile host=&quot;Electron&quot; agent=&quot;make-diagram.mjs&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Ori Architecture&quot; id=&quot;diagram-1&quot;&gt;&lt;mxGraphModel dx=&quot;1000&quot; dy=&quot;600&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;1169&quot; pageHeight=&quot;826&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;&lt;mxCell id=&quot;3&quot; value=&quot;Browser&amp;amp;#10;React SPA&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=0;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;20&quot; width=&quot;220&quot; height=&quot;110&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;4&quot; value=&quot;UI Components&amp;amp;#10;· Agent Panel&amp;amp;#10;· Workspace Panel&amp;amp;#10;· File Tree&amp;amp;#10;· Monaco Editor&amp;amp;#10;· Terminal (xterm.js)&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;150&quot; width=&quot;220&quot; height=&quot;150&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;5&quot; value=&quot;httpserver&amp;amp;#10;Serves SPA + /ws&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;20&quot; width=&quot;280&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;6&quot; value=&quot;bridge&amp;amp;#10;· WebSocket ⇄ ACP&amp;amp;#10;· Event broadcast&amp;amp;#10;· Permission routing&amp;amp;#10;· History replay&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;140&quot; width=&quot;280&quot; height=&quot;130&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;7&quot; value=&quot;agent&amp;amp;#10;ACP session&amp;amp;#10;lifecycle&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;300&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;8&quot; value=&quot;files&amp;amp;#10;Workspace&amp;amp;#10;file API&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;515&quot; y=&quot;300&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;9&quot; value=&quot;terminal&amp;amp;#10;PTY per&amp;amp;#10;connection&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;410&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;10&quot; value=&quot;mockagent&amp;amp;#10;Demo agent&amp;amp;#10;(testing)&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;515&quot; y=&quot;410&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;11&quot; value=&quot;ACP Agent&amp;amp;#10;(claude-code-acp / Gemini CLI)&amp;amp;#10;&amp;amp;#10;JSON-RPC over stdio&amp;amp;#10;session_update · permission_request&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#0062FF;fontColor=#0062FF;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;800&quot; y=&quot;230&quot; width=&quot;400&quot; height=&quot;130&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;12&quot; value=&quot;Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=15;fontStyle=0;strokeWidth=2;dashed=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;560&quot; width=&quot;1180&quot; height=&quot;60&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;13&quot; value=&quot;1. load SPA&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;3&quot; target=&quot;5&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;14&quot; value=&quot;2. WebSocket&amp;amp;#10;/ws&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.7272727272727273;exitDx=0;exitDy=0;entryX=0;entryY=0.3076923076923077;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;3&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;15&quot; value=&quot;3. prompt +&amp;amp;#10;permission&amp;amp;#10;responses&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.6538461538461539;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;16&quot; value=&quot;4. session_update&amp;amp;#10;events (replay)&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;6&quot; target=&quot;4&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;Array as=&quot;points&quot;&gt;&lt;mxPoint x=&quot;310&quot; y=&quot;230&quot;/&gt;&lt;mxPoint x=&quot;280&quot; y=&quot;230&quot;/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;17&quot; value=&quot;5. ACP&amp;amp;#10;lifecycle&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=0.2857142857142857;exitY=1;exitDx=0;exitDy=0;entryX=0.4962962962962963;entryY=0;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;6&quot; target=&quot;7&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;18&quot; value=&quot;6. stdio&amp;amp;#10;JSON-RPC&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;7&quot; target=&quot;11&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;19&quot; value=&quot;7. updates +&amp;amp;#10;permissions&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;11&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;Array as=&quot;points&quot;&gt;&lt;mxPoint x=&quot;720&quot; y=&quot;200&quot;/&gt;&lt;mxPoint x=&quot;650&quot; y=&quot;200&quot;/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;20&quot; value=&quot;file ops&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;exitX=1;exitY=0.8666666666666667;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;8&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;21&quot; value=&quot;shell I/O&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;9&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;&lt;/diagram&gt;&lt;/mxfile&gt;">
2+ <defs><marker id="arrow-dark" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#15202b"/></marker><marker id="arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0062FF"/></marker></defs>
3+ <path d="M 242 75 L 364 75" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
4+ <rect x="254.2" y="62.5" width="97.6" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
5+ <text x="303" y="75" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">1. load SPA</text>
6+ <path d="M 242 100 L 364 180" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
7+ <rect x="250.4" y="119" width="105.19999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
8+ <text x="303" y="131.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">2. WebSocket</text>
9+ <text x="303" y="148.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">/ws</text>
10+ <path d="M 242 225 L 364 225" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
11+ <rect x="254.2" y="195.5" width="97.6" height="59" rx="4" fill="#fdfdfb" opacity="0.92"/>
12+ <text x="303" y="208" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">3. prompt +</text>
13+ <text x="303" y="225" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">permission</text>
14+ <text x="303" y="242" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">responses</text>
15+ <path d="M 368 205 L 310 230 L 280 230 L 246 225" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
16+ <rect x="267.4" y="196.5" width="143.2" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
17+ <text x="339" y="209" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">4. session_update</text>
18+ <text x="339" y="226" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">events (replay)</text>
19+ <path d="M 450 272 L 437 294" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
20+ <rect x="402.3" y="262" width="82.39999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
21+ <text x="443.5" y="274.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">5. ACP</text>
22+ <text x="443.5" y="291.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">lifecycle</text>
23+ <path d="M 507 345 L 794 295" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
24+ <rect x="613.1" y="299" width="74.8" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
25+ <text x="650.5" y="311.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">6. stdio</text>
26+ <text x="650.5" y="328.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">JSON-RPC</text>
27+ <path d="M 798 295 L 720 200 L 650 200 L 656 205" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
28+ <rect x="706.4" y="226.5" width="105.19999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
29+ <text x="759" y="239" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">7. updates +</text>
30+ <text x="759" y="256" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">permissions</text>
31+ <path d="M 242 280 L 509 345" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
32+ <rect x="338.1" y="300" width="74.8" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
33+ <text x="375.5" y="312.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">file ops</text>
34+ <path d="M 130 302 L 364 455" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
35+ <rect x="205.8" y="366" width="82.39999999999999" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
36+ <text x="247" y="378.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">shell I/O</text>
37+ <rect x="20" y="20" width="220" height="110" rx="10" ry="10" fill="#fdfdfb" stroke="#15202b" stroke-width="2"/>
38+ <text x="130" y="64.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="normal" fill="#15202b">Browser</text>
39+ <text x="130" y="85.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="normal" fill="#15202b">React SPA</text>
40+ <rect x="20" y="150" width="220" height="150" rx="10" ry="10" fill="#e8ebda" stroke="#15202b" stroke-width="2"/>
41+ <text x="130" y="172.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">UI Components</text>
42+ <text x="130" y="193.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Agent Panel</text>
43+ <text x="130" y="214.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Workspace Panel</text>
44+ <text x="130" y="235.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· File Tree</text>
45+ <text x="130" y="256.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Monaco Editor</text>
46+ <text x="130" y="277.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Terminal (xterm.js)</text>
47+ <rect x="370" y="20" width="280" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
48+ <text x="510" y="54.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">httpserver</text>
49+ <text x="510" y="75.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">Serves SPA + /ws</text>
50+ <rect x="370" y="140" width="280" height="130" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
51+ <text x="510" y="163" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">bridge</text>
52+ <text x="510" y="184" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· WebSocket ⇄ ACP</text>
53+ <text x="510" y="205" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· Event broadcast</text>
54+ <text x="510" y="226" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· Permission routing</text>
55+ <text x="510" y="247" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· History replay</text>
56+ <rect x="370" y="300" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
57+ <text x="437.5" y="324" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">agent</text>
58+ <text x="437.5" y="345" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">ACP session</text>
59+ <text x="437.5" y="366" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">lifecycle</text>
60+ <rect x="515" y="300" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
61+ <text x="582.5" y="324" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">files</text>
62+ <text x="582.5" y="345" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">Workspace</text>
63+ <text x="582.5" y="366" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">file API</text>
64+ <rect x="370" y="410" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
65+ <text x="437.5" y="434" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">terminal</text>
66+ <text x="437.5" y="455" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">PTY per</text>
67+ <text x="437.5" y="476" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">connection</text>
68+ <rect x="515" y="410" width="135" height="90" rx="10" ry="10" fill="#e8ebda" stroke="#15202b" stroke-width="2"/>
69+ <text x="582.5" y="434" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">mockagent</text>
70+ <text x="582.5" y="455" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">Demo agent</text>
71+ <text x="582.5" y="476" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">(testing)</text>
72+ <rect x="800" y="230" width="400" height="130" rx="10" ry="10" fill="#fdfdfb" stroke="#0062FF" stroke-width="2"/>
73+ <text x="1000" y="253" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">ACP Agent</text>
74+ <text x="1000" y="274" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">(claude-code-acp / Gemini CLI)</text>
75+ <text x="1000" y="295" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF"></text>
76+ <text x="1000" y="316" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">JSON-RPC over stdio</text>
77+ <text x="1000" y="337" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">session_update · permission_request</text>
78+ <rect x="20" y="560" width="1180" height="60" rx="10" ry="10" fill="#fdfdfb" stroke="#15202b" stroke-width="2" stroke-dasharray="6 5"/>
79+ <text x="610" y="590" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="15" font-weight="normal" fill="#15202b">Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.</text>
80+</svg>
new file mode 100644
@@ -0,0 +1,80 @@
1+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1220" height="640" viewBox="0 0 1220 640" content="&lt;mxfile host=&quot;Electron&quot; agent=&quot;make-diagram.mjs&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Ori Architecture&quot; id=&quot;diagram-1&quot;&gt;&lt;mxGraphModel dx=&quot;1000&quot; dy=&quot;600&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;1169&quot; pageHeight=&quot;826&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;&lt;mxCell id=&quot;3&quot; value=&quot;Browser&amp;amp;#10;React SPA&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=0;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;20&quot; width=&quot;220&quot; height=&quot;110&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;4&quot; value=&quot;UI Components&amp;amp;#10;· Agent Panel&amp;amp;#10;· Workspace Panel&amp;amp;#10;· File Tree&amp;amp;#10;· Monaco Editor&amp;amp;#10;· Terminal (xterm.js)&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;150&quot; width=&quot;220&quot; height=&quot;150&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;5&quot; value=&quot;httpserver&amp;amp;#10;Serves SPA + /ws&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;20&quot; width=&quot;280&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;6&quot; value=&quot;bridge&amp;amp;#10;· WebSocket ⇄ ACP&amp;amp;#10;· Event broadcast&amp;amp;#10;· Permission routing&amp;amp;#10;· History replay&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;140&quot; width=&quot;280&quot; height=&quot;130&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;7&quot; value=&quot;agent&amp;amp;#10;ACP session&amp;amp;#10;lifecycle&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;300&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;8&quot; value=&quot;files&amp;amp;#10;Workspace&amp;amp;#10;file API&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;515&quot; y=&quot;300&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;9&quot; value=&quot;terminal&amp;amp;#10;PTY per&amp;amp;#10;connection&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;370&quot; y=&quot;410&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;10&quot; value=&quot;mockagent&amp;amp;#10;Demo agent&amp;amp;#10;(testing)&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;515&quot; y=&quot;410&quot; width=&quot;135&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;11&quot; value=&quot;ACP Agent&amp;amp;#10;(claude-code-acp / Gemini CLI)&amp;amp;#10;&amp;amp;#10;JSON-RPC over stdio&amp;amp;#10;session_update · permission_request&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#0062FF;fontColor=#0062FF;fontSize=17;fontStyle=1;strokeWidth=2;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;800&quot; y=&quot;230&quot; width=&quot;400&quot; height=&quot;130&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;12&quot; value=&quot;Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.&quot; style=&quot;rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=15;fontStyle=0;strokeWidth=2;dashed=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;560&quot; width=&quot;1180&quot; height=&quot;60&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;13&quot; value=&quot;1. load SPA&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;3&quot; target=&quot;5&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;14&quot; value=&quot;2. WebSocket&amp;amp;#10;/ws&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.7272727272727273;exitDx=0;exitDy=0;entryX=0;entryY=0.3076923076923077;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;3&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;15&quot; value=&quot;3. prompt +&amp;amp;#10;permission&amp;amp;#10;responses&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.6538461538461539;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;16&quot; value=&quot;4. session_update&amp;amp;#10;events (replay)&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;6&quot; target=&quot;4&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;Array as=&quot;points&quot;&gt;&lt;mxPoint x=&quot;310&quot; y=&quot;230&quot;/&gt;&lt;mxPoint x=&quot;280&quot; y=&quot;230&quot;/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;17&quot; value=&quot;5. ACP&amp;amp;#10;lifecycle&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=0.2857142857142857;exitY=1;exitDx=0;exitDy=0;entryX=0.4962962962962963;entryY=0;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;6&quot; target=&quot;7&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;18&quot; value=&quot;6. stdio&amp;amp;#10;JSON-RPC&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;7&quot; target=&quot;11&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;19&quot; value=&quot;7. updates +&amp;amp;#10;permissions&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;11&quot; target=&quot;6&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;Array as=&quot;points&quot;&gt;&lt;mxPoint x=&quot;720&quot; y=&quot;200&quot;/&gt;&lt;mxPoint x=&quot;650&quot; y=&quot;200&quot;/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;20&quot; value=&quot;file ops&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;exitX=1;exitY=0.8666666666666667;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;8&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;21&quot; value=&quot;shell I/O&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;4&quot; target=&quot;9&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;&lt;/diagram&gt;&lt;/mxfile&gt;">
2+ <defs><marker id="arrow-dark" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#15202b"/></marker><marker id="arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0062FF"/></marker></defs>
3+ <path d="M 242 75 L 364 75" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
4+ <rect x="254.2" y="62.5" width="97.6" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
5+ <text x="303" y="75" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">1. load SPA</text>
6+ <path d="M 242 100 L 364 180" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
7+ <rect x="250.4" y="119" width="105.19999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
8+ <text x="303" y="131.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">2. WebSocket</text>
9+ <text x="303" y="148.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">/ws</text>
10+ <path d="M 242 225 L 364 225" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
11+ <rect x="254.2" y="195.5" width="97.6" height="59" rx="4" fill="#fdfdfb" opacity="0.92"/>
12+ <text x="303" y="208" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">3. prompt +</text>
13+ <text x="303" y="225" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">permission</text>
14+ <text x="303" y="242" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">responses</text>
15+ <path d="M 368 205 L 310 230 L 280 230 L 246 225" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
16+ <rect x="267.4" y="196.5" width="143.2" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
17+ <text x="339" y="209" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">4. session_update</text>
18+ <text x="339" y="226" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">events (replay)</text>
19+ <path d="M 450 272 L 437 294" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
20+ <rect x="402.3" y="262" width="82.39999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
21+ <text x="443.5" y="274.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">5. ACP</text>
22+ <text x="443.5" y="291.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">lifecycle</text>
23+ <path d="M 507 345 L 794 295" fill="none" stroke="#15202b" stroke-width="2" marker-end="url(#arrow-dark)"/>
24+ <rect x="613.1" y="299" width="74.8" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
25+ <text x="650.5" y="311.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">6. stdio</text>
26+ <text x="650.5" y="328.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#15202b">JSON-RPC</text>
27+ <path d="M 798 295 L 720 200 L 650 200 L 656 205" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
28+ <rect x="706.4" y="226.5" width="105.19999999999999" height="42" rx="4" fill="#fdfdfb" opacity="0.92"/>
29+ <text x="759" y="239" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">7. updates +</text>
30+ <text x="759" y="256" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">permissions</text>
31+ <path d="M 242 280 L 509 345" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
32+ <rect x="338.1" y="300" width="74.8" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
33+ <text x="375.5" y="312.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">file ops</text>
34+ <path d="M 130 302 L 364 455" fill="none" stroke="#0062FF" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrow-blue)"/>
35+ <rect x="205.8" y="366" width="82.39999999999999" height="25" rx="4" fill="#fdfdfb" opacity="0.92"/>
36+ <text x="247" y="378.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="14" fill="#0062FF">shell I/O</text>
37+ <rect x="20" y="20" width="220" height="110" rx="10" ry="10" fill="#fdfdfb" stroke="#15202b" stroke-width="2"/>
38+ <text x="130" y="64.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="normal" fill="#15202b">Browser</text>
39+ <text x="130" y="85.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="normal" fill="#15202b">React SPA</text>
40+ <rect x="20" y="150" width="220" height="150" rx="10" ry="10" fill="#e8ebda" stroke="#15202b" stroke-width="2"/>
41+ <text x="130" y="172.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">UI Components</text>
42+ <text x="130" y="193.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Agent Panel</text>
43+ <text x="130" y="214.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Workspace Panel</text>
44+ <text x="130" y="235.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· File Tree</text>
45+ <text x="130" y="256.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Monaco Editor</text>
46+ <text x="130" y="277.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">· Terminal (xterm.js)</text>
47+ <rect x="370" y="20" width="280" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
48+ <text x="510" y="54.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">httpserver</text>
49+ <text x="510" y="75.5" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">Serves SPA + /ws</text>
50+ <rect x="370" y="140" width="280" height="130" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
51+ <text x="510" y="163" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">bridge</text>
52+ <text x="510" y="184" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· WebSocket ⇄ ACP</text>
53+ <text x="510" y="205" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· Event broadcast</text>
54+ <text x="510" y="226" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· Permission routing</text>
55+ <text x="510" y="247" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">· History replay</text>
56+ <rect x="370" y="300" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
57+ <text x="437.5" y="324" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">agent</text>
58+ <text x="437.5" y="345" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">ACP session</text>
59+ <text x="437.5" y="366" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">lifecycle</text>
60+ <rect x="515" y="300" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
61+ <text x="582.5" y="324" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">files</text>
62+ <text x="582.5" y="345" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">Workspace</text>
63+ <text x="582.5" y="366" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">file API</text>
64+ <rect x="370" y="410" width="135" height="90" rx="10" ry="10" fill="#15202b" stroke="#15202b" stroke-width="2"/>
65+ <text x="437.5" y="434" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">terminal</text>
66+ <text x="437.5" y="455" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">PTY per</text>
67+ <text x="437.5" y="476" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#fdfdfb">connection</text>
68+ <rect x="515" y="410" width="135" height="90" rx="10" ry="10" fill="#e8ebda" stroke="#15202b" stroke-width="2"/>
69+ <text x="582.5" y="434" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">mockagent</text>
70+ <text x="582.5" y="455" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">Demo agent</text>
71+ <text x="582.5" y="476" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#15202b">(testing)</text>
72+ <rect x="800" y="230" width="400" height="130" rx="10" ry="10" fill="#fdfdfb" stroke="#0062FF" stroke-width="2"/>
73+ <text x="1000" y="253" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">ACP Agent</text>
74+ <text x="1000" y="274" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">(claude-code-acp / Gemini CLI)</text>
75+ <text x="1000" y="295" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF"></text>
76+ <text x="1000" y="316" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">JSON-RPC over stdio</text>
77+ <text x="1000" y="337" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="17" font-weight="bold" fill="#0062FF">session_update · permission_request</text>
78+ <rect x="20" y="560" width="1180" height="60" rx="10" ry="10" fill="#fdfdfb" stroke="#15202b" stroke-width="2" stroke-dasharray="6 5"/>
79+ <text x="610" y="590" text-anchor="middle" dominant-baseline="central" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="15" font-weight="normal" fill="#15202b">Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.</text>
80+</svg>
added docs/.assets/ori-architecture.json +170 -0
new file mode 100644
@@ -0,0 +1,170 @@
1+{
2+ "name": "Ori Architecture",
3+ "width": 1220,
4+ "height": 640,
5+ "nodes": [
6+ {
7+ "id": "browser",
8+ "x": 20,
9+ "y": 20,
10+ "w": 220,
11+ "h": 110,
12+ "label": "Browser\\nReact SPA",
13+ "kind": "box"
14+ },
15+ {
16+ "id": "ui-components",
17+ "x": 20,
18+ "y": 150,
19+ "w": 220,
20+ "h": 150,
21+ "label": "UI Components\\n· Agent Panel\\n· Workspace Panel\\n· File Tree\\n· Monaco Editor\\n· Terminal (xterm.js)",
22+ "kind": "accent"
23+ },
24+ {
25+ "id": "httpserver",
26+ "x": 370,
27+ "y": 20,
28+ "w": 280,
29+ "h": 90,
30+ "label": "httpserver\\nServes SPA + /ws",
31+ "kind": "dark"
32+ },
33+ {
34+ "id": "bridge",
35+ "x": 370,
36+ "y": 140,
37+ "w": 280,
38+ "h": 130,
39+ "label": "bridge\\n· WebSocket ⇄ ACP\\n· Event broadcast\\n· Permission routing\\n· History replay",
40+ "kind": "dark"
41+ },
42+ {
43+ "id": "agent",
44+ "x": 370,
45+ "y": 300,
46+ "w": 135,
47+ "h": 90,
48+ "label": "agent\\nACP session\\nlifecycle",
49+ "kind": "dark"
50+ },
51+ {
52+ "id": "files",
53+ "x": 515,
54+ "y": 300,
55+ "w": 135,
56+ "h": 90,
57+ "label": "files\\nWorkspace\\nfile API",
58+ "kind": "dark"
59+ },
60+ {
61+ "id": "terminal",
62+ "x": 370,
63+ "y": 410,
64+ "w": 135,
65+ "h": 90,
66+ "label": "terminal\\nPTY per\\nconnection",
67+ "kind": "dark"
68+ },
69+ {
70+ "id": "mockagent",
71+ "x": 515,
72+ "y": 410,
73+ "w": 135,
74+ "h": 90,
75+ "label": "mockagent\\nDemo agent\\n(testing)",
76+ "kind": "accent"
77+ },
78+ {
79+ "id": "acp-agent",
80+ "x": 800,
81+ "y": 230,
82+ "w": 400,
83+ "h": 130,
84+ "label": "ACP Agent\\n(claude-code-acp / Gemini CLI)\\n\\nJSON-RPC over stdio\\nsession_update · permission_request",
85+ "kind": "intent"
86+ },
87+ {
88+ "id": "note",
89+ "x": 20,
90+ "y": 560,
91+ "w": 1180,
92+ "h": 60,
93+ "label": "Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.",
94+ "kind": "note"
95+ }
96+ ],
97+ "edges": [
98+ { "from": "browser", "to": "httpserver", "label": "1. load SPA" },
99+ {
100+ "from": "browser",
101+ "to": "bridge",
102+ "label": "2. WebSocket\\n/ws",
103+ "fromSide": "right",
104+ "fromAt": 100,
105+ "toSide": "left",
106+ "toAt": 180
107+ },
108+ {
109+ "from": "ui-components",
110+ "to": "bridge",
111+ "label": "3. prompt +\\npermission\\nresponses",
112+ "fromSide": "right",
113+ "fromAt": 225,
114+ "toSide": "left",
115+ "toAt": 225
116+ },
117+ {
118+ "from": "bridge",
119+ "to": "ui-components",
120+ "label": "4. session_update\\nevents (replay)",
121+ "dashed": true,
122+ "waypoints": [{ "x": 310, "y": 230 }, { "x": 280, "y": 230 }]
123+ },
124+ {
125+ "from": "bridge",
126+ "to": "agent",
127+ "label": "5. ACP\\nlifecycle",
128+ "fromSide": "bottom",
129+ "fromAt": 450,
130+ "toSide": "top",
131+ "toAt": 437
132+ },
133+ {
134+ "from": "agent",
135+ "to": "acp-agent",
136+ "label": "6. stdio\\nJSON-RPC",
137+ "fromSide": "right",
138+ "fromAt": 345,
139+ "toSide": "left",
140+ "toAt": 295
141+ },
142+ {
143+ "from": "acp-agent",
144+ "to": "bridge",
145+ "label": "7. updates +\\npermissions",
146+ "dashed": true,
147+ "waypoints": [{ "x": 720, "y": 200 }, { "x": 650, "y": 200 }]
148+ },
149+ {
150+ "from": "ui-components",
151+ "to": "files",
152+ "label": "file ops",
153+ "fromSide": "right",
154+ "fromAt": 280,
155+ "toSide": "left",
156+ "toAt": 345,
157+ "dashed": true
158+ },
159+ {
160+ "from": "ui-components",
161+ "to": "terminal",
162+ "label": "shell I/O",
163+ "fromSide": "bottom",
164+ "fromAt": 130,
165+ "toSide": "left",
166+ "toAt": 455,
167+ "dashed": true
168+ }
169+ ]
170+}
new file mode 100644
@@ -0,0 +1,170 @@
1+{
2+ "name": "Ori Architecture",
3+ "width": 1220,
4+ "height": 640,
5+ "nodes": [
6+ {
7+ "id": "browser",
8+ "x": 20,
9+ "y": 20,
10+ "w": 220,
11+ "h": 110,
12+ "label": "Browser\\nReact SPA",
13+ "kind": "box"
14+ },
15+ {
16+ "id": "ui-components",
17+ "x": 20,
18+ "y": 150,
19+ "w": 220,
20+ "h": 150,
21+ "label": "UI Components\\n· Agent Panel\\n· Workspace Panel\\n· File Tree\\n· Monaco Editor\\n· Terminal (xterm.js)",
22+ "kind": "accent"
23+ },
24+ {
25+ "id": "httpserver",
26+ "x": 370,
27+ "y": 20,
28+ "w": 280,
29+ "h": 90,
30+ "label": "httpserver\\nServes SPA + /ws",
31+ "kind": "dark"
32+ },
33+ {
34+ "id": "bridge",
35+ "x": 370,
36+ "y": 140,
37+ "w": 280,
38+ "h": 130,
39+ "label": "bridge\\n· WebSocket ⇄ ACP\\n· Event broadcast\\n· Permission routing\\n· History replay",
40+ "kind": "dark"
41+ },
42+ {
43+ "id": "agent",
44+ "x": 370,
45+ "y": 300,
46+ "w": 135,
47+ "h": 90,
48+ "label": "agent\\nACP session\\nlifecycle",
49+ "kind": "dark"
50+ },
51+ {
52+ "id": "files",
53+ "x": 515,
54+ "y": 300,
55+ "w": 135,
56+ "h": 90,
57+ "label": "files\\nWorkspace\\nfile API",
58+ "kind": "dark"
59+ },
60+ {
61+ "id": "terminal",
62+ "x": 370,
63+ "y": 410,
64+ "w": 135,
65+ "h": 90,
66+ "label": "terminal\\nPTY per\\nconnection",
67+ "kind": "dark"
68+ },
69+ {
70+ "id": "mockagent",
71+ "x": 515,
72+ "y": 410,
73+ "w": 135,
74+ "h": 90,
75+ "label": "mockagent\\nDemo agent\\n(testing)",
76+ "kind": "accent"
77+ },
78+ {
79+ "id": "acp-agent",
80+ "x": 800,
81+ "y": 230,
82+ "w": 400,
83+ "h": 130,
84+ "label": "ACP Agent\\n(claude-code-acp / Gemini CLI)\\n\\nJSON-RPC over stdio\\nsession_update · permission_request",
85+ "kind": "intent"
86+ },
87+ {
88+ "id": "note",
89+ "x": 20,
90+ "y": 560,
91+ "w": 1180,
92+ "h": 60,
93+ "label": "Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call.",
94+ "kind": "note"
95+ }
96+ ],
97+ "edges": [
98+ { "from": "browser", "to": "httpserver", "label": "1. load SPA" },
99+ {
100+ "from": "browser",
101+ "to": "bridge",
102+ "label": "2. WebSocket\\n/ws",
103+ "fromSide": "right",
104+ "fromAt": 100,
105+ "toSide": "left",
106+ "toAt": 180
107+ },
108+ {
109+ "from": "ui-components",
110+ "to": "bridge",
111+ "label": "3. prompt +\\npermission\\nresponses",
112+ "fromSide": "right",
113+ "fromAt": 225,
114+ "toSide": "left",
115+ "toAt": 225
116+ },
117+ {
118+ "from": "bridge",
119+ "to": "ui-components",
120+ "label": "4. session_update\\nevents (replay)",
121+ "dashed": true,
122+ "waypoints": [{ "x": 310, "y": 230 }, { "x": 280, "y": 230 }]
123+ },
124+ {
125+ "from": "bridge",
126+ "to": "agent",
127+ "label": "5. ACP\\nlifecycle",
128+ "fromSide": "bottom",
129+ "fromAt": 450,
130+ "toSide": "top",
131+ "toAt": 437
132+ },
133+ {
134+ "from": "agent",
135+ "to": "acp-agent",
136+ "label": "6. stdio\\nJSON-RPC",
137+ "fromSide": "right",
138+ "fromAt": 345,
139+ "toSide": "left",
140+ "toAt": 295
141+ },
142+ {
143+ "from": "acp-agent",
144+ "to": "bridge",
145+ "label": "7. updates +\\npermissions",
146+ "dashed": true,
147+ "waypoints": [{ "x": 720, "y": 200 }, { "x": 650, "y": 200 }]
148+ },
149+ {
150+ "from": "ui-components",
151+ "to": "files",
152+ "label": "file ops",
153+ "fromSide": "right",
154+ "fromAt": 280,
155+ "toSide": "left",
156+ "toAt": 345,
157+ "dashed": true
158+ },
159+ {
160+ "from": "ui-components",
161+ "to": "terminal",
162+ "label": "shell I/O",
163+ "fromSide": "bottom",
164+ "fromAt": 130,
165+ "toSide": "left",
166+ "toAt": 455,
167+ "dashed": true
168+ }
169+ ]
170+}
added docs/README.md +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+# Ori documentation
2+
3+Choose your language / Choisissez votre langue :
4+
5+- **[English](en/README.md)**
6+- **[Français](fr/README.md)**
7+
8+Both editions follow the [Diátaxis](https://diataxis.fr) structure: tutorials to learn, how-to guides to get tasks done, reference to look details up, and explanation to understand the design.
new file mode 100644
@@ -0,0 +1,8 @@
1+# Ori documentation
2+
3+Choose your language / Choisissez votre langue :
4+
5+- **[English](en/README.md)**
6+- **[Français](fr/README.md)**
7+
8+Both editions follow the [Diátaxis](https://diataxis.fr) structure: tutorials to learn, how-to guides to get tasks done, reference to look details up, and explanation to understand the design.
added docs/diagrams/packages.drawio +112 -0
new file mode 100644
@@ -0,0 +1,112 @@
1+<mxfile host="65bd71144e">
2+ <diagram id="ori-packages" name="ori package dependencies">
3+ <mxGraphModel dx="727" dy="240" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="850" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="spa" value="ui/src React SPA&#10;(chat panel + workspace: file tree, Monaco preview/editor,&#10;markdown/asciidoc rendering, xterm.js terminal)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
8+ <mxGeometry x="40" y="30" width="260" height="80" as="geometry"/>
9+ </mxCell>
10+ <mxCell id="uiembed" value="ui (Go package)&#10;(go:embed of the SPA production build)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
11+ <mxGeometry x="380" y="40" width="240" height="60" as="geometry"/>
12+ </mxCell>
13+ <mxCell id="cmdori" value="cmd/ori&#10;(entry point: wires config, agent, bridge, http)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
14+ <mxGeometry x="380" y="180" width="240" height="60" as="geometry"/>
15+ </mxCell>
16+ <mxCell id="config" value="internal/config&#10;(CLI flag parsing and validation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
17+ <mxGeometry x="60" y="300" width="220" height="60" as="geometry"/>
18+ </mxCell>
19+ <mxCell id="httpserver" value="internal/httpserver&#10;(serves the embedded SPA, /healthz, mounts /ws)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
20+ <mxGeometry x="320" y="300" width="240" height="60" as="geometry"/>
21+ </mxCell>
22+ <mxCell id="bridge" value="internal/bridge&#10;(WS hub: broadcast, history replay, permission routing)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
23+ <mxGeometry x="600" y="300" width="250" height="60" as="geometry"/>
24+ </mxCell>
25+ <mxCell id="agentpkg" value="internal/agent&#10;(spawns the agent process, drives the ACP session)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
26+ <mxGeometry x="890" y="300" width="250" height="60" as="geometry"/>
27+ </mxCell>
28+ <mxCell id="mockagent" value="internal/mockagent&#10;(deterministic demo ACP agent scenario)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
29+ <mxGeometry x="890" y="180" width="250" height="60" as="geometry"/>
30+ </mxCell>
31+ <mxCell id="cmdmock" value="cmd/ori-mock-agent&#10;(the demo agent as a stdio binary)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
32+ <mxGeometry x="890" y="60" width="250" height="60" as="geometry"/>
33+ </mxCell>
34+ <mxCell id="acpsdk" value="github.com/coder/acp-go-sdk&#10;(ACP JSON-RPC client and agent library)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
35+ <mxGeometry x="740" y="460" width="250" height="60" as="geometry"/>
36+ </mxCell>
37+ <mxCell id="wslib" value="github.com/coder/websocket&#10;(WebSocket implementation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
38+ <mxGeometry x="450" y="460" width="230" height="60" as="geometry"/>
39+ </mxCell>
40+ <mxCell id="adapter" value="npx @zed-industries/claude-code-acp&#10;(Claude Code ACP adapter runtime subprocess)" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
41+ <mxGeometry x="890" y="580" width="270" height="60" as="geometry"/>
42+ </mxCell>
43+ <mxCell id="filespkg" value="internal/files&#10;(workspace file API: list, read, write)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
44+ <mxGeometry x="60" y="420" width="220" height="60" as="geometry"/>
45+ </mxCell>
46+ <mxCell id="terminalpkg" value="internal/terminal&#10;(one shell per WebSocket, in a PTY)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
47+ <mxGeometry x="60" y="520" width="220" height="60" as="geometry"/>
48+ </mxCell>
49+ <mxCell id="ptylib" value="github.com/creack/pty&#10;(pseudo-terminal allocation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
50+ <mxGeometry x="320" y="560" width="230" height="60" as="geometry"/>
51+ </mxCell>
52+ <mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="config" edge="1">
53+ <mxGeometry relative="1" as="geometry"/>
54+ </mxCell>
55+ <mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="httpserver" edge="1">
56+ <mxGeometry relative="1" as="geometry"/>
57+ </mxCell>
58+ <mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="bridge" edge="1">
59+ <mxGeometry relative="1" as="geometry"/>
60+ </mxCell>
61+ <mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="agentpkg" edge="1">
62+ <mxGeometry relative="1" as="geometry"/>
63+ </mxCell>
64+ <mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="uiembed" edge="1">
65+ <mxGeometry relative="1" as="geometry"/>
66+ </mxCell>
67+ <mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdmock" target="mockagent" edge="1">
68+ <mxGeometry relative="1" as="geometry"/>
69+ </mxCell>
70+ <mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="mockagent" target="acpsdk" edge="1">
71+ <mxGeometry relative="1" as="geometry"/>
72+ </mxCell>
73+ <mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="agentpkg" target="acpsdk" edge="1">
74+ <mxGeometry relative="1" as="geometry"/>
75+ </mxCell>
76+ <mxCell id="e9" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="bridge" target="acpsdk" edge="1">
77+ <mxGeometry relative="1" as="geometry"/>
78+ </mxCell>
79+ <mxCell id="e10" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="bridge" target="wslib" edge="1">
80+ <mxGeometry relative="1" as="geometry"/>
81+ </mxCell>
82+ <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="cmdori" target="filespkg">
83+ <mxGeometry relative="1" as="geometry"/>
84+ </mxCell>
85+ <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="cmdori" target="terminalpkg">
86+ <mxGeometry relative="1" as="geometry"/>
87+ </mxCell>
88+ <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="terminalpkg" target="ptylib">
89+ <mxGeometry relative="1" as="geometry"/>
90+ </mxCell>
91+ <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="terminalpkg" target="wslib">
92+ <mxGeometry relative="1" as="geometry"/>
93+ </mxCell>
94+ <mxCell id="r1" value="build embedded via go:embed" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="spa" target="uiembed" edge="1">
95+ <mxGeometry relative="1" as="geometry"/>
96+ </mxCell>
97+ <mxCell id="r4" value="HTTP /api/files, /api/file" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" edge="1" parent="1" source="spa" target="filespkg">
98+ <mxGeometry relative="1" as="geometry"/>
99+ </mxCell>
100+ <mxCell id="r5" value="WebSocket /ws/terminal" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" edge="1" parent="1" source="spa" target="terminalpkg">
101+ <mxGeometry relative="1" as="geometry"/>
102+ </mxCell>
103+ <mxCell id="r2" value="WebSocket /ws (runtime)" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="spa" target="bridge" edge="1">
104+ <mxGeometry relative="1" as="geometry"/>
105+ </mxCell>
106+ <mxCell id="r3" value="spawns (runtime, default --agent-cmd)" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="agentpkg" target="adapter" edge="1">
107+ <mxGeometry relative="1" as="geometry"/>
108+ </mxCell>
109+ </root>
110+ </mxGraphModel>
111+ </diagram>
112+</mxfile>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,112 @@
1+<mxfile host="65bd71144e">
2+ <diagram id="ori-packages" name="ori package dependencies">
3+ <mxGraphModel dx="727" dy="240" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="850" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="spa" value="ui/src React SPA&#10;(chat panel + workspace: file tree, Monaco preview/editor,&#10;markdown/asciidoc rendering, xterm.js terminal)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
8+ <mxGeometry x="40" y="30" width="260" height="80" as="geometry"/>
9+ </mxCell>
10+ <mxCell id="uiembed" value="ui (Go package)&#10;(go:embed of the SPA production build)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
11+ <mxGeometry x="380" y="40" width="240" height="60" as="geometry"/>
12+ </mxCell>
13+ <mxCell id="cmdori" value="cmd/ori&#10;(entry point: wires config, agent, bridge, http)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
14+ <mxGeometry x="380" y="180" width="240" height="60" as="geometry"/>
15+ </mxCell>
16+ <mxCell id="config" value="internal/config&#10;(CLI flag parsing and validation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
17+ <mxGeometry x="60" y="300" width="220" height="60" as="geometry"/>
18+ </mxCell>
19+ <mxCell id="httpserver" value="internal/httpserver&#10;(serves the embedded SPA, /healthz, mounts /ws)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
20+ <mxGeometry x="320" y="300" width="240" height="60" as="geometry"/>
21+ </mxCell>
22+ <mxCell id="bridge" value="internal/bridge&#10;(WS hub: broadcast, history replay, permission routing)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
23+ <mxGeometry x="600" y="300" width="250" height="60" as="geometry"/>
24+ </mxCell>
25+ <mxCell id="agentpkg" value="internal/agent&#10;(spawns the agent process, drives the ACP session)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
26+ <mxGeometry x="890" y="300" width="250" height="60" as="geometry"/>
27+ </mxCell>
28+ <mxCell id="mockagent" value="internal/mockagent&#10;(deterministic demo ACP agent scenario)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
29+ <mxGeometry x="890" y="180" width="250" height="60" as="geometry"/>
30+ </mxCell>
31+ <mxCell id="cmdmock" value="cmd/ori-mock-agent&#10;(the demo agent as a stdio binary)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
32+ <mxGeometry x="890" y="60" width="250" height="60" as="geometry"/>
33+ </mxCell>
34+ <mxCell id="acpsdk" value="github.com/coder/acp-go-sdk&#10;(ACP JSON-RPC client and agent library)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
35+ <mxGeometry x="740" y="460" width="250" height="60" as="geometry"/>
36+ </mxCell>
37+ <mxCell id="wslib" value="github.com/coder/websocket&#10;(WebSocket implementation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
38+ <mxGeometry x="450" y="460" width="230" height="60" as="geometry"/>
39+ </mxCell>
40+ <mxCell id="adapter" value="npx @zed-industries/claude-code-acp&#10;(Claude Code ACP adapter runtime subprocess)" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
41+ <mxGeometry x="890" y="580" width="270" height="60" as="geometry"/>
42+ </mxCell>
43+ <mxCell id="filespkg" value="internal/files&#10;(workspace file API: list, read, write)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
44+ <mxGeometry x="60" y="420" width="220" height="60" as="geometry"/>
45+ </mxCell>
46+ <mxCell id="terminalpkg" value="internal/terminal&#10;(one shell per WebSocket, in a PTY)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
47+ <mxGeometry x="60" y="520" width="220" height="60" as="geometry"/>
48+ </mxCell>
49+ <mxCell id="ptylib" value="github.com/creack/pty&#10;(pseudo-terminal allocation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
50+ <mxGeometry x="320" y="560" width="230" height="60" as="geometry"/>
51+ </mxCell>
52+ <mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="config" edge="1">
53+ <mxGeometry relative="1" as="geometry"/>
54+ </mxCell>
55+ <mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="httpserver" edge="1">
56+ <mxGeometry relative="1" as="geometry"/>
57+ </mxCell>
58+ <mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="bridge" edge="1">
59+ <mxGeometry relative="1" as="geometry"/>
60+ </mxCell>
61+ <mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="agentpkg" edge="1">
62+ <mxGeometry relative="1" as="geometry"/>
63+ </mxCell>
64+ <mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdori" target="uiembed" edge="1">
65+ <mxGeometry relative="1" as="geometry"/>
66+ </mxCell>
67+ <mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="cmdmock" target="mockagent" edge="1">
68+ <mxGeometry relative="1" as="geometry"/>
69+ </mxCell>
70+ <mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="mockagent" target="acpsdk" edge="1">
71+ <mxGeometry relative="1" as="geometry"/>
72+ </mxCell>
73+ <mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="agentpkg" target="acpsdk" edge="1">
74+ <mxGeometry relative="1" as="geometry"/>
75+ </mxCell>
76+ <mxCell id="e9" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="bridge" target="acpsdk" edge="1">
77+ <mxGeometry relative="1" as="geometry"/>
78+ </mxCell>
79+ <mxCell id="e10" style="edgeStyle=orthogonalEdgeStyle;html=1;" parent="1" source="bridge" target="wslib" edge="1">
80+ <mxGeometry relative="1" as="geometry"/>
81+ </mxCell>
82+ <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="cmdori" target="filespkg">
83+ <mxGeometry relative="1" as="geometry"/>
84+ </mxCell>
85+ <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="cmdori" target="terminalpkg">
86+ <mxGeometry relative="1" as="geometry"/>
87+ </mxCell>
88+ <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="terminalpkg" target="ptylib">
89+ <mxGeometry relative="1" as="geometry"/>
90+ </mxCell>
91+ <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;html=1;" edge="1" parent="1" source="terminalpkg" target="wslib">
92+ <mxGeometry relative="1" as="geometry"/>
93+ </mxCell>
94+ <mxCell id="r1" value="build embedded via go:embed" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="spa" target="uiembed" edge="1">
95+ <mxGeometry relative="1" as="geometry"/>
96+ </mxCell>
97+ <mxCell id="r4" value="HTTP /api/files, /api/file" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" edge="1" parent="1" source="spa" target="filespkg">
98+ <mxGeometry relative="1" as="geometry"/>
99+ </mxCell>
100+ <mxCell id="r5" value="WebSocket /ws/terminal" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" edge="1" parent="1" source="spa" target="terminalpkg">
101+ <mxGeometry relative="1" as="geometry"/>
102+ </mxCell>
103+ <mxCell id="r2" value="WebSocket /ws (runtime)" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="spa" target="bridge" edge="1">
104+ <mxGeometry relative="1" as="geometry"/>
105+ </mxCell>
106+ <mxCell id="r3" value="spawns (runtime, default --agent-cmd)" style="edgeStyle=orthogonalEdgeStyle;html=1;dashed=1;" parent="1" source="agentpkg" target="adapter" edge="1">
107+ <mxGeometry relative="1" as="geometry"/>
108+ </mxCell>
109+ </root>
110+ </mxGraphModel>
111+ </diagram>
112+</mxfile>
\ No newline at end of file\ No newline at end of file
added docs/en/README.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Ori — documentation (English)
2+
3+Ori is a web application that acts as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) client for AI code agents: a Go backend serves a React single-page application and connects, over ACP, to an agent such as Claude Code. The browser gets an agent-panel experience in the spirit of Zed's: streamed answers, visible tool calls, plans and permission prompts.
4+
5+This documentation follows the [Diátaxis](https://diataxis.fr) structure — pick the section matching what you need right now:
6+
7+| You want to… | Go to |
8+| --- | --- |
9+| Learn by doing: run Ori and hold your first conversation | [Tutorial: getting started](tutorials/getting-started.md) |
10+| Run Ori with Claude Code | [How to run Ori with Claude Code](how-to/run-with-claude-code.md) |
11+| Connect a different ACP agent | [How to use another ACP agent](how-to/use-another-agent.md) |
12+| Package and run Ori in a Docker sandbox | [How to run Ori in a Docker sandbox](how-to/run-in-a-sandbox.md) |
13+| Browse, preview (code, Markdown, AsciiDoc, images, draw.io), edit files and use the terminal | [How to use the workspace panel](how-to/use-the-workspace.md) |
14+| Mention files with `@` and invoke skills with `/` in the composer | [How to mention files and invoke skills](how-to/mention-files-and-skills.md) |
15+| Switch between the light and dark themes | [How to switch the theme](how-to/switch-the-theme.md) |
16+| Run the test suites | [How to run the tests](how-to/run-the-tests.md) |
17+| Look up the CLI flags | [Reference: the ori command](reference/cli.md) |
18+| Look up the browser ⇆ backend protocol | [Reference: WebSocket protocol](reference/websocket-protocol.md) |
19+| Look up the file API, the skills endpoint and the terminal | [Reference: workspace API](reference/workspace-api.md) |
20+| Understand the architecture and its trade-offs | [Explanation: architecture](explanation/architecture.md) |
new file mode 100644
@@ -0,0 +1,20 @@
1+# Ori — documentation (English)
2+
3+Ori is a web application that acts as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) client for AI code agents: a Go backend serves a React single-page application and connects, over ACP, to an agent such as Claude Code. The browser gets an agent-panel experience in the spirit of Zed's: streamed answers, visible tool calls, plans and permission prompts.
4+
5+This documentation follows the [Diátaxis](https://diataxis.fr) structure — pick the section matching what you need right now:
6+
7+| You want to… | Go to |
8+| --- | --- |
9+| Learn by doing: run Ori and hold your first conversation | [Tutorial: getting started](tutorials/getting-started.md) |
10+| Run Ori with Claude Code | [How to run Ori with Claude Code](how-to/run-with-claude-code.md) |
11+| Connect a different ACP agent | [How to use another ACP agent](how-to/use-another-agent.md) |
12+| Package and run Ori in a Docker sandbox | [How to run Ori in a Docker sandbox](how-to/run-in-a-sandbox.md) |
13+| Browse, preview (code, Markdown, AsciiDoc, images, draw.io), edit files and use the terminal | [How to use the workspace panel](how-to/use-the-workspace.md) |
14+| Mention files with `@` and invoke skills with `/` in the composer | [How to mention files and invoke skills](how-to/mention-files-and-skills.md) |
15+| Switch between the light and dark themes | [How to switch the theme](how-to/switch-the-theme.md) |
16+| Run the test suites | [How to run the tests](how-to/run-the-tests.md) |
17+| Look up the CLI flags | [Reference: the ori command](reference/cli.md) |
18+| Look up the browser ⇆ backend protocol | [Reference: WebSocket protocol](reference/websocket-protocol.md) |
19+| Look up the file API, the skills endpoint and the terminal | [Reference: workspace API](reference/workspace-api.md) |
20+| Understand the architecture and its trade-offs | [Explanation: architecture](explanation/architecture.md) |
added docs/en/explanation/architecture.md +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+# Architecture — explanation
2+
3+## What is this about?
4+
5+Ori brings the experience of an editor's agent panel — Zed's, specifically — to the browser. The interesting constraint is that the [Agent Client Protocol](https://agentclientprotocol.com) was designed for editors: a client spawns the agent as a subprocess and speaks JSON-RPC over its stdio. A browser cannot spawn processes, so something must stand in the middle. That something is Ori's Go backend.
6+
7+```
8+browser ⇆ WebSocket ⇆ Go backend ⇆ stdio (ACP / JSON-RPC) ⇆ agent (claude-code-acp, …)
9+```
10+
11+The package-dependency diagram is kept at [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (open it with diagrams.net or the VS Code Draw.io extension).
12+
13+## The pieces
14+
15+- **`ui/` (React SPA)** — renders the conversation: streamed markdown, collapsible thoughts, tool call cards with statuses and diffs, the agent's plan, permission prompts. All its state derives from server events folded by a pure reducer, which is what makes the UI testable without a browser or network.
16+- **`internal/httpserver`** — serves the SPA (embedded in the binary via `go:embed`, so Ori ships as a single file) and mounts extra routes such as `/ws`.
17+- **`internal/bridge`** — the heart of the backend. It translates between two asynchronous worlds: the agent streams updates and blocks on permission requests, while browsers connect, disconnect and answer at human speed. It broadcasts events, replays history to late joiners, and routes each permission decision back to the exact JSON-RPC call waiting for it.
18+- **`internal/agent`** — spawns the agent process and drives the ACP session lifecycle (initialize, session/new, prompt, cancel) through the [`coder/acp-go-sdk`](https://github.com/coder/acp-go-sdk) client. It is agent-agnostic: anything speaking ACP on stdio plugs in.
19+- **`internal/mockagent` + `cmd/ori-mock-agent`** — a deterministic ACP agent used by the end-to-end test and by `make run-mock`, so the whole application can be exercised without any AI agent, account or network.
20+- **`internal/skills`** — discovers Claude Code skills (`SKILL.md` frontmatter) in the project and the user's home for the composer's `/` selector.
21+- **`internal/files`** — the workspace file API (list, search, read, raw bytes, write) behind the SPA's file tree, preview and editor. Access is deliberately unrestricted: ori targets already-isolated sandboxes, and the agent has the same reach anyway.
22+- **`internal/terminal`** — one real shell per WebSocket connection, inside a pseudo-terminal (`creack/pty`), driven by xterm.js in the browser.
23+
24+## Mentions, skills and the ACP prompt
25+
26+The composer's `@` selector had to answer one design question: how does a file reach the agent? ACP already has the answer — every agent must accept `resource_link` content blocks in a prompt — so the browser sends the text plus a small `attachments` list, and the bridge converts each into a `resource_link` (`file://` URI) next to the text block. The `@path` mention stays visible in the text, so the conversation reads naturally in every client and in replays, while the structured link lets the agent open the file without guessing. The `/` selector needed no protocol at all: Claude Code invokes a skill or slash command from the text `/name …`, so the popup only inserts text, merging the skills found on disk with the commands the agent announces through `available_commands_update`.
27+
28+## Previews: images and draw.io
29+
30+Images are streamed by `GET /api/raw` and rendered by the browser itself, which is both the simplest and the most capable renderer available. draw.io diagrams are the one preview that leaves the "bundled, no CDN" rule Monaco set: there is no offline renderer ori could ship — draw.io's own `viewer.min.js` is not published on npm, the archived `mxgraph` package lacks draw.io's shape library, and the one browser converter on npm (`@markdown-viewer/drawio2svg`) is GPL-3.0-only. So the pane embeds `embed.diagrams.net` in an iframe and hands it the XML over its postMessage protocol — the file never leaves the browser — and falls back to a notice plus the XML source when the host is unreachable, as it may be in an isolated sandbox. Vendoring `viewer-static.min.js` from the Apache-2.0 draw.io repository (about 3 MB) remains the path to an offline renderer if that trade-off ever changes.
31+
32+## The workspace panel
33+
34+The SPA grew from a single chat column into chat-plus-workspace: a file tree and three tabs (Preview, Editor, Terminal). Three choices shape it. Monaco serves both the read-only code preview and the editor — one heavy dependency instead of two highlighting stacks — and is bundled with its workers so nothing loads from a CDN; it is lazy-loaded, keeping the initial bundle small. Markdown renders through the same component as chat messages, and AsciiDoc through a lazily-imported Asciidoctor. Finally, inactive panes are hidden rather than unmounted, so the terminal's shell session and the editor's unsaved buffer survive tab switches and the panel being collapsed. The file tree column is resizable, and its width follows the same "the store owns the state" rule as the rest of the panel: it lives in the workspace store, reaches the stylesheet as a CSS custom property (`--filetree-width`) set on the panel, and is persisted in localStorage — a per-browser convenience, not server state, so it deliberately stays out of the WebSocket protocol.
35+
36+The colour theme is the same kind of state. Light is the default; the dark palette is keyed on a `data-theme="dark"` attribute that a small zustand store (`ui/src/theme.ts`) stamps on `<html>` and remembers in localStorage, and Monaco reads that store rather than the OS preference so the editor never disagrees with the page. The earlier behaviour — following `prefers-color-scheme` — was dropped in favour of an explicit choice, because a shared Ori server is often viewed from machines whose OS settings the user does not control (a sandbox's browser, a colleague's laptop). Like the file tree width, the theme is a per-browser convenience, so it lives outside the WebSocket protocol and the server never hears about it.
37+
38+## Why the server owns the conversation
39+
40+The bridge records every event of the session and replays it on each connection, and it even echoes the user's own prompts back as `user_message` events. This makes the server the single source of truth: the SPA never trusts its local memory, it resets on every `hello` and rebuilds from the replay. The payoff is that reconnections (laptop sleep, network blip, opening a second tab) are trivially correct — every browser converges on the same thread — at the cost of a bounded in-memory history (4096 events).
41+
42+## Why ACP payloads cross the WebSocket verbatim
43+
44+The bridge could have translated ACP updates into its own message shapes. It deliberately does not: `session_update` and `permission_request` carry the raw ACP objects. The protocol is well documented and evolving (plans, modes, richer tool content); relaying it verbatim means new update kinds reach the frontend without touching Go code, and the frontend can adopt them at its own pace — unknown kinds are ignored by design.
45+
46+## Why an adapter process for Claude Code
47+
48+The Claude Code CLI has no native ACP mode (verified against v2.1.274: no such option or subcommand). Zed's own integration goes through [`@zed-industries/claude-code-acp`](https://www.npmjs.com/package/@zed-industries/claude-code-acp), which wraps the Claude Code SDK and exposes ACP on stdio. Ori spawns exactly that adapter by default.
49+
50+## Rejected alternatives
51+
52+- **Driving the `claude` CLI's `stream-json` mode directly** — a hand-written bridge to Claude's proprietary streaming format would have been more work, tied Ori to one agent, and thrown away the ecosystem of ACP agents (Gemini CLI, and whatever comes next).
53+- **Server-sent events or polling instead of a WebSocket** — permission requests need messages in both directions on one connection; SSE would have required a second channel for responses.
54+- **Multiple concurrent sessions in v1** — the bridge deliberately drives one agent session shared by all connected browsers. The `Prompter` interface and the per-connection subscription model are the seams where multi-session support can be added without reshaping the design.
55+
56+## How the tests mirror the architecture
57+
58+Each seam has its own double: the agent package is tested against an in-memory ACP agent (no subprocess), the bridge against a fake session (no agent), the SPA reducer against plain message objects (no socket), and one end-to-end test compiles the real mock agent binary and drives the full production wiring through a real WebSocket. See [how to run the tests](../how-to/run-the-tests.md).
new file mode 100644
@@ -0,0 +1,58 @@
1+# Architecture — explanation
2+
3+## What is this about?
4+
5+Ori brings the experience of an editor's agent panel — Zed's, specifically — to the browser. The interesting constraint is that the [Agent Client Protocol](https://agentclientprotocol.com) was designed for editors: a client spawns the agent as a subprocess and speaks JSON-RPC over its stdio. A browser cannot spawn processes, so something must stand in the middle. That something is Ori's Go backend.
6+
7+```
8+browser ⇆ WebSocket ⇆ Go backend ⇆ stdio (ACP / JSON-RPC) ⇆ agent (claude-code-acp, …)
9+```
10+
11+The package-dependency diagram is kept at [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (open it with diagrams.net or the VS Code Draw.io extension).
12+
13+## The pieces
14+
15+- **`ui/` (React SPA)** — renders the conversation: streamed markdown, collapsible thoughts, tool call cards with statuses and diffs, the agent's plan, permission prompts. All its state derives from server events folded by a pure reducer, which is what makes the UI testable without a browser or network.
16+- **`internal/httpserver`** — serves the SPA (embedded in the binary via `go:embed`, so Ori ships as a single file) and mounts extra routes such as `/ws`.
17+- **`internal/bridge`** — the heart of the backend. It translates between two asynchronous worlds: the agent streams updates and blocks on permission requests, while browsers connect, disconnect and answer at human speed. It broadcasts events, replays history to late joiners, and routes each permission decision back to the exact JSON-RPC call waiting for it.
18+- **`internal/agent`** — spawns the agent process and drives the ACP session lifecycle (initialize, session/new, prompt, cancel) through the [`coder/acp-go-sdk`](https://github.com/coder/acp-go-sdk) client. It is agent-agnostic: anything speaking ACP on stdio plugs in.
19+- **`internal/mockagent` + `cmd/ori-mock-agent`** — a deterministic ACP agent used by the end-to-end test and by `make run-mock`, so the whole application can be exercised without any AI agent, account or network.
20+- **`internal/skills`** — discovers Claude Code skills (`SKILL.md` frontmatter) in the project and the user's home for the composer's `/` selector.
21+- **`internal/files`** — the workspace file API (list, search, read, raw bytes, write) behind the SPA's file tree, preview and editor. Access is deliberately unrestricted: ori targets already-isolated sandboxes, and the agent has the same reach anyway.
22+- **`internal/terminal`** — one real shell per WebSocket connection, inside a pseudo-terminal (`creack/pty`), driven by xterm.js in the browser.
23+
24+## Mentions, skills and the ACP prompt
25+
26+The composer's `@` selector had to answer one design question: how does a file reach the agent? ACP already has the answer — every agent must accept `resource_link` content blocks in a prompt — so the browser sends the text plus a small `attachments` list, and the bridge converts each into a `resource_link` (`file://` URI) next to the text block. The `@path` mention stays visible in the text, so the conversation reads naturally in every client and in replays, while the structured link lets the agent open the file without guessing. The `/` selector needed no protocol at all: Claude Code invokes a skill or slash command from the text `/name …`, so the popup only inserts text, merging the skills found on disk with the commands the agent announces through `available_commands_update`.
27+
28+## Previews: images and draw.io
29+
30+Images are streamed by `GET /api/raw` and rendered by the browser itself, which is both the simplest and the most capable renderer available. draw.io diagrams are the one preview that leaves the "bundled, no CDN" rule Monaco set: there is no offline renderer ori could ship — draw.io's own `viewer.min.js` is not published on npm, the archived `mxgraph` package lacks draw.io's shape library, and the one browser converter on npm (`@markdown-viewer/drawio2svg`) is GPL-3.0-only. So the pane embeds `embed.diagrams.net` in an iframe and hands it the XML over its postMessage protocol — the file never leaves the browser — and falls back to a notice plus the XML source when the host is unreachable, as it may be in an isolated sandbox. Vendoring `viewer-static.min.js` from the Apache-2.0 draw.io repository (about 3 MB) remains the path to an offline renderer if that trade-off ever changes.
31+
32+## The workspace panel
33+
34+The SPA grew from a single chat column into chat-plus-workspace: a file tree and three tabs (Preview, Editor, Terminal). Three choices shape it. Monaco serves both the read-only code preview and the editor — one heavy dependency instead of two highlighting stacks — and is bundled with its workers so nothing loads from a CDN; it is lazy-loaded, keeping the initial bundle small. Markdown renders through the same component as chat messages, and AsciiDoc through a lazily-imported Asciidoctor. Finally, inactive panes are hidden rather than unmounted, so the terminal's shell session and the editor's unsaved buffer survive tab switches and the panel being collapsed. The file tree column is resizable, and its width follows the same "the store owns the state" rule as the rest of the panel: it lives in the workspace store, reaches the stylesheet as a CSS custom property (`--filetree-width`) set on the panel, and is persisted in localStorage — a per-browser convenience, not server state, so it deliberately stays out of the WebSocket protocol.
35+
36+The colour theme is the same kind of state. Light is the default; the dark palette is keyed on a `data-theme="dark"` attribute that a small zustand store (`ui/src/theme.ts`) stamps on `<html>` and remembers in localStorage, and Monaco reads that store rather than the OS preference so the editor never disagrees with the page. The earlier behaviour — following `prefers-color-scheme` — was dropped in favour of an explicit choice, because a shared Ori server is often viewed from machines whose OS settings the user does not control (a sandbox's browser, a colleague's laptop). Like the file tree width, the theme is a per-browser convenience, so it lives outside the WebSocket protocol and the server never hears about it.
37+
38+## Why the server owns the conversation
39+
40+The bridge records every event of the session and replays it on each connection, and it even echoes the user's own prompts back as `user_message` events. This makes the server the single source of truth: the SPA never trusts its local memory, it resets on every `hello` and rebuilds from the replay. The payoff is that reconnections (laptop sleep, network blip, opening a second tab) are trivially correct — every browser converges on the same thread — at the cost of a bounded in-memory history (4096 events).
41+
42+## Why ACP payloads cross the WebSocket verbatim
43+
44+The bridge could have translated ACP updates into its own message shapes. It deliberately does not: `session_update` and `permission_request` carry the raw ACP objects. The protocol is well documented and evolving (plans, modes, richer tool content); relaying it verbatim means new update kinds reach the frontend without touching Go code, and the frontend can adopt them at its own pace — unknown kinds are ignored by design.
45+
46+## Why an adapter process for Claude Code
47+
48+The Claude Code CLI has no native ACP mode (verified against v2.1.274: no such option or subcommand). Zed's own integration goes through [`@zed-industries/claude-code-acp`](https://www.npmjs.com/package/@zed-industries/claude-code-acp), which wraps the Claude Code SDK and exposes ACP on stdio. Ori spawns exactly that adapter by default.
49+
50+## Rejected alternatives
51+
52+- **Driving the `claude` CLI's `stream-json` mode directly** — a hand-written bridge to Claude's proprietary streaming format would have been more work, tied Ori to one agent, and thrown away the ecosystem of ACP agents (Gemini CLI, and whatever comes next).
53+- **Server-sent events or polling instead of a WebSocket** — permission requests need messages in both directions on one connection; SSE would have required a second channel for responses.
54+- **Multiple concurrent sessions in v1** — the bridge deliberately drives one agent session shared by all connected browsers. The `Prompter` interface and the per-connection subscription model are the seams where multi-session support can be added without reshaping the design.
55+
56+## How the tests mirror the architecture
57+
58+Each seam has its own double: the agent package is tested against an in-memory ACP agent (no subprocess), the bridge against a fake session (no agent), the SPA reducer against plain message objects (no socket), and one end-to-end test compiles the real mock agent binary and drives the full production wiring through a real WebSocket. See [how to run the tests](../how-to/run-the-tests.md).
added docs/en/how-to/mention-files-and-skills.md +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+# How to mention files and invoke skills from the composer
2+
3+This guide shows how to point the agent at workspace files with `@` and how to invoke a Claude Code skill or slash command with `/`, straight from the message composer. It assumes Ori is running (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Mention a file with `@`
6+
7+1. In the composer, type `@` at the start of the message or after a space. A popup lists the workspace files; keep typing to filter — `@main` matches `src/main.go` and `src/server/main_test.go`, and a fragment of a directory name works too.
8+2. Move with **↑ / ↓**, pick with **Enter** or **Tab** (or click). The mention becomes `@src/main.go ` in the text.
9+3. Send as usual. The agent receives the text plus a `resource_link` to the file (`file:///…/src/main.go`), which the Claude Code adapter turns into a file reference the model can open.
10+
11+If you delete a mention before sending, its attachment is dropped too; an `@` glued to a word (`me@example.com`) never opens the popup. **Esc** closes the popup for the current mention.
12+
13+## Invoke a skill or command with `/`
14+
15+1. Type `/` as the **first** character of the message. The popup lists, sorted by name, the skills found in `<cwd>/.claude/skills/` and `~/.claude/skills/` (tagged **skill**, with the description from their `SKILL.md`) and the slash commands the agent announced over ACP (tagged **command**).
16+2. Filter by name or description, pick with **Enter** / **Tab**: the text becomes `/quality `. Add arguments if the skill takes some, then send.
17+
18+The skill is invoked the way Claude Code invokes it: the text `/quality …` is what gets sent, there is no separate ACP method. A `/` anywhere else in the message is plain text.
19+
20+## Variants
21+
22+- With the mock agent (`make run-mock`), the `/` popup shows two demo commands (`review`, `compact`) and the answer echoes each attached file as `[attached: <name>]` — handy to check the plumbing without Claude.
23+- Skills are listed when the popup first opens; add a skill on disk and reload the page to see it.
24+
25+## See also
26+
27+- Wire format of `attachments`: [WebSocket protocol reference](../reference/websocket-protocol.md)
28+- The search and skills endpoints: [workspace API reference](../reference/workspace-api.md)
new file mode 100644
@@ -0,0 +1,28 @@
1+# How to mention files and invoke skills from the composer
2+
3+This guide shows how to point the agent at workspace files with `@` and how to invoke a Claude Code skill or slash command with `/`, straight from the message composer. It assumes Ori is running (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Mention a file with `@`
6+
7+1. In the composer, type `@` at the start of the message or after a space. A popup lists the workspace files; keep typing to filter — `@main` matches `src/main.go` and `src/server/main_test.go`, and a fragment of a directory name works too.
8+2. Move with **↑ / ↓**, pick with **Enter** or **Tab** (or click). The mention becomes `@src/main.go ` in the text.
9+3. Send as usual. The agent receives the text plus a `resource_link` to the file (`file:///…/src/main.go`), which the Claude Code adapter turns into a file reference the model can open.
10+
11+If you delete a mention before sending, its attachment is dropped too; an `@` glued to a word (`me@example.com`) never opens the popup. **Esc** closes the popup for the current mention.
12+
13+## Invoke a skill or command with `/`
14+
15+1. Type `/` as the **first** character of the message. The popup lists, sorted by name, the skills found in `<cwd>/.claude/skills/` and `~/.claude/skills/` (tagged **skill**, with the description from their `SKILL.md`) and the slash commands the agent announced over ACP (tagged **command**).
16+2. Filter by name or description, pick with **Enter** / **Tab**: the text becomes `/quality `. Add arguments if the skill takes some, then send.
17+
18+The skill is invoked the way Claude Code invokes it: the text `/quality …` is what gets sent, there is no separate ACP method. A `/` anywhere else in the message is plain text.
19+
20+## Variants
21+
22+- With the mock agent (`make run-mock`), the `/` popup shows two demo commands (`review`, `compact`) and the answer echoes each attached file as `[attached: <name>]` — handy to check the plumbing without Claude.
23+- Skills are listed when the popup first opens; add a skill on disk and reload the page to see it.
24+
25+## See also
26+
27+- Wire format of `attachments`: [WebSocket protocol reference](../reference/websocket-protocol.md)
28+- The search and skills endpoints: [workspace API reference](../reference/workspace-api.md)
added docs/en/how-to/run-in-a-sandbox.md +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+# How to run Ori in a Docker sandbox
2+
3+This guide shows how to package Ori as a Docker Sandboxes template and run it with `sbx`, so the whole stack — Claude Code included — lives in an isolated sandbox you reach from your browser. It assumes Docker and the `sbx` CLI on the host.
4+
5+## Steps
6+
7+1. Get the template image — it is published on Docker Hub as `k33g/ori:0.0.0`. To (re)build and push it yourself, from the repository root:
8+
9+ ```bash
10+ ./template/build.sh
11+ ```
12+
13+ The script runs a multi-arch `docker buildx build --push`: the SPA and the Go binaries are built in intermediate stages, then layered with the `claude-code-acp` adapter on top of the official `docker/sandbox-templates:claude-code` image. For a local-only image use `make template` instead.
14+
15+2. Create the sandbox from your project directory, with the template and the ori kit, in detached mode:
16+
17+ ```bash
18+ sbx run -d claude ~/path/to/your/project \
19+ --template k33g/ori:0.0.0 \
20+ --kit /path/to/ori/kits/ori
21+ ```
22+
23+ `-d` (`--detached`) matters: sbx stops a sandbox 30 seconds after the last CLI session on it closes, and browser traffic on a published port is not a session. `sbx create` holds a session only while it runs, so a sandbox created with it goes `stopped` half a minute later. A detached sandbox is exempt from this auto-stop until you `sbx stop` or `sbx rm` it. The flag exists only on `sbx run`, and the mode is fixed at creation.
24+
25+ The kit's startup command launches the ori server on port 8888 at every container start; the claude agent kit injects the Anthropic credentials through the sandbox proxy, so nothing needs a login inside.
26+
27+3. Open the UI: the kit declares port 8888, so the command output prints the ephemeral address it published — `Published web: localhost:<port> -> 8888/tcp`. Browse there, or pin a fixed port with `-p 8888:8888` at creation (or later with `sbx ports <sandbox-name> --publish 8888:8888/tcp`).
28+
29+## Variants
30+
31+- The listen port is a kit argument (default 8888): add `--kit-arg ori.port=9000` to change it (and publish that port instead).
32+- If the image was never pushed to Docker Hub, hand it to the sandbox runtime's own image store first: `docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
33+- Standalone, without sbx: `docker run --rm -p 8888:8888 k33g/ori` (provide `ANTHROPIC_API_KEY`, or use the demo agent below).
34+- Credential-free demo: inside the sandbox, `pkill -x ori` then relaunch with `--agent-cmd ori-mock-agent`.
35+- The server log inside the sandbox is `/home/agent/.ori.log`.
36+- A stopped sandbox restarts with `sbx run -d --name <sandbox-name>` (or `sbx attach`); the kit relaunches the server on every start.
37+- If a sandbox stops on its own anyway, check the daemon log (`sbx daemon status` prints its path) for `auto-stop` lines: they mean the sandbox was not created detached.
38+
39+## See also
40+
41+- Kit details and template contents: [`kits/ori/README.md`](../../../kits/ori/README.md)
42+- Flags of the server the kit starts: [reference: the ori command](../reference/cli.md)
new file mode 100644
@@ -0,0 +1,42 @@
1+# How to run Ori in a Docker sandbox
2+
3+This guide shows how to package Ori as a Docker Sandboxes template and run it with `sbx`, so the whole stack — Claude Code included — lives in an isolated sandbox you reach from your browser. It assumes Docker and the `sbx` CLI on the host.
4+
5+## Steps
6+
7+1. Get the template image — it is published on Docker Hub as `k33g/ori:0.0.0`. To (re)build and push it yourself, from the repository root:
8+
9+ ```bash
10+ ./template/build.sh
11+ ```
12+
13+ The script runs a multi-arch `docker buildx build --push`: the SPA and the Go binaries are built in intermediate stages, then layered with the `claude-code-acp` adapter on top of the official `docker/sandbox-templates:claude-code` image. For a local-only image use `make template` instead.
14+
15+2. Create the sandbox from your project directory, with the template and the ori kit, in detached mode:
16+
17+ ```bash
18+ sbx run -d claude ~/path/to/your/project \
19+ --template k33g/ori:0.0.0 \
20+ --kit /path/to/ori/kits/ori
21+ ```
22+
23+ `-d` (`--detached`) matters: sbx stops a sandbox 30 seconds after the last CLI session on it closes, and browser traffic on a published port is not a session. `sbx create` holds a session only while it runs, so a sandbox created with it goes `stopped` half a minute later. A detached sandbox is exempt from this auto-stop until you `sbx stop` or `sbx rm` it. The flag exists only on `sbx run`, and the mode is fixed at creation.
24+
25+ The kit's startup command launches the ori server on port 8888 at every container start; the claude agent kit injects the Anthropic credentials through the sandbox proxy, so nothing needs a login inside.
26+
27+3. Open the UI: the kit declares port 8888, so the command output prints the ephemeral address it published — `Published web: localhost:<port> -> 8888/tcp`. Browse there, or pin a fixed port with `-p 8888:8888` at creation (or later with `sbx ports <sandbox-name> --publish 8888:8888/tcp`).
28+
29+## Variants
30+
31+- The listen port is a kit argument (default 8888): add `--kit-arg ori.port=9000` to change it (and publish that port instead).
32+- If the image was never pushed to Docker Hub, hand it to the sandbox runtime's own image store first: `docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
33+- Standalone, without sbx: `docker run --rm -p 8888:8888 k33g/ori` (provide `ANTHROPIC_API_KEY`, or use the demo agent below).
34+- Credential-free demo: inside the sandbox, `pkill -x ori` then relaunch with `--agent-cmd ori-mock-agent`.
35+- The server log inside the sandbox is `/home/agent/.ori.log`.
36+- A stopped sandbox restarts with `sbx run -d --name <sandbox-name>` (or `sbx attach`); the kit relaunches the server on every start.
37+- If a sandbox stops on its own anyway, check the daemon log (`sbx daemon status` prints its path) for `auto-stop` lines: they mean the sandbox was not created detached.
38+
39+## See also
40+
41+- Kit details and template contents: [`kits/ori/README.md`](../../../kits/ori/README.md)
42+- Flags of the server the kit starts: [reference: the ori command](../reference/cli.md)
added docs/en/how-to/run-the-tests.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to run the tests
2+
3+This guide shows how to run Ori's test suites. It assumes the frontend dependencies are installed (`make deps`).
4+
5+## Steps
6+
7+1. From the repository root, run everything:
8+
9+ ```bash
10+ make test
11+ ```
12+
13+ This runs the Go suite (`go test ./...`) followed by the frontend suite (`vitest run`). Both must end green.
14+
15+## Variants
16+
17+- Backend only: `make test-go` — unit tests for every package plus an end-to-end test that compiles the mock agent, spawns it as a real subprocess and drives a real WebSocket client through a full turn.
18+- Frontend only: `make test-ui` — reducer, WebSocket client and component tests under jsdom.
19+- With the race detector: `go test -race ./...`.
20+- Skipping the end-to-end test (fast feedback): `go test -short ./...`.
21+
22+## See also
23+
24+- Where each suite lives and what it covers: [explanation: architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to run the tests
2+
3+This guide shows how to run Ori's test suites. It assumes the frontend dependencies are installed (`make deps`).
4+
5+## Steps
6+
7+1. From the repository root, run everything:
8+
9+ ```bash
10+ make test
11+ ```
12+
13+ This runs the Go suite (`go test ./...`) followed by the frontend suite (`vitest run`). Both must end green.
14+
15+## Variants
16+
17+- Backend only: `make test-go` — unit tests for every package plus an end-to-end test that compiles the mock agent, spawns it as a real subprocess and drives a real WebSocket client through a full turn.
18+- Frontend only: `make test-ui` — reducer, WebSocket client and component tests under jsdom.
19+- With the race detector: `go test -race ./...`.
20+- Skipping the end-to-end test (fast feedback): `go test -short ./...`.
21+
22+## See also
23+
24+- Where each suite lives and what it covers: [explanation: architecture](../explanation/architecture.md)
added docs/en/how-to/run-with-claude-code.md +26 -0
new file mode 100644
@@ -0,0 +1,26 @@
1+# How to run Ori with Claude Code
2+
3+This guide shows how to start Ori connected to Claude Code. It assumes you have already built and run Ori once (see the [getting started tutorial](../tutorials/getting-started.md)) and that you have a working Claude account or API key.
4+
5+## Steps
6+
7+1. Make sure Node.js and npm are available: the backend spawns Claude Code's official ACP adapter with `npx -y @zed-industries/claude-code-acp`.
8+2. Make sure Claude Code is authenticated on this machine (`claude` login flow, or an `ANTHROPIC_API_KEY` environment variable the adapter can use).
9+3. From the repository root, start the server:
10+
11+ ```bash
12+ make run
13+ ```
14+
15+4. Open [http://localhost:8888](http://localhost:8888) and start prompting. Tool calls, plans and permission requests appear exactly as with the demo agent.
16+
17+## Variants
18+
19+- To work on a specific project directory, pass the agent's working directory explicitly: `./bin/ori --cwd /path/to/your/project`.
20+- To serve on another port: `./bin/ori --addr :9000`.
21+- The first `npx` run downloads the adapter package; pin it by installing it globally (`npm install -g @zed-industries/claude-code-acp`) and passing `--agent-cmd "claude-code-acp"`.
22+
23+## See also
24+
25+- Flags reference: [the ori command](../reference/cli.md)
26+- Connecting something else than Claude: [use another ACP agent](use-another-agent.md)
new file mode 100644
@@ -0,0 +1,26 @@
1+# How to run Ori with Claude Code
2+
3+This guide shows how to start Ori connected to Claude Code. It assumes you have already built and run Ori once (see the [getting started tutorial](../tutorials/getting-started.md)) and that you have a working Claude account or API key.
4+
5+## Steps
6+
7+1. Make sure Node.js and npm are available: the backend spawns Claude Code's official ACP adapter with `npx -y @zed-industries/claude-code-acp`.
8+2. Make sure Claude Code is authenticated on this machine (`claude` login flow, or an `ANTHROPIC_API_KEY` environment variable the adapter can use).
9+3. From the repository root, start the server:
10+
11+ ```bash
12+ make run
13+ ```
14+
15+4. Open [http://localhost:8888](http://localhost:8888) and start prompting. Tool calls, plans and permission requests appear exactly as with the demo agent.
16+
17+## Variants
18+
19+- To work on a specific project directory, pass the agent's working directory explicitly: `./bin/ori --cwd /path/to/your/project`.
20+- To serve on another port: `./bin/ori --addr :9000`.
21+- The first `npx` run downloads the adapter package; pin it by installing it globally (`npm install -g @zed-industries/claude-code-acp`) and passing `--agent-cmd "claude-code-acp"`.
22+
23+## See also
24+
25+- Flags reference: [the ori command](../reference/cli.md)
26+- Connecting something else than Claude: [use another ACP agent](use-another-agent.md)
added docs/en/how-to/switch-the-theme.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# How to switch between the light and dark themes
2+
3+This guide shows how to change Ori's colour theme. It assumes Ori is open in your browser (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Steps
6+
7+1. Look at the right of the header, next to **⊞ Workspace**: the **☾** button switches to the dark theme; once dark, it becomes **☀** and switches back to light.
8+2. Click it. The whole page follows — conversation, workspace panel, code previews and editor (Monaco switches between its `light` and `vs-dark` palettes).
9+
10+## Variants
11+
12+- Light is the default. Ori no longer follows the operating system's light/dark preference: the theme is whatever was chosen last in this browser.
13+- The choice is remembered per browser (localStorage key `ori.theme`, values `light` or `dark`) and applied before the first paint on the next load. Clearing the site data restores light.
14+- The terminal tab keeps a dark background in both themes, like an IDE's integrated terminal.
15+- Several browsers connected to the same Ori server each keep their own theme: it is a browser preference, not part of the shared session.
16+
17+## See also
18+
19+- Why the theme is browser state and not server state: [explanation: architecture](../explanation/architecture.md)
20+- The other workspace conveniences: [How to use the workspace panel](use-the-workspace.md)
new file mode 100644
@@ -0,0 +1,20 @@
1+# How to switch between the light and dark themes
2+
3+This guide shows how to change Ori's colour theme. It assumes Ori is open in your browser (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Steps
6+
7+1. Look at the right of the header, next to **⊞ Workspace**: the **☾** button switches to the dark theme; once dark, it becomes **☀** and switches back to light.
8+2. Click it. The whole page follows — conversation, workspace panel, code previews and editor (Monaco switches between its `light` and `vs-dark` palettes).
9+
10+## Variants
11+
12+- Light is the default. Ori no longer follows the operating system's light/dark preference: the theme is whatever was chosen last in this browser.
13+- The choice is remembered per browser (localStorage key `ori.theme`, values `light` or `dark`) and applied before the first paint on the next load. Clearing the site data restores light.
14+- The terminal tab keeps a dark background in both themes, like an IDE's integrated terminal.
15+- Several browsers connected to the same Ori server each keep their own theme: it is a browser preference, not part of the shared session.
16+
17+## See also
18+
19+- Why the theme is browser state and not server state: [explanation: architecture](../explanation/architecture.md)
20+- The other workspace conveniences: [How to use the workspace panel](use-the-workspace.md)
added docs/en/how-to/use-another-agent.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to use another ACP agent
2+
3+This guide shows how to connect Ori to any agent that speaks ACP on stdio. It assumes you know how to start Ori (see [run with Claude Code](run-with-claude-code.md)).
4+
5+## Steps
6+
7+1. Identify the command that starts your agent in ACP mode. The agent must read JSON-RPC from stdin and write it to stdout.
8+2. Pass that command to Ori with `--agent-cmd`:
9+
10+ ```bash
11+ ./bin/ori --agent-cmd "gemini --experimental-acp"
12+ ```
13+
14+3. Open the panel: the `hello` header shows the session id the agent returned.
15+
16+## Variants
17+
18+- The bundled demo agent is itself just another ACP agent: `./bin/ori --agent-cmd "bin/ori-mock-agent"` (that is what `make run-mock` does).
19+- The command is split on whitespace: the first word is the executable, the rest are its arguments. Arguments containing spaces are not supported — wrap such a command in a small shell script and pass the script instead.
20+
21+## See also
22+
23+- Flags reference: [the ori command](../reference/cli.md)
24+- What Ori expects from the agent: [explanation: architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to use another ACP agent
2+
3+This guide shows how to connect Ori to any agent that speaks ACP on stdio. It assumes you know how to start Ori (see [run with Claude Code](run-with-claude-code.md)).
4+
5+## Steps
6+
7+1. Identify the command that starts your agent in ACP mode. The agent must read JSON-RPC from stdin and write it to stdout.
8+2. Pass that command to Ori with `--agent-cmd`:
9+
10+ ```bash
11+ ./bin/ori --agent-cmd "gemini --experimental-acp"
12+ ```
13+
14+3. Open the panel: the `hello` header shows the session id the agent returned.
15+
16+## Variants
17+
18+- The bundled demo agent is itself just another ACP agent: `./bin/ori --agent-cmd "bin/ori-mock-agent"` (that is what `make run-mock` does).
19+- The command is split on whitespace: the first word is the executable, the rest are its arguments. Arguments containing spaces are not supported — wrap such a command in a small shell script and pass the script instead.
20+
21+## See also
22+
23+- Flags reference: [the ori command](../reference/cli.md)
24+- What Ori expects from the agent: [explanation: architecture](../explanation/architecture.md)
added docs/en/how-to/use-the-workspace.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to use the workspace panel
2+
3+This guide shows how to browse, preview, edit and run commands next to the conversation. It assumes Ori is running (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Steps
6+
7+1. Click **⊞ Workspace** in the header. The right-hand panel opens with the file tree of the agent's working directory (`--cwd`).
8+2. **Preview a file**: click it in the tree. Code appears with syntax highlighting; `.md` and `.adoc` files open in their rendered form, with a **Rendered / Source** toggle in the pane header. Images (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`, `.ico`, `.avif` — including draw.io's `.drawio.svg` / `.drawio.png` exports) show at natural size over a checkerboard, with their pixel dimensions underneath. Diagrams saved as `.drawio` / `.dio` XML render in the diagrams.net viewer, with the XML behind **Source**.
9+3. **Edit a file**: double-click it in the tree (or press **Edit** in the preview header). A **●** marks unsaved changes; save with the **Save** button or **Ctrl/Cmd+S**.
10+4. **Open a terminal**: select the **Terminal** tab. A real shell starts in the working directory; it keeps running while you switch tabs or collapse the panel.
11+5. **Resize the file tree**: drag the thin vertical line between the tree and the tabs. It highlights on hover; the column follows the pointer between 120 px and 800 px. Double-click the line to return to the default width (14rem).
12+
13+## Variants
14+
15+- The tree's **⟳** button re-reads the working directory after the agent created or deleted files.
16+- Binary files and files over 5 MiB are refused by the preview and editor with an explicit message; images are the exception, streamed by `GET /api/raw` without a cap.
17+- The draw.io viewer is `embed.diagrams.net`, loaded in an iframe and fed the XML locally over its postMessage protocol (the diagram is not uploaded anywhere). In a browser that cannot reach that host, the pane says so after a few seconds and the **Source** view remains available. See the [architecture explanation](../explanation/architecture.md) for why there is no offline renderer.
18+- The shell is taken from `$SHELL` (falling back to bash, then sh); resize is automatic.
19+- The file tree divider is keyboard-operable: focus it with **Tab**, then **←** / **→** move it by 16 px, **Home** / **End** jump to the narrowest / widest width. The chosen width is remembered per browser (localStorage key `ori.fileTreeWidth`) and survives a page reload; clearing site data restores the default.
20+
21+## See also
22+
23+- Endpoints and wire formats: [workspace API reference](../reference/workspace-api.md)
24+- Why panes survive tab switches: [explanation: architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to use the workspace panel
2+
3+This guide shows how to browse, preview, edit and run commands next to the conversation. It assumes Ori is running (see the [getting started tutorial](../tutorials/getting-started.md)).
4+
5+## Steps
6+
7+1. Click **⊞ Workspace** in the header. The right-hand panel opens with the file tree of the agent's working directory (`--cwd`).
8+2. **Preview a file**: click it in the tree. Code appears with syntax highlighting; `.md` and `.adoc` files open in their rendered form, with a **Rendered / Source** toggle in the pane header. Images (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`, `.ico`, `.avif` — including draw.io's `.drawio.svg` / `.drawio.png` exports) show at natural size over a checkerboard, with their pixel dimensions underneath. Diagrams saved as `.drawio` / `.dio` XML render in the diagrams.net viewer, with the XML behind **Source**.
9+3. **Edit a file**: double-click it in the tree (or press **Edit** in the preview header). A **●** marks unsaved changes; save with the **Save** button or **Ctrl/Cmd+S**.
10+4. **Open a terminal**: select the **Terminal** tab. A real shell starts in the working directory; it keeps running while you switch tabs or collapse the panel.
11+5. **Resize the file tree**: drag the thin vertical line between the tree and the tabs. It highlights on hover; the column follows the pointer between 120 px and 800 px. Double-click the line to return to the default width (14rem).
12+
13+## Variants
14+
15+- The tree's **⟳** button re-reads the working directory after the agent created or deleted files.
16+- Binary files and files over 5 MiB are refused by the preview and editor with an explicit message; images are the exception, streamed by `GET /api/raw` without a cap.
17+- The draw.io viewer is `embed.diagrams.net`, loaded in an iframe and fed the XML locally over its postMessage protocol (the diagram is not uploaded anywhere). In a browser that cannot reach that host, the pane says so after a few seconds and the **Source** view remains available. See the [architecture explanation](../explanation/architecture.md) for why there is no offline renderer.
18+- The shell is taken from `$SHELL` (falling back to bash, then sh); resize is automatic.
19+- The file tree divider is keyboard-operable: focus it with **Tab**, then **←** / **→** move it by 16 px, **Home** / **End** jump to the narrowest / widest width. The chosen width is remembered per browser (localStorage key `ori.fileTreeWidth`) and survives a page reload; clearing site data restores the default.
20+
21+## See also
22+
23+- Endpoints and wire formats: [workspace API reference](../reference/workspace-api.md)
24+- Why panes survive tab switches: [explanation: architecture](../explanation/architecture.md)
added docs/en/reference/cli.md +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+# Reference: the ori command
2+
3+> Neutral, exhaustive description of the `ori` server binary and its flags.
4+
5+## Synopsis
6+
7+```bash
8+ori [--addr ADDR] [--cwd DIR] [--agent-cmd COMMAND]
9+```
10+
11+At startup, `ori` spawns the agent command, performs the ACP handshake (`initialize`, then `session/new`), then serves the web application and its WebSocket endpoint. If the agent cannot be started or the handshake fails, `ori` exits with a non-zero status and an error on stderr.
12+
13+## Flags
14+
15+| Flag | Type | Default | Description |
16+| --- | --- | --- | --- |
17+| `--addr` | string | `:8888` | TCP address the HTTP server listens on. The empty host binds all interfaces. |
18+| `--cwd` | string | current directory | Working directory handed to the agent session. Must exist and be a directory; resolved to an absolute path. |
19+| `--agent-cmd` | string | `npx -y @zed-industries/claude-code-acp` | Command spawning the ACP agent, split on whitespace (first field: executable; the rest: arguments). |
20+
21+## HTTP endpoints
22+
23+| Route | Description |
24+| --- | --- |
25+| `GET /healthz` | Returns `{"status":"ok"}` with status 200. |
26+| `GET /ws` | WebSocket endpoint of the panel; see the [WebSocket protocol reference](websocket-protocol.md). |
27+| `GET /*` | The embedded single-page application; unknown paths fall back to `index.html`. Returns 503 if the binary was built without a UI build. |
28+
29+## Example
30+
31+```bash
32+./bin/ori --addr :9000 --cwd /work/my-project --agent-cmd "bin/ori-mock-agent"
33+```
34+
35+## Errors
36+
37+| Error | Cause |
38+| --- | --- |
39+| `--agent-cmd must not be empty` | The flag was set to blanks. |
40+| `--cwd: … no such file or directory` | The working directory does not exist. |
41+| `--cwd: … is not a directory` | The path exists but is a plain file. |
42+| `start agent "…": …` | The agent executable could not be started. |
43+| `initialize: …` / `new session: …` | The ACP handshake with the agent failed. |
44+
45+## Companion binary: ori-mock-agent
46+
47+```bash
48+ori-mock-agent [--delay DURATION]
49+```
50+
51+| Flag | Type | Default | Description |
52+| --- | --- | --- | --- |
53+| `--delay` | duration | `300ms` | Pause between streamed updates. `0s` disables pacing. |
54+
55+`ori-mock-agent` speaks ACP on stdio and plays one deterministic scenario per prompt: a thought, a two-entry plan, a read tool call, one permission request, and — if granted — an edit tool call with a diff and a final message.
new file mode 100644
@@ -0,0 +1,55 @@
1+# Reference: the ori command
2+
3+> Neutral, exhaustive description of the `ori` server binary and its flags.
4+
5+## Synopsis
6+
7+```bash
8+ori [--addr ADDR] [--cwd DIR] [--agent-cmd COMMAND]
9+```
10+
11+At startup, `ori` spawns the agent command, performs the ACP handshake (`initialize`, then `session/new`), then serves the web application and its WebSocket endpoint. If the agent cannot be started or the handshake fails, `ori` exits with a non-zero status and an error on stderr.
12+
13+## Flags
14+
15+| Flag | Type | Default | Description |
16+| --- | --- | --- | --- |
17+| `--addr` | string | `:8888` | TCP address the HTTP server listens on. The empty host binds all interfaces. |
18+| `--cwd` | string | current directory | Working directory handed to the agent session. Must exist and be a directory; resolved to an absolute path. |
19+| `--agent-cmd` | string | `npx -y @zed-industries/claude-code-acp` | Command spawning the ACP agent, split on whitespace (first field: executable; the rest: arguments). |
20+
21+## HTTP endpoints
22+
23+| Route | Description |
24+| --- | --- |
25+| `GET /healthz` | Returns `{"status":"ok"}` with status 200. |
26+| `GET /ws` | WebSocket endpoint of the panel; see the [WebSocket protocol reference](websocket-protocol.md). |
27+| `GET /*` | The embedded single-page application; unknown paths fall back to `index.html`. Returns 503 if the binary was built without a UI build. |
28+
29+## Example
30+
31+```bash
32+./bin/ori --addr :9000 --cwd /work/my-project --agent-cmd "bin/ori-mock-agent"
33+```
34+
35+## Errors
36+
37+| Error | Cause |
38+| --- | --- |
39+| `--agent-cmd must not be empty` | The flag was set to blanks. |
40+| `--cwd: … no such file or directory` | The working directory does not exist. |
41+| `--cwd: … is not a directory` | The path exists but is a plain file. |
42+| `start agent "…": …` | The agent executable could not be started. |
43+| `initialize: …` / `new session: …` | The ACP handshake with the agent failed. |
44+
45+## Companion binary: ori-mock-agent
46+
47+```bash
48+ori-mock-agent [--delay DURATION]
49+```
50+
51+| Flag | Type | Default | Description |
52+| --- | --- | --- | --- |
53+| `--delay` | duration | `300ms` | Pause between streamed updates. `0s` disables pacing. |
54+
55+`ori-mock-agent` speaks ACP on stdio and plays one deterministic scenario per prompt: a thought, a two-entry plan, a read tool call, one permission request, and — if granted — an edit tool call with a diff and a final message.
added docs/en/reference/websocket-protocol.md +103 -0
new file mode 100644
@@ -0,0 +1,103 @@
1+# Reference: WebSocket protocol
2+
3+> Neutral, exhaustive description of the messages exchanged between the browser and the ori backend on `GET /ws`. One JSON object per text frame. ACP payloads are relayed verbatim; their shapes are specified by the [Agent Client Protocol](https://agentclientprotocol.com).
4+
5+## Connection sequence
6+
7+On every connection the server sends `hello`, then replays the recorded session history (up to 4096 events), then any permission request still awaiting an answer, then live events as they happen.
8+
9+## Browser → server messages
10+
11+### `prompt`
12+
13+Starts a turn.
14+
15+| Field | Type | Required | Description |
16+| --- | --- | --- | --- |
17+| `type` | string | yes | `"prompt"` |
18+| `text` | string | yes | The user's message. |
19+| `attachments` | array | no | Files mentioned with `@` in the text: `{ "path": "<absolute or root-relative path>", "name": "<label>" }`. |
20+
21+```json
22+{ "type": "prompt", "text": "Explain @src/main.go", "attachments": [ { "path": "/work/src/main.go", "name": "src/main.go" } ] }
23+```
24+
25+The server sends the ACP prompt as a `text` content block followed by one `resource_link` block per attachment (`uri: "file://<absolute path>"`, `name` as given, defaulting to the path). Relative paths are joined to the server's `--cwd`; attachments with an empty `path` are dropped. The `@name` mention stays in the text.
26+
27+### `cancel`
28+
29+Interrupts the running turn.
30+
31+| Field | Type | Required | Description |
32+| --- | --- | --- | --- |
33+| `type` | string | yes | `"cancel"` |
34+
35+### `permission_response`
36+
37+Answers a `permission_request`.
38+
39+| Field | Type | Required | Description |
40+| --- | --- | --- | --- |
41+| `type` | string | yes | `"permission_response"` |
42+| `requestId` | string | yes | The id from the `permission_request` event. |
43+| `optionId` | string | one of the two | The chosen ACP permission option. |
44+| `cancelled` | boolean | one of the two | `true` dismisses the request without choosing. |
45+
46+## Server → browser messages
47+
48+### `hello`
49+
50+| Field | Type | Description |
51+| --- | --- | --- |
52+| `sessionId` | string | The ACP session id, empty if no agent is attached. |
53+| `turnActive` | boolean | `true` if a turn is currently running. |
54+
55+### `user_message`
56+
57+Echo of the prompt that started a turn; also present in replays.
58+
59+| Field | Type | Description |
60+| --- | --- | --- |
61+| `text` | string | The user's message. |
62+| `attachments` | array | The `attachments` of the prompt, verbatim (absent when there were none). |
63+
64+### `turn_started` / `turn_ended`
65+
66+| Field | Type | Description |
67+| --- | --- | --- |
68+| `stopReason` | string | `turn_ended` only: the ACP stop reason (`end_turn`, `cancelled`, `refusal`, `max_tokens`, `max_turn_requests`). |
69+
70+### `session_update`
71+
72+| Field | Type | Description |
73+| --- | --- | --- |
74+| `update` | object | One raw ACP `SessionUpdate`, discriminated by its `sessionUpdate` field (`agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, `plan`, `available_commands_update`, …). |
75+
76+The SPA renders message and thought chunks, tool calls and the plan, and keeps the `availableCommands` of the last `available_commands_update` for the composer's `/` selector (the mock agent sends one at session start; the Claude Code adapter sends Claude Code's slash commands). Other kinds are ignored.
77+
78+```json
79+{ "type": "session_update", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "Hello" } } }
80+```
81+
82+### `permission_request`
83+
84+| Field | Type | Description |
85+| --- | --- | --- |
86+| `requestId` | string | Correlates with `permission_response` and `permission_resolved`. |
87+| `request` | object | The raw ACP `RequestPermissionRequest` (`toolCall`, `options`). |
88+
89+### `permission_resolved`
90+
91+| Field | Type | Description |
92+| --- | --- | --- |
93+| `requestId` | string | The request that was answered (by any connected client, or cancelled by the agent). |
94+
95+### `error`
96+
97+| Field | Type | Description |
98+| --- | --- | --- |
99+| `message` | string | Human-readable error (`"a turn is already running"`, `"no agent session"`, `"prompt failed: …"`, `"cancel failed: …"`, `"malformed message: …"`, `"unknown message type …"`). |
100+
101+## Recorded vs transient events
102+
103+Events replayed to late-joining clients: `user_message`, `turn_started`, `turn_ended`, `session_update`, `permission_resolved`, plus pending `permission_request`s. Not recorded: `hello` (regenerated per connection) and `error` events emitted outside a turn failure.
new file mode 100644
@@ -0,0 +1,103 @@
1+# Reference: WebSocket protocol
2+
3+> Neutral, exhaustive description of the messages exchanged between the browser and the ori backend on `GET /ws`. One JSON object per text frame. ACP payloads are relayed verbatim; their shapes are specified by the [Agent Client Protocol](https://agentclientprotocol.com).
4+
5+## Connection sequence
6+
7+On every connection the server sends `hello`, then replays the recorded session history (up to 4096 events), then any permission request still awaiting an answer, then live events as they happen.
8+
9+## Browser → server messages
10+
11+### `prompt`
12+
13+Starts a turn.
14+
15+| Field | Type | Required | Description |
16+| --- | --- | --- | --- |
17+| `type` | string | yes | `"prompt"` |
18+| `text` | string | yes | The user's message. |
19+| `attachments` | array | no | Files mentioned with `@` in the text: `{ "path": "<absolute or root-relative path>", "name": "<label>" }`. |
20+
21+```json
22+{ "type": "prompt", "text": "Explain @src/main.go", "attachments": [ { "path": "/work/src/main.go", "name": "src/main.go" } ] }
23+```
24+
25+The server sends the ACP prompt as a `text` content block followed by one `resource_link` block per attachment (`uri: "file://<absolute path>"`, `name` as given, defaulting to the path). Relative paths are joined to the server's `--cwd`; attachments with an empty `path` are dropped. The `@name` mention stays in the text.
26+
27+### `cancel`
28+
29+Interrupts the running turn.
30+
31+| Field | Type | Required | Description |
32+| --- | --- | --- | --- |
33+| `type` | string | yes | `"cancel"` |
34+
35+### `permission_response`
36+
37+Answers a `permission_request`.
38+
39+| Field | Type | Required | Description |
40+| --- | --- | --- | --- |
41+| `type` | string | yes | `"permission_response"` |
42+| `requestId` | string | yes | The id from the `permission_request` event. |
43+| `optionId` | string | one of the two | The chosen ACP permission option. |
44+| `cancelled` | boolean | one of the two | `true` dismisses the request without choosing. |
45+
46+## Server → browser messages
47+
48+### `hello`
49+
50+| Field | Type | Description |
51+| --- | --- | --- |
52+| `sessionId` | string | The ACP session id, empty if no agent is attached. |
53+| `turnActive` | boolean | `true` if a turn is currently running. |
54+
55+### `user_message`
56+
57+Echo of the prompt that started a turn; also present in replays.
58+
59+| Field | Type | Description |
60+| --- | --- | --- |
61+| `text` | string | The user's message. |
62+| `attachments` | array | The `attachments` of the prompt, verbatim (absent when there were none). |
63+
64+### `turn_started` / `turn_ended`
65+
66+| Field | Type | Description |
67+| --- | --- | --- |
68+| `stopReason` | string | `turn_ended` only: the ACP stop reason (`end_turn`, `cancelled`, `refusal`, `max_tokens`, `max_turn_requests`). |
69+
70+### `session_update`
71+
72+| Field | Type | Description |
73+| --- | --- | --- |
74+| `update` | object | One raw ACP `SessionUpdate`, discriminated by its `sessionUpdate` field (`agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, `plan`, `available_commands_update`, …). |
75+
76+The SPA renders message and thought chunks, tool calls and the plan, and keeps the `availableCommands` of the last `available_commands_update` for the composer's `/` selector (the mock agent sends one at session start; the Claude Code adapter sends Claude Code's slash commands). Other kinds are ignored.
77+
78+```json
79+{ "type": "session_update", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "Hello" } } }
80+```
81+
82+### `permission_request`
83+
84+| Field | Type | Description |
85+| --- | --- | --- |
86+| `requestId` | string | Correlates with `permission_response` and `permission_resolved`. |
87+| `request` | object | The raw ACP `RequestPermissionRequest` (`toolCall`, `options`). |
88+
89+### `permission_resolved`
90+
91+| Field | Type | Description |
92+| --- | --- | --- |
93+| `requestId` | string | The request that was answered (by any connected client, or cancelled by the agent). |
94+
95+### `error`
96+
97+| Field | Type | Description |
98+| --- | --- | --- |
99+| `message` | string | Human-readable error (`"a turn is already running"`, `"no agent session"`, `"prompt failed: …"`, `"cancel failed: …"`, `"malformed message: …"`, `"unknown message type …"`). |
100+
101+## Recorded vs transient events
102+
103+Events replayed to late-joining clients: `user_message`, `turn_started`, `turn_ended`, `session_update`, `permission_resolved`, plus pending `permission_request`s. Not recorded: `hello` (regenerated per connection) and `error` events emitted outside a turn failure.
added docs/en/reference/workspace-api.md +108 -0
new file mode 100644
@@ -0,0 +1,108 @@
1+# Reference: workspace API
2+
3+> Neutral, exhaustive description of the endpoints behind the workspace panel and the composer's selectors: the file API, the skills endpoint and the terminal WebSocket. Relative paths resolve against the server's `--cwd`; absolute paths are used as-is (access is unrestricted by design — ori targets already-isolated sandboxes).
4+
5+## GET /api/files
6+
7+Lists a directory.
8+
9+| Query parameter | Type | Default | Description |
10+| --- | --- | --- | --- |
11+| `path` | string | the workspace root | Directory to list. |
12+
13+Response `200`:
14+
15+```json
16+{ "path": "/work", "entries": [ { "name": "src", "path": "/work/src", "isDir": true, "size": 0 } ] }
17+```
18+
19+Entries are sorted directories first, then files, each group alphabetically.
20+
21+## GET /api/files/search
22+
23+Searches files recursively under the workspace root, for the composer's `@` selector.
24+
25+| Query parameter | Type | Default | Description |
26+| --- | --- | --- | --- |
27+| `q` | string | `""` | Case-insensitive substring matched against the path relative to the root. Empty returns the first files. |
28+| `limit` | integer | 50 | Maximum number of hits; capped at 500. |
29+
30+Response `200`:
31+
32+```json
33+{ "root": "/work", "files": [ { "name": "main.go", "path": "/work/src/main.go", "relPath": "src/main.go" } ] }
34+```
35+
36+`path` is absolute (usable in the other endpoints), `relPath` uses forward slashes. Hits are ranked: files whose base name contains the query first, then shorter relative paths, then alphabetically. The walk never enters `.git` or `node_modules`, skips unreadable entries, and stops after 50 000 directory entries. `files` is always an array (`[]` when nothing matches). A missing root is a `404`.
37+
38+## GET /api/file
39+
40+Reads a text file.
41+
42+| Query parameter | Type | Default | Description |
43+| --- | --- | --- | --- |
44+| `path` | string | — | File to read. |
45+
46+Response `200`: `{ "path": "/work/README.md", "content": "…" }`
47+
48+## PUT /api/file
49+
50+Writes a text file, creating parent directories as needed.
51+
52+| Query parameter | Type | Default | Description |
53+| --- | --- | --- | --- |
54+| `path` | string | — (required) | File to write. |
55+
56+Body: `{ "content": "…" }` — response `200`: `{ "path": "…", "status": "saved" }`
57+
58+## GET /api/raw
59+
60+Streams a file's bytes as-is — how the preview pane shows images. No size cap, no text requirement; `Range` requests are honoured (`http.ServeContent`).
61+
62+| Query parameter | Type | Default | Description |
63+| --- | --- | --- | --- |
64+| `path` | string | — (required) | File to stream. |
65+
66+Response `200` with the file bytes, `X-Content-Type-Options: nosniff`, and a `Content-Type` chosen in this order:
67+
68+| Extension | Content-Type |
69+| --- | --- |
70+| `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` `.bmp` `.ico` `.avif` | pinned image types (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/avif`) |
71+| any other known extension | the host's mime table (e.g. `text/markdown; charset=utf-8`) |
72+| unknown | sniffed from the first 512 bytes |
73+
74+## GET /api/skills
75+
76+Lists the Claude Code skills available to the session, for the composer's `/` selector.
77+
78+Response `200`:
79+
80+```json
81+{ "skills": [ { "name": "quality", "description": "Audit code quality with qlty.", "path": "/work/.claude/skills/quality/SKILL.md", "source": "project" } ] }
82+```
83+
84+A skill is a directory holding a `SKILL.md`, looked up in `<cwd>/.claude/skills/*/` (`source: "project"`) and `~/.claude/skills/*/` (`source: "user"`). `name` and `description` come from the file's YAML frontmatter (`key: value`, quoted values and `>` / `|` block scalars are understood); a missing name falls back to the directory name, a missing description to `""`. A project skill shadows a user skill of the same name. Results are sorted by name; `skills` is always an array.
85+
86+## File API errors
87+
88+All errors are JSON: `{ "error": "message" }`.
89+
90+| Status | Cause |
91+| --- | --- |
92+| 400 | Missing `path` on PUT or `GET /api/raw`, invalid JSON body, or reading a directory. |
93+| 403 | Filesystem permission denied. |
94+| 404 | Path (or the workspace root, for the search) does not exist. |
95+| 413 | File larger than 5 MiB (`GET /api/file` only; `/api/raw` has no cap). |
96+| 415 | File is not valid UTF-8 text (`GET /api/file` only). |
97+
98+## GET /ws/terminal
99+
100+Upgrades to a WebSocket and starts one shell (from `$SHELL`, falling back to `bash` then `sh`, with `TERM=xterm-256color`) inside a pseudo-terminal, in the server's `--cwd`.
101+
102+| Direction | Frame type | Content |
103+| --- | --- | --- |
104+| server → client | binary | Raw terminal output bytes. |
105+| client → server | text | `{"type":"input","data":"…"}` — keystrokes written to the pty. |
106+| client → server | text | `{"type":"resize","cols":N,"rows":N}` — viewport change. |
107+
108+The socket closes with a normal-closure status when the shell exits; closing the socket kills the shell. Malformed text frames are ignored.
new file mode 100644
@@ -0,0 +1,108 @@
1+# Reference: workspace API
2+
3+> Neutral, exhaustive description of the endpoints behind the workspace panel and the composer's selectors: the file API, the skills endpoint and the terminal WebSocket. Relative paths resolve against the server's `--cwd`; absolute paths are used as-is (access is unrestricted by design — ori targets already-isolated sandboxes).
4+
5+## GET /api/files
6+
7+Lists a directory.
8+
9+| Query parameter | Type | Default | Description |
10+| --- | --- | --- | --- |
11+| `path` | string | the workspace root | Directory to list. |
12+
13+Response `200`:
14+
15+```json
16+{ "path": "/work", "entries": [ { "name": "src", "path": "/work/src", "isDir": true, "size": 0 } ] }
17+```
18+
19+Entries are sorted directories first, then files, each group alphabetically.
20+
21+## GET /api/files/search
22+
23+Searches files recursively under the workspace root, for the composer's `@` selector.
24+
25+| Query parameter | Type | Default | Description |
26+| --- | --- | --- | --- |
27+| `q` | string | `""` | Case-insensitive substring matched against the path relative to the root. Empty returns the first files. |
28+| `limit` | integer | 50 | Maximum number of hits; capped at 500. |
29+
30+Response `200`:
31+
32+```json
33+{ "root": "/work", "files": [ { "name": "main.go", "path": "/work/src/main.go", "relPath": "src/main.go" } ] }
34+```
35+
36+`path` is absolute (usable in the other endpoints), `relPath` uses forward slashes. Hits are ranked: files whose base name contains the query first, then shorter relative paths, then alphabetically. The walk never enters `.git` or `node_modules`, skips unreadable entries, and stops after 50 000 directory entries. `files` is always an array (`[]` when nothing matches). A missing root is a `404`.
37+
38+## GET /api/file
39+
40+Reads a text file.
41+
42+| Query parameter | Type | Default | Description |
43+| --- | --- | --- | --- |
44+| `path` | string | — | File to read. |
45+
46+Response `200`: `{ "path": "/work/README.md", "content": "…" }`
47+
48+## PUT /api/file
49+
50+Writes a text file, creating parent directories as needed.
51+
52+| Query parameter | Type | Default | Description |
53+| --- | --- | --- | --- |
54+| `path` | string | — (required) | File to write. |
55+
56+Body: `{ "content": "…" }` — response `200`: `{ "path": "…", "status": "saved" }`
57+
58+## GET /api/raw
59+
60+Streams a file's bytes as-is — how the preview pane shows images. No size cap, no text requirement; `Range` requests are honoured (`http.ServeContent`).
61+
62+| Query parameter | Type | Default | Description |
63+| --- | --- | --- | --- |
64+| `path` | string | — (required) | File to stream. |
65+
66+Response `200` with the file bytes, `X-Content-Type-Options: nosniff`, and a `Content-Type` chosen in this order:
67+
68+| Extension | Content-Type |
69+| --- | --- |
70+| `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` `.bmp` `.ico` `.avif` | pinned image types (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/avif`) |
71+| any other known extension | the host's mime table (e.g. `text/markdown; charset=utf-8`) |
72+| unknown | sniffed from the first 512 bytes |
73+
74+## GET /api/skills
75+
76+Lists the Claude Code skills available to the session, for the composer's `/` selector.
77+
78+Response `200`:
79+
80+```json
81+{ "skills": [ { "name": "quality", "description": "Audit code quality with qlty.", "path": "/work/.claude/skills/quality/SKILL.md", "source": "project" } ] }
82+```
83+
84+A skill is a directory holding a `SKILL.md`, looked up in `<cwd>/.claude/skills/*/` (`source: "project"`) and `~/.claude/skills/*/` (`source: "user"`). `name` and `description` come from the file's YAML frontmatter (`key: value`, quoted values and `>` / `|` block scalars are understood); a missing name falls back to the directory name, a missing description to `""`. A project skill shadows a user skill of the same name. Results are sorted by name; `skills` is always an array.
85+
86+## File API errors
87+
88+All errors are JSON: `{ "error": "message" }`.
89+
90+| Status | Cause |
91+| --- | --- |
92+| 400 | Missing `path` on PUT or `GET /api/raw`, invalid JSON body, or reading a directory. |
93+| 403 | Filesystem permission denied. |
94+| 404 | Path (or the workspace root, for the search) does not exist. |
95+| 413 | File larger than 5 MiB (`GET /api/file` only; `/api/raw` has no cap). |
96+| 415 | File is not valid UTF-8 text (`GET /api/file` only). |
97+
98+## GET /ws/terminal
99+
100+Upgrades to a WebSocket and starts one shell (from `$SHELL`, falling back to `bash` then `sh`, with `TERM=xterm-256color`) inside a pseudo-terminal, in the server's `--cwd`.
101+
102+| Direction | Frame type | Content |
103+| --- | --- | --- |
104+| server → client | binary | Raw terminal output bytes. |
105+| client → server | text | `{"type":"input","data":"…"}` — keystrokes written to the pty. |
106+| client → server | text | `{"type":"resize","cols":N,"rows":N}` — viewport change. |
107+
108+The socket closes with a normal-closure status when the shell exits; closing the socket kills the shell. Malformed text frames are ignored.
added docs/en/tutorials/getting-started.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Tutorial: getting started with Ori
2+
3+By the end of this tutorial, you will have built Ori, started it with its bundled demo agent, and held a complete conversation in your browser — including a streamed answer, a visible plan, tool calls and a permission you grant yourself. No prior knowledge of ACP is required.
4+
5+## Prerequisites
6+
7+- Go 1.26 or newer (`go version`)
8+- Node.js 24 or newer with npm (`node --version`)
9+- A web browser
10+
11+## Step 1 — Install the frontend dependencies
12+
13+From the repository root, type:
14+
15+```bash
16+make deps
17+```
18+
19+You should see npm resolve the packages and end with a line like:
20+
21+```
22+added 158 packages, and audited 159 packages in 9s
23+```
24+
25+We have just installed everything the SPA build needs.
26+
27+## Step 2 — Build and start Ori with the demo agent
28+
29+Type:
30+
31+```bash
32+make run-mock
33+```
34+
35+You should see the SPA build, the Go build, then:
36+
37+```
38+INFO starting agent command="[bin/ori-mock-agent]" cwd=…
39+INFO agent session ready sessionId=mock-1
40+INFO ori listening addr=:8888
41+```
42+
43+We have just started the full application: the server is up and already connected, over ACP, to `ori-mock-agent` — a small deterministic agent that needs no account and no network.
44+
45+## Step 3 — Open the panel
46+
47+Open [http://localhost:8888](http://localhost:8888) in your browser.
48+
49+You should see the Ori panel: a header showing `online · mock-1`, an empty conversation, and a message box at the bottom.
50+
51+## Step 4 — Send your first message
52+
53+Type `hello` in the message box and press Enter.
54+
55+You should see a spinner appear at the bottom of the thread with rotating status phrases (*Pondering…*, *Brewing ideas…*), then, streaming in one after the other:
56+
57+- your own message, aligned to the right;
58+- a **💭 Thinking…** block showing the agent's reasoning live while it streams (it collapses into *💭 Thought for a moment* once the turn ends — click it to reopen);
59+- a **Plan (0/2)** checklist;
60+- a tool call card **📖 Reading README.md** whose badge turns to **done**;
61+- the beginning of the agent's answer.
62+
63+We have just run the first half of a prompt turn: the agent streams updates and the panel renders each kind in its own shape.
64+
65+## Step 5 — Grant the permission
66+
67+The panel now shows a **🔐 Write hello.txt** card with two buttons.
68+
69+Click **Allow once**.
70+
71+You should see a second tool call card **✏️ Writing hello.txt** with a green diff, then the agent's final answer — `Done! You said: hello` followed by a small checklist — and the plan showing **Plan (2/2)**.
72+
73+We have just completed the whole loop: the agent asked for a permission, your click travelled back to it through the backend, and it finished its turn.
74+
75+## What now?
76+
77+You have run Ori end to end with the demo agent. To go further:
78+
79+- To talk to a real agent → [How to run Ori with Claude Code](../how-to/run-with-claude-code.md)
80+- To plug in another agent → [How to use another ACP agent](../how-to/use-another-agent.md)
81+- To understand what happened between the browser and the agent → [Explanation: architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,81 @@
1+# Tutorial: getting started with Ori
2+
3+By the end of this tutorial, you will have built Ori, started it with its bundled demo agent, and held a complete conversation in your browser — including a streamed answer, a visible plan, tool calls and a permission you grant yourself. No prior knowledge of ACP is required.
4+
5+## Prerequisites
6+
7+- Go 1.26 or newer (`go version`)
8+- Node.js 24 or newer with npm (`node --version`)
9+- A web browser
10+
11+## Step 1 — Install the frontend dependencies
12+
13+From the repository root, type:
14+
15+```bash
16+make deps
17+```
18+
19+You should see npm resolve the packages and end with a line like:
20+
21+```
22+added 158 packages, and audited 159 packages in 9s
23+```
24+
25+We have just installed everything the SPA build needs.
26+
27+## Step 2 — Build and start Ori with the demo agent
28+
29+Type:
30+
31+```bash
32+make run-mock
33+```
34+
35+You should see the SPA build, the Go build, then:
36+
37+```
38+INFO starting agent command="[bin/ori-mock-agent]" cwd=…
39+INFO agent session ready sessionId=mock-1
40+INFO ori listening addr=:8888
41+```
42+
43+We have just started the full application: the server is up and already connected, over ACP, to `ori-mock-agent` — a small deterministic agent that needs no account and no network.
44+
45+## Step 3 — Open the panel
46+
47+Open [http://localhost:8888](http://localhost:8888) in your browser.
48+
49+You should see the Ori panel: a header showing `online · mock-1`, an empty conversation, and a message box at the bottom.
50+
51+## Step 4 — Send your first message
52+
53+Type `hello` in the message box and press Enter.
54+
55+You should see a spinner appear at the bottom of the thread with rotating status phrases (*Pondering…*, *Brewing ideas…*), then, streaming in one after the other:
56+
57+- your own message, aligned to the right;
58+- a **💭 Thinking…** block showing the agent's reasoning live while it streams (it collapses into *💭 Thought for a moment* once the turn ends — click it to reopen);
59+- a **Plan (0/2)** checklist;
60+- a tool call card **📖 Reading README.md** whose badge turns to **done**;
61+- the beginning of the agent's answer.
62+
63+We have just run the first half of a prompt turn: the agent streams updates and the panel renders each kind in its own shape.
64+
65+## Step 5 — Grant the permission
66+
67+The panel now shows a **🔐 Write hello.txt** card with two buttons.
68+
69+Click **Allow once**.
70+
71+You should see a second tool call card **✏️ Writing hello.txt** with a green diff, then the agent's final answer — `Done! You said: hello` followed by a small checklist — and the plan showing **Plan (2/2)**.
72+
73+We have just completed the whole loop: the agent asked for a permission, your click travelled back to it through the backend, and it finished its turn.
74+
75+## What now?
76+
77+You have run Ori end to end with the demo agent. To go further:
78+
79+- To talk to a real agent → [How to run Ori with Claude Code](../how-to/run-with-claude-code.md)
80+- To plug in another agent → [How to use another ACP agent](../how-to/use-another-agent.md)
81+- To understand what happened between the browser and the agent → [Explanation: architecture](../explanation/architecture.md)
added docs/fr/README.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Ori — documentation (français)
2+
3+Ori est une application web qui joue le rôle de client [ACP (Agent Client Protocol)](https://agentclientprotocol.com) pour des agents de code IA : un backend Go sert une SPA React et se connecte, en ACP, à un agent tel que Claude Code. Le navigateur retrouve l'expérience d'un panel d'agent dans l'esprit de celui de Zed : réponses streamées, tool calls visibles, plans et demandes de permission.
4+
5+Cette documentation suit la structure [Diátaxis](https://diataxis.fr) — choisissez la section qui correspond à votre besoin du moment :
6+
7+| Vous voulez… | Allez à |
8+| --- | --- |
9+| Apprendre en faisant : lancer Ori et mener votre première conversation | [Tutoriel : premiers pas](tutorials/getting-started.md) |
10+| Lancer Ori avec Claude Code | [Lancer Ori avec Claude Code](how-to/run-with-claude-code.md) |
11+| Brancher un autre agent ACP | [Utiliser un autre agent ACP](how-to/use-another-agent.md) |
12+| Empaqueter et lancer Ori dans une sandbox Docker | [Lancer Ori dans une sandbox Docker](how-to/run-in-a-sandbox.md) |
13+| Parcourir, prévisualiser (code, Markdown, AsciiDoc, images, draw.io), éditer des fichiers et utiliser le terminal | [Utiliser le panneau workspace](how-to/use-the-workspace.md) |
14+| Mentionner des fichiers avec `@` et invoquer des skills avec `/` dans le composer | [Mentionner des fichiers et invoquer des skills](how-to/mention-files-and-skills.md) |
15+| Basculer entre les thèmes clair et sombre | [Basculer le thème](how-to/switch-the-theme.md) |
16+| Lancer les suites de tests | [Lancer les tests](how-to/run-the-tests.md) |
17+| Consulter les options de la CLI | [Référence : la commande ori](reference/cli.md) |
18+| Consulter le protocole navigateur ⇆ backend | [Référence : protocole WebSocket](reference/websocket-protocol.md) |
19+| Consulter l'API fichiers, l'endpoint des skills et le terminal | [Référence : API workspace](reference/workspace-api.md) |
20+| Comprendre l'architecture et ses compromis | [Explication : architecture](explanation/architecture.md) |
new file mode 100644
@@ -0,0 +1,20 @@
1+# Ori — documentation (français)
2+
3+Ori est une application web qui joue le rôle de client [ACP (Agent Client Protocol)](https://agentclientprotocol.com) pour des agents de code IA : un backend Go sert une SPA React et se connecte, en ACP, à un agent tel que Claude Code. Le navigateur retrouve l'expérience d'un panel d'agent dans l'esprit de celui de Zed : réponses streamées, tool calls visibles, plans et demandes de permission.
4+
5+Cette documentation suit la structure [Diátaxis](https://diataxis.fr) — choisissez la section qui correspond à votre besoin du moment :
6+
7+| Vous voulez… | Allez à |
8+| --- | --- |
9+| Apprendre en faisant : lancer Ori et mener votre première conversation | [Tutoriel : premiers pas](tutorials/getting-started.md) |
10+| Lancer Ori avec Claude Code | [Lancer Ori avec Claude Code](how-to/run-with-claude-code.md) |
11+| Brancher un autre agent ACP | [Utiliser un autre agent ACP](how-to/use-another-agent.md) |
12+| Empaqueter et lancer Ori dans une sandbox Docker | [Lancer Ori dans une sandbox Docker](how-to/run-in-a-sandbox.md) |
13+| Parcourir, prévisualiser (code, Markdown, AsciiDoc, images, draw.io), éditer des fichiers et utiliser le terminal | [Utiliser le panneau workspace](how-to/use-the-workspace.md) |
14+| Mentionner des fichiers avec `@` et invoquer des skills avec `/` dans le composer | [Mentionner des fichiers et invoquer des skills](how-to/mention-files-and-skills.md) |
15+| Basculer entre les thèmes clair et sombre | [Basculer le thème](how-to/switch-the-theme.md) |
16+| Lancer les suites de tests | [Lancer les tests](how-to/run-the-tests.md) |
17+| Consulter les options de la CLI | [Référence : la commande ori](reference/cli.md) |
18+| Consulter le protocole navigateur ⇆ backend | [Référence : protocole WebSocket](reference/websocket-protocol.md) |
19+| Consulter l'API fichiers, l'endpoint des skills et le terminal | [Référence : API workspace](reference/workspace-api.md) |
20+| Comprendre l'architecture et ses compromis | [Explication : architecture](explanation/architecture.md) |
added docs/fr/explanation/architecture.md +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+# Architecture — explication
2+
3+## De quoi s'agit-il ?
4+
5+Ori transpose dans le navigateur l'expérience d'un panel d'agent d'éditeur — celui de Zed, en l'occurrence. La contrainte intéressante est que l'[Agent Client Protocol](https://agentclientprotocol.com) a été conçu pour des éditeurs : un client lance l'agent comme sous-processus et lui parle en JSON-RPC sur son stdio. Un navigateur ne peut pas lancer de processus ; il faut donc quelque chose au milieu. Ce quelque chose, c'est le backend Go d'Ori.
6+
7+```
8+navigateur ⇆ WebSocket ⇆ backend Go ⇆ stdio (ACP / JSON-RPC) ⇆ agent (claude-code-acp, …)
9+```
10+
11+Le diagramme de dépendances des packages est maintenu dans [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (à ouvrir avec diagrams.net ou l'extension Draw.io de VS Code).
12+
13+## Les pièces
14+
15+- **`ui/` (SPA React)** — affiche la conversation : markdown streamé, réflexions repliables, cartes de tool calls avec statuts et diffs, plan de l'agent, demandes de permission. Tout son état dérive des événements serveur pliés par un reducer pur, ce qui rend l'interface testable sans navigateur ni réseau.
16+- **`internal/httpserver`** — sert la SPA (embarquée dans le binaire via `go:embed`, si bien qu'Ori se distribue en un seul fichier) et monte les routes supplémentaires comme `/ws`.
17+- **`internal/bridge`** — le cœur du backend. Il traduit entre deux mondes asynchrones : l'agent streame ses updates et bloque sur ses demandes de permission, pendant que les navigateurs se connectent, se déconnectent et répondent à vitesse humaine. Il diffuse les événements, rejoue l'historique aux retardataires, et route chaque décision de permission vers l'appel JSON-RPC exact qui l'attend.
18+- **`internal/agent`** — lance le processus de l'agent et pilote le cycle de vie de la session ACP (initialize, session/new, prompt, cancel) via le client [`coder/acp-go-sdk`](https://github.com/coder/acp-go-sdk). Il est agnostique de l'agent : tout ce qui parle ACP sur stdio se branche.
19+- **`internal/mockagent` + `cmd/ori-mock-agent`** — un agent ACP déterministe utilisé par le test de bout en bout et par `make run-mock`, pour exercer toute l'application sans agent IA, sans compte et sans réseau.
20+- **`internal/skills`** — découvre les skills Claude Code (frontmatter des `SKILL.md`) dans le projet et le répertoire personnel, pour le sélecteur `/` du composer.
21+- **`internal/files`** — l'API fichiers du workspace (lister, chercher, lire, octets bruts, écrire) derrière l'arborescence, la preview et l'éditeur de la SPA. L'accès est volontairement non restreint : ori vise des sandboxes déjà isolées, et l'agent a de toute façon la même portée.
22+- **`internal/terminal`** — un vrai shell par connexion WebSocket, dans un pseudo-terminal (`creack/pty`), piloté par xterm.js dans le navigateur.
23+
24+## Mentions, skills et le prompt ACP
25+
26+Le sélecteur `@` du composer posait une question de conception : comment un fichier parvient-il à l'agent ? ACP a déjà la réponse — tout agent doit accepter des blocs de contenu `resource_link` dans un prompt — donc le navigateur envoie le texte plus une petite liste `attachments`, et le bridge convertit chacune en `resource_link` (URI `file://`) à côté du bloc texte. La mention `@chemin` reste visible dans le texte, si bien que la conversation se lit naturellement dans tous les clients et dans les rejeux, tandis que le lien structuré permet à l'agent d'ouvrir le fichier sans deviner. Le sélecteur `/` n'a demandé aucun protocole : Claude Code invoque un skill ou une commande slash à partir du texte `/nom …`, donc la liste ne fait qu'insérer du texte, en fusionnant les skills trouvés sur disque et les commandes que l'agent annonce via `available_commands_update`.
27+
28+## Previews : images et draw.io
29+
30+Les images sont diffusées par `GET /api/raw` et rendues par le navigateur lui-même, à la fois le moteur le plus simple et le plus capable disponible. Les diagrammes draw.io sont la seule preview qui s'écarte de la règle « embarqué, pas de CDN » fixée par Monaco : il n'existe pas de moteur de rendu hors ligne qu'ori pourrait livrer — le `viewer.min.js` de draw.io n'est pas publié sur npm, le paquet archivé `mxgraph` n'a pas la bibliothèque de formes de draw.io, et le seul convertisseur navigateur sur npm (`@markdown-viewer/drawio2svg`) est sous licence GPL-3.0 uniquement. Le panneau embarque donc `embed.diagrams.net` dans une iframe et lui transmet le XML par son protocole postMessage — le fichier ne quitte pas le navigateur — et se replie sur un avertissement plus la source XML quand l'hôte est injoignable, comme il peut l'être dans une sandbox isolée. Vendoriser `viewer-static.min.js` depuis le dépôt draw.io sous Apache-2.0 (environ 3 Mo) reste la voie vers un rendu hors ligne si ce compromis change un jour.
31+
32+## Le panneau workspace
33+
34+La SPA est passée d'une colonne de chat unique à chat-plus-workspace : une arborescence de fichiers et trois onglets (Preview, Editor, Terminal). Trois choix la structurent. Monaco sert à la fois la preview de code en lecture seule et l'éditeur — une seule grosse dépendance au lieu de deux piles de colorisation — et il est embarqué avec ses workers, rien ne se charge depuis un CDN ; il est chargé à la demande, ce qui garde le bundle initial léger. Le markdown se rend via le même composant que les messages du chat, et l'AsciiDoc via un Asciidoctor importé paresseusement. Enfin, les panneaux inactifs sont cachés plutôt que démontés : la session shell du terminal et le tampon non sauvé de l'éditeur survivent aux changements d'onglet et au repli du panneau. La colonne de l'arborescence est redimensionnable et sa largeur suit la même règle « le store possède l'état » que le reste du panneau : elle vit dans le store workspace, atteint la feuille de style par une propriété CSS personnalisée (`--filetree-width`) posée sur le panneau, et est persistée en localStorage — une commodité par navigateur, pas un état du serveur, qui reste donc volontairement hors du protocole WebSocket.
35+
36+Le thème de couleurs est un état de même nature. Le clair est le défaut ; la palette sombre est conditionnée par un attribut `data-theme="dark"` qu'un petit store zustand (`ui/src/theme.ts`) pose sur `<html>` et mémorise en localStorage, et Monaco lit ce store plutôt que la préférence de l'OS pour que l'éditeur ne contredise jamais la page. L'ancien comportement — suivre `prefers-color-scheme` — a été abandonné au profit d'un choix explicite, parce qu'un serveur Ori partagé est souvent consulté depuis des machines dont l'utilisateur ne maîtrise pas les réglages système (le navigateur d'une sandbox, le portable d'un collègue). Comme la largeur de l'arborescence, le thème est une commodité par navigateur : il vit hors du protocole WebSocket et le serveur n'en entend jamais parler.
37+
38+## Pourquoi le serveur possède la conversation
39+
40+Le bridge enregistre chaque événement de la session et le rejoue à chaque connexion ; il renvoie même les prompts de l'utilisateur sous forme d'événements `user_message`. Le serveur devient ainsi l'unique source de vérité : la SPA ne fait jamais confiance à sa mémoire locale, elle se remet à zéro à chaque `hello` et se reconstruit depuis le rejeu. Le bénéfice : les reconnexions (mise en veille, coupure réseau, second onglet) sont trivialement correctes — tous les navigateurs convergent vers le même fil — au prix d'un historique borné en mémoire (4096 événements).
41+
42+## Pourquoi les payloads ACP traversent le WebSocket tels quels
43+
44+Le bridge aurait pu traduire les updates ACP dans ses propres formes de messages. Il ne le fait délibérément pas : `session_update` et `permission_request` transportent les objets ACP bruts. Le protocole est bien documenté et il évolue (plans, modes, contenus d'outils plus riches) ; le relayer tel quel signifie que les nouveaux types d'update atteignent le front sans toucher au code Go, et que le front peut les adopter à son rythme — les types inconnus sont ignorés par construction.
45+
46+## Pourquoi un processus adaptateur pour Claude Code
47+
48+La CLI Claude Code n'a pas de mode ACP natif (vérifié sur la v2.1.274 : aucune option ni sous-commande de ce type). L'intégration de Zed lui-même passe par [`@zed-industries/claude-code-acp`](https://www.npmjs.com/package/@zed-industries/claude-code-acp), qui enveloppe le SDK Claude Code et expose ACP sur stdio. Ori lance exactement cet adaptateur par défaut.
49+
50+## Alternatives écartées
51+
52+- **Piloter directement le mode `stream-json` de la CLI `claude`** — un pont maison vers le format de streaming propriétaire de Claude aurait demandé plus de travail, lié Ori à un seul agent, et renoncé à l'écosystème des agents ACP (Gemini CLI, et ce qui viendra ensuite).
53+- **Server-sent events ou polling au lieu d'un WebSocket** — les demandes de permission exigent des messages dans les deux sens sur une même connexion ; le SSE aurait imposé un second canal pour les réponses.
54+- **Plusieurs sessions simultanées dès la v1** — le bridge pilote délibérément une seule session d'agent partagée par tous les navigateurs connectés. L'interface `Prompter` et le modèle d'abonnement par connexion sont les coutures où le multi-sessions pourra s'ajouter sans refondre la conception.
55+
56+## Comment les tests reflètent l'architecture
57+
58+Chaque couture a son double : le package agent est testé contre un agent ACP en mémoire (sans sous-processus), le bridge contre une session factice (sans agent), le reducer de la SPA contre de simples objets messages (sans socket), et un test de bout en bout compile le vrai binaire de l'agent mock et traverse le câblage de production complet via un vrai WebSocket. Voir [lancer les tests](../how-to/run-the-tests.md).
new file mode 100644
@@ -0,0 +1,58 @@
1+# Architecture — explication
2+
3+## De quoi s'agit-il ?
4+
5+Ori transpose dans le navigateur l'expérience d'un panel d'agent d'éditeur — celui de Zed, en l'occurrence. La contrainte intéressante est que l'[Agent Client Protocol](https://agentclientprotocol.com) a été conçu pour des éditeurs : un client lance l'agent comme sous-processus et lui parle en JSON-RPC sur son stdio. Un navigateur ne peut pas lancer de processus ; il faut donc quelque chose au milieu. Ce quelque chose, c'est le backend Go d'Ori.
6+
7+```
8+navigateur ⇆ WebSocket ⇆ backend Go ⇆ stdio (ACP / JSON-RPC) ⇆ agent (claude-code-acp, …)
9+```
10+
11+Le diagramme de dépendances des packages est maintenu dans [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (à ouvrir avec diagrams.net ou l'extension Draw.io de VS Code).
12+
13+## Les pièces
14+
15+- **`ui/` (SPA React)** — affiche la conversation : markdown streamé, réflexions repliables, cartes de tool calls avec statuts et diffs, plan de l'agent, demandes de permission. Tout son état dérive des événements serveur pliés par un reducer pur, ce qui rend l'interface testable sans navigateur ni réseau.
16+- **`internal/httpserver`** — sert la SPA (embarquée dans le binaire via `go:embed`, si bien qu'Ori se distribue en un seul fichier) et monte les routes supplémentaires comme `/ws`.
17+- **`internal/bridge`** — le cœur du backend. Il traduit entre deux mondes asynchrones : l'agent streame ses updates et bloque sur ses demandes de permission, pendant que les navigateurs se connectent, se déconnectent et répondent à vitesse humaine. Il diffuse les événements, rejoue l'historique aux retardataires, et route chaque décision de permission vers l'appel JSON-RPC exact qui l'attend.
18+- **`internal/agent`** — lance le processus de l'agent et pilote le cycle de vie de la session ACP (initialize, session/new, prompt, cancel) via le client [`coder/acp-go-sdk`](https://github.com/coder/acp-go-sdk). Il est agnostique de l'agent : tout ce qui parle ACP sur stdio se branche.
19+- **`internal/mockagent` + `cmd/ori-mock-agent`** — un agent ACP déterministe utilisé par le test de bout en bout et par `make run-mock`, pour exercer toute l'application sans agent IA, sans compte et sans réseau.
20+- **`internal/skills`** — découvre les skills Claude Code (frontmatter des `SKILL.md`) dans le projet et le répertoire personnel, pour le sélecteur `/` du composer.
21+- **`internal/files`** — l'API fichiers du workspace (lister, chercher, lire, octets bruts, écrire) derrière l'arborescence, la preview et l'éditeur de la SPA. L'accès est volontairement non restreint : ori vise des sandboxes déjà isolées, et l'agent a de toute façon la même portée.
22+- **`internal/terminal`** — un vrai shell par connexion WebSocket, dans un pseudo-terminal (`creack/pty`), piloté par xterm.js dans le navigateur.
23+
24+## Mentions, skills et le prompt ACP
25+
26+Le sélecteur `@` du composer posait une question de conception : comment un fichier parvient-il à l'agent ? ACP a déjà la réponse — tout agent doit accepter des blocs de contenu `resource_link` dans un prompt — donc le navigateur envoie le texte plus une petite liste `attachments`, et le bridge convertit chacune en `resource_link` (URI `file://`) à côté du bloc texte. La mention `@chemin` reste visible dans le texte, si bien que la conversation se lit naturellement dans tous les clients et dans les rejeux, tandis que le lien structuré permet à l'agent d'ouvrir le fichier sans deviner. Le sélecteur `/` n'a demandé aucun protocole : Claude Code invoque un skill ou une commande slash à partir du texte `/nom …`, donc la liste ne fait qu'insérer du texte, en fusionnant les skills trouvés sur disque et les commandes que l'agent annonce via `available_commands_update`.
27+
28+## Previews : images et draw.io
29+
30+Les images sont diffusées par `GET /api/raw` et rendues par le navigateur lui-même, à la fois le moteur le plus simple et le plus capable disponible. Les diagrammes draw.io sont la seule preview qui s'écarte de la règle « embarqué, pas de CDN » fixée par Monaco : il n'existe pas de moteur de rendu hors ligne qu'ori pourrait livrer — le `viewer.min.js` de draw.io n'est pas publié sur npm, le paquet archivé `mxgraph` n'a pas la bibliothèque de formes de draw.io, et le seul convertisseur navigateur sur npm (`@markdown-viewer/drawio2svg`) est sous licence GPL-3.0 uniquement. Le panneau embarque donc `embed.diagrams.net` dans une iframe et lui transmet le XML par son protocole postMessage — le fichier ne quitte pas le navigateur — et se replie sur un avertissement plus la source XML quand l'hôte est injoignable, comme il peut l'être dans une sandbox isolée. Vendoriser `viewer-static.min.js` depuis le dépôt draw.io sous Apache-2.0 (environ 3 Mo) reste la voie vers un rendu hors ligne si ce compromis change un jour.
31+
32+## Le panneau workspace
33+
34+La SPA est passée d'une colonne de chat unique à chat-plus-workspace : une arborescence de fichiers et trois onglets (Preview, Editor, Terminal). Trois choix la structurent. Monaco sert à la fois la preview de code en lecture seule et l'éditeur — une seule grosse dépendance au lieu de deux piles de colorisation — et il est embarqué avec ses workers, rien ne se charge depuis un CDN ; il est chargé à la demande, ce qui garde le bundle initial léger. Le markdown se rend via le même composant que les messages du chat, et l'AsciiDoc via un Asciidoctor importé paresseusement. Enfin, les panneaux inactifs sont cachés plutôt que démontés : la session shell du terminal et le tampon non sauvé de l'éditeur survivent aux changements d'onglet et au repli du panneau. La colonne de l'arborescence est redimensionnable et sa largeur suit la même règle « le store possède l'état » que le reste du panneau : elle vit dans le store workspace, atteint la feuille de style par une propriété CSS personnalisée (`--filetree-width`) posée sur le panneau, et est persistée en localStorage — une commodité par navigateur, pas un état du serveur, qui reste donc volontairement hors du protocole WebSocket.
35+
36+Le thème de couleurs est un état de même nature. Le clair est le défaut ; la palette sombre est conditionnée par un attribut `data-theme="dark"` qu'un petit store zustand (`ui/src/theme.ts`) pose sur `<html>` et mémorise en localStorage, et Monaco lit ce store plutôt que la préférence de l'OS pour que l'éditeur ne contredise jamais la page. L'ancien comportement — suivre `prefers-color-scheme` — a été abandonné au profit d'un choix explicite, parce qu'un serveur Ori partagé est souvent consulté depuis des machines dont l'utilisateur ne maîtrise pas les réglages système (le navigateur d'une sandbox, le portable d'un collègue). Comme la largeur de l'arborescence, le thème est une commodité par navigateur : il vit hors du protocole WebSocket et le serveur n'en entend jamais parler.
37+
38+## Pourquoi le serveur possède la conversation
39+
40+Le bridge enregistre chaque événement de la session et le rejoue à chaque connexion ; il renvoie même les prompts de l'utilisateur sous forme d'événements `user_message`. Le serveur devient ainsi l'unique source de vérité : la SPA ne fait jamais confiance à sa mémoire locale, elle se remet à zéro à chaque `hello` et se reconstruit depuis le rejeu. Le bénéfice : les reconnexions (mise en veille, coupure réseau, second onglet) sont trivialement correctes — tous les navigateurs convergent vers le même fil — au prix d'un historique borné en mémoire (4096 événements).
41+
42+## Pourquoi les payloads ACP traversent le WebSocket tels quels
43+
44+Le bridge aurait pu traduire les updates ACP dans ses propres formes de messages. Il ne le fait délibérément pas : `session_update` et `permission_request` transportent les objets ACP bruts. Le protocole est bien documenté et il évolue (plans, modes, contenus d'outils plus riches) ; le relayer tel quel signifie que les nouveaux types d'update atteignent le front sans toucher au code Go, et que le front peut les adopter à son rythme — les types inconnus sont ignorés par construction.
45+
46+## Pourquoi un processus adaptateur pour Claude Code
47+
48+La CLI Claude Code n'a pas de mode ACP natif (vérifié sur la v2.1.274 : aucune option ni sous-commande de ce type). L'intégration de Zed lui-même passe par [`@zed-industries/claude-code-acp`](https://www.npmjs.com/package/@zed-industries/claude-code-acp), qui enveloppe le SDK Claude Code et expose ACP sur stdio. Ori lance exactement cet adaptateur par défaut.
49+
50+## Alternatives écartées
51+
52+- **Piloter directement le mode `stream-json` de la CLI `claude`** — un pont maison vers le format de streaming propriétaire de Claude aurait demandé plus de travail, lié Ori à un seul agent, et renoncé à l'écosystème des agents ACP (Gemini CLI, et ce qui viendra ensuite).
53+- **Server-sent events ou polling au lieu d'un WebSocket** — les demandes de permission exigent des messages dans les deux sens sur une même connexion ; le SSE aurait imposé un second canal pour les réponses.
54+- **Plusieurs sessions simultanées dès la v1** — le bridge pilote délibérément une seule session d'agent partagée par tous les navigateurs connectés. L'interface `Prompter` et le modèle d'abonnement par connexion sont les coutures où le multi-sessions pourra s'ajouter sans refondre la conception.
55+
56+## Comment les tests reflètent l'architecture
57+
58+Chaque couture a son double : le package agent est testé contre un agent ACP en mémoire (sans sous-processus), le bridge contre une session factice (sans agent), le reducer de la SPA contre de simples objets messages (sans socket), et un test de bout en bout compile le vrai binaire de l'agent mock et traverse le câblage de production complet via un vrai WebSocket. Voir [lancer les tests](../how-to/run-the-tests.md).
added docs/fr/how-to/mention-files-and-skills.md +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+# Mentionner des fichiers et invoquer des skills depuis le composer
2+
3+Ce guide montre comment désigner des fichiers du workspace à l'agent avec `@` et comment invoquer un skill ou une commande slash de Claude Code avec `/`, directement depuis le composer de messages. Il suppose qu'Ori tourne (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Mentionner un fichier avec `@`
6+
7+1. Dans le composer, tapez `@` en début de message ou après une espace. Une liste des fichiers du workspace apparaît ; continuez à taper pour filtrer — `@main` trouve `src/main.go` et `src/server/main_test.go`, et un fragment de nom de répertoire fonctionne aussi.
8+2. Déplacez-vous avec **↑ / ↓**, choisissez avec **Entrée** ou **Tab** (ou en cliquant). La mention devient `@src/main.go ` dans le texte.
9+3. Envoyez comme d'habitude. L'agent reçoit le texte plus un `resource_link` vers le fichier (`file:///…/src/main.go`), que l'adaptateur Claude Code transforme en référence de fichier que le modèle peut ouvrir.
10+
11+Si vous effacez une mention avant d'envoyer, sa pièce jointe disparaît aussi ; un `@` collé à un mot (`me@example.com`) n'ouvre jamais la liste. **Échap** ferme la liste pour la mention en cours.
12+
13+## Invoquer un skill ou une commande avec `/`
14+
15+1. Tapez `/` comme **premier** caractère du message. La liste présente, triés par nom, les skills trouvés dans `<cwd>/.claude/skills/` et `~/.claude/skills/` (étiquetés **skill**, avec la description de leur `SKILL.md`) et les commandes slash annoncées par l'agent via ACP (étiquetées **command**).
16+2. Filtrez par nom ou description, choisissez avec **Entrée** / **Tab** : le texte devient `/quality `. Ajoutez des arguments si le skill en prend, puis envoyez.
17+
18+Le skill est invoqué comme Claude Code l'invoque : c'est le texte `/quality …` qui part, il n'existe pas de méthode ACP séparée. Un `/` ailleurs dans le message est du texte ordinaire.
19+
20+## Variantes
21+
22+- Avec l'agent mock (`make run-mock`), la liste `/` montre deux commandes de démonstration (`review`, `compact`) et la réponse reprend chaque fichier joint sous la forme `[attached: <nom>]` — pratique pour vérifier la plomberie sans Claude.
23+- Les skills sont listés à la première ouverture de la liste ; ajoutez un skill sur disque et rechargez la page pour le voir.
24+
25+## Voir aussi
26+
27+- Format d'échange des `attachments` : [référence du protocole WebSocket](../reference/websocket-protocol.md)
28+- Les endpoints de recherche et de skills : [référence de l'API workspace](../reference/workspace-api.md)
new file mode 100644
@@ -0,0 +1,28 @@
1+# Mentionner des fichiers et invoquer des skills depuis le composer
2+
3+Ce guide montre comment désigner des fichiers du workspace à l'agent avec `@` et comment invoquer un skill ou une commande slash de Claude Code avec `/`, directement depuis le composer de messages. Il suppose qu'Ori tourne (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Mentionner un fichier avec `@`
6+
7+1. Dans le composer, tapez `@` en début de message ou après une espace. Une liste des fichiers du workspace apparaît ; continuez à taper pour filtrer — `@main` trouve `src/main.go` et `src/server/main_test.go`, et un fragment de nom de répertoire fonctionne aussi.
8+2. Déplacez-vous avec **↑ / ↓**, choisissez avec **Entrée** ou **Tab** (ou en cliquant). La mention devient `@src/main.go ` dans le texte.
9+3. Envoyez comme d'habitude. L'agent reçoit le texte plus un `resource_link` vers le fichier (`file:///…/src/main.go`), que l'adaptateur Claude Code transforme en référence de fichier que le modèle peut ouvrir.
10+
11+Si vous effacez une mention avant d'envoyer, sa pièce jointe disparaît aussi ; un `@` collé à un mot (`me@example.com`) n'ouvre jamais la liste. **Échap** ferme la liste pour la mention en cours.
12+
13+## Invoquer un skill ou une commande avec `/`
14+
15+1. Tapez `/` comme **premier** caractère du message. La liste présente, triés par nom, les skills trouvés dans `<cwd>/.claude/skills/` et `~/.claude/skills/` (étiquetés **skill**, avec la description de leur `SKILL.md`) et les commandes slash annoncées par l'agent via ACP (étiquetées **command**).
16+2. Filtrez par nom ou description, choisissez avec **Entrée** / **Tab** : le texte devient `/quality `. Ajoutez des arguments si le skill en prend, puis envoyez.
17+
18+Le skill est invoqué comme Claude Code l'invoque : c'est le texte `/quality …` qui part, il n'existe pas de méthode ACP séparée. Un `/` ailleurs dans le message est du texte ordinaire.
19+
20+## Variantes
21+
22+- Avec l'agent mock (`make run-mock`), la liste `/` montre deux commandes de démonstration (`review`, `compact`) et la réponse reprend chaque fichier joint sous la forme `[attached: <nom>]` — pratique pour vérifier la plomberie sans Claude.
23+- Les skills sont listés à la première ouverture de la liste ; ajoutez un skill sur disque et rechargez la page pour le voir.
24+
25+## Voir aussi
26+
27+- Format d'échange des `attachments` : [référence du protocole WebSocket](../reference/websocket-protocol.md)
28+- Les endpoints de recherche et de skills : [référence de l'API workspace](../reference/workspace-api.md)
added docs/fr/how-to/run-in-a-sandbox.md +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+# Lancer Ori dans une sandbox Docker
2+
3+Ce guide montre comment empaqueter Ori en template Docker Sandboxes et le lancer avec `sbx`, pour que toute la pile — Claude Code compris — vive dans une sandbox isolée accessible depuis votre navigateur. Il suppose Docker et la CLI `sbx` sur l'hôte.
4+
5+## Étapes
6+
7+1. Récupérez l'image du template — elle est publiée sur Docker Hub sous `k33g/ori:0.0.0`. Pour la (re)construire et la pousser vous-même, depuis la racine du dépôt :
8+
9+ ```bash
10+ ./template/build.sh
11+ ```
12+
13+ Le script exécute un `docker buildx build --push` multi-architectures : la SPA et les binaires Go sont compilés dans des étapes intermédiaires, puis posés avec l'adaptateur `claude-code-acp` au-dessus de l'image officielle `docker/sandbox-templates:claude-code`. Pour une image locale uniquement, utilisez plutôt `make template`.
14+
15+2. Créez la sandbox depuis votre répertoire de projet, avec le template et le kit ori, en mode détaché :
16+
17+ ```bash
18+ sbx run -d claude ~/chemin/vers/votre/projet \
19+ --template k33g/ori:0.0.0 \
20+ --kit /chemin/vers/ori/kits/ori
21+ ```
22+
23+ Le `-d` (`--detached`) est important : sbx arrête une sandbox 30 secondes après la fermeture de la dernière session CLI ouverte dessus, et le trafic du navigateur sur un port publié n'est pas une session. `sbx create` ne tient une session que le temps de son exécution, donc une sandbox créée ainsi passe en `stopped` une demi-minute plus tard. Une sandbox détachée échappe à cet arrêt automatique jusqu'à ce que vous fassiez `sbx stop` ou `sbx rm`. L'option n'existe que sur `sbx run`, et le mode est figé à la création.
24+
25+ La commande de démarrage du kit lance le serveur ori sur le port 8888 à chaque démarrage du conteneur ; le kit agent claude injecte les identifiants Anthropic via le proxy de la sandbox, donc aucune connexion n'est nécessaire à l'intérieur.
26+
27+3. Ouvrez l'interface : le kit déclare le port 8888, la sortie de la commande affiche donc l'adresse éphémère publiée — `Published web: localhost:<port> -> 8888/tcp`. Ouvrez-la, ou fixez un port stable avec `-p 8888:8888` à la création (ou après coup avec `sbx ports <nom-de-la-sandbox> --publish 8888:8888/tcp`).
28+
29+## Variantes
30+
31+- Le port d'écoute est un argument du kit (défaut 8888) : ajoutez `--kit-arg ori.port=9000` pour le changer (et publiez ce port-là à la place).
32+- Si l'image n'a jamais été poussée sur Docker Hub, confiez-la d'abord au magasin d'images du runtime des sandboxes : `docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
33+- Autonome, sans sbx : `docker run --rm -p 8888:8888 k33g/ori` (fournissez `ANTHROPIC_API_KEY`, ou utilisez l'agent de démonstration ci-dessous).
34+- Démo sans identifiants : dans la sandbox, `pkill -x ori` puis relancez avec `--agent-cmd ori-mock-agent`.
35+- Le log du serveur dans la sandbox est `/home/agent/.ori.log`.
36+- Une sandbox arrêtée redémarre avec `sbx run -d --name <nom-de-la-sandbox>` (ou `sbx attach`) ; le kit relance le serveur à chaque démarrage.
37+- Si une sandbox s'arrête quand même toute seule, cherchez des lignes `auto-stop` dans le journal du démon (`sbx daemon status` affiche son chemin) : elles signifient qu'elle n'a pas été créée détachée.
38+
39+## Voir aussi
40+
41+- Détails du kit et contenu du template : [`kits/ori/README.md`](../../../kits/ori/README.md)
42+- Options du serveur que le kit démarre : [référence : la commande ori](../reference/cli.md)
new file mode 100644
@@ -0,0 +1,42 @@
1+# Lancer Ori dans une sandbox Docker
2+
3+Ce guide montre comment empaqueter Ori en template Docker Sandboxes et le lancer avec `sbx`, pour que toute la pile — Claude Code compris — vive dans une sandbox isolée accessible depuis votre navigateur. Il suppose Docker et la CLI `sbx` sur l'hôte.
4+
5+## Étapes
6+
7+1. Récupérez l'image du template — elle est publiée sur Docker Hub sous `k33g/ori:0.0.0`. Pour la (re)construire et la pousser vous-même, depuis la racine du dépôt :
8+
9+ ```bash
10+ ./template/build.sh
11+ ```
12+
13+ Le script exécute un `docker buildx build --push` multi-architectures : la SPA et les binaires Go sont compilés dans des étapes intermédiaires, puis posés avec l'adaptateur `claude-code-acp` au-dessus de l'image officielle `docker/sandbox-templates:claude-code`. Pour une image locale uniquement, utilisez plutôt `make template`.
14+
15+2. Créez la sandbox depuis votre répertoire de projet, avec le template et le kit ori, en mode détaché :
16+
17+ ```bash
18+ sbx run -d claude ~/chemin/vers/votre/projet \
19+ --template k33g/ori:0.0.0 \
20+ --kit /chemin/vers/ori/kits/ori
21+ ```
22+
23+ Le `-d` (`--detached`) est important : sbx arrête une sandbox 30 secondes après la fermeture de la dernière session CLI ouverte dessus, et le trafic du navigateur sur un port publié n'est pas une session. `sbx create` ne tient une session que le temps de son exécution, donc une sandbox créée ainsi passe en `stopped` une demi-minute plus tard. Une sandbox détachée échappe à cet arrêt automatique jusqu'à ce que vous fassiez `sbx stop` ou `sbx rm`. L'option n'existe que sur `sbx run`, et le mode est figé à la création.
24+
25+ La commande de démarrage du kit lance le serveur ori sur le port 8888 à chaque démarrage du conteneur ; le kit agent claude injecte les identifiants Anthropic via le proxy de la sandbox, donc aucune connexion n'est nécessaire à l'intérieur.
26+
27+3. Ouvrez l'interface : le kit déclare le port 8888, la sortie de la commande affiche donc l'adresse éphémère publiée — `Published web: localhost:<port> -> 8888/tcp`. Ouvrez-la, ou fixez un port stable avec `-p 8888:8888` à la création (ou après coup avec `sbx ports <nom-de-la-sandbox> --publish 8888:8888/tcp`).
28+
29+## Variantes
30+
31+- Le port d'écoute est un argument du kit (défaut 8888) : ajoutez `--kit-arg ori.port=9000` pour le changer (et publiez ce port-là à la place).
32+- Si l'image n'a jamais été poussée sur Docker Hub, confiez-la d'abord au magasin d'images du runtime des sandboxes : `docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
33+- Autonome, sans sbx : `docker run --rm -p 8888:8888 k33g/ori` (fournissez `ANTHROPIC_API_KEY`, ou utilisez l'agent de démonstration ci-dessous).
34+- Démo sans identifiants : dans la sandbox, `pkill -x ori` puis relancez avec `--agent-cmd ori-mock-agent`.
35+- Le log du serveur dans la sandbox est `/home/agent/.ori.log`.
36+- Une sandbox arrêtée redémarre avec `sbx run -d --name <nom-de-la-sandbox>` (ou `sbx attach`) ; le kit relance le serveur à chaque démarrage.
37+- Si une sandbox s'arrête quand même toute seule, cherchez des lignes `auto-stop` dans le journal du démon (`sbx daemon status` affiche son chemin) : elles signifient qu'elle n'a pas été créée détachée.
38+
39+## Voir aussi
40+
41+- Détails du kit et contenu du template : [`kits/ori/README.md`](../../../kits/ori/README.md)
42+- Options du serveur que le kit démarre : [référence : la commande ori](../reference/cli.md)
added docs/fr/how-to/run-the-tests.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# Lancer les tests
2+
3+Ce guide montre comment lancer les suites de tests d'Ori. Il suppose que les dépendances du front sont installées (`make deps`).
4+
5+## Étapes
6+
7+1. Depuis la racine du dépôt, lancez tout :
8+
9+ ```bash
10+ make test
11+ ```
12+
13+ Cette commande exécute la suite Go (`go test ./...`) puis la suite front (`vitest run`). Les deux doivent finir au vert.
14+
15+## Variantes
16+
17+- Backend seul : `make test-go` — tests unitaires de chaque package plus un test de bout en bout qui compile l'agent mock, le lance comme vrai sous-processus et pilote un vrai client WebSocket sur un tour complet.
18+- Front seul : `make test-ui` — tests du reducer, du client WebSocket et des composants sous jsdom.
19+- Avec le détecteur de data races : `go test -race ./...`.
20+- En sautant le test de bout en bout (retour rapide) : `go test -short ./...`.
21+
22+## Voir aussi
23+
24+- Où vit chaque suite et ce qu'elle couvre : [explication : architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# Lancer les tests
2+
3+Ce guide montre comment lancer les suites de tests d'Ori. Il suppose que les dépendances du front sont installées (`make deps`).
4+
5+## Étapes
6+
7+1. Depuis la racine du dépôt, lancez tout :
8+
9+ ```bash
10+ make test
11+ ```
12+
13+ Cette commande exécute la suite Go (`go test ./...`) puis la suite front (`vitest run`). Les deux doivent finir au vert.
14+
15+## Variantes
16+
17+- Backend seul : `make test-go` — tests unitaires de chaque package plus un test de bout en bout qui compile l'agent mock, le lance comme vrai sous-processus et pilote un vrai client WebSocket sur un tour complet.
18+- Front seul : `make test-ui` — tests du reducer, du client WebSocket et des composants sous jsdom.
19+- Avec le détecteur de data races : `go test -race ./...`.
20+- En sautant le test de bout en bout (retour rapide) : `go test -short ./...`.
21+
22+## Voir aussi
23+
24+- Où vit chaque suite et ce qu'elle couvre : [explication : architecture](../explanation/architecture.md)
added docs/fr/how-to/run-with-claude-code.md +26 -0
new file mode 100644
@@ -0,0 +1,26 @@
1+# Lancer Ori avec Claude Code
2+
3+Ce guide montre comment démarrer Ori connecté à Claude Code. Il suppose que vous avez déjà compilé et lancé Ori une fois (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)) et que vous disposez d'un compte Claude ou d'une clé API opérationnels.
4+
5+## Étapes
6+
7+1. Vérifiez que Node.js et npm sont disponibles : le backend lance l'adaptateur ACP officiel de Claude Code avec `npx -y @zed-industries/claude-code-acp`.
8+2. Vérifiez que Claude Code est authentifié sur cette machine (procédure de connexion de `claude`, ou une variable d'environnement `ANTHROPIC_API_KEY` utilisable par l'adaptateur).
9+3. Depuis la racine du dépôt, démarrez le serveur :
10+
11+ ```bash
12+ make run
13+ ```
14+
15+4. Ouvrez [http://localhost:8888](http://localhost:8888) et commencez à dialoguer. Tool calls, plans et demandes de permission apparaissent exactement comme avec l'agent de démonstration.
16+
17+## Variantes
18+
19+- Pour travailler sur un répertoire de projet précis, passez explicitement le répertoire de travail de l'agent : `./bin/ori --cwd /chemin/vers/votre/projet`.
20+- Pour servir sur un autre port : `./bin/ori --addr :9000`.
21+- Le premier `npx` télécharge le paquet de l'adaptateur ; figez-le en l'installant globalement (`npm install -g @zed-industries/claude-code-acp`) et en passant `--agent-cmd "claude-code-acp"`.
22+
23+## Voir aussi
24+
25+- Référence des options : [la commande ori](../reference/cli.md)
26+- Brancher autre chose que Claude : [utiliser un autre agent ACP](use-another-agent.md)
new file mode 100644
@@ -0,0 +1,26 @@
1+# Lancer Ori avec Claude Code
2+
3+Ce guide montre comment démarrer Ori connecté à Claude Code. Il suppose que vous avez déjà compilé et lancé Ori une fois (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)) et que vous disposez d'un compte Claude ou d'une clé API opérationnels.
4+
5+## Étapes
6+
7+1. Vérifiez que Node.js et npm sont disponibles : le backend lance l'adaptateur ACP officiel de Claude Code avec `npx -y @zed-industries/claude-code-acp`.
8+2. Vérifiez que Claude Code est authentifié sur cette machine (procédure de connexion de `claude`, ou une variable d'environnement `ANTHROPIC_API_KEY` utilisable par l'adaptateur).
9+3. Depuis la racine du dépôt, démarrez le serveur :
10+
11+ ```bash
12+ make run
13+ ```
14+
15+4. Ouvrez [http://localhost:8888](http://localhost:8888) et commencez à dialoguer. Tool calls, plans et demandes de permission apparaissent exactement comme avec l'agent de démonstration.
16+
17+## Variantes
18+
19+- Pour travailler sur un répertoire de projet précis, passez explicitement le répertoire de travail de l'agent : `./bin/ori --cwd /chemin/vers/votre/projet`.
20+- Pour servir sur un autre port : `./bin/ori --addr :9000`.
21+- Le premier `npx` télécharge le paquet de l'adaptateur ; figez-le en l'installant globalement (`npm install -g @zed-industries/claude-code-acp`) et en passant `--agent-cmd "claude-code-acp"`.
22+
23+## Voir aussi
24+
25+- Référence des options : [la commande ori](../reference/cli.md)
26+- Brancher autre chose que Claude : [utiliser un autre agent ACP](use-another-agent.md)
added docs/fr/how-to/switch-the-theme.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Basculer entre les thèmes clair et sombre
2+
3+Ce guide montre comment changer le thème de couleurs d'Ori. Il suppose qu'Ori est ouvert dans votre navigateur (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Étapes
6+
7+1. Regardez à droite de l'entête, à côté de **⊞ Workspace** : le bouton **☾** passe au thème sombre ; une fois en sombre, il devient **☀** et revient au clair.
8+2. Cliquez dessus. Toute la page suit — conversation, panneau workspace, prévisualisations de code et éditeur (Monaco bascule entre ses palettes `light` et `vs-dark`).
9+
10+## Variantes
11+
12+- Le clair est le thème par défaut. Ori ne suit plus la préférence clair/sombre du système d'exploitation : le thème est celui choisi en dernier dans ce navigateur.
13+- Le choix est mémorisé par navigateur (clé localStorage `ori.theme`, valeurs `light` ou `dark`) et appliqué avant le premier rendu au chargement suivant. Effacer les données du site rétablit le clair.
14+- L'onglet terminal garde un fond sombre dans les deux thèmes, comme le terminal intégré d'un IDE.
15+- Plusieurs navigateurs connectés au même serveur Ori gardent chacun leur thème : c'est une préférence du navigateur, pas un élément de la session partagée.
16+
17+## Voir aussi
18+
19+- Pourquoi le thème est un état du navigateur et non du serveur : [explication : architecture](../explanation/architecture.md)
20+- Les autres commodités du workspace : [Utiliser le panneau workspace](use-the-workspace.md)
new file mode 100644
@@ -0,0 +1,20 @@
1+# Basculer entre les thèmes clair et sombre
2+
3+Ce guide montre comment changer le thème de couleurs d'Ori. Il suppose qu'Ori est ouvert dans votre navigateur (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Étapes
6+
7+1. Regardez à droite de l'entête, à côté de **⊞ Workspace** : le bouton **☾** passe au thème sombre ; une fois en sombre, il devient **☀** et revient au clair.
8+2. Cliquez dessus. Toute la page suit — conversation, panneau workspace, prévisualisations de code et éditeur (Monaco bascule entre ses palettes `light` et `vs-dark`).
9+
10+## Variantes
11+
12+- Le clair est le thème par défaut. Ori ne suit plus la préférence clair/sombre du système d'exploitation : le thème est celui choisi en dernier dans ce navigateur.
13+- Le choix est mémorisé par navigateur (clé localStorage `ori.theme`, valeurs `light` ou `dark`) et appliqué avant le premier rendu au chargement suivant. Effacer les données du site rétablit le clair.
14+- L'onglet terminal garde un fond sombre dans les deux thèmes, comme le terminal intégré d'un IDE.
15+- Plusieurs navigateurs connectés au même serveur Ori gardent chacun leur thème : c'est une préférence du navigateur, pas un élément de la session partagée.
16+
17+## Voir aussi
18+
19+- Pourquoi le thème est un état du navigateur et non du serveur : [explication : architecture](../explanation/architecture.md)
20+- Les autres commodités du workspace : [Utiliser le panneau workspace](use-the-workspace.md)
added docs/fr/how-to/use-another-agent.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# Utiliser un autre agent ACP
2+
3+Ce guide montre comment connecter Ori à n'importe quel agent qui parle ACP sur stdio. Il suppose que vous savez démarrer Ori (voir [lancer avec Claude Code](run-with-claude-code.md)).
4+
5+## Étapes
6+
7+1. Identifiez la commande qui démarre votre agent en mode ACP. L'agent doit lire du JSON-RPC sur stdin et l'écrire sur stdout.
8+2. Passez cette commande à Ori avec `--agent-cmd` :
9+
10+ ```bash
11+ ./bin/ori --agent-cmd "gemini --experimental-acp"
12+ ```
13+
14+3. Ouvrez le panel : l'entête `hello` affiche l'identifiant de session renvoyé par l'agent.
15+
16+## Variantes
17+
18+- L'agent de démonstration intégré n'est lui-même qu'un agent ACP parmi d'autres : `./bin/ori --agent-cmd "bin/ori-mock-agent"` (c'est ce que fait `make run-mock`).
19+- La commande est découpée sur les espaces : le premier mot est l'exécutable, le reste ses arguments. Les arguments contenant des espaces ne sont pas pris en charge — enveloppez une telle commande dans un petit script shell et passez le script à la place.
20+
21+## Voir aussi
22+
23+- Référence des options : [la commande ori](../reference/cli.md)
24+- Ce qu'Ori attend de l'agent : [explication : architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# Utiliser un autre agent ACP
2+
3+Ce guide montre comment connecter Ori à n'importe quel agent qui parle ACP sur stdio. Il suppose que vous savez démarrer Ori (voir [lancer avec Claude Code](run-with-claude-code.md)).
4+
5+## Étapes
6+
7+1. Identifiez la commande qui démarre votre agent en mode ACP. L'agent doit lire du JSON-RPC sur stdin et l'écrire sur stdout.
8+2. Passez cette commande à Ori avec `--agent-cmd` :
9+
10+ ```bash
11+ ./bin/ori --agent-cmd "gemini --experimental-acp"
12+ ```
13+
14+3. Ouvrez le panel : l'entête `hello` affiche l'identifiant de session renvoyé par l'agent.
15+
16+## Variantes
17+
18+- L'agent de démonstration intégré n'est lui-même qu'un agent ACP parmi d'autres : `./bin/ori --agent-cmd "bin/ori-mock-agent"` (c'est ce que fait `make run-mock`).
19+- La commande est découpée sur les espaces : le premier mot est l'exécutable, le reste ses arguments. Les arguments contenant des espaces ne sont pas pris en charge — enveloppez une telle commande dans un petit script shell et passez le script à la place.
20+
21+## Voir aussi
22+
23+- Référence des options : [la commande ori](../reference/cli.md)
24+- Ce qu'Ori attend de l'agent : [explication : architecture](../explanation/architecture.md)
added docs/fr/how-to/use-the-workspace.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# Utiliser le panneau workspace
2+
3+Ce guide montre comment parcourir, prévisualiser, éditer des fichiers et lancer des commandes à côté de la conversation. Il suppose qu'Ori tourne (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Étapes
6+
7+1. Cliquez sur **⊞ Workspace** dans l'entête. Le panneau droit s'ouvre avec l'arborescence du répertoire de travail de l'agent (`--cwd`).
8+2. **Prévisualiser un fichier** : cliquez dessus dans l'arborescence. Le code s'affiche avec colorisation syntaxique ; les fichiers `.md` et `.adoc` s'ouvrent en version rendue, avec une bascule **Rendered / Source** dans l'entête du panneau. Les images (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`, `.ico`, `.avif` — y compris les exports draw.io `.drawio.svg` / `.drawio.png`) s'affichent à leur taille naturelle sur un damier, avec leurs dimensions en pixels en dessous. Les diagrammes enregistrés en XML `.drawio` / `.dio` se rendent dans le viewer diagrams.net, le XML restant accessible par **Source**.
9+3. **Éditer un fichier** : double-cliquez dessus dans l'arborescence (ou bouton **Edit** de la preview). Un **●** signale les modifications non sauvées ; sauvegardez avec le bouton **Save** ou **Ctrl/Cmd+S**.
10+4. **Ouvrir un terminal** : sélectionnez l'onglet **Terminal**. Un vrai shell démarre dans le répertoire de travail ; il continue de tourner quand vous changez d'onglet ou repliez le panneau.
11+5. **Redimensionner l'arborescence** : faites glisser le fin trait vertical entre l'arborescence et les onglets. Il se colore au survol ; la colonne suit le pointeur entre 120 px et 800 px. Un double-clic sur le trait revient à la largeur par défaut (14rem).
12+
13+## Variantes
14+
15+- Le bouton **⟳** de l'arborescence relit le répertoire de travail après que l'agent a créé ou supprimé des fichiers.
16+- Les fichiers binaires et les fichiers de plus de 5 Mio sont refusés par la preview et l'éditeur avec un message explicite ; les images font exception, diffusées par `GET /api/raw` sans plafond.
17+- Le viewer draw.io est `embed.diagrams.net`, chargé dans une iframe et alimenté localement en XML par son protocole postMessage (le diagramme n'est envoyé nulle part). Dans un navigateur qui ne peut pas joindre cet hôte, le panneau le signale après quelques secondes et la vue **Source** reste disponible. Voir l'[explication de l'architecture](../explanation/architecture.md) pour l'absence de rendu hors ligne.
18+- Le shell vient de `$SHELL` (repli sur bash, puis sh) ; le redimensionnement est automatique.
19+- Le séparateur de l'arborescence se manipule aussi au clavier : donnez-lui le focus avec **Tab**, puis **←** / **→** le déplacent de 16 px, **Début** / **Fin** sautent à la largeur minimale / maximale. La largeur choisie est mémorisée par navigateur (clé localStorage `ori.fileTreeWidth`) et survit au rechargement de la page ; effacer les données du site rétablit la valeur par défaut.
20+
21+## Voir aussi
22+
23+- Endpoints et formats d'échange : [référence de l'API workspace](../reference/workspace-api.md)
24+- Pourquoi les panneaux survivent aux changements d'onglet : [explication : architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# Utiliser le panneau workspace
2+
3+Ce guide montre comment parcourir, prévisualiser, éditer des fichiers et lancer des commandes à côté de la conversation. Il suppose qu'Ori tourne (voir le [tutoriel de premiers pas](../tutorials/getting-started.md)).
4+
5+## Étapes
6+
7+1. Cliquez sur **⊞ Workspace** dans l'entête. Le panneau droit s'ouvre avec l'arborescence du répertoire de travail de l'agent (`--cwd`).
8+2. **Prévisualiser un fichier** : cliquez dessus dans l'arborescence. Le code s'affiche avec colorisation syntaxique ; les fichiers `.md` et `.adoc` s'ouvrent en version rendue, avec une bascule **Rendered / Source** dans l'entête du panneau. Les images (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`, `.ico`, `.avif` — y compris les exports draw.io `.drawio.svg` / `.drawio.png`) s'affichent à leur taille naturelle sur un damier, avec leurs dimensions en pixels en dessous. Les diagrammes enregistrés en XML `.drawio` / `.dio` se rendent dans le viewer diagrams.net, le XML restant accessible par **Source**.
9+3. **Éditer un fichier** : double-cliquez dessus dans l'arborescence (ou bouton **Edit** de la preview). Un **●** signale les modifications non sauvées ; sauvegardez avec le bouton **Save** ou **Ctrl/Cmd+S**.
10+4. **Ouvrir un terminal** : sélectionnez l'onglet **Terminal**. Un vrai shell démarre dans le répertoire de travail ; il continue de tourner quand vous changez d'onglet ou repliez le panneau.
11+5. **Redimensionner l'arborescence** : faites glisser le fin trait vertical entre l'arborescence et les onglets. Il se colore au survol ; la colonne suit le pointeur entre 120 px et 800 px. Un double-clic sur le trait revient à la largeur par défaut (14rem).
12+
13+## Variantes
14+
15+- Le bouton **⟳** de l'arborescence relit le répertoire de travail après que l'agent a créé ou supprimé des fichiers.
16+- Les fichiers binaires et les fichiers de plus de 5 Mio sont refusés par la preview et l'éditeur avec un message explicite ; les images font exception, diffusées par `GET /api/raw` sans plafond.
17+- Le viewer draw.io est `embed.diagrams.net`, chargé dans une iframe et alimenté localement en XML par son protocole postMessage (le diagramme n'est envoyé nulle part). Dans un navigateur qui ne peut pas joindre cet hôte, le panneau le signale après quelques secondes et la vue **Source** reste disponible. Voir l'[explication de l'architecture](../explanation/architecture.md) pour l'absence de rendu hors ligne.
18+- Le shell vient de `$SHELL` (repli sur bash, puis sh) ; le redimensionnement est automatique.
19+- Le séparateur de l'arborescence se manipule aussi au clavier : donnez-lui le focus avec **Tab**, puis **←** / **→** le déplacent de 16 px, **Début** / **Fin** sautent à la largeur minimale / maximale. La largeur choisie est mémorisée par navigateur (clé localStorage `ori.fileTreeWidth`) et survit au rechargement de la page ; effacer les données du site rétablit la valeur par défaut.
20+
21+## Voir aussi
22+
23+- Endpoints et formats d'échange : [référence de l'API workspace](../reference/workspace-api.md)
24+- Pourquoi les panneaux survivent aux changements d'onglet : [explication : architecture](../explanation/architecture.md)
added docs/fr/reference/cli.md +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+# Référence : la commande ori
2+
3+> Description neutre et exhaustive du binaire serveur `ori` et de ses options.
4+
5+## Synopsis
6+
7+```bash
8+ori [--addr ADDR] [--cwd DIR] [--agent-cmd COMMANDE]
9+```
10+
11+Au démarrage, `ori` lance la commande de l'agent, effectue la poignée de main ACP (`initialize`, puis `session/new`), puis sert l'application web et son endpoint WebSocket. Si l'agent ne démarre pas ou si la poignée de main échoue, `ori` se termine avec un statut non nul et une erreur sur stderr.
12+
13+## Options
14+
15+| Option | Type | Défaut | Description |
16+| --- | --- | --- | --- |
17+| `--addr` | chaîne | `:8888` | Adresse TCP d'écoute du serveur HTTP. Un hôte vide écoute sur toutes les interfaces. |
18+| `--cwd` | chaîne | répertoire courant | Répertoire de travail transmis à la session de l'agent. Doit exister et être un répertoire ; résolu en chemin absolu. |
19+| `--agent-cmd` | chaîne | `npx -y @zed-industries/claude-code-acp` | Commande lançant l'agent ACP, découpée sur les espaces (premier champ : exécutable ; le reste : arguments). |
20+
21+## Endpoints HTTP
22+
23+| Route | Description |
24+| --- | --- |
25+| `GET /healthz` | Renvoie `{"status":"ok"}` avec le statut 200. |
26+| `GET /ws` | Endpoint WebSocket du panel ; voir la [référence du protocole WebSocket](websocket-protocol.md). |
27+| `GET /*` | La SPA embarquée ; les chemins inconnus retombent sur `index.html`. Renvoie 503 si le binaire a été compilé sans build de l'interface. |
28+
29+## Exemple
30+
31+```bash
32+./bin/ori --addr :9000 --cwd /travail/mon-projet --agent-cmd "bin/ori-mock-agent"
33+```
34+
35+## Erreurs
36+
37+| Erreur | Cause |
38+| --- | --- |
39+| `--agent-cmd must not be empty` | L'option ne contient que des blancs. |
40+| `--cwd: … no such file or directory` | Le répertoire de travail n'existe pas. |
41+| `--cwd: … is not a directory` | Le chemin existe mais est un fichier ordinaire. |
42+| `start agent "…": …` | L'exécutable de l'agent n'a pas pu être lancé. |
43+| `initialize: …` / `new session: …` | La poignée de main ACP avec l'agent a échoué. |
44+
45+## Binaire compagnon : ori-mock-agent
46+
47+```bash
48+ori-mock-agent [--delay DURÉE]
49+```
50+
51+| Option | Type | Défaut | Description |
52+| --- | --- | --- | --- |
53+| `--delay` | durée | `300ms` | Pause entre les updates streamées. `0s` désactive la cadence. |
54+
55+`ori-mock-agent` parle ACP sur stdio et joue un scénario déterministe par prompt : une réflexion, un plan à deux entrées, un tool call de lecture, une demande de permission, et — si elle est accordée — un tool call d'édition avec un diff et un message final.
new file mode 100644
@@ -0,0 +1,55 @@
1+# Référence : la commande ori
2+
3+> Description neutre et exhaustive du binaire serveur `ori` et de ses options.
4+
5+## Synopsis
6+
7+```bash
8+ori [--addr ADDR] [--cwd DIR] [--agent-cmd COMMANDE]
9+```
10+
11+Au démarrage, `ori` lance la commande de l'agent, effectue la poignée de main ACP (`initialize`, puis `session/new`), puis sert l'application web et son endpoint WebSocket. Si l'agent ne démarre pas ou si la poignée de main échoue, `ori` se termine avec un statut non nul et une erreur sur stderr.
12+
13+## Options
14+
15+| Option | Type | Défaut | Description |
16+| --- | --- | --- | --- |
17+| `--addr` | chaîne | `:8888` | Adresse TCP d'écoute du serveur HTTP. Un hôte vide écoute sur toutes les interfaces. |
18+| `--cwd` | chaîne | répertoire courant | Répertoire de travail transmis à la session de l'agent. Doit exister et être un répertoire ; résolu en chemin absolu. |
19+| `--agent-cmd` | chaîne | `npx -y @zed-industries/claude-code-acp` | Commande lançant l'agent ACP, découpée sur les espaces (premier champ : exécutable ; le reste : arguments). |
20+
21+## Endpoints HTTP
22+
23+| Route | Description |
24+| --- | --- |
25+| `GET /healthz` | Renvoie `{"status":"ok"}` avec le statut 200. |
26+| `GET /ws` | Endpoint WebSocket du panel ; voir la [référence du protocole WebSocket](websocket-protocol.md). |
27+| `GET /*` | La SPA embarquée ; les chemins inconnus retombent sur `index.html`. Renvoie 503 si le binaire a été compilé sans build de l'interface. |
28+
29+## Exemple
30+
31+```bash
32+./bin/ori --addr :9000 --cwd /travail/mon-projet --agent-cmd "bin/ori-mock-agent"
33+```
34+
35+## Erreurs
36+
37+| Erreur | Cause |
38+| --- | --- |
39+| `--agent-cmd must not be empty` | L'option ne contient que des blancs. |
40+| `--cwd: … no such file or directory` | Le répertoire de travail n'existe pas. |
41+| `--cwd: … is not a directory` | Le chemin existe mais est un fichier ordinaire. |
42+| `start agent "…": …` | L'exécutable de l'agent n'a pas pu être lancé. |
43+| `initialize: …` / `new session: …` | La poignée de main ACP avec l'agent a échoué. |
44+
45+## Binaire compagnon : ori-mock-agent
46+
47+```bash
48+ori-mock-agent [--delay DURÉE]
49+```
50+
51+| Option | Type | Défaut | Description |
52+| --- | --- | --- | --- |
53+| `--delay` | durée | `300ms` | Pause entre les updates streamées. `0s` désactive la cadence. |
54+
55+`ori-mock-agent` parle ACP sur stdio et joue un scénario déterministe par prompt : une réflexion, un plan à deux entrées, un tool call de lecture, une demande de permission, et — si elle est accordée — un tool call d'édition avec un diff et un message final.
added docs/fr/reference/websocket-protocol.md +103 -0
new file mode 100644
@@ -0,0 +1,103 @@
1+# Référence : protocole WebSocket
2+
3+> Description neutre et exhaustive des messages échangés entre le navigateur et le backend ori sur `GET /ws`. Un objet JSON par trame texte. Les payloads ACP sont relayés tels quels ; leurs formes sont spécifiées par l'[Agent Client Protocol](https://agentclientprotocol.com).
4+
5+## Séquence de connexion
6+
7+À chaque connexion, le serveur envoie `hello`, puis rejoue l'historique enregistré de la session (jusqu'à 4096 événements), puis toute demande de permission encore en attente de réponse, puis les événements au fil de l'eau.
8+
9+## Messages navigateur → serveur
10+
11+### `prompt`
12+
13+Démarre un tour.
14+
15+| Champ | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `type` | chaîne | oui | `"prompt"` |
18+| `text` | chaîne | oui | Le message de l'utilisateur. |
19+| `attachments` | tableau | non | Fichiers mentionnés avec `@` dans le texte : `{ "path": "<chemin absolu ou relatif à la racine>", "name": "<libellé>" }`. |
20+
21+```json
22+{ "type": "prompt", "text": "Explique @src/main.go", "attachments": [ { "path": "/travail/src/main.go", "name": "src/main.go" } ] }
23+```
24+
25+Le serveur envoie le prompt ACP sous forme d'un bloc de contenu `text` suivi d'un bloc `resource_link` par pièce jointe (`uri: "file://<chemin absolu>"`, `name` tel que fourni, par défaut le chemin). Les chemins relatifs sont joints au `--cwd` du serveur ; les pièces jointes au `path` vide sont écartées. La mention `@name` reste dans le texte.
26+
27+### `cancel`
28+
29+Interrompt le tour en cours.
30+
31+| Champ | Type | Requis | Description |
32+| --- | --- | --- | --- |
33+| `type` | chaîne | oui | `"cancel"` |
34+
35+### `permission_response`
36+
37+Répond à une `permission_request`.
38+
39+| Champ | Type | Requis | Description |
40+| --- | --- | --- | --- |
41+| `type` | chaîne | oui | `"permission_response"` |
42+| `requestId` | chaîne | oui | L'identifiant reçu dans l'événement `permission_request`. |
43+| `optionId` | chaîne | l'un des deux | L'option de permission ACP choisie. |
44+| `cancelled` | booléen | l'un des deux | `true` écarte la demande sans choisir. |
45+
46+## Messages serveur → navigateur
47+
48+### `hello`
49+
50+| Champ | Type | Description |
51+| --- | --- | --- |
52+| `sessionId` | chaîne | L'identifiant de session ACP, vide si aucun agent n'est attaché. |
53+| `turnActive` | booléen | `true` si un tour est en cours. |
54+
55+### `user_message`
56+
57+Écho du prompt qui a démarré un tour ; présent aussi dans les rejeux.
58+
59+| Champ | Type | Description |
60+| --- | --- | --- |
61+| `text` | chaîne | Le message de l'utilisateur. |
62+| `attachments` | tableau | Les `attachments` du prompt, tels quels (absent s'il n'y en avait pas). |
63+
64+### `turn_started` / `turn_ended`
65+
66+| Champ | Type | Description |
67+| --- | --- | --- |
68+| `stopReason` | chaîne | `turn_ended` uniquement : la raison d'arrêt ACP (`end_turn`, `cancelled`, `refusal`, `max_tokens`, `max_turn_requests`). |
69+
70+### `session_update`
71+
72+| Champ | Type | Description |
73+| --- | --- | --- |
74+| `update` | objet | Une `SessionUpdate` ACP brute, discriminée par son champ `sessionUpdate` (`agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, `plan`, `available_commands_update`, …). |
75+
76+La SPA rend les fragments de message et de réflexion, les tool calls et le plan, et conserve les `availableCommands` du dernier `available_commands_update` pour le sélecteur `/` du composer (l'agent mock en envoie un à l'ouverture de session ; l'adaptateur Claude Code envoie les commandes slash de Claude Code). Les autres genres sont ignorés.
77+
78+```json
79+{ "type": "session_update", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "Bonjour" } } }
80+```
81+
82+### `permission_request`
83+
84+| Champ | Type | Description |
85+| --- | --- | --- |
86+| `requestId` | chaîne | Corrèle avec `permission_response` et `permission_resolved`. |
87+| `request` | objet | La `RequestPermissionRequest` ACP brute (`toolCall`, `options`). |
88+
89+### `permission_resolved`
90+
91+| Champ | Type | Description |
92+| --- | --- | --- |
93+| `requestId` | chaîne | La demande qui a reçu une réponse (d'un des clients connectés, ou annulée par l'agent). |
94+
95+### `error`
96+
97+| Champ | Type | Description |
98+| --- | --- | --- |
99+| `message` | chaîne | Erreur lisible (`"a turn is already running"`, `"no agent session"`, `"prompt failed: …"`, `"cancel failed: …"`, `"malformed message: …"`, `"unknown message type …"`). |
100+
101+## Événements enregistrés vs transitoires
102+
103+Événements rejoués aux clients qui arrivent en cours de session : `user_message`, `turn_started`, `turn_ended`, `session_update`, `permission_resolved`, plus les `permission_request` en attente. Non enregistrés : `hello` (régénéré à chaque connexion) et les événements `error` émis hors échec d'un tour.
new file mode 100644
@@ -0,0 +1,103 @@
1+# Référence : protocole WebSocket
2+
3+> Description neutre et exhaustive des messages échangés entre le navigateur et le backend ori sur `GET /ws`. Un objet JSON par trame texte. Les payloads ACP sont relayés tels quels ; leurs formes sont spécifiées par l'[Agent Client Protocol](https://agentclientprotocol.com).
4+
5+## Séquence de connexion
6+
7+À chaque connexion, le serveur envoie `hello`, puis rejoue l'historique enregistré de la session (jusqu'à 4096 événements), puis toute demande de permission encore en attente de réponse, puis les événements au fil de l'eau.
8+
9+## Messages navigateur → serveur
10+
11+### `prompt`
12+
13+Démarre un tour.
14+
15+| Champ | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `type` | chaîne | oui | `"prompt"` |
18+| `text` | chaîne | oui | Le message de l'utilisateur. |
19+| `attachments` | tableau | non | Fichiers mentionnés avec `@` dans le texte : `{ "path": "<chemin absolu ou relatif à la racine>", "name": "<libellé>" }`. |
20+
21+```json
22+{ "type": "prompt", "text": "Explique @src/main.go", "attachments": [ { "path": "/travail/src/main.go", "name": "src/main.go" } ] }
23+```
24+
25+Le serveur envoie le prompt ACP sous forme d'un bloc de contenu `text` suivi d'un bloc `resource_link` par pièce jointe (`uri: "file://<chemin absolu>"`, `name` tel que fourni, par défaut le chemin). Les chemins relatifs sont joints au `--cwd` du serveur ; les pièces jointes au `path` vide sont écartées. La mention `@name` reste dans le texte.
26+
27+### `cancel`
28+
29+Interrompt le tour en cours.
30+
31+| Champ | Type | Requis | Description |
32+| --- | --- | --- | --- |
33+| `type` | chaîne | oui | `"cancel"` |
34+
35+### `permission_response`
36+
37+Répond à une `permission_request`.
38+
39+| Champ | Type | Requis | Description |
40+| --- | --- | --- | --- |
41+| `type` | chaîne | oui | `"permission_response"` |
42+| `requestId` | chaîne | oui | L'identifiant reçu dans l'événement `permission_request`. |
43+| `optionId` | chaîne | l'un des deux | L'option de permission ACP choisie. |
44+| `cancelled` | booléen | l'un des deux | `true` écarte la demande sans choisir. |
45+
46+## Messages serveur → navigateur
47+
48+### `hello`
49+
50+| Champ | Type | Description |
51+| --- | --- | --- |
52+| `sessionId` | chaîne | L'identifiant de session ACP, vide si aucun agent n'est attaché. |
53+| `turnActive` | booléen | `true` si un tour est en cours. |
54+
55+### `user_message`
56+
57+Écho du prompt qui a démarré un tour ; présent aussi dans les rejeux.
58+
59+| Champ | Type | Description |
60+| --- | --- | --- |
61+| `text` | chaîne | Le message de l'utilisateur. |
62+| `attachments` | tableau | Les `attachments` du prompt, tels quels (absent s'il n'y en avait pas). |
63+
64+### `turn_started` / `turn_ended`
65+
66+| Champ | Type | Description |
67+| --- | --- | --- |
68+| `stopReason` | chaîne | `turn_ended` uniquement : la raison d'arrêt ACP (`end_turn`, `cancelled`, `refusal`, `max_tokens`, `max_turn_requests`). |
69+
70+### `session_update`
71+
72+| Champ | Type | Description |
73+| --- | --- | --- |
74+| `update` | objet | Une `SessionUpdate` ACP brute, discriminée par son champ `sessionUpdate` (`agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, `plan`, `available_commands_update`, …). |
75+
76+La SPA rend les fragments de message et de réflexion, les tool calls et le plan, et conserve les `availableCommands` du dernier `available_commands_update` pour le sélecteur `/` du composer (l'agent mock en envoie un à l'ouverture de session ; l'adaptateur Claude Code envoie les commandes slash de Claude Code). Les autres genres sont ignorés.
77+
78+```json
79+{ "type": "session_update", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "Bonjour" } } }
80+```
81+
82+### `permission_request`
83+
84+| Champ | Type | Description |
85+| --- | --- | --- |
86+| `requestId` | chaîne | Corrèle avec `permission_response` et `permission_resolved`. |
87+| `request` | objet | La `RequestPermissionRequest` ACP brute (`toolCall`, `options`). |
88+
89+### `permission_resolved`
90+
91+| Champ | Type | Description |
92+| --- | --- | --- |
93+| `requestId` | chaîne | La demande qui a reçu une réponse (d'un des clients connectés, ou annulée par l'agent). |
94+
95+### `error`
96+
97+| Champ | Type | Description |
98+| --- | --- | --- |
99+| `message` | chaîne | Erreur lisible (`"a turn is already running"`, `"no agent session"`, `"prompt failed: …"`, `"cancel failed: …"`, `"malformed message: …"`, `"unknown message type …"`). |
100+
101+## Événements enregistrés vs transitoires
102+
103+Événements rejoués aux clients qui arrivent en cours de session : `user_message`, `turn_started`, `turn_ended`, `session_update`, `permission_resolved`, plus les `permission_request` en attente. Non enregistrés : `hello` (régénéré à chaque connexion) et les événements `error` émis hors échec d'un tour.
added docs/fr/reference/workspace-api.md +108 -0
new file mode 100644
@@ -0,0 +1,108 @@
1+# Référence : API workspace
2+
3+> Description neutre et exhaustive des endpoints derrière le panneau workspace et les sélecteurs du composer : l'API fichiers, l'endpoint des skills et le WebSocket du terminal. Les chemins relatifs se résolvent contre le `--cwd` du serveur ; les chemins absolus sont utilisés tels quels (accès volontairement non restreint — ori vise des sandboxes déjà isolées).
4+
5+## GET /api/files
6+
7+Liste un répertoire.
8+
9+| Paramètre de requête | Type | Défaut | Description |
10+| --- | --- | --- | --- |
11+| `path` | chaîne | la racine du workspace | Répertoire à lister. |
12+
13+Réponse `200` :
14+
15+```json
16+{ "path": "/travail", "entries": [ { "name": "src", "path": "/travail/src", "isDir": true, "size": 0 } ] }
17+```
18+
19+Les entrées sont triées répertoires d'abord, puis fichiers, chaque groupe par ordre alphabétique.
20+
21+## GET /api/files/search
22+
23+Recherche des fichiers récursivement sous la racine du workspace, pour le sélecteur `@` du composer.
24+
25+| Paramètre de requête | Type | Défaut | Description |
26+| --- | --- | --- | --- |
27+| `q` | chaîne | `""` | Sous-chaîne, insensible à la casse, cherchée dans le chemin relatif à la racine. Vide renvoie les premiers fichiers. |
28+| `limit` | entier | 50 | Nombre maximal de résultats ; plafonné à 500. |
29+
30+Réponse `200` :
31+
32+```json
33+{ "root": "/travail", "files": [ { "name": "main.go", "path": "/travail/src/main.go", "relPath": "src/main.go" } ] }
34+```
35+
36+`path` est absolu (utilisable dans les autres endpoints), `relPath` utilise des barres obliques. Les résultats sont classés : d'abord les fichiers dont le nom de base contient la requête, puis les chemins relatifs les plus courts, puis l'ordre alphabétique. Le parcours n'entre jamais dans `.git` ni `node_modules`, ignore les entrées illisibles et s'arrête après 50 000 entrées de répertoire. `files` est toujours un tableau (`[]` sans correspondance). Une racine absente donne un `404`.
37+
38+## GET /api/file
39+
40+Lit un fichier texte.
41+
42+| Paramètre de requête | Type | Défaut | Description |
43+| --- | --- | --- | --- |
44+| `path` | chaîne | — | Fichier à lire. |
45+
46+Réponse `200` : `{ "path": "/travail/README.md", "content": "…" }`
47+
48+## PUT /api/file
49+
50+Écrit un fichier texte, en créant les répertoires parents si besoin.
51+
52+| Paramètre de requête | Type | Défaut | Description |
53+| --- | --- | --- | --- |
54+| `path` | chaîne | — (requis) | Fichier à écrire. |
55+
56+Corps : `{ "content": "…" }` — réponse `200` : `{ "path": "…", "status": "saved" }`
57+
58+## GET /api/raw
59+
60+Diffuse les octets d'un fichier tels quels — c'est ainsi que la preview affiche les images. Pas de plafond de taille, pas d'exigence de texte ; les requêtes `Range` sont honorées (`http.ServeContent`).
61+
62+| Paramètre de requête | Type | Défaut | Description |
63+| --- | --- | --- | --- |
64+| `path` | chaîne | — (requis) | Fichier à diffuser. |
65+
66+Réponse `200` avec les octets du fichier, `X-Content-Type-Options: nosniff`, et un `Content-Type` choisi dans cet ordre :
67+
68+| Extension | Content-Type |
69+| --- | --- |
70+| `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` `.bmp` `.ico` `.avif` | types image figés (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/avif`) |
71+| toute autre extension connue | la table mime de l'hôte (p. ex. `text/markdown; charset=utf-8`) |
72+| inconnue | détecté sur les 512 premiers octets |
73+
74+## GET /api/skills
75+
76+Liste les skills Claude Code disponibles pour la session, pour le sélecteur `/` du composer.
77+
78+Réponse `200` :
79+
80+```json
81+{ "skills": [ { "name": "quality", "description": "Audit code quality with qlty.", "path": "/travail/.claude/skills/quality/SKILL.md", "source": "project" } ] }
82+```
83+
84+Un skill est un répertoire contenant un `SKILL.md`, cherché dans `<cwd>/.claude/skills/*/` (`source: "project"`) et `~/.claude/skills/*/` (`source: "user"`). `name` et `description` viennent du frontmatter YAML du fichier (`clé: valeur`, valeurs entre guillemets et scalaires bloc `>` / `|` compris) ; un nom absent est remplacé par le nom du répertoire, une description absente par `""`. Un skill projet masque un skill utilisateur du même nom. Résultats triés par nom ; `skills` est toujours un tableau.
85+
86+## Erreurs de l'API fichiers
87+
88+Toutes les erreurs sont en JSON : `{ "error": "message" }`.
89+
90+| Statut | Cause |
91+| --- | --- |
92+| 400 | `path` manquant sur PUT ou `GET /api/raw`, corps JSON invalide, ou lecture d'un répertoire. |
93+| 403 | Permission refusée par le système de fichiers. |
94+| 404 | Le chemin (ou la racine du workspace, pour la recherche) n'existe pas. |
95+| 413 | Fichier de plus de 5 Mio (`GET /api/file` seulement ; `/api/raw` n'a pas de plafond). |
96+| 415 | Le fichier n'est pas du texte UTF-8 valide (`GET /api/file` seulement). |
97+
98+## GET /ws/terminal
99+
100+Bascule en WebSocket et démarre un shell (depuis `$SHELL`, repli sur `bash` puis `sh`, avec `TERM=xterm-256color`) dans un pseudo-terminal, dans le `--cwd` du serveur.
101+
102+| Direction | Type de trame | Contenu |
103+| --- | --- | --- |
104+| serveur → client | binaire | Octets bruts de la sortie du terminal. |
105+| client → serveur | texte | `{"type":"input","data":"…"}` — frappes écrites dans le pty. |
106+| client → serveur | texte | `{"type":"resize","cols":N,"rows":N}` — changement de taille. |
107+
108+Le socket se ferme avec un statut de fermeture normale quand le shell se termine ; fermer le socket tue le shell. Les trames texte malformées sont ignorées.
new file mode 100644
@@ -0,0 +1,108 @@
1+# Référence : API workspace
2+
3+> Description neutre et exhaustive des endpoints derrière le panneau workspace et les sélecteurs du composer : l'API fichiers, l'endpoint des skills et le WebSocket du terminal. Les chemins relatifs se résolvent contre le `--cwd` du serveur ; les chemins absolus sont utilisés tels quels (accès volontairement non restreint — ori vise des sandboxes déjà isolées).
4+
5+## GET /api/files
6+
7+Liste un répertoire.
8+
9+| Paramètre de requête | Type | Défaut | Description |
10+| --- | --- | --- | --- |
11+| `path` | chaîne | la racine du workspace | Répertoire à lister. |
12+
13+Réponse `200` :
14+
15+```json
16+{ "path": "/travail", "entries": [ { "name": "src", "path": "/travail/src", "isDir": true, "size": 0 } ] }
17+```
18+
19+Les entrées sont triées répertoires d'abord, puis fichiers, chaque groupe par ordre alphabétique.
20+
21+## GET /api/files/search
22+
23+Recherche des fichiers récursivement sous la racine du workspace, pour le sélecteur `@` du composer.
24+
25+| Paramètre de requête | Type | Défaut | Description |
26+| --- | --- | --- | --- |
27+| `q` | chaîne | `""` | Sous-chaîne, insensible à la casse, cherchée dans le chemin relatif à la racine. Vide renvoie les premiers fichiers. |
28+| `limit` | entier | 50 | Nombre maximal de résultats ; plafonné à 500. |
29+
30+Réponse `200` :
31+
32+```json
33+{ "root": "/travail", "files": [ { "name": "main.go", "path": "/travail/src/main.go", "relPath": "src/main.go" } ] }
34+```
35+
36+`path` est absolu (utilisable dans les autres endpoints), `relPath` utilise des barres obliques. Les résultats sont classés : d'abord les fichiers dont le nom de base contient la requête, puis les chemins relatifs les plus courts, puis l'ordre alphabétique. Le parcours n'entre jamais dans `.git` ni `node_modules`, ignore les entrées illisibles et s'arrête après 50 000 entrées de répertoire. `files` est toujours un tableau (`[]` sans correspondance). Une racine absente donne un `404`.
37+
38+## GET /api/file
39+
40+Lit un fichier texte.
41+
42+| Paramètre de requête | Type | Défaut | Description |
43+| --- | --- | --- | --- |
44+| `path` | chaîne | — | Fichier à lire. |
45+
46+Réponse `200` : `{ "path": "/travail/README.md", "content": "…" }`
47+
48+## PUT /api/file
49+
50+Écrit un fichier texte, en créant les répertoires parents si besoin.
51+
52+| Paramètre de requête | Type | Défaut | Description |
53+| --- | --- | --- | --- |
54+| `path` | chaîne | — (requis) | Fichier à écrire. |
55+
56+Corps : `{ "content": "…" }` — réponse `200` : `{ "path": "…", "status": "saved" }`
57+
58+## GET /api/raw
59+
60+Diffuse les octets d'un fichier tels quels — c'est ainsi que la preview affiche les images. Pas de plafond de taille, pas d'exigence de texte ; les requêtes `Range` sont honorées (`http.ServeContent`).
61+
62+| Paramètre de requête | Type | Défaut | Description |
63+| --- | --- | --- | --- |
64+| `path` | chaîne | — (requis) | Fichier à diffuser. |
65+
66+Réponse `200` avec les octets du fichier, `X-Content-Type-Options: nosniff`, et un `Content-Type` choisi dans cet ordre :
67+
68+| Extension | Content-Type |
69+| --- | --- |
70+| `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` `.bmp` `.ico` `.avif` | types image figés (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/avif`) |
71+| toute autre extension connue | la table mime de l'hôte (p. ex. `text/markdown; charset=utf-8`) |
72+| inconnue | détecté sur les 512 premiers octets |
73+
74+## GET /api/skills
75+
76+Liste les skills Claude Code disponibles pour la session, pour le sélecteur `/` du composer.
77+
78+Réponse `200` :
79+
80+```json
81+{ "skills": [ { "name": "quality", "description": "Audit code quality with qlty.", "path": "/travail/.claude/skills/quality/SKILL.md", "source": "project" } ] }
82+```
83+
84+Un skill est un répertoire contenant un `SKILL.md`, cherché dans `<cwd>/.claude/skills/*/` (`source: "project"`) et `~/.claude/skills/*/` (`source: "user"`). `name` et `description` viennent du frontmatter YAML du fichier (`clé: valeur`, valeurs entre guillemets et scalaires bloc `>` / `|` compris) ; un nom absent est remplacé par le nom du répertoire, une description absente par `""`. Un skill projet masque un skill utilisateur du même nom. Résultats triés par nom ; `skills` est toujours un tableau.
85+
86+## Erreurs de l'API fichiers
87+
88+Toutes les erreurs sont en JSON : `{ "error": "message" }`.
89+
90+| Statut | Cause |
91+| --- | --- |
92+| 400 | `path` manquant sur PUT ou `GET /api/raw`, corps JSON invalide, ou lecture d'un répertoire. |
93+| 403 | Permission refusée par le système de fichiers. |
94+| 404 | Le chemin (ou la racine du workspace, pour la recherche) n'existe pas. |
95+| 413 | Fichier de plus de 5 Mio (`GET /api/file` seulement ; `/api/raw` n'a pas de plafond). |
96+| 415 | Le fichier n'est pas du texte UTF-8 valide (`GET /api/file` seulement). |
97+
98+## GET /ws/terminal
99+
100+Bascule en WebSocket et démarre un shell (depuis `$SHELL`, repli sur `bash` puis `sh`, avec `TERM=xterm-256color`) dans un pseudo-terminal, dans le `--cwd` du serveur.
101+
102+| Direction | Type de trame | Contenu |
103+| --- | --- | --- |
104+| serveur → client | binaire | Octets bruts de la sortie du terminal. |
105+| client → serveur | texte | `{"type":"input","data":"…"}` — frappes écrites dans le pty. |
106+| client → serveur | texte | `{"type":"resize","cols":N,"rows":N}` — changement de taille. |
107+
108+Le socket se ferme avec un statut de fermeture normale quand le shell se termine ; fermer le socket tue le shell. Les trames texte malformées sont ignorées.
added docs/fr/tutorials/getting-started.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Tutoriel : premiers pas avec Ori
2+
3+À la fin de ce tutoriel, vous aurez compilé Ori, lancé l'application avec son agent de démonstration intégré, et mené une conversation complète dans votre navigateur — réponse streamée, plan visible, tool calls et une permission que vous accorderez vous-même. Aucune connaissance préalable d'ACP n'est requise.
4+
5+## Prérequis
6+
7+- Go 1.26 ou plus récent (`go version`)
8+- Node.js 24 ou plus récent avec npm (`node --version`)
9+- Un navigateur web
10+
11+## Étape 1 — Installer les dépendances du front
12+
13+Depuis la racine du dépôt, tapez :
14+
15+```bash
16+make deps
17+```
18+
19+Vous devriez voir npm résoudre les paquets et terminer par une ligne du type :
20+
21+```
22+added 158 packages, and audited 159 packages in 9s
23+```
24+
25+Nous venons d'installer tout ce dont le build de la SPA a besoin.
26+
27+## Étape 2 — Compiler et lancer Ori avec l'agent de démonstration
28+
29+Tapez :
30+
31+```bash
32+make run-mock
33+```
34+
35+Vous devriez voir le build de la SPA, le build Go, puis :
36+
37+```
38+INFO starting agent command="[bin/ori-mock-agent]" cwd=…
39+INFO agent session ready sessionId=mock-1
40+INFO ori listening addr=:8888
41+```
42+
43+Nous venons de démarrer l'application complète : le serveur écoute et il est déjà connecté, en ACP, à `ori-mock-agent` — un petit agent déterministe qui n'exige ni compte ni réseau.
44+
45+## Étape 3 — Ouvrir le panel
46+
47+Ouvrez [http://localhost:8888](http://localhost:8888) dans votre navigateur.
48+
49+Vous devriez voir le panel Ori : un entête affichant `online · mock-1`, une conversation vide, et une zone de saisie en bas.
50+
51+## Étape 4 — Envoyer votre premier message
52+
53+Tapez `bonjour` dans la zone de saisie et appuyez sur Entrée.
54+
55+Vous devriez voir un spinner apparaître en bas du fil avec des phrases d'attente tournantes (*Pondering…*, *Brewing ideas…*), puis, en streaming, l'un après l'autre :
56+
57+- votre propre message, aligné à droite ;
58+- un bloc **💭 Thinking…** montrant la réflexion de l'agent en direct pendant qu'elle streame (il se replie en *💭 Thought for a moment* à la fin du tour — cliquez pour le rouvrir) ;
59+- une checklist **Plan (0/2)** ;
60+- une carte de tool call **📖 Reading README.md** dont le badge passe à **done** ;
61+- le début de la réponse de l'agent.
62+
63+Nous venons d'exécuter la première moitié d'un tour : l'agent streame ses updates et le panel donne à chaque type sa propre forme.
64+
65+## Étape 5 — Accorder la permission
66+
67+Le panel affiche maintenant une carte **🔐 Write hello.txt** avec deux boutons.
68+
69+Cliquez sur **Allow once**.
70+
71+Vous devriez voir une seconde carte de tool call **✏️ Writing hello.txt** avec un diff en vert, puis la réponse finale de l'agent — `Done! You said: bonjour` suivie d'une petite checklist — et le plan afficher **Plan (2/2)**.
72+
73+Nous venons de boucler le cycle complet : l'agent a demandé une permission, votre clic lui est revenu à travers le backend, et il a terminé son tour.
74+
75+## Et maintenant ?
76+
77+Vous avez fait tourner Ori de bout en bout avec l'agent de démonstration. Pour aller plus loin :
78+
79+- Pour parler à un vrai agent → [Lancer Ori avec Claude Code](../how-to/run-with-claude-code.md)
80+- Pour brancher un autre agent → [Utiliser un autre agent ACP](../how-to/use-another-agent.md)
81+- Pour comprendre ce qui s'est passé entre le navigateur et l'agent → [Explication : architecture](../explanation/architecture.md)
new file mode 100644
@@ -0,0 +1,81 @@
1+# Tutoriel : premiers pas avec Ori
2+
3+À la fin de ce tutoriel, vous aurez compilé Ori, lancé l'application avec son agent de démonstration intégré, et mené une conversation complète dans votre navigateur — réponse streamée, plan visible, tool calls et une permission que vous accorderez vous-même. Aucune connaissance préalable d'ACP n'est requise.
4+
5+## Prérequis
6+
7+- Go 1.26 ou plus récent (`go version`)
8+- Node.js 24 ou plus récent avec npm (`node --version`)
9+- Un navigateur web
10+
11+## Étape 1 — Installer les dépendances du front
12+
13+Depuis la racine du dépôt, tapez :
14+
15+```bash
16+make deps
17+```
18+
19+Vous devriez voir npm résoudre les paquets et terminer par une ligne du type :
20+
21+```
22+added 158 packages, and audited 159 packages in 9s
23+```
24+
25+Nous venons d'installer tout ce dont le build de la SPA a besoin.
26+
27+## Étape 2 — Compiler et lancer Ori avec l'agent de démonstration
28+
29+Tapez :
30+
31+```bash
32+make run-mock
33+```
34+
35+Vous devriez voir le build de la SPA, le build Go, puis :
36+
37+```
38+INFO starting agent command="[bin/ori-mock-agent]" cwd=…
39+INFO agent session ready sessionId=mock-1
40+INFO ori listening addr=:8888
41+```
42+
43+Nous venons de démarrer l'application complète : le serveur écoute et il est déjà connecté, en ACP, à `ori-mock-agent` — un petit agent déterministe qui n'exige ni compte ni réseau.
44+
45+## Étape 3 — Ouvrir le panel
46+
47+Ouvrez [http://localhost:8888](http://localhost:8888) dans votre navigateur.
48+
49+Vous devriez voir le panel Ori : un entête affichant `online · mock-1`, une conversation vide, et une zone de saisie en bas.
50+
51+## Étape 4 — Envoyer votre premier message
52+
53+Tapez `bonjour` dans la zone de saisie et appuyez sur Entrée.
54+
55+Vous devriez voir un spinner apparaître en bas du fil avec des phrases d'attente tournantes (*Pondering…*, *Brewing ideas…*), puis, en streaming, l'un après l'autre :
56+
57+- votre propre message, aligné à droite ;
58+- un bloc **💭 Thinking…** montrant la réflexion de l'agent en direct pendant qu'elle streame (il se replie en *💭 Thought for a moment* à la fin du tour — cliquez pour le rouvrir) ;
59+- une checklist **Plan (0/2)** ;
60+- une carte de tool call **📖 Reading README.md** dont le badge passe à **done** ;
61+- le début de la réponse de l'agent.
62+
63+Nous venons d'exécuter la première moitié d'un tour : l'agent streame ses updates et le panel donne à chaque type sa propre forme.
64+
65+## Étape 5 — Accorder la permission
66+
67+Le panel affiche maintenant une carte **🔐 Write hello.txt** avec deux boutons.
68+
69+Cliquez sur **Allow once**.
70+
71+Vous devriez voir une seconde carte de tool call **✏️ Writing hello.txt** avec un diff en vert, puis la réponse finale de l'agent — `Done! You said: bonjour` suivie d'une petite checklist — et le plan afficher **Plan (2/2)**.
72+
73+Nous venons de boucler le cycle complet : l'agent a demandé une permission, votre clic lui est revenu à travers le backend, et il a terminé son tour.
74+
75+## Et maintenant ?
76+
77+Vous avez fait tourner Ori de bout en bout avec l'agent de démonstration. Pour aller plus loin :
78+
79+- Pour parler à un vrai agent → [Lancer Ori avec Claude Code](../how-to/run-with-claude-code.md)
80+- Pour brancher un autre agent → [Utiliser un autre agent ACP](../how-to/use-another-agent.md)
81+- Pour comprendre ce qui s'est passé entre le navigateur et l'agent → [Explication : architecture](../explanation/architecture.md)
added docs/source-code-analysis.adoc +261 -0
new file mode 100644
@@ -0,0 +1,261 @@
1+= Ori Source Code Analysis
2+:toc: left
3+:toclevels: 3
4+:icons: font
5+:source-highlighter: highlight.js
6+:revdate: 2026-09-17 (updated 2026-09-17)
7+
8+== Overview
9+
10+Ori is a web client for https://agentclientprotocol.com[ACP (Agent Client Protocol)] code agents.
11+A Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent
12+subprocess Claude Code via the Zed adapter (`npx -y @zed-industries/claude-code-acp`) by default.
13+The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts (live
14+during streaming, collapsible when complete), working spinner with rotating phrases, tool call
15+cards (status, locations, diffs), the agent's plan, and permission prompts answered from the UI —
16+plus a workspace panel with a VS Code-style file tree (Material icons, indent guides, chevrons),
17+Monaco preview/editor with rendered Markdown and AsciiDoc, and an xterm.js terminal.
18+
19+[source]
20+----
21+browser WebSocket /ws Go backend stdio (ACP) agent subprocess
22+----
23+
24+* Go module: `rickub.com/bots-garden/ori`
25+* ~4,063 lines of source: Go 1,555 (21 files), TypeScript/TSX 2,508 (39 files)
26+
27+== Backend (Go)
28+
29+=== Entry points
30+
31+[cols="1,3"]
32+|===
33+| Package | Responsibility
34+
35+| `cmd/ori`
36+| Server binary. Parses flags (`--addr`, default `:8888`; `--cwd`; `--agent-cmd`,
37+whitespace-split), spawns the agent, wires the bridge and HTTP routes.
38+
39+| `cmd/ori-mock-agent`
40+| Deterministic demo ACP agent on stdio (thought plan read tool call permission
41+edit tool call with diff answer). Used by `make run-mock` and the e2e test.
42+|===
43+
44+=== Internal packages
45+
46+[cols="1,3"]
47+|===
48+| Package | Responsibility
49+
50+| `internal/config`
51+| CLI flag parsing and validation (`Config`, `FromArgs()`).
52+
53+| `internal/agent`
54+| Spawns the agent subprocess and drives the ACP lifecycle via
55+`github.com/coder/acp-go-sdk` v0.13.5. Exposes a generic `Handler` interface (session
56+updates + blocking permission requests). Implements the `acp.Client` side: real
57+filesystem read/write; terminal methods refused (capability not announced).
58+
59+| `internal/bridge`
60+| Hub between the agent and the browsers (~550 LOC). Broadcast with per-client buffers
61+(256 items; slow clients are dropped), append-only history capped at 4,096 events and
62+replayed on every connection, `user_message` echo so the server is the single source of
63+truth, permission routing by `requestId` (pending requests replayed to late joiners).
64+Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost. A
65+`Prompter` interface is the seam for future multi-session support.
66+
67+| `internal/httpserver`
68+| Root HTTP mux: `/healthz`, embedded SPA with fallback routing, injection point for
69+extra routes (`/ws`, `/ws/terminal`, `/api/*`).
70+
71+| `internal/files`
72+| Workspace file API: list / read / write. Relative paths resolved against `--cwd`,
73+absolute paths allowed access is deliberately unrestricted (ori targets
74+already-isolated sandboxes). Binary content 415, files > 5 MiB 413.
75+
76+| `internal/terminal`
77+| One shell per WebSocket on `/ws/terminal`, in a PTY (`creack/pty` v1.1.24). Binary
78+frames out; JSON `input` / `resize` frames in. Shell chosen from `$SHELL` bash sh.
79+
80+| `internal/mockagent`
81+| The scripted demo agent implementation behind `cmd/ori-mock-agent`.
82+
83+| `ui/embed.go`
84+| `go:embed all:dist` the built SPA embedded into the Go binary.
85+|===
86+
87+== Frontend (`ui/`)
88+
89+Vite 7 + React 19 + TypeScript strict. State is held in zustand stores wrapped around pure
90+reducers, keeping the logic testable without React or WebSocket coupling.
91+
92+=== Core modules
93+
94+[cols="1,3"]
95+|===
96+| File | Role
97+
98+| `src/App.tsx`
99+| Root layout: topbar (connection status, workspace toggle), chat column (thread, plan,
100+permission prompts, input), workspace panel.
101+
102+| `src/reducer.ts`
103+| Pure state machine (handler-table style) folding server messages and connection
104+events into the chat state.
105+
106+| `src/store.ts`
107+| Zustand wrapper around the reducer (`useChatState()`, `dispatch()`).
108+
109+| `src/ws.ts`
110+| WebSocket client with exponential-backoff reconnect; the thread resets on `hello` so
111+history replays are idempotent.
112+
113+| `src/protocol.ts`
114+| Wire contract mirroring `internal/bridge/protocol.go`; ACP payloads are typed but
115+relayed verbatim, keeping the front forward-compatible (unknown update kinds ignored).
116+
117+| `src/workspace.ts`
118+| Separate zustand store for the workspace panel (file tree, tabs, open files).
119+
120+| `src/api.ts`
121+| Client for `/api/files` and `/api/file`.
122+
123+| `src/lang.ts`
124+| File extension language ID mapping for Monaco editor.
125+
126+| `src/monaco-setup.ts`
127+| Monaco editor environment configuration (bundled workers, no CDN).
128+|===
129+
130+=== Components
131+
132+* **Chat panel**: `ChatThread` (with `ThoughtEntry` for live/collapsed thoughts), `Markdown`,
133+ `InlineText` (backtick spans `<code>` for tool titles), `ToolCallCard` (status badge,
134+ collapsible content, diffs, file locations), `PermissionPrompt`, `PlanView`, `PromptInput`,
135+ `WorkingIndicator` (spinner with rotating phrases).
136+* **Workspace panel**: `Workspace` (tabs), `FileTree` (VS Code style with Material icons from
137+ `vscode-material-icons` 910 SVGs copied to `public/material-icons/` by
138+ `scripts/copy-icons.mjs`; click = preview, double-click = edit, chevrons, indent guides,
139+ active highlight), `PreviewPane` (Monaco read-only for code, rendered Markdown via
140+ `react-markdown` + `remark-gfm`, rendered AsciiDoc via lazily-imported `@asciidoctor/core`
141+ v4 async API, Rendered/Source toggle), `EditorPane` (Monaco, dirty indicator, Save /
142+ Ctrl+S), `TerminalPane` (xterm.js on `/ws/terminal` with `@xterm/addon-fit`, dark theme,
143+ started on first activation), `MonacoViewer` (shared by preview and editor), `CodeView`
144+ (syntax-highlighted code block for diffs).
145+* Inactive panes are hidden, not unmounted, so shell and editor state survive tab switches.
146+* `execute` tool-call output renders in a monospace `<pre>`, never as markdown.
147+* Monaco workers are bundled (no CDN); the editor loads as a lazy chunk (~1 MB gzip).
148+
149+== HTTP / WebSocket endpoints
150+
151+[cols="1,1,3"]
152+|===
153+| Endpoint | Method | Purpose
154+
155+| `/healthz` | GET | Health check (`{"status":"ok"}`).
156+| `/ws` | GET (upgrade) | Agent browser relay: history replay then live events.
157+| `/ws/terminal` | GET (upgrade) | Interactive PTY shell.
158+| `/api/files` | GET | List a directory (`?path=`).
159+| `/api/file` | GET | Read a file (max 5 MiB).
160+| `/api/file` | PUT | Write a file.
161+| `/` | GET | Embedded SPA (index.html fallback).
162+|===
163+
164+== WebSocket protocol (bridge)
165+
166+Browser server: `prompt`, `cancel`, `permission_response` (by `requestId`).
167+
168+Server browser: `hello` (session id, turn state resets the client thread),
169+`user_message` (echo), `session_update` (raw ACP `SessionUpdate`), `permission_request`
170+(raw ACP `RequestPermissionRequest` + `requestId`), `permission_resolved`,
171+`turn_started`, `turn_ended` (stop reason), `error`.
172+
173+Reference: `internal/bridge/protocol.go`, `ui/src/protocol.ts`, and
174+`docs/en/reference/websocket-protocol.md`.
175+
176+== Tests
177+
178+* *Go*: 55 tests across 8 packages plus one end-to-end test (`cmd/ori/e2e_test.go`,
179+ full server + mock agent subprocess + real WebSocket; skipped with `-short`).
180+ Race-clean (`go test -race ./...`).
181+* *Frontend*: 75 vitest tests across 13 test files reducer (48), components
182+ (WorkingIndicator, InlineText, AsciiDocView with real converter, ToolCallCard, FileTree,
183+ EditorPane, PreviewPane, TerminalPane, App), WebSocket client, workspace store, language
184+ detection.
185+* Quality gate (qlty): PASS as of 2026-09-17 0 errors, 0 warnings, 0 smells (2 formatting
186+ notes).
187+
188+== Build and run
189+
190+[cols="1,3"]
191+|===
192+| Target | Effect
193+
194+| `make deps` | `npm install` in `ui/`.
195+| `make build` | SPA build then Go binaries order matters: the Go build embeds `ui/dist`.
196+| `make test` | `go test ./...` + `vitest run`.
197+| `make run` | Run with the Claude Code ACP adapter.
198+| `make run-mock` | Run with the bundled demo agent (no Claude, no network needed).
199+| `make dev` | Vite dev server on :5173.
200+|===
201+
202+== Key design decisions
203+
204+* ACP payloads cross the WebSocket *verbatim*, keeping the frontend forward-compatible.
205+* The server owns the thread (history + `user_message` echo); the SPA rebuilds from
206+ replay on every (re)connection.
207+* One agent session shared by all browsers in v1; a second `prompt` during an active
208+ turn is rejected. Multi-session is a planned evolution behind the `bridge.Prompter`
209+ seam.
210+* File API access is deliberately unrestricted: ori targets already-isolated sandboxes.
211+* Claude Code is reached through the Zed adapter (`@zed-industries/claude-code-acp`,
212+ note: renamed upstream to `@agentclientprotocol/claude-agent-acp` but default not yet
213+ updated) the `claude` CLI has no native ACP mode.
214+* Material icons are copied from `vscode-material-icons` package to `public/` via an npm
215+ pre-hook script, making them static assets bundled with the SPA.
216+* AsciiDoc rendering uses `@asciidoctor/core` v4's async `convert` named export (v3's
217+ factory function no longer exists).
218+* Terminal always renders with a dark theme (`#1e1e1e`) regardless of app theme.
219+
220+== Known limitations
221+
222+* `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
223+* One prompt turn at a time per shared session.
224+* ACP terminal methods are not supported (capability not announced; answered with JSON-RPC
225+ method-not-found).
226+* Only text content blocks are rendered in messages; images/audio/resources are not yet
227+ supported.
228+* Running ori inside a Claude Code sandbox requires `env -u CLAUDECODE ./bin/ori` to bypass
229+ the nested-session guard.
230+
231+== Sandbox deployment
232+
233+Ori ships as a Docker Sandbox template (`template/Dockerfile`, FROM
234+`docker/sandbox-templates:claude-code`) paired with a kit (`kits/ori/`):
235+
236+* Create: `sbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori-repo>/kits/ori --name ori -p 8888:8888`
237+* The `--detached` (`-d`) flag is **required**: sandboxd auto-stops a sandbox 30 seconds
238+ after its last CLI sentinel session closes; without `-d`, the container stops shortly after
239+ the prompt returns. Browser traffic on a published port does not hold the sandbox up.
240+* The `detached` field is create-time only (no PATCH); an existing non-detached sandbox must
241+ be `sbx rm`'d and recreated with `sbx run -d`.
242+* Build the template image: `make template` (builds binaries first, copies to the image).
243+
244+=== Environment considerations (sandbox development)
245+
246+When developing ori inside a Claude Code sandbox:
247+
248+* `~/.qlty` and `~/.npm` are 488MB dedicated volumes. The qlty tool cache was moved to
249+ `~/.qlty-cache` (symlinked from `~/.qlty/cache`), and npm cache to `~/.npm-big` (via
250+ `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`) to avoid "No space left" errors.
251+* npm blocks install scripts by default; esbuild's postinstall was approved via
252+ `npm approve-scripts` (recorded in `ui/package.json` `allowScripts` field).
253+* The `radarlint-python` qlty plugin was removed (its ~300MB JDK cannot fit the dedicated
254+ volume; diagnosis in `.qlty/qlty.toml` comments).
255+
256+== Documentation
257+
258+`docs/` follows the Diátaxis method, in English (`docs/en/`) and French (`docs/fr/`):
259+tutorials (getting started), how-to guides (run the tests, run with Claude Code, run in a
260+sandbox, use the workspace, use another agent), reference (CLI, WebSocket protocol, workspace
261+API) and explanation (architecture).
new file mode 100644
@@ -0,0 +1,261 @@
1+= Ori Source Code Analysis
2+:toc: left
3+:toclevels: 3
4+:icons: font
5+:source-highlighter: highlight.js
6+:revdate: 2026-09-17 (updated 2026-09-17)
7+
8+== Overview
9+
10+Ori is a web client for https://agentclientprotocol.com[ACP (Agent Client Protocol)] code agents.
11+A Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent
12+subprocess Claude Code via the Zed adapter (`npx -y @zed-industries/claude-code-acp`) by default.
13+The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts (live
14+during streaming, collapsible when complete), working spinner with rotating phrases, tool call
15+cards (status, locations, diffs), the agent's plan, and permission prompts answered from the UI —
16+plus a workspace panel with a VS Code-style file tree (Material icons, indent guides, chevrons),
17+Monaco preview/editor with rendered Markdown and AsciiDoc, and an xterm.js terminal.
18+
19+[source]
20+----
21+browser WebSocket /ws Go backend stdio (ACP) agent subprocess
22+----
23+
24+* Go module: `rickub.com/bots-garden/ori`
25+* ~4,063 lines of source: Go 1,555 (21 files), TypeScript/TSX 2,508 (39 files)
26+
27+== Backend (Go)
28+
29+=== Entry points
30+
31+[cols="1,3"]
32+|===
33+| Package | Responsibility
34+
35+| `cmd/ori`
36+| Server binary. Parses flags (`--addr`, default `:8888`; `--cwd`; `--agent-cmd`,
37+whitespace-split), spawns the agent, wires the bridge and HTTP routes.
38+
39+| `cmd/ori-mock-agent`
40+| Deterministic demo ACP agent on stdio (thought plan read tool call permission
41+edit tool call with diff answer). Used by `make run-mock` and the e2e test.
42+|===
43+
44+=== Internal packages
45+
46+[cols="1,3"]
47+|===
48+| Package | Responsibility
49+
50+| `internal/config`
51+| CLI flag parsing and validation (`Config`, `FromArgs()`).
52+
53+| `internal/agent`
54+| Spawns the agent subprocess and drives the ACP lifecycle via
55+`github.com/coder/acp-go-sdk` v0.13.5. Exposes a generic `Handler` interface (session
56+updates + blocking permission requests). Implements the `acp.Client` side: real
57+filesystem read/write; terminal methods refused (capability not announced).
58+
59+| `internal/bridge`
60+| Hub between the agent and the browsers (~550 LOC). Broadcast with per-client buffers
61+(256 items; slow clients are dropped), append-only history capped at 4,096 events and
62+replayed on every connection, `user_message` echo so the server is the single source of
63+truth, permission routing by `requestId` (pending requests replayed to late joiners).
64+Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost. A
65+`Prompter` interface is the seam for future multi-session support.
66+
67+| `internal/httpserver`
68+| Root HTTP mux: `/healthz`, embedded SPA with fallback routing, injection point for
69+extra routes (`/ws`, `/ws/terminal`, `/api/*`).
70+
71+| `internal/files`
72+| Workspace file API: list / read / write. Relative paths resolved against `--cwd`,
73+absolute paths allowed access is deliberately unrestricted (ori targets
74+already-isolated sandboxes). Binary content 415, files > 5 MiB 413.
75+
76+| `internal/terminal`
77+| One shell per WebSocket on `/ws/terminal`, in a PTY (`creack/pty` v1.1.24). Binary
78+frames out; JSON `input` / `resize` frames in. Shell chosen from `$SHELL` bash sh.
79+
80+| `internal/mockagent`
81+| The scripted demo agent implementation behind `cmd/ori-mock-agent`.
82+
83+| `ui/embed.go`
84+| `go:embed all:dist` the built SPA embedded into the Go binary.
85+|===
86+
87+== Frontend (`ui/`)
88+
89+Vite 7 + React 19 + TypeScript strict. State is held in zustand stores wrapped around pure
90+reducers, keeping the logic testable without React or WebSocket coupling.
91+
92+=== Core modules
93+
94+[cols="1,3"]
95+|===
96+| File | Role
97+
98+| `src/App.tsx`
99+| Root layout: topbar (connection status, workspace toggle), chat column (thread, plan,
100+permission prompts, input), workspace panel.
101+
102+| `src/reducer.ts`
103+| Pure state machine (handler-table style) folding server messages and connection
104+events into the chat state.
105+
106+| `src/store.ts`
107+| Zustand wrapper around the reducer (`useChatState()`, `dispatch()`).
108+
109+| `src/ws.ts`
110+| WebSocket client with exponential-backoff reconnect; the thread resets on `hello` so
111+history replays are idempotent.
112+
113+| `src/protocol.ts`
114+| Wire contract mirroring `internal/bridge/protocol.go`; ACP payloads are typed but
115+relayed verbatim, keeping the front forward-compatible (unknown update kinds ignored).
116+
117+| `src/workspace.ts`
118+| Separate zustand store for the workspace panel (file tree, tabs, open files).
119+
120+| `src/api.ts`
121+| Client for `/api/files` and `/api/file`.
122+
123+| `src/lang.ts`
124+| File extension language ID mapping for Monaco editor.
125+
126+| `src/monaco-setup.ts`
127+| Monaco editor environment configuration (bundled workers, no CDN).
128+|===
129+
130+=== Components
131+
132+* **Chat panel**: `ChatThread` (with `ThoughtEntry` for live/collapsed thoughts), `Markdown`,
133+ `InlineText` (backtick spans `<code>` for tool titles), `ToolCallCard` (status badge,
134+ collapsible content, diffs, file locations), `PermissionPrompt`, `PlanView`, `PromptInput`,
135+ `WorkingIndicator` (spinner with rotating phrases).
136+* **Workspace panel**: `Workspace` (tabs), `FileTree` (VS Code style with Material icons from
137+ `vscode-material-icons` 910 SVGs copied to `public/material-icons/` by
138+ `scripts/copy-icons.mjs`; click = preview, double-click = edit, chevrons, indent guides,
139+ active highlight), `PreviewPane` (Monaco read-only for code, rendered Markdown via
140+ `react-markdown` + `remark-gfm`, rendered AsciiDoc via lazily-imported `@asciidoctor/core`
141+ v4 async API, Rendered/Source toggle), `EditorPane` (Monaco, dirty indicator, Save /
142+ Ctrl+S), `TerminalPane` (xterm.js on `/ws/terminal` with `@xterm/addon-fit`, dark theme,
143+ started on first activation), `MonacoViewer` (shared by preview and editor), `CodeView`
144+ (syntax-highlighted code block for diffs).
145+* Inactive panes are hidden, not unmounted, so shell and editor state survive tab switches.
146+* `execute` tool-call output renders in a monospace `<pre>`, never as markdown.
147+* Monaco workers are bundled (no CDN); the editor loads as a lazy chunk (~1 MB gzip).
148+
149+== HTTP / WebSocket endpoints
150+
151+[cols="1,1,3"]
152+|===
153+| Endpoint | Method | Purpose
154+
155+| `/healthz` | GET | Health check (`{"status":"ok"}`).
156+| `/ws` | GET (upgrade) | Agent browser relay: history replay then live events.
157+| `/ws/terminal` | GET (upgrade) | Interactive PTY shell.
158+| `/api/files` | GET | List a directory (`?path=`).
159+| `/api/file` | GET | Read a file (max 5 MiB).
160+| `/api/file` | PUT | Write a file.
161+| `/` | GET | Embedded SPA (index.html fallback).
162+|===
163+
164+== WebSocket protocol (bridge)
165+
166+Browser server: `prompt`, `cancel`, `permission_response` (by `requestId`).
167+
168+Server browser: `hello` (session id, turn state resets the client thread),
169+`user_message` (echo), `session_update` (raw ACP `SessionUpdate`), `permission_request`
170+(raw ACP `RequestPermissionRequest` + `requestId`), `permission_resolved`,
171+`turn_started`, `turn_ended` (stop reason), `error`.
172+
173+Reference: `internal/bridge/protocol.go`, `ui/src/protocol.ts`, and
174+`docs/en/reference/websocket-protocol.md`.
175+
176+== Tests
177+
178+* *Go*: 55 tests across 8 packages plus one end-to-end test (`cmd/ori/e2e_test.go`,
179+ full server + mock agent subprocess + real WebSocket; skipped with `-short`).
180+ Race-clean (`go test -race ./...`).
181+* *Frontend*: 75 vitest tests across 13 test files reducer (48), components
182+ (WorkingIndicator, InlineText, AsciiDocView with real converter, ToolCallCard, FileTree,
183+ EditorPane, PreviewPane, TerminalPane, App), WebSocket client, workspace store, language
184+ detection.
185+* Quality gate (qlty): PASS as of 2026-09-17 0 errors, 0 warnings, 0 smells (2 formatting
186+ notes).
187+
188+== Build and run
189+
190+[cols="1,3"]
191+|===
192+| Target | Effect
193+
194+| `make deps` | `npm install` in `ui/`.
195+| `make build` | SPA build then Go binaries order matters: the Go build embeds `ui/dist`.
196+| `make test` | `go test ./...` + `vitest run`.
197+| `make run` | Run with the Claude Code ACP adapter.
198+| `make run-mock` | Run with the bundled demo agent (no Claude, no network needed).
199+| `make dev` | Vite dev server on :5173.
200+|===
201+
202+== Key design decisions
203+
204+* ACP payloads cross the WebSocket *verbatim*, keeping the frontend forward-compatible.
205+* The server owns the thread (history + `user_message` echo); the SPA rebuilds from
206+ replay on every (re)connection.
207+* One agent session shared by all browsers in v1; a second `prompt` during an active
208+ turn is rejected. Multi-session is a planned evolution behind the `bridge.Prompter`
209+ seam.
210+* File API access is deliberately unrestricted: ori targets already-isolated sandboxes.
211+* Claude Code is reached through the Zed adapter (`@zed-industries/claude-code-acp`,
212+ note: renamed upstream to `@agentclientprotocol/claude-agent-acp` but default not yet
213+ updated) the `claude` CLI has no native ACP mode.
214+* Material icons are copied from `vscode-material-icons` package to `public/` via an npm
215+ pre-hook script, making them static assets bundled with the SPA.
216+* AsciiDoc rendering uses `@asciidoctor/core` v4's async `convert` named export (v3's
217+ factory function no longer exists).
218+* Terminal always renders with a dark theme (`#1e1e1e`) regardless of app theme.
219+
220+== Known limitations
221+
222+* `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
223+* One prompt turn at a time per shared session.
224+* ACP terminal methods are not supported (capability not announced; answered with JSON-RPC
225+ method-not-found).
226+* Only text content blocks are rendered in messages; images/audio/resources are not yet
227+ supported.
228+* Running ori inside a Claude Code sandbox requires `env -u CLAUDECODE ./bin/ori` to bypass
229+ the nested-session guard.
230+
231+== Sandbox deployment
232+
233+Ori ships as a Docker Sandbox template (`template/Dockerfile`, FROM
234+`docker/sandbox-templates:claude-code`) paired with a kit (`kits/ori/`):
235+
236+* Create: `sbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori-repo>/kits/ori --name ori -p 8888:8888`
237+* The `--detached` (`-d`) flag is **required**: sandboxd auto-stops a sandbox 30 seconds
238+ after its last CLI sentinel session closes; without `-d`, the container stops shortly after
239+ the prompt returns. Browser traffic on a published port does not hold the sandbox up.
240+* The `detached` field is create-time only (no PATCH); an existing non-detached sandbox must
241+ be `sbx rm`'d and recreated with `sbx run -d`.
242+* Build the template image: `make template` (builds binaries first, copies to the image).
243+
244+=== Environment considerations (sandbox development)
245+
246+When developing ori inside a Claude Code sandbox:
247+
248+* `~/.qlty` and `~/.npm` are 488MB dedicated volumes. The qlty tool cache was moved to
249+ `~/.qlty-cache` (symlinked from `~/.qlty/cache`), and npm cache to `~/.npm-big` (via
250+ `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`) to avoid "No space left" errors.
251+* npm blocks install scripts by default; esbuild's postinstall was approved via
252+ `npm approve-scripts` (recorded in `ui/package.json` `allowScripts` field).
253+* The `radarlint-python` qlty plugin was removed (its ~300MB JDK cannot fit the dedicated
254+ volume; diagnosis in `.qlty/qlty.toml` comments).
255+
256+== Documentation
257+
258+`docs/` follows the Diátaxis method, in English (`docs/en/`) and French (`docs/fr/`):
259+tutorials (getting started), how-to guides (run the tests, run with Claude Code, run in a
260+sandbox, use the workspace, use another agent), reference (CLI, WebSocket protocol, workspace
261+API) and explanation (architecture).
modified go.mod +3 -1
@@ -1,7 +1,9 @@
1-module github.com/bots-garden/ori
1+module rickub.com/bots-garden/ori
22
33 go 1.26.5
44
55 require github.com/coder/acp-go-sdk v0.13.5
66
77 require github.com/coder/websocket v1.8.15
8+
9+require github.com/creack/pty v1.1.24
@@ -1,7 +1,9 @@
1-module github.com/bots-garden/ori1+module rickub.com/bots-garden/ori
2 2
3 go 1.26.53 go 1.26.5
4 4
5 require github.com/coder/acp-go-sdk v0.13.55 require github.com/coder/acp-go-sdk v0.13.5
6 6
7 require github.com/coder/websocket v1.8.157 require github.com/coder/websocket v1.8.15
8+
9+require github.com/creack/pty v1.1.24
modified go.sum +2 -0
@@ -2,3 +2,5 @@ github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9AS
22 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
33 github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
44 github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
5+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
6+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -2,3 +2,5 @@ github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9AS
2 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=2 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
3 github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=3 github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
4 github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=4 github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
5+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
6+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
modified internal/agent/agent_test.go +1 -1
@@ -12,7 +12,7 @@ import (
1212
1313 acp "github.com/coder/acp-go-sdk"
1414
15- "github.com/bots-garden/ori/internal/agent"
15+ "rickub.com/bots-garden/ori/internal/agent"
1616 )
1717
1818 // mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent
@@ -12,7 +12,7 @@ import (
12 12
13 acp "github.com/coder/acp-go-sdk"13 acp "github.com/coder/acp-go-sdk"
14 14
15- "github.com/bots-garden/ori/internal/agent"15+ "rickub.com/bots-garden/ori/internal/agent"
16 )16 )
17 17
18 // mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent18 // mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent
modified internal/bridge/bridge.go +44 -5
@@ -13,6 +13,7 @@ import (
1313 "encoding/json"
1414 "fmt"
1515 "log/slog"
16+ "path/filepath"
1617 "strconv"
1718 "sync"
1819
@@ -23,7 +24,7 @@ import (
2324 // satisfied by *agent.Session and by test fakes.
2425 type Prompter interface {
2526 ID() string
26- PromptText(ctx context.Context, text string) (acp.StopReason, error)
27+ Prompt(ctx context.Context, blocks []acp.ContentBlock) (acp.StopReason, error)
2728 Cancel(ctx context.Context) error
2829 }
2930
@@ -54,6 +55,7 @@ type Bridge struct {
5455
5556 mu sync.Mutex
5657 session Prompter
58+ root string
5759 subscribers map[chan []byte]struct{}
5860 history [][]byte
5961 turnActive bool
@@ -85,6 +87,14 @@ func (b *Bridge) SetSession(session Prompter) {
8587 b.session = session
8688 }
8789
90+// SetWorkspaceRoot sets the directory relative attachment paths resolve
91+// against (the agent's cwd). Without it, relative paths are sent as given.
92+func (b *Bridge) SetWorkspaceRoot(root string) {
93+ b.mu.Lock()
94+ defer b.mu.Unlock()
95+ b.root = root
96+}
97+
8898 // Subscribe registers a new client. It returns the client's event channel,
8999 // the events to replay so the client catches up (history, then any pending
90100 // permission requests), and an unsubscribe function.
@@ -127,7 +137,7 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
127137
128138 switch msg.Type {
129139 case IncomingPrompt:
130- b.startTurn(msg.Text)
140+ b.startTurn(msg.Text, msg.Attachments)
131141 case IncomingCancel:
132142 b.cancelTurn(ctx)
133143 case IncomingPermissionResponse:
@@ -139,9 +149,10 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
139149
140150 // startTurn launches a prompt turn in the background; updates stream through
141151 // HandleSessionUpdate while it runs.
142-func (b *Bridge) startTurn(text string) {
152+func (b *Bridge) startTurn(text string, attachments []Attachment) {
143153 b.mu.Lock()
144154 session := b.session
155+ root := b.root
145156 if session == nil {
146157 b.mu.Unlock()
147158 b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false)
@@ -155,13 +166,14 @@ func (b *Bridge) startTurn(text string) {
155166 b.turnActive = true
156167 b.mu.Unlock()
157168
158- b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text}), true)
169+ b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text, Attachments: attachments}), true)
159170 b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true)
160171
172+ blocks := promptBlocks(text, attachments, root)
161173 go func() {
162174 // The turn outlives the WebSocket message that started it, so it
163175 // runs under its own context, ended by session/cancel only.
164- stop, err := session.PromptText(context.Background(), text)
176+ stop, err := session.Prompt(context.Background(), blocks)
165177
166178 b.mu.Lock()
167179 b.turnActive = false
@@ -177,6 +189,33 @@ func (b *Bridge) startTurn(text string) {
177189 }()
178190 }
179191
192+// promptBlocks builds the ACP prompt: the text first, then one resource_link
193+// per attachment (file:// URI, named after the mention), which every ACP
194+// agent must accept.
195+//
196+// Example:
197+//
198+// promptBlocks("Explain @src/main.go", []Attachment{{Path: "src/main.go"}}, "/work")
199+// // [text "Explain @src/main.go", resource_link name=src/main.go uri=file:///work/src/main.go]
200+func promptBlocks(text string, attachments []Attachment, root string) []acp.ContentBlock {
201+ blocks := []acp.ContentBlock{acp.TextBlock(text)}
202+ for _, attachment := range attachments {
203+ if attachment.Path == "" {
204+ continue
205+ }
206+ path := attachment.Path
207+ if !filepath.IsAbs(path) && root != "" {
208+ path = filepath.Join(root, path)
209+ }
210+ name := attachment.Name
211+ if name == "" {
212+ name = attachment.Path
213+ }
214+ blocks = append(blocks, acp.ResourceLinkBlock(name, "file://"+filepath.ToSlash(path)))
215+ }
216+ return blocks
217+}
218+
180219 func (b *Bridge) cancelTurn(ctx context.Context) {
181220 b.mu.Lock()
182221 session := b.session
@@ -13,6 +13,7 @@ import (
13 "encoding/json"13 "encoding/json"
14 "fmt"14 "fmt"
15 "log/slog"15 "log/slog"
16+ "path/filepath"
16 "strconv"17 "strconv"
17 "sync"18 "sync"
18 19
@@ -23,7 +24,7 @@ import (
23 // satisfied by *agent.Session and by test fakes.24 // satisfied by *agent.Session and by test fakes.
24 type Prompter interface {25 type Prompter interface {
25 ID() string26 ID() string
26- PromptText(ctx context.Context, text string) (acp.StopReason, error)27+ Prompt(ctx context.Context, blocks []acp.ContentBlock) (acp.StopReason, error)
27 Cancel(ctx context.Context) error28 Cancel(ctx context.Context) error
28 }29 }
29 30
@@ -54,6 +55,7 @@ type Bridge struct {
54 55
55 mu sync.Mutex56 mu sync.Mutex
56 session Prompter57 session Prompter
58+ root string
57 subscribers map[chan []byte]struct{}59 subscribers map[chan []byte]struct{}
58 history [][]byte60 history [][]byte
59 turnActive bool61 turnActive bool
@@ -85,6 +87,14 @@ func (b *Bridge) SetSession(session Prompter) {
85 b.session = session87 b.session = session
86 }88 }
87 89
90+// SetWorkspaceRoot sets the directory relative attachment paths resolve
91+// against (the agent's cwd). Without it, relative paths are sent as given.
92+func (b *Bridge) SetWorkspaceRoot(root string) {
93+ b.mu.Lock()
94+ defer b.mu.Unlock()
95+ b.root = root
96+}
97+
88 // Subscribe registers a new client. It returns the client's event channel,98 // Subscribe registers a new client. It returns the client's event channel,
89 // the events to replay so the client catches up (history, then any pending99 // the events to replay so the client catches up (history, then any pending
90 // permission requests), and an unsubscribe function.100 // permission requests), and an unsubscribe function.
@@ -127,7 +137,7 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
127 137
128 switch msg.Type {138 switch msg.Type {
129 case IncomingPrompt:139 case IncomingPrompt:
130- b.startTurn(msg.Text)140+ b.startTurn(msg.Text, msg.Attachments)
131 case IncomingCancel:141 case IncomingCancel:
132 b.cancelTurn(ctx)142 b.cancelTurn(ctx)
133 case IncomingPermissionResponse:143 case IncomingPermissionResponse:
@@ -139,9 +149,10 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
139 149
140 // startTurn launches a prompt turn in the background; updates stream through150 // startTurn launches a prompt turn in the background; updates stream through
141 // HandleSessionUpdate while it runs.151 // HandleSessionUpdate while it runs.
142-func (b *Bridge) startTurn(text string) {152+func (b *Bridge) startTurn(text string, attachments []Attachment) {
143 b.mu.Lock()153 b.mu.Lock()
144 session := b.session154 session := b.session
155+ root := b.root
145 if session == nil {156 if session == nil {
146 b.mu.Unlock()157 b.mu.Unlock()
147 b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false)158 b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false)
@@ -155,13 +166,14 @@ func (b *Bridge) startTurn(text string) {
155 b.turnActive = true166 b.turnActive = true
156 b.mu.Unlock()167 b.mu.Unlock()
157 168
158- b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text}), true)169+ b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text, Attachments: attachments}), true)
159 b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true)170 b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true)
160 171
172+ blocks := promptBlocks(text, attachments, root)
161 go func() {173 go func() {
162 // The turn outlives the WebSocket message that started it, so it174 // The turn outlives the WebSocket message that started it, so it
163 // runs under its own context, ended by session/cancel only.175 // runs under its own context, ended by session/cancel only.
164- stop, err := session.PromptText(context.Background(), text)176+ stop, err := session.Prompt(context.Background(), blocks)
165 177
166 b.mu.Lock()178 b.mu.Lock()
167 b.turnActive = false179 b.turnActive = false
@@ -177,6 +189,33 @@ func (b *Bridge) startTurn(text string) {
177 }()189 }()
178 }190 }
179 191
192+// promptBlocks builds the ACP prompt: the text first, then one resource_link
193+// per attachment (file:// URI, named after the mention), which every ACP
194+// agent must accept.
195+//
196+// Example:
197+//
198+// promptBlocks("Explain @src/main.go", []Attachment{{Path: "src/main.go"}}, "/work")
199+// // [text "Explain @src/main.go", resource_link name=src/main.go uri=file:///work/src/main.go]
200+func promptBlocks(text string, attachments []Attachment, root string) []acp.ContentBlock {
201+ blocks := []acp.ContentBlock{acp.TextBlock(text)}
202+ for _, attachment := range attachments {
203+ if attachment.Path == "" {
204+ continue
205+ }
206+ path := attachment.Path
207+ if !filepath.IsAbs(path) && root != "" {
208+ path = filepath.Join(root, path)
209+ }
210+ name := attachment.Name
211+ if name == "" {
212+ name = attachment.Path
213+ }
214+ blocks = append(blocks, acp.ResourceLinkBlock(name, "file://"+filepath.ToSlash(path)))
215+ }
216+ return blocks
217+}
218+
180 func (b *Bridge) cancelTurn(ctx context.Context) {219 func (b *Bridge) cancelTurn(ctx context.Context) {
181 b.mu.Lock()220 b.mu.Lock()
182 session := b.session221 session := b.session
modified internal/bridge/bridge_test.go +48 -4
@@ -10,25 +10,31 @@ import (
1010
1111 acp "github.com/coder/acp-go-sdk"
1212
13- "github.com/bots-garden/ori/internal/bridge"
13+ "rickub.com/bots-garden/ori/internal/bridge"
1414 )
1515
1616 // fakeSession implements bridge.Prompter without any agent behind it.
1717 type fakeSession struct {
1818 mu sync.Mutex
1919 prompts []string
20+ blocks [][]acp.ContentBlock
2021 cancelled bool
21- // respond controls what PromptText returns; nil means end_turn.
22+ // respond controls what Prompt returns; nil means end_turn.
2223 respond func(text string) (acp.StopReason, error)
23- // block, when non-nil, is closed by the test to let PromptText return.
24+ // block, when non-nil, is closed by the test to let Prompt return.
2425 block chan struct{}
2526 }
2627
2728 func (f *fakeSession) ID() string { return "sess-fake" }
2829
29-func (f *fakeSession) PromptText(_ context.Context, text string) (acp.StopReason, error) {
30+func (f *fakeSession) Prompt(_ context.Context, blocks []acp.ContentBlock) (acp.StopReason, error) {
31+ text := ""
32+ if len(blocks) > 0 && blocks[0].Text != nil {
33+ text = blocks[0].Text.Text
34+ }
3035 f.mu.Lock()
3136 f.prompts = append(f.prompts, text)
37+ f.blocks = append(f.blocks, blocks)
3238 respond := f.respond
3339 block := f.block
3440 f.mu.Unlock()
@@ -340,3 +346,41 @@ func TestPendingPermissionIsReplayedToLateJoiner(t *testing.T) {
340346 b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`))
341347 <-answered
342348 }
349+
350+func TestPromptAttachmentsBecomeResourceLinks(t *testing.T) {
351+ b := bridge.New(nil)
352+ session := &fakeSession{}
353+ b.SetSession(session)
354+ b.SetWorkspaceRoot("/work")
355+
356+ events, _, unsubscribe := b.Subscribe()
357+ defer unsubscribe()
358+
359+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"explain @src/main.go and @/etc/hosts",
360+ "attachments":[{"path":"src/main.go","name":"src/main.go"},{"path":"/etc/hosts"},{"path":""}]}`))
361+
362+ seen := collect(t, events, bridge.OutgoingTurnEnded)
363+ if len(seen[0].Attachments) != 3 || seen[0].Attachments[0].Name != "src/main.go" {
364+ t.Errorf("user_message attachments = %+v, want the three sent", seen[0].Attachments)
365+ }
366+
367+ session.mu.Lock()
368+ defer session.mu.Unlock()
369+ blocks := session.blocks[0]
370+ if len(blocks) != 3 {
371+ t.Fatalf("prompt blocks = %d, want text + 2 resource links (empty path skipped)", len(blocks))
372+ }
373+ if blocks[0].Text == nil || blocks[0].Text.Text != "explain @src/main.go and @/etc/hosts" {
374+ t.Errorf("first block = %+v, want the text", blocks[0])
375+ }
376+ cases := []struct{ name, uri string }{
377+ {"src/main.go", "file:///work/src/main.go"},
378+ {"/etc/hosts", "file:///etc/hosts"},
379+ }
380+ for i, tc := range cases {
381+ link := blocks[i+1].ResourceLink
382+ if link == nil || link.Name != tc.name || link.Uri != tc.uri || link.Type != "resource_link" {
383+ t.Errorf("block[%d] = %+v, want resource_link %s → %s", i+1, blocks[i+1], tc.name, tc.uri)
384+ }
385+ }
386+}
@@ -10,25 +10,31 @@ import (
10 10
11 acp "github.com/coder/acp-go-sdk"11 acp "github.com/coder/acp-go-sdk"
12 12
13- "github.com/bots-garden/ori/internal/bridge"13+ "rickub.com/bots-garden/ori/internal/bridge"
14 )14 )
15 15
16 // fakeSession implements bridge.Prompter without any agent behind it.16 // fakeSession implements bridge.Prompter without any agent behind it.
17 type fakeSession struct {17 type fakeSession struct {
18 mu sync.Mutex18 mu sync.Mutex
19 prompts []string19 prompts []string
20+ blocks [][]acp.ContentBlock
20 cancelled bool21 cancelled bool
21- // respond controls what PromptText returns; nil means end_turn.22+ // respond controls what Prompt returns; nil means end_turn.
22 respond func(text string) (acp.StopReason, error)23 respond func(text string) (acp.StopReason, error)
23- // block, when non-nil, is closed by the test to let PromptText return.24+ // block, when non-nil, is closed by the test to let Prompt return.
24 block chan struct{}25 block chan struct{}
25 }26 }
26 27
27 func (f *fakeSession) ID() string { return "sess-fake" }28 func (f *fakeSession) ID() string { return "sess-fake" }
28 29
29-func (f *fakeSession) PromptText(_ context.Context, text string) (acp.StopReason, error) {30+func (f *fakeSession) Prompt(_ context.Context, blocks []acp.ContentBlock) (acp.StopReason, error) {
31+ text := ""
32+ if len(blocks) > 0 && blocks[0].Text != nil {
33+ text = blocks[0].Text.Text
34+ }
30 f.mu.Lock()35 f.mu.Lock()
31 f.prompts = append(f.prompts, text)36 f.prompts = append(f.prompts, text)
37+ f.blocks = append(f.blocks, blocks)
32 respond := f.respond38 respond := f.respond
33 block := f.block39 block := f.block
34 f.mu.Unlock()40 f.mu.Unlock()
@@ -340,3 +346,41 @@ func TestPendingPermissionIsReplayedToLateJoiner(t *testing.T) {
340 b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`))346 b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`))
341 <-answered347 <-answered
342 }348 }
349+
350+func TestPromptAttachmentsBecomeResourceLinks(t *testing.T) {
351+ b := bridge.New(nil)
352+ session := &fakeSession{}
353+ b.SetSession(session)
354+ b.SetWorkspaceRoot("/work")
355+
356+ events, _, unsubscribe := b.Subscribe()
357+ defer unsubscribe()
358+
359+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"explain @src/main.go and @/etc/hosts",
360+ "attachments":[{"path":"src/main.go","name":"src/main.go"},{"path":"/etc/hosts"},{"path":""}]}`))
361+
362+ seen := collect(t, events, bridge.OutgoingTurnEnded)
363+ if len(seen[0].Attachments) != 3 || seen[0].Attachments[0].Name != "src/main.go" {
364+ t.Errorf("user_message attachments = %+v, want the three sent", seen[0].Attachments)
365+ }
366+
367+ session.mu.Lock()
368+ defer session.mu.Unlock()
369+ blocks := session.blocks[0]
370+ if len(blocks) != 3 {
371+ t.Fatalf("prompt blocks = %d, want text + 2 resource links (empty path skipped)", len(blocks))
372+ }
373+ if blocks[0].Text == nil || blocks[0].Text.Text != "explain @src/main.go and @/etc/hosts" {
374+ t.Errorf("first block = %+v, want the text", blocks[0])
375+ }
376+ cases := []struct{ name, uri string }{
377+ {"src/main.go", "file:///work/src/main.go"},
378+ {"/etc/hosts", "file:///etc/hosts"},
379+ }
380+ for i, tc := range cases {
381+ link := blocks[i+1].ResourceLink
382+ if link == nil || link.Name != tc.name || link.Uri != tc.uri || link.Type != "resource_link" {
383+ t.Errorf("block[%d] = %+v, want resource_link %s → %s", i+1, blocks[i+1], tc.name, tc.uri)
384+ }
385+ }
386+}
modified internal/bridge/protocol.go +17 -1
@@ -10,7 +10,7 @@ import "encoding/json"
1010
1111 // Incoming message types (browser → server).
1212 const (
13- // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}.
13+ // IncomingPrompt starts a turn: {"type":"prompt","text":"...","attachments":[...]}.
1414 IncomingPrompt = "prompt"
1515 // IncomingCancel interrupts the current turn: {"type":"cancel"}.
1616 IncomingCancel = "cancel"
@@ -45,11 +45,25 @@ const (
4545 OutgoingError = "error"
4646 )
4747
48+// Attachment is a workspace file the user mentioned in a prompt (with the
49+// "@" selector). The server turns it into an ACP resource_link content block
50+// so the agent can open the file, while the "@name" mention stays visible in
51+// the prompt text.
52+type Attachment struct {
53+ // Path is the file path, absolute or relative to the workspace root.
54+ Path string `json:"path"`
55+ // Name is the label shown for the file (the relative path in the UI);
56+ // it defaults to Path.
57+ Name string `json:"name,omitempty"`
58+}
59+
4860 // Incoming is a message sent by the browser.
4961 type Incoming struct {
5062 Type string `json:"type"`
5163 // Text is the prompt text (IncomingPrompt).
5264 Text string `json:"text,omitempty"`
65+ // Attachments are the files mentioned in the prompt (IncomingPrompt).
66+ Attachments []Attachment `json:"attachments,omitempty"`
5367 // RequestId identifies the permission request being answered.
5468 RequestId string `json:"requestId,omitempty"`
5569 // OptionId is the chosen ACP permission option.
@@ -73,6 +87,8 @@ type Outgoing struct {
7387 Request json.RawMessage `json:"request,omitempty"`
7488 // Text is the user's prompt text (OutgoingUserMessage).
7589 Text string `json:"text,omitempty"`
90+ // Attachments are the files the prompt mentioned (OutgoingUserMessage).
91+ Attachments []Attachment `json:"attachments,omitempty"`
7692 // StopReason is the ACP stop reason (OutgoingTurnEnded).
7793 StopReason string `json:"stopReason,omitempty"`
7894 // Message is a human-readable error (OutgoingError).
@@ -10,7 +10,7 @@ import "encoding/json"
10 10
11 // Incoming message types (browser → server).11 // Incoming message types (browser → server).
12 const (12 const (
13- // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}.13+ // IncomingPrompt starts a turn: {"type":"prompt","text":"...","attachments":[...]}.
14 IncomingPrompt = "prompt"14 IncomingPrompt = "prompt"
15 // IncomingCancel interrupts the current turn: {"type":"cancel"}.15 // IncomingCancel interrupts the current turn: {"type":"cancel"}.
16 IncomingCancel = "cancel"16 IncomingCancel = "cancel"
@@ -45,11 +45,25 @@ const (
45 OutgoingError = "error"45 OutgoingError = "error"
46 )46 )
47 47
48+// Attachment is a workspace file the user mentioned in a prompt (with the
49+// "@" selector). The server turns it into an ACP resource_link content block
50+// so the agent can open the file, while the "@name" mention stays visible in
51+// the prompt text.
52+type Attachment struct {
53+ // Path is the file path, absolute or relative to the workspace root.
54+ Path string `json:"path"`
55+ // Name is the label shown for the file (the relative path in the UI);
56+ // it defaults to Path.
57+ Name string `json:"name,omitempty"`
58+}
59+
48 // Incoming is a message sent by the browser.60 // Incoming is a message sent by the browser.
49 type Incoming struct {61 type Incoming struct {
50 Type string `json:"type"`62 Type string `json:"type"`
51 // Text is the prompt text (IncomingPrompt).63 // Text is the prompt text (IncomingPrompt).
52 Text string `json:"text,omitempty"`64 Text string `json:"text,omitempty"`
65+ // Attachments are the files mentioned in the prompt (IncomingPrompt).
66+ Attachments []Attachment `json:"attachments,omitempty"`
53 // RequestId identifies the permission request being answered.67 // RequestId identifies the permission request being answered.
54 RequestId string `json:"requestId,omitempty"`68 RequestId string `json:"requestId,omitempty"`
55 // OptionId is the chosen ACP permission option.69 // OptionId is the chosen ACP permission option.
@@ -73,6 +87,8 @@ type Outgoing struct {
73 Request json.RawMessage `json:"request,omitempty"`87 Request json.RawMessage `json:"request,omitempty"`
74 // Text is the user's prompt text (OutgoingUserMessage).88 // Text is the user's prompt text (OutgoingUserMessage).
75 Text string `json:"text,omitempty"`89 Text string `json:"text,omitempty"`
90+ // Attachments are the files the prompt mentioned (OutgoingUserMessage).
91+ Attachments []Attachment `json:"attachments,omitempty"`
76 // StopReason is the ACP stop reason (OutgoingTurnEnded).92 // StopReason is the ACP stop reason (OutgoingTurnEnded).
77 StopReason string `json:"stopReason,omitempty"`93 StopReason string `json:"stopReason,omitempty"`
78 // Message is a human-readable error (OutgoingError).94 // Message is a human-readable error (OutgoingError).
modified internal/bridge/ws_test.go +1 -1
@@ -11,7 +11,7 @@ import (
1111 "github.com/coder/websocket"
1212 "github.com/coder/websocket/wsjson"
1313
14- "github.com/bots-garden/ori/internal/bridge"
14+ "rickub.com/bots-garden/ori/internal/bridge"
1515 )
1616
1717 // dialTestServer starts an HTTP test server exposing the bridge's WebSocket
@@ -11,7 +11,7 @@ import (
11 "github.com/coder/websocket"11 "github.com/coder/websocket"
12 "github.com/coder/websocket/wsjson"12 "github.com/coder/websocket/wsjson"
13 13
14- "github.com/bots-garden/ori/internal/bridge"14+ "rickub.com/bots-garden/ori/internal/bridge"
15 )15 )
16 16
17 // dialTestServer starts an HTTP test server exposing the bridge's WebSocket17 // dialTestServer starts an HTTP test server exposing the bridge's WebSocket
modified internal/config/config_test.go +1 -1
@@ -7,7 +7,7 @@ import (
77 "strings"
88 "testing"
99
10- "github.com/bots-garden/ori/internal/config"
10+ "rickub.com/bots-garden/ori/internal/config"
1111 )
1212
1313 func TestFromArgsDefaults(t *testing.T) {
@@ -7,7 +7,7 @@ import (
7 "strings"7 "strings"
8 "testing"8 "testing"
9 9
10- "github.com/bots-garden/ori/internal/config"10+ "rickub.com/bots-garden/ori/internal/config"
11 )11 )
12 12
13 func TestFromArgsDefaults(t *testing.T) {13 func TestFromArgsDefaults(t *testing.T) {
added internal/files/files.go +188 -0
new file mode 100644
@@ -0,0 +1,188 @@
1+// Package files exposes the workspace file API consumed by the SPA's file
2+// tree, preview and editor panels: list a directory, search files by name,
3+// read a text file, stream a file's raw bytes, write a file back.
4+//
5+// Access is deliberately unrestricted (absolute paths welcome): ori is meant
6+// to run inside an already-isolated sandbox, and the agent itself has the
7+// same reach. Relative paths resolve against the configured root (the --cwd
8+// given to the agent session).
9+package files
10+
11+import (
12+ "encoding/json"
13+ "errors"
14+ "io/fs"
15+ "net/http"
16+ "os"
17+ "path/filepath"
18+ "sort"
19+ "unicode/utf8"
20+)
21+
22+// maxFileBytes bounds what the read endpoint returns; larger files are
23+// refused rather than truncated, so the editor never silently saves a
24+// partial file back.
25+const maxFileBytes = 5 << 20 // 5 MiB
26+
27+// Service answers the file API requests for one workspace root.
28+type Service struct {
29+ root string
30+}
31+
32+// New creates a Service whose relative paths resolve against root.
33+//
34+// Example:
35+//
36+// svc := files.New("/work/my-project")
37+// for pattern, handler := range svc.Routes() {
38+// mux.Handle(pattern, handler)
39+// }
40+func New(root string) *Service {
41+ return &Service{root: root}
42+}
43+
44+// Routes returns the API endpoints, keyed by http.ServeMux pattern, ready to
45+// be merged into the server's route table.
46+func (s *Service) Routes() map[string]http.Handler {
47+ return map[string]http.Handler{
48+ "GET /api/files": http.HandlerFunc(s.listDirectory),
49+ "GET /api/files/search": http.HandlerFunc(s.searchFiles),
50+ "GET /api/file": http.HandlerFunc(s.readFile),
51+ "PUT /api/file": http.HandlerFunc(s.writeFile),
52+ "GET /api/raw": http.HandlerFunc(s.rawFile),
53+ }
54+}
55+
56+// Entry describes one child of a listed directory.
57+type Entry struct {
58+ // Name is the base name of the entry.
59+ Name string `json:"name"`
60+ // Path is the resolved path, directly usable in further API calls.
61+ Path string `json:"path"`
62+ // IsDir tells directories and files apart.
63+ IsDir bool `json:"isDir"`
64+ // Size is the file size in bytes (0 for directories).
65+ Size int64 `json:"size"`
66+}
67+
68+// resolve turns a request path into the path to operate on: empty means the
69+// root, relative paths are joined to it, absolute paths are used as-is.
70+func (s *Service) resolve(path string) string {
71+ if path == "" {
72+ return s.root
73+ }
74+ if !filepath.IsAbs(path) {
75+ return filepath.Join(s.root, path)
76+ }
77+ return filepath.Clean(path)
78+}
79+
80+func (s *Service) listDirectory(w http.ResponseWriter, r *http.Request) {
81+ dir := s.resolve(r.URL.Query().Get("path"))
82+ dirEntries, err := os.ReadDir(dir)
83+ if err != nil {
84+ writeFSError(w, err)
85+ return
86+ }
87+
88+ entries := make([]Entry, 0, len(dirEntries))
89+ for _, e := range dirEntries {
90+ info, infoErr := e.Info()
91+ size := int64(0)
92+ if infoErr == nil && !e.IsDir() {
93+ size = info.Size()
94+ }
95+ entries = append(entries, Entry{
96+ Name: e.Name(),
97+ Path: filepath.Join(dir, e.Name()),
98+ IsDir: e.IsDir(),
99+ Size: size,
100+ })
101+ }
102+ // Directories first, then files, each group alphabetically — the order
103+ // the file tree renders in.
104+ sort.Slice(entries, func(i, j int) bool {
105+ if entries[i].IsDir != entries[j].IsDir {
106+ return entries[i].IsDir
107+ }
108+ return entries[i].Name < entries[j].Name
109+ })
110+
111+ writeJSON(w, http.StatusOK, map[string]any{"path": dir, "entries": entries})
112+}
113+
114+func (s *Service) readFile(w http.ResponseWriter, r *http.Request) {
115+ path := s.resolve(r.URL.Query().Get("path"))
116+ info, err := os.Stat(path)
117+ if err != nil {
118+ writeFSError(w, err)
119+ return
120+ }
121+ if info.IsDir() {
122+ writeError(w, http.StatusBadRequest, "path is a directory")
123+ return
124+ }
125+ if info.Size() > maxFileBytes {
126+ writeError(w, http.StatusRequestEntityTooLarge, "file larger than 5 MiB")
127+ return
128+ }
129+ content, err := os.ReadFile(path)
130+ if err != nil {
131+ writeFSError(w, err)
132+ return
133+ }
134+ if !utf8.Valid(content) {
135+ writeError(w, http.StatusUnsupportedMediaType, "not a text file")
136+ return
137+ }
138+ writeJSON(w, http.StatusOK, map[string]any{"path": path, "content": string(content)})
139+}
140+
141+func (s *Service) writeFile(w http.ResponseWriter, r *http.Request) {
142+ path := s.resolve(r.URL.Query().Get("path"))
143+ if r.URL.Query().Get("path") == "" {
144+ writeError(w, http.StatusBadRequest, "path query parameter is required")
145+ return
146+ }
147+
148+ var body struct {
149+ Content string `json:"content"`
150+ }
151+ if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxFileBytes)).Decode(&body); err != nil {
152+ writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error())
153+ return
154+ }
155+ if dir := filepath.Dir(path); dir != "" {
156+ if err := os.MkdirAll(dir, 0o755); err != nil {
157+ writeFSError(w, err)
158+ return
159+ }
160+ }
161+ if err := os.WriteFile(path, []byte(body.Content), 0o644); err != nil {
162+ writeFSError(w, err)
163+ return
164+ }
165+ writeJSON(w, http.StatusOK, map[string]any{"path": path, "status": "saved"})
166+}
167+
168+// writeFSError maps filesystem errors to HTTP statuses.
169+func writeFSError(w http.ResponseWriter, err error) {
170+ status := http.StatusInternalServerError
171+ switch {
172+ case errors.Is(err, fs.ErrNotExist):
173+ status = http.StatusNotFound
174+ case errors.Is(err, fs.ErrPermission):
175+ status = http.StatusForbidden
176+ }
177+ writeError(w, status, err.Error())
178+}
179+
180+func writeError(w http.ResponseWriter, status int, message string) {
181+ writeJSON(w, status, map[string]any{"error": message})
182+}
183+
184+func writeJSON(w http.ResponseWriter, status int, payload any) {
185+ w.Header().Set("Content-Type", "application/json")
186+ w.WriteHeader(status)
187+ _ = json.NewEncoder(w).Encode(payload)
188+}
new file mode 100644
@@ -0,0 +1,188 @@
1+// Package files exposes the workspace file API consumed by the SPA's file
2+// tree, preview and editor panels: list a directory, search files by name,
3+// read a text file, stream a file's raw bytes, write a file back.
4+//
5+// Access is deliberately unrestricted (absolute paths welcome): ori is meant
6+// to run inside an already-isolated sandbox, and the agent itself has the
7+// same reach. Relative paths resolve against the configured root (the --cwd
8+// given to the agent session).
9+package files
10+
11+import (
12+ "encoding/json"
13+ "errors"
14+ "io/fs"
15+ "net/http"
16+ "os"
17+ "path/filepath"
18+ "sort"
19+ "unicode/utf8"
20+)
21+
22+// maxFileBytes bounds what the read endpoint returns; larger files are
23+// refused rather than truncated, so the editor never silently saves a
24+// partial file back.
25+const maxFileBytes = 5 << 20 // 5 MiB
26+
27+// Service answers the file API requests for one workspace root.
28+type Service struct {
29+ root string
30+}
31+
32+// New creates a Service whose relative paths resolve against root.
33+//
34+// Example:
35+//
36+// svc := files.New("/work/my-project")
37+// for pattern, handler := range svc.Routes() {
38+// mux.Handle(pattern, handler)
39+// }
40+func New(root string) *Service {
41+ return &Service{root: root}
42+}
43+
44+// Routes returns the API endpoints, keyed by http.ServeMux pattern, ready to
45+// be merged into the server's route table.
46+func (s *Service) Routes() map[string]http.Handler {
47+ return map[string]http.Handler{
48+ "GET /api/files": http.HandlerFunc(s.listDirectory),
49+ "GET /api/files/search": http.HandlerFunc(s.searchFiles),
50+ "GET /api/file": http.HandlerFunc(s.readFile),
51+ "PUT /api/file": http.HandlerFunc(s.writeFile),
52+ "GET /api/raw": http.HandlerFunc(s.rawFile),
53+ }
54+}
55+
56+// Entry describes one child of a listed directory.
57+type Entry struct {
58+ // Name is the base name of the entry.
59+ Name string `json:"name"`
60+ // Path is the resolved path, directly usable in further API calls.
61+ Path string `json:"path"`
62+ // IsDir tells directories and files apart.
63+ IsDir bool `json:"isDir"`
64+ // Size is the file size in bytes (0 for directories).
65+ Size int64 `json:"size"`
66+}
67+
68+// resolve turns a request path into the path to operate on: empty means the
69+// root, relative paths are joined to it, absolute paths are used as-is.
70+func (s *Service) resolve(path string) string {
71+ if path == "" {
72+ return s.root
73+ }
74+ if !filepath.IsAbs(path) {
75+ return filepath.Join(s.root, path)
76+ }
77+ return filepath.Clean(path)
78+}
79+
80+func (s *Service) listDirectory(w http.ResponseWriter, r *http.Request) {
81+ dir := s.resolve(r.URL.Query().Get("path"))
82+ dirEntries, err := os.ReadDir(dir)
83+ if err != nil {
84+ writeFSError(w, err)
85+ return
86+ }
87+
88+ entries := make([]Entry, 0, len(dirEntries))
89+ for _, e := range dirEntries {
90+ info, infoErr := e.Info()
91+ size := int64(0)
92+ if infoErr == nil && !e.IsDir() {
93+ size = info.Size()
94+ }
95+ entries = append(entries, Entry{
96+ Name: e.Name(),
97+ Path: filepath.Join(dir, e.Name()),
98+ IsDir: e.IsDir(),
99+ Size: size,
100+ })
101+ }
102+ // Directories first, then files, each group alphabetically — the order
103+ // the file tree renders in.
104+ sort.Slice(entries, func(i, j int) bool {
105+ if entries[i].IsDir != entries[j].IsDir {
106+ return entries[i].IsDir
107+ }
108+ return entries[i].Name < entries[j].Name
109+ })
110+
111+ writeJSON(w, http.StatusOK, map[string]any{"path": dir, "entries": entries})
112+}
113+
114+func (s *Service) readFile(w http.ResponseWriter, r *http.Request) {
115+ path := s.resolve(r.URL.Query().Get("path"))
116+ info, err := os.Stat(path)
117+ if err != nil {
118+ writeFSError(w, err)
119+ return
120+ }
121+ if info.IsDir() {
122+ writeError(w, http.StatusBadRequest, "path is a directory")
123+ return
124+ }
125+ if info.Size() > maxFileBytes {
126+ writeError(w, http.StatusRequestEntityTooLarge, "file larger than 5 MiB")
127+ return
128+ }
129+ content, err := os.ReadFile(path)
130+ if err != nil {
131+ writeFSError(w, err)
132+ return
133+ }
134+ if !utf8.Valid(content) {
135+ writeError(w, http.StatusUnsupportedMediaType, "not a text file")
136+ return
137+ }
138+ writeJSON(w, http.StatusOK, map[string]any{"path": path, "content": string(content)})
139+}
140+
141+func (s *Service) writeFile(w http.ResponseWriter, r *http.Request) {
142+ path := s.resolve(r.URL.Query().Get("path"))
143+ if r.URL.Query().Get("path") == "" {
144+ writeError(w, http.StatusBadRequest, "path query parameter is required")
145+ return
146+ }
147+
148+ var body struct {
149+ Content string `json:"content"`
150+ }
151+ if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxFileBytes)).Decode(&body); err != nil {
152+ writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error())
153+ return
154+ }
155+ if dir := filepath.Dir(path); dir != "" {
156+ if err := os.MkdirAll(dir, 0o755); err != nil {
157+ writeFSError(w, err)
158+ return
159+ }
160+ }
161+ if err := os.WriteFile(path, []byte(body.Content), 0o644); err != nil {
162+ writeFSError(w, err)
163+ return
164+ }
165+ writeJSON(w, http.StatusOK, map[string]any{"path": path, "status": "saved"})
166+}
167+
168+// writeFSError maps filesystem errors to HTTP statuses.
169+func writeFSError(w http.ResponseWriter, err error) {
170+ status := http.StatusInternalServerError
171+ switch {
172+ case errors.Is(err, fs.ErrNotExist):
173+ status = http.StatusNotFound
174+ case errors.Is(err, fs.ErrPermission):
175+ status = http.StatusForbidden
176+ }
177+ writeError(w, status, err.Error())
178+}
179+
180+func writeError(w http.ResponseWriter, status int, message string) {
181+ writeJSON(w, status, map[string]any{"error": message})
182+}
183+
184+func writeJSON(w http.ResponseWriter, status int, payload any) {
185+ w.Header().Set("Content-Type", "application/json")
186+ w.WriteHeader(status)
187+ _ = json.NewEncoder(w).Encode(payload)
188+}
added internal/files/files_test.go +166 -0
new file mode 100644
@@ -0,0 +1,166 @@
1+package files_test
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+
12+ "rickub.com/bots-garden/ori/internal/files"
13+)
14+
15+// workspace builds a small tree and returns a mux serving the file API on it.
16+func workspace(t *testing.T) (string, http.Handler) {
17+ t.Helper()
18+ root := t.TempDir()
19+ if err := os.MkdirAll(filepath.Join(root, "src"), 0o755); err != nil {
20+ t.Fatal(err)
21+ }
22+ if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# hello"), 0o644); err != nil {
23+ t.Fatal(err)
24+ }
25+ if err := os.WriteFile(filepath.Join(root, "src", "main.go"), []byte("package main"), 0o644); err != nil {
26+ t.Fatal(err)
27+ }
28+
29+ mux := http.NewServeMux()
30+ for pattern, handler := range files.New(root).Routes() {
31+ mux.Handle(pattern, handler)
32+ }
33+ return root, mux
34+}
35+
36+func do(t *testing.T, h http.Handler, method, target, body string) *httptest.ResponseRecorder {
37+ t.Helper()
38+ var req *http.Request
39+ if body == "" {
40+ req = httptest.NewRequest(method, target, nil)
41+ } else {
42+ req = httptest.NewRequest(method, target, strings.NewReader(body))
43+ }
44+ rec := httptest.NewRecorder()
45+ h.ServeHTTP(rec, req)
46+ return rec
47+}
48+
49+func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
50+ t.Helper()
51+ var payload map[string]any
52+ if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
53+ t.Fatalf("response is not JSON: %v (%q)", err, rec.Body.String())
54+ }
55+ return payload
56+}
57+
58+func TestListRootSortsDirectoriesFirst(t *testing.T) {
59+ _, h := workspace(t)
60+ rec := do(t, h, http.MethodGet, "/api/files", "")
61+ if rec.Code != http.StatusOK {
62+ t.Fatalf("GET /api/files = %d, want 200 (%s)", rec.Code, rec.Body.String())
63+ }
64+ payload := decodeBody(t, rec)
65+ entries := payload["entries"].([]any)
66+ if len(entries) != 2 {
67+ t.Fatalf("entries = %d, want 2", len(entries))
68+ }
69+ first := entries[0].(map[string]any)
70+ if first["name"] != "src" || first["isDir"] != true {
71+ t.Errorf("first entry = %v, want the src directory first", first)
72+ }
73+}
74+
75+func TestListSubdirectoryByRelativePath(t *testing.T) {
76+ _, h := workspace(t)
77+ rec := do(t, h, http.MethodGet, "/api/files?path=src", "")
78+ if rec.Code != http.StatusOK {
79+ t.Fatalf("GET /api/files?path=src = %d (%s)", rec.Code, rec.Body.String())
80+ }
81+ entries := decodeBody(t, rec)["entries"].([]any)
82+ if len(entries) != 1 || entries[0].(map[string]any)["name"] != "main.go" {
83+ t.Errorf("entries = %v, want [main.go]", entries)
84+ }
85+}
86+
87+func TestListMissingDirectoryIs404(t *testing.T) {
88+ _, h := workspace(t)
89+ rec := do(t, h, http.MethodGet, "/api/files?path=nope", "")
90+ if rec.Code != http.StatusNotFound {
91+ t.Errorf("GET missing dir = %d, want 404", rec.Code)
92+ }
93+}
94+
95+func TestReadFileByAbsoluteAndRelativePath(t *testing.T) {
96+ root, h := workspace(t)
97+
98+ rec := do(t, h, http.MethodGet, "/api/file?path=README.md", "")
99+ if rec.Code != http.StatusOK {
100+ t.Fatalf("GET relative = %d (%s)", rec.Code, rec.Body.String())
101+ }
102+ if decodeBody(t, rec)["content"] != "# hello" {
103+ t.Errorf("relative read content = %v", decodeBody(t, rec)["content"])
104+ }
105+
106+ abs := filepath.Join(root, "src", "main.go")
107+ rec = do(t, h, http.MethodGet, "/api/file?path="+abs, "")
108+ if rec.Code != http.StatusOK || decodeBody(t, rec)["content"] != "package main" {
109+ t.Errorf("absolute read = %d %v", rec.Code, decodeBody(t, rec))
110+ }
111+}
112+
113+func TestReadDirectoryIsRejected(t *testing.T) {
114+ _, h := workspace(t)
115+ rec := do(t, h, http.MethodGet, "/api/file?path=src", "")
116+ if rec.Code != http.StatusBadRequest {
117+ t.Errorf("GET a directory = %d, want 400", rec.Code)
118+ }
119+}
120+
121+func TestReadBinaryFileIs415(t *testing.T) {
122+ root, h := workspace(t)
123+ if err := os.WriteFile(filepath.Join(root, "blob.bin"), []byte{0xff, 0xfe, 0x00, 0x80}, 0o644); err != nil {
124+ t.Fatal(err)
125+ }
126+ rec := do(t, h, http.MethodGet, "/api/file?path=blob.bin", "")
127+ if rec.Code != http.StatusUnsupportedMediaType {
128+ t.Errorf("GET binary = %d, want 415", rec.Code)
129+ }
130+}
131+
132+func TestReadMissingFileIs404(t *testing.T) {
133+ _, h := workspace(t)
134+ rec := do(t, h, http.MethodGet, "/api/file?path=ghost.txt", "")
135+ if rec.Code != http.StatusNotFound {
136+ t.Errorf("GET missing file = %d, want 404", rec.Code)
137+ }
138+}
139+
140+func TestWriteRoundTripAndParentCreation(t *testing.T) {
141+ root, h := workspace(t)
142+
143+ rec := do(t, h, http.MethodPut, "/api/file?path=deep/dir/new.txt", `{"content":"saved by test"}`)
144+ if rec.Code != http.StatusOK {
145+ t.Fatalf("PUT = %d (%s)", rec.Code, rec.Body.String())
146+ }
147+ onDisk, err := os.ReadFile(filepath.Join(root, "deep", "dir", "new.txt"))
148+ if err != nil || string(onDisk) != "saved by test" {
149+ t.Fatalf("written file = %q, %v", onDisk, err)
150+ }
151+
152+ rec = do(t, h, http.MethodGet, "/api/file?path=deep/dir/new.txt", "")
153+ if decodeBody(t, rec)["content"] != "saved by test" {
154+ t.Errorf("read-back content = %v", decodeBody(t, rec)["content"])
155+ }
156+}
157+
158+func TestWriteRequiresPathAndValidJSON(t *testing.T) {
159+ _, h := workspace(t)
160+ if rec := do(t, h, http.MethodPut, "/api/file", `{"content":"x"}`); rec.Code != http.StatusBadRequest {
161+ t.Errorf("PUT without path = %d, want 400", rec.Code)
162+ }
163+ if rec := do(t, h, http.MethodPut, "/api/file?path=x.txt", `{not json`); rec.Code != http.StatusBadRequest {
164+ t.Errorf("PUT invalid JSON = %d, want 400", rec.Code)
165+ }
166+}
new file mode 100644
@@ -0,0 +1,166 @@
1+package files_test
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+
12+ "rickub.com/bots-garden/ori/internal/files"
13+)
14+
15+// workspace builds a small tree and returns a mux serving the file API on it.
16+func workspace(t *testing.T) (string, http.Handler) {
17+ t.Helper()
18+ root := t.TempDir()
19+ if err := os.MkdirAll(filepath.Join(root, "src"), 0o755); err != nil {
20+ t.Fatal(err)
21+ }
22+ if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# hello"), 0o644); err != nil {
23+ t.Fatal(err)
24+ }
25+ if err := os.WriteFile(filepath.Join(root, "src", "main.go"), []byte("package main"), 0o644); err != nil {
26+ t.Fatal(err)
27+ }
28+
29+ mux := http.NewServeMux()
30+ for pattern, handler := range files.New(root).Routes() {
31+ mux.Handle(pattern, handler)
32+ }
33+ return root, mux
34+}
35+
36+func do(t *testing.T, h http.Handler, method, target, body string) *httptest.ResponseRecorder {
37+ t.Helper()
38+ var req *http.Request
39+ if body == "" {
40+ req = httptest.NewRequest(method, target, nil)
41+ } else {
42+ req = httptest.NewRequest(method, target, strings.NewReader(body))
43+ }
44+ rec := httptest.NewRecorder()
45+ h.ServeHTTP(rec, req)
46+ return rec
47+}
48+
49+func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
50+ t.Helper()
51+ var payload map[string]any
52+ if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
53+ t.Fatalf("response is not JSON: %v (%q)", err, rec.Body.String())
54+ }
55+ return payload
56+}
57+
58+func TestListRootSortsDirectoriesFirst(t *testing.T) {
59+ _, h := workspace(t)
60+ rec := do(t, h, http.MethodGet, "/api/files", "")
61+ if rec.Code != http.StatusOK {
62+ t.Fatalf("GET /api/files = %d, want 200 (%s)", rec.Code, rec.Body.String())
63+ }
64+ payload := decodeBody(t, rec)
65+ entries := payload["entries"].([]any)
66+ if len(entries) != 2 {
67+ t.Fatalf("entries = %d, want 2", len(entries))
68+ }
69+ first := entries[0].(map[string]any)
70+ if first["name"] != "src" || first["isDir"] != true {
71+ t.Errorf("first entry = %v, want the src directory first", first)
72+ }
73+}
74+
75+func TestListSubdirectoryByRelativePath(t *testing.T) {
76+ _, h := workspace(t)
77+ rec := do(t, h, http.MethodGet, "/api/files?path=src", "")
78+ if rec.Code != http.StatusOK {
79+ t.Fatalf("GET /api/files?path=src = %d (%s)", rec.Code, rec.Body.String())
80+ }
81+ entries := decodeBody(t, rec)["entries"].([]any)
82+ if len(entries) != 1 || entries[0].(map[string]any)["name"] != "main.go" {
83+ t.Errorf("entries = %v, want [main.go]", entries)
84+ }
85+}
86+
87+func TestListMissingDirectoryIs404(t *testing.T) {
88+ _, h := workspace(t)
89+ rec := do(t, h, http.MethodGet, "/api/files?path=nope", "")
90+ if rec.Code != http.StatusNotFound {
91+ t.Errorf("GET missing dir = %d, want 404", rec.Code)
92+ }
93+}
94+
95+func TestReadFileByAbsoluteAndRelativePath(t *testing.T) {
96+ root, h := workspace(t)
97+
98+ rec := do(t, h, http.MethodGet, "/api/file?path=README.md", "")
99+ if rec.Code != http.StatusOK {
100+ t.Fatalf("GET relative = %d (%s)", rec.Code, rec.Body.String())
101+ }
102+ if decodeBody(t, rec)["content"] != "# hello" {
103+ t.Errorf("relative read content = %v", decodeBody(t, rec)["content"])
104+ }
105+
106+ abs := filepath.Join(root, "src", "main.go")
107+ rec = do(t, h, http.MethodGet, "/api/file?path="+abs, "")
108+ if rec.Code != http.StatusOK || decodeBody(t, rec)["content"] != "package main" {
109+ t.Errorf("absolute read = %d %v", rec.Code, decodeBody(t, rec))
110+ }
111+}
112+
113+func TestReadDirectoryIsRejected(t *testing.T) {
114+ _, h := workspace(t)
115+ rec := do(t, h, http.MethodGet, "/api/file?path=src", "")
116+ if rec.Code != http.StatusBadRequest {
117+ t.Errorf("GET a directory = %d, want 400", rec.Code)
118+ }
119+}
120+
121+func TestReadBinaryFileIs415(t *testing.T) {
122+ root, h := workspace(t)
123+ if err := os.WriteFile(filepath.Join(root, "blob.bin"), []byte{0xff, 0xfe, 0x00, 0x80}, 0o644); err != nil {
124+ t.Fatal(err)
125+ }
126+ rec := do(t, h, http.MethodGet, "/api/file?path=blob.bin", "")
127+ if rec.Code != http.StatusUnsupportedMediaType {
128+ t.Errorf("GET binary = %d, want 415", rec.Code)
129+ }
130+}
131+
132+func TestReadMissingFileIs404(t *testing.T) {
133+ _, h := workspace(t)
134+ rec := do(t, h, http.MethodGet, "/api/file?path=ghost.txt", "")
135+ if rec.Code != http.StatusNotFound {
136+ t.Errorf("GET missing file = %d, want 404", rec.Code)
137+ }
138+}
139+
140+func TestWriteRoundTripAndParentCreation(t *testing.T) {
141+ root, h := workspace(t)
142+
143+ rec := do(t, h, http.MethodPut, "/api/file?path=deep/dir/new.txt", `{"content":"saved by test"}`)
144+ if rec.Code != http.StatusOK {
145+ t.Fatalf("PUT = %d (%s)", rec.Code, rec.Body.String())
146+ }
147+ onDisk, err := os.ReadFile(filepath.Join(root, "deep", "dir", "new.txt"))
148+ if err != nil || string(onDisk) != "saved by test" {
149+ t.Fatalf("written file = %q, %v", onDisk, err)
150+ }
151+
152+ rec = do(t, h, http.MethodGet, "/api/file?path=deep/dir/new.txt", "")
153+ if decodeBody(t, rec)["content"] != "saved by test" {
154+ t.Errorf("read-back content = %v", decodeBody(t, rec)["content"])
155+ }
156+}
157+
158+func TestWriteRequiresPathAndValidJSON(t *testing.T) {
159+ _, h := workspace(t)
160+ if rec := do(t, h, http.MethodPut, "/api/file", `{"content":"x"}`); rec.Code != http.StatusBadRequest {
161+ t.Errorf("PUT without path = %d, want 400", rec.Code)
162+ }
163+ if rec := do(t, h, http.MethodPut, "/api/file?path=x.txt", `{not json`); rec.Code != http.StatusBadRequest {
164+ t.Errorf("PUT invalid JSON = %d, want 400", rec.Code)
165+ }
166+}
added internal/files/raw.go +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+package files
2+
3+import (
4+ "mime"
5+ "net/http"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+)
10+
11+// imageTypes pins the Content-Type of the image formats the preview pane
12+// renders, independently of the host's mime database (which may lack
13+// entries such as .webp or .avif, or spell .ico differently).
14+var imageTypes = map[string]string{
15+ ".avif": "image/avif",
16+ ".bmp": "image/bmp",
17+ ".gif": "image/gif",
18+ ".ico": "image/x-icon",
19+ ".jpeg": "image/jpeg",
20+ ".jpg": "image/jpeg",
21+ ".png": "image/png",
22+ ".svg": "image/svg+xml",
23+ ".webp": "image/webp",
24+}
25+
26+// ContentTypeFor returns the Content-Type the raw endpoint serves a file
27+// under: pinned image types first, then the host mime database, and a
28+// content sniff of the first bytes as a last resort.
29+//
30+// Example:
31+//
32+// files.ContentTypeFor("logo.webp", nil) // "image/webp"
33+func ContentTypeFor(path string, head []byte) string {
34+ ext := strings.ToLower(filepath.Ext(path))
35+ if typ, ok := imageTypes[ext]; ok {
36+ return typ
37+ }
38+ if typ := mime.TypeByExtension(ext); typ != "" {
39+ return typ
40+ }
41+ return http.DetectContentType(head)
42+}
43+
44+// rawFile streams a file's bytes as-is, for the browser to render (images)
45+// or download. Unlike readFile it has no size cap and no text requirement:
46+// http.ServeContent streams and honours Range requests.
47+func (s *Service) rawFile(w http.ResponseWriter, r *http.Request) {
48+ if r.URL.Query().Get("path") == "" {
49+ writeError(w, http.StatusBadRequest, "path query parameter is required")
50+ return
51+ }
52+ path := s.resolve(r.URL.Query().Get("path"))
53+ info, err := os.Stat(path)
54+ if err != nil {
55+ writeFSError(w, err)
56+ return
57+ }
58+ if info.IsDir() {
59+ writeError(w, http.StatusBadRequest, "path is a directory")
60+ return
61+ }
62+ file, err := os.Open(path)
63+ if err != nil {
64+ writeFSError(w, err)
65+ return
66+ }
67+ // Read-only handle: a Close error carries no information the client
68+ // could act on, and the response headers are already committed.
69+ defer func() { _ = file.Close() }()
70+
71+ head := make([]byte, 512)
72+ n, _ := file.Read(head)
73+ if _, err := file.Seek(0, 0); err != nil {
74+ writeFSError(w, err)
75+ return
76+ }
77+ w.Header().Set("Content-Type", ContentTypeFor(path, head[:n]))
78+ w.Header().Set("X-Content-Type-Options", "nosniff")
79+ http.ServeContent(w, r, info.Name(), info.ModTime(), file)
80+}
new file mode 100644
@@ -0,0 +1,80 @@
1+package files
2+
3+import (
4+ "mime"
5+ "net/http"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+)
10+
11+// imageTypes pins the Content-Type of the image formats the preview pane
12+// renders, independently of the host's mime database (which may lack
13+// entries such as .webp or .avif, or spell .ico differently).
14+var imageTypes = map[string]string{
15+ ".avif": "image/avif",
16+ ".bmp": "image/bmp",
17+ ".gif": "image/gif",
18+ ".ico": "image/x-icon",
19+ ".jpeg": "image/jpeg",
20+ ".jpg": "image/jpeg",
21+ ".png": "image/png",
22+ ".svg": "image/svg+xml",
23+ ".webp": "image/webp",
24+}
25+
26+// ContentTypeFor returns the Content-Type the raw endpoint serves a file
27+// under: pinned image types first, then the host mime database, and a
28+// content sniff of the first bytes as a last resort.
29+//
30+// Example:
31+//
32+// files.ContentTypeFor("logo.webp", nil) // "image/webp"
33+func ContentTypeFor(path string, head []byte) string {
34+ ext := strings.ToLower(filepath.Ext(path))
35+ if typ, ok := imageTypes[ext]; ok {
36+ return typ
37+ }
38+ if typ := mime.TypeByExtension(ext); typ != "" {
39+ return typ
40+ }
41+ return http.DetectContentType(head)
42+}
43+
44+// rawFile streams a file's bytes as-is, for the browser to render (images)
45+// or download. Unlike readFile it has no size cap and no text requirement:
46+// http.ServeContent streams and honours Range requests.
47+func (s *Service) rawFile(w http.ResponseWriter, r *http.Request) {
48+ if r.URL.Query().Get("path") == "" {
49+ writeError(w, http.StatusBadRequest, "path query parameter is required")
50+ return
51+ }
52+ path := s.resolve(r.URL.Query().Get("path"))
53+ info, err := os.Stat(path)
54+ if err != nil {
55+ writeFSError(w, err)
56+ return
57+ }
58+ if info.IsDir() {
59+ writeError(w, http.StatusBadRequest, "path is a directory")
60+ return
61+ }
62+ file, err := os.Open(path)
63+ if err != nil {
64+ writeFSError(w, err)
65+ return
66+ }
67+ // Read-only handle: a Close error carries no information the client
68+ // could act on, and the response headers are already committed.
69+ defer func() { _ = file.Close() }()
70+
71+ head := make([]byte, 512)
72+ n, _ := file.Read(head)
73+ if _, err := file.Seek(0, 0); err != nil {
74+ writeFSError(w, err)
75+ return
76+ }
77+ w.Header().Set("Content-Type", ContentTypeFor(path, head[:n]))
78+ w.Header().Set("X-Content-Type-Options", "nosniff")
79+ http.ServeContent(w, r, info.Name(), info.ModTime(), file)
80+}
added internal/files/raw_test.go +87 -0
new file mode 100644
@@ -0,0 +1,87 @@
1+package files_test
2+
3+import (
4+ "net/http"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "rickub.com/bots-garden/ori/internal/files"
10+)
11+
12+var pngHeader = []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0}
13+
14+func TestRawStreamsBytesWithImageContentType(t *testing.T) {
15+ root, h := workspace(t)
16+ if err := os.WriteFile(filepath.Join(root, "logo.png"), pngHeader, 0o644); err != nil {
17+ t.Fatal(err)
18+ }
19+
20+ rec := do(t, h, http.MethodGet, "/api/raw?path=logo.png", "")
21+ if rec.Code != http.StatusOK {
22+ t.Fatalf("GET raw = %d (%s)", rec.Code, rec.Body.String())
23+ }
24+ if got := rec.Header().Get("Content-Type"); got != "image/png" {
25+ t.Errorf("Content-Type = %q, want image/png", got)
26+ }
27+ if got := rec.Body.Bytes(); string(got) != string(pngHeader) {
28+ t.Errorf("body = %v, want the exact file bytes", got)
29+ }
30+ if rec.Header().Get("Content-Length") != "12" {
31+ t.Errorf("Content-Length = %q, want 12", rec.Header().Get("Content-Length"))
32+ }
33+}
34+
35+func TestRawAcceptsAbsolutePathsLikeTheOtherEndpoints(t *testing.T) {
36+ root, h := workspace(t)
37+ rec := do(t, h, http.MethodGet, "/api/raw?path="+filepath.Join(root, "README.md"), "")
38+ if rec.Code != http.StatusOK || rec.Body.String() != "# hello" {
39+ t.Errorf("absolute raw read = %d %q", rec.Code, rec.Body.String())
40+ }
41+ if got := rec.Header().Get("Content-Type"); got != "text/markdown; charset=utf-8" {
42+ t.Errorf("Content-Type = %q, want the mime table's markdown type", got)
43+ }
44+}
45+
46+func TestRawErrors(t *testing.T) {
47+ _, h := workspace(t)
48+ cases := []struct {
49+ target string
50+ want int
51+ }{
52+ {"/api/raw", http.StatusBadRequest},
53+ {"/api/raw?path=src", http.StatusBadRequest},
54+ {"/api/raw?path=ghost.png", http.StatusNotFound},
55+ }
56+ for _, tc := range cases {
57+ if rec := do(t, h, http.MethodGet, tc.target, ""); rec.Code != tc.want {
58+ t.Errorf("GET %s = %d, want %d", tc.target, rec.Code, tc.want)
59+ }
60+ }
61+}
62+
63+func TestContentTypeFor(t *testing.T) {
64+ cases := []struct {
65+ path string
66+ head []byte
67+ want string
68+ }{
69+ {"a.png", nil, "image/png"},
70+ {"a.JPG", nil, "image/jpeg"},
71+ {"a.jpeg", nil, "image/jpeg"},
72+ {"a.gif", nil, "image/gif"},
73+ {"a.webp", nil, "image/webp"},
74+ {"a.svg", nil, "image/svg+xml"},
75+ {"diagram.drawio.svg", nil, "image/svg+xml"},
76+ {"a.bmp", nil, "image/bmp"},
77+ {"a.ico", nil, "image/x-icon"},
78+ {"a.avif", nil, "image/avif"},
79+ {"a.json", nil, "application/json"},
80+ {"noext", []byte("plain words"), "text/plain; charset=utf-8"},
81+ }
82+ for _, tc := range cases {
83+ if got := files.ContentTypeFor(tc.path, tc.head); got != tc.want {
84+ t.Errorf("ContentTypeFor(%q) = %q, want %q", tc.path, got, tc.want)
85+ }
86+ }
87+}
new file mode 100644
@@ -0,0 +1,87 @@
1+package files_test
2+
3+import (
4+ "net/http"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "rickub.com/bots-garden/ori/internal/files"
10+)
11+
12+var pngHeader = []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0}
13+
14+func TestRawStreamsBytesWithImageContentType(t *testing.T) {
15+ root, h := workspace(t)
16+ if err := os.WriteFile(filepath.Join(root, "logo.png"), pngHeader, 0o644); err != nil {
17+ t.Fatal(err)
18+ }
19+
20+ rec := do(t, h, http.MethodGet, "/api/raw?path=logo.png", "")
21+ if rec.Code != http.StatusOK {
22+ t.Fatalf("GET raw = %d (%s)", rec.Code, rec.Body.String())
23+ }
24+ if got := rec.Header().Get("Content-Type"); got != "image/png" {
25+ t.Errorf("Content-Type = %q, want image/png", got)
26+ }
27+ if got := rec.Body.Bytes(); string(got) != string(pngHeader) {
28+ t.Errorf("body = %v, want the exact file bytes", got)
29+ }
30+ if rec.Header().Get("Content-Length") != "12" {
31+ t.Errorf("Content-Length = %q, want 12", rec.Header().Get("Content-Length"))
32+ }
33+}
34+
35+func TestRawAcceptsAbsolutePathsLikeTheOtherEndpoints(t *testing.T) {
36+ root, h := workspace(t)
37+ rec := do(t, h, http.MethodGet, "/api/raw?path="+filepath.Join(root, "README.md"), "")
38+ if rec.Code != http.StatusOK || rec.Body.String() != "# hello" {
39+ t.Errorf("absolute raw read = %d %q", rec.Code, rec.Body.String())
40+ }
41+ if got := rec.Header().Get("Content-Type"); got != "text/markdown; charset=utf-8" {
42+ t.Errorf("Content-Type = %q, want the mime table's markdown type", got)
43+ }
44+}
45+
46+func TestRawErrors(t *testing.T) {
47+ _, h := workspace(t)
48+ cases := []struct {
49+ target string
50+ want int
51+ }{
52+ {"/api/raw", http.StatusBadRequest},
53+ {"/api/raw?path=src", http.StatusBadRequest},
54+ {"/api/raw?path=ghost.png", http.StatusNotFound},
55+ }
56+ for _, tc := range cases {
57+ if rec := do(t, h, http.MethodGet, tc.target, ""); rec.Code != tc.want {
58+ t.Errorf("GET %s = %d, want %d", tc.target, rec.Code, tc.want)
59+ }
60+ }
61+}
62+
63+func TestContentTypeFor(t *testing.T) {
64+ cases := []struct {
65+ path string
66+ head []byte
67+ want string
68+ }{
69+ {"a.png", nil, "image/png"},
70+ {"a.JPG", nil, "image/jpeg"},
71+ {"a.jpeg", nil, "image/jpeg"},
72+ {"a.gif", nil, "image/gif"},
73+ {"a.webp", nil, "image/webp"},
74+ {"a.svg", nil, "image/svg+xml"},
75+ {"diagram.drawio.svg", nil, "image/svg+xml"},
76+ {"a.bmp", nil, "image/bmp"},
77+ {"a.ico", nil, "image/x-icon"},
78+ {"a.avif", nil, "image/avif"},
79+ {"a.json", nil, "application/json"},
80+ {"noext", []byte("plain words"), "text/plain; charset=utf-8"},
81+ }
82+ for _, tc := range cases {
83+ if got := files.ContentTypeFor(tc.path, tc.head); got != tc.want {
84+ t.Errorf("ContentTypeFor(%q) = %q, want %q", tc.path, got, tc.want)
85+ }
86+ }
87+}
added internal/files/search.go +144 -0
new file mode 100644
@@ -0,0 +1,144 @@
1+package files
2+
3+import (
4+ "io/fs"
5+ "net/http"
6+ "os"
7+ "path/filepath"
8+ "sort"
9+ "strconv"
10+ "strings"
11+)
12+
13+// Search limits: the endpoint feeds an autocomplete popup, so it favours a
14+// fast, bounded answer over exhaustiveness on huge trees.
15+const (
16+ defaultSearchLimit = 50
17+ maxSearchLimit = 500
18+ // maxSearchVisited bounds the number of directory entries walked per
19+ // request, so a giant workspace cannot stall the server.
20+ maxSearchVisited = 50000
21+)
22+
23+// skippedDirectories are never descended into: their contents are noise for a
24+// file mention (and node_modules alone can hold hundreds of thousands of files).
25+var skippedDirectories = map[string]bool{
26+ ".git": true,
27+ "node_modules": true,
28+}
29+
30+// Hit is one file matched by the search endpoint.
31+type Hit struct {
32+ // Name is the base name of the file.
33+ Name string `json:"name"`
34+ // Path is the absolute path, directly usable in further API calls.
35+ Path string `json:"path"`
36+ // RelPath is the path relative to the workspace root, using forward
37+ // slashes: the form a mention such as "@src/main.go" displays.
38+ RelPath string `json:"relPath"`
39+}
40+
41+// Search walks the workspace root recursively and returns the files whose
42+// relative path contains query (case-insensitive), at most limit of them.
43+// An empty query returns the first files in walk order. Results are ranked:
44+// base-name matches first, then shorter paths.
45+//
46+// Example:
47+//
48+// hits := files.New("/work").Search("main", 20)
49+// // [{Name:"main.go" Path:"/work/cmd/main.go" RelPath:"cmd/main.go"}]
50+func (s *Service) Search(query string, limit int) []Hit {
51+ if limit <= 0 {
52+ limit = defaultSearchLimit
53+ }
54+ if limit > maxSearchLimit {
55+ limit = maxSearchLimit
56+ }
57+ walker := &searchWalker{root: s.root, needle: strings.ToLower(query)}
58+ _ = filepath.WalkDir(s.root, walker.visit)
59+
60+ rankHits(walker.hits, walker.needle)
61+ if len(walker.hits) > limit {
62+ walker.hits = walker.hits[:limit]
63+ }
64+ return walker.hits
65+}
66+
67+// searchWalker accumulates the files matching needle during one WalkDir.
68+type searchWalker struct {
69+ root string
70+ needle string
71+ visited int
72+ hits []Hit
73+}
74+
75+// visit is the WalkDir callback: it bounds the walk, prunes noisy
76+// directories and collects matching files.
77+func (w *searchWalker) visit(path string, d fs.DirEntry, err error) error {
78+ if err != nil {
79+ return nil // unreadable entries are skipped, not fatal
80+ }
81+ w.visited++
82+ if w.visited > maxSearchVisited {
83+ return fs.SkipAll
84+ }
85+ if d.IsDir() {
86+ return w.visitDir(path, d)
87+ }
88+ w.collect(path, d)
89+ return nil
90+}
91+
92+// visitDir skips the directories whose contents are noise for a mention.
93+func (w *searchWalker) visitDir(path string, d fs.DirEntry) error {
94+ if path != w.root && skippedDirectories[d.Name()] {
95+ return fs.SkipDir
96+ }
97+ return nil
98+}
99+
100+// collect records the file when its relative path contains the needle.
101+func (w *searchWalker) collect(path string, d fs.DirEntry) {
102+ rel, err := filepath.Rel(w.root, path)
103+ if err != nil {
104+ return
105+ }
106+ rel = filepath.ToSlash(rel)
107+ if w.needle != "" && !strings.Contains(strings.ToLower(rel), w.needle) {
108+ return
109+ }
110+ w.hits = append(w.hits, Hit{Name: d.Name(), Path: path, RelPath: rel})
111+}
112+
113+// rankHits orders matches by relevance: files whose base name contains the
114+// query before those matched only through a directory name, then shorter
115+// paths first, then alphabetically for a stable order.
116+func rankHits(hits []Hit, needle string) {
117+ nameMatches := func(h Hit) bool {
118+ return needle == "" || strings.Contains(strings.ToLower(h.Name), needle)
119+ }
120+ sort.SliceStable(hits, func(i, j int) bool {
121+ ni, nj := nameMatches(hits[i]), nameMatches(hits[j])
122+ if ni != nj {
123+ return ni
124+ }
125+ if len(hits[i].RelPath) != len(hits[j].RelPath) {
126+ return len(hits[i].RelPath) < len(hits[j].RelPath)
127+ }
128+ return hits[i].RelPath < hits[j].RelPath
129+ })
130+}
131+
132+func (s *Service) searchFiles(w http.ResponseWriter, r *http.Request) {
133+ query := r.URL.Query()
134+ limit, _ := strconv.Atoi(query.Get("limit"))
135+ if _, err := os.Stat(s.root); err != nil {
136+ writeFSError(w, err)
137+ return
138+ }
139+ hits := s.Search(query.Get("q"), limit)
140+ if hits == nil {
141+ hits = []Hit{}
142+ }
143+ writeJSON(w, http.StatusOK, map[string]any{"root": s.root, "files": hits})
144+}
new file mode 100644
@@ -0,0 +1,144 @@
1+package files
2+
3+import (
4+ "io/fs"
5+ "net/http"
6+ "os"
7+ "path/filepath"
8+ "sort"
9+ "strconv"
10+ "strings"
11+)
12+
13+// Search limits: the endpoint feeds an autocomplete popup, so it favours a
14+// fast, bounded answer over exhaustiveness on huge trees.
15+const (
16+ defaultSearchLimit = 50
17+ maxSearchLimit = 500
18+ // maxSearchVisited bounds the number of directory entries walked per
19+ // request, so a giant workspace cannot stall the server.
20+ maxSearchVisited = 50000
21+)
22+
23+// skippedDirectories are never descended into: their contents are noise for a
24+// file mention (and node_modules alone can hold hundreds of thousands of files).
25+var skippedDirectories = map[string]bool{
26+ ".git": true,
27+ "node_modules": true,
28+}
29+
30+// Hit is one file matched by the search endpoint.
31+type Hit struct {
32+ // Name is the base name of the file.
33+ Name string `json:"name"`
34+ // Path is the absolute path, directly usable in further API calls.
35+ Path string `json:"path"`
36+ // RelPath is the path relative to the workspace root, using forward
37+ // slashes: the form a mention such as "@src/main.go" displays.
38+ RelPath string `json:"relPath"`
39+}
40+
41+// Search walks the workspace root recursively and returns the files whose
42+// relative path contains query (case-insensitive), at most limit of them.
43+// An empty query returns the first files in walk order. Results are ranked:
44+// base-name matches first, then shorter paths.
45+//
46+// Example:
47+//
48+// hits := files.New("/work").Search("main", 20)
49+// // [{Name:"main.go" Path:"/work/cmd/main.go" RelPath:"cmd/main.go"}]
50+func (s *Service) Search(query string, limit int) []Hit {
51+ if limit <= 0 {
52+ limit = defaultSearchLimit
53+ }
54+ if limit > maxSearchLimit {
55+ limit = maxSearchLimit
56+ }
57+ walker := &searchWalker{root: s.root, needle: strings.ToLower(query)}
58+ _ = filepath.WalkDir(s.root, walker.visit)
59+
60+ rankHits(walker.hits, walker.needle)
61+ if len(walker.hits) > limit {
62+ walker.hits = walker.hits[:limit]
63+ }
64+ return walker.hits
65+}
66+
67+// searchWalker accumulates the files matching needle during one WalkDir.
68+type searchWalker struct {
69+ root string
70+ needle string
71+ visited int
72+ hits []Hit
73+}
74+
75+// visit is the WalkDir callback: it bounds the walk, prunes noisy
76+// directories and collects matching files.
77+func (w *searchWalker) visit(path string, d fs.DirEntry, err error) error {
78+ if err != nil {
79+ return nil // unreadable entries are skipped, not fatal
80+ }
81+ w.visited++
82+ if w.visited > maxSearchVisited {
83+ return fs.SkipAll
84+ }
85+ if d.IsDir() {
86+ return w.visitDir(path, d)
87+ }
88+ w.collect(path, d)
89+ return nil
90+}
91+
92+// visitDir skips the directories whose contents are noise for a mention.
93+func (w *searchWalker) visitDir(path string, d fs.DirEntry) error {
94+ if path != w.root && skippedDirectories[d.Name()] {
95+ return fs.SkipDir
96+ }
97+ return nil
98+}
99+
100+// collect records the file when its relative path contains the needle.
101+func (w *searchWalker) collect(path string, d fs.DirEntry) {
102+ rel, err := filepath.Rel(w.root, path)
103+ if err != nil {
104+ return
105+ }
106+ rel = filepath.ToSlash(rel)
107+ if w.needle != "" && !strings.Contains(strings.ToLower(rel), w.needle) {
108+ return
109+ }
110+ w.hits = append(w.hits, Hit{Name: d.Name(), Path: path, RelPath: rel})
111+}
112+
113+// rankHits orders matches by relevance: files whose base name contains the
114+// query before those matched only through a directory name, then shorter
115+// paths first, then alphabetically for a stable order.
116+func rankHits(hits []Hit, needle string) {
117+ nameMatches := func(h Hit) bool {
118+ return needle == "" || strings.Contains(strings.ToLower(h.Name), needle)
119+ }
120+ sort.SliceStable(hits, func(i, j int) bool {
121+ ni, nj := nameMatches(hits[i]), nameMatches(hits[j])
122+ if ni != nj {
123+ return ni
124+ }
125+ if len(hits[i].RelPath) != len(hits[j].RelPath) {
126+ return len(hits[i].RelPath) < len(hits[j].RelPath)
127+ }
128+ return hits[i].RelPath < hits[j].RelPath
129+ })
130+}
131+
132+func (s *Service) searchFiles(w http.ResponseWriter, r *http.Request) {
133+ query := r.URL.Query()
134+ limit, _ := strconv.Atoi(query.Get("limit"))
135+ if _, err := os.Stat(s.root); err != nil {
136+ writeFSError(w, err)
137+ return
138+ }
139+ hits := s.Search(query.Get("q"), limit)
140+ if hits == nil {
141+ hits = []Hit{}
142+ }
143+ writeJSON(w, http.StatusOK, map[string]any{"root": s.root, "files": hits})
144+}
added internal/files/search_test.go +121 -0
new file mode 100644
@@ -0,0 +1,121 @@
1+package files_test
2+
3+import (
4+ "net/http"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "rickub.com/bots-garden/ori/internal/files"
10+)
11+
12+// searchWorkspace extends the basic tree with nested and noisy directories.
13+func searchWorkspace(t *testing.T) (string, http.Handler) {
14+ t.Helper()
15+ root, h := workspace(t)
16+ for _, rel := range []string{
17+ "src/server/main_test.go",
18+ "docs/guide.md",
19+ "node_modules/pkg/index.js",
20+ ".git/HEAD",
21+ } {
22+ full := filepath.Join(root, rel)
23+ if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
24+ t.Fatal(err)
25+ }
26+ if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
27+ t.Fatal(err)
28+ }
29+ }
30+ return root, h
31+}
32+
33+func relPaths(hits []files.Hit) []string {
34+ out := make([]string, 0, len(hits))
35+ for _, h := range hits {
36+ out = append(out, h.RelPath)
37+ }
38+ return out
39+}
40+
41+func TestSearchIsRecursiveAndSkipsNoise(t *testing.T) {
42+ root, _ := searchWorkspace(t)
43+ hits := files.New(root).Search("", 0)
44+ got := relPaths(hits)
45+ // Ranked: shorter relative paths first, alphabetical among equals.
46+ want := []string{"README.md", "src/main.go", "docs/guide.md", "src/server/main_test.go"}
47+ if len(got) != len(want) {
48+ t.Fatalf("hits = %v, want %v", got, want)
49+ }
50+ for i := range want {
51+ if got[i] != want[i] {
52+ t.Errorf("hit[%d] = %q, want %q", i, got[i], want[i])
53+ }
54+ }
55+ if hits[1].Path != filepath.Join(root, "src", "main.go") || hits[1].Name != "main.go" {
56+ t.Errorf("hit = %+v, want absolute path and base name", hits[1])
57+ }
58+}
59+
60+func TestSearchFiltersCaseInsensitivelyAndRanksBaseNames(t *testing.T) {
61+ root, _ := searchWorkspace(t)
62+ cases := []struct {
63+ query string
64+ want []string
65+ }{
66+ {"MAIN", []string{"src/main.go", "src/server/main_test.go"}},
67+ // "src" matches only through the directory: still found.
68+ {"src/", []string{"src/main.go", "src/server/main_test.go"}},
69+ {"guide", []string{"docs/guide.md"}},
70+ {"nothing-here", []string{}},
71+ }
72+ for _, tc := range cases {
73+ got := relPaths(files.New(root).Search(tc.query, 10))
74+ if len(got) != len(tc.want) {
75+ t.Errorf("Search(%q) = %v, want %v", tc.query, got, tc.want)
76+ continue
77+ }
78+ for i := range tc.want {
79+ if got[i] != tc.want[i] {
80+ t.Errorf("Search(%q)[%d] = %q, want %q", tc.query, i, got[i], tc.want[i])
81+ }
82+ }
83+ }
84+}
85+
86+func TestSearchHonoursLimit(t *testing.T) {
87+ root, _ := searchWorkspace(t)
88+ if got := files.New(root).Search("", 2); len(got) != 2 {
89+ t.Errorf("limit 2 returned %d hits", len(got))
90+ }
91+ if got := files.New(root).Search("", 100000); len(got) != 4 {
92+ t.Errorf("oversized limit returned %d hits, want all 4", len(got))
93+ }
94+}
95+
96+func TestSearchEndpoint(t *testing.T) {
97+ _, h := searchWorkspace(t)
98+ rec := do(t, h, http.MethodGet, "/api/files/search?q=main&limit=1", "")
99+ if rec.Code != http.StatusOK {
100+ t.Fatalf("GET search = %d (%s)", rec.Code, rec.Body.String())
101+ }
102+ hits := decodeBody(t, rec)["files"].([]any)
103+ if len(hits) != 1 || hits[0].(map[string]any)["relPath"] != "src/main.go" {
104+ t.Errorf("files = %v, want [src/main.go]", hits)
105+ }
106+
107+ rec = do(t, h, http.MethodGet, "/api/files/search?q=zzz", "")
108+ if got := decodeBody(t, rec)["files"].([]any); len(got) != 0 {
109+ t.Errorf("no-match search = %v, want an empty array", got)
110+ }
111+}
112+
113+func TestSearchEndpointOnMissingRootIs404(t *testing.T) {
114+ mux := http.NewServeMux()
115+ for pattern, handler := range files.New(filepath.Join(t.TempDir(), "gone")).Routes() {
116+ mux.Handle(pattern, handler)
117+ }
118+ if rec := do(t, mux, http.MethodGet, "/api/files/search", ""); rec.Code != http.StatusNotFound {
119+ t.Errorf("search on missing root = %d, want 404", rec.Code)
120+ }
121+}
new file mode 100644
@@ -0,0 +1,121 @@
1+package files_test
2+
3+import (
4+ "net/http"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "rickub.com/bots-garden/ori/internal/files"
10+)
11+
12+// searchWorkspace extends the basic tree with nested and noisy directories.
13+func searchWorkspace(t *testing.T) (string, http.Handler) {
14+ t.Helper()
15+ root, h := workspace(t)
16+ for _, rel := range []string{
17+ "src/server/main_test.go",
18+ "docs/guide.md",
19+ "node_modules/pkg/index.js",
20+ ".git/HEAD",
21+ } {
22+ full := filepath.Join(root, rel)
23+ if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
24+ t.Fatal(err)
25+ }
26+ if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
27+ t.Fatal(err)
28+ }
29+ }
30+ return root, h
31+}
32+
33+func relPaths(hits []files.Hit) []string {
34+ out := make([]string, 0, len(hits))
35+ for _, h := range hits {
36+ out = append(out, h.RelPath)
37+ }
38+ return out
39+}
40+
41+func TestSearchIsRecursiveAndSkipsNoise(t *testing.T) {
42+ root, _ := searchWorkspace(t)
43+ hits := files.New(root).Search("", 0)
44+ got := relPaths(hits)
45+ // Ranked: shorter relative paths first, alphabetical among equals.
46+ want := []string{"README.md", "src/main.go", "docs/guide.md", "src/server/main_test.go"}
47+ if len(got) != len(want) {
48+ t.Fatalf("hits = %v, want %v", got, want)
49+ }
50+ for i := range want {
51+ if got[i] != want[i] {
52+ t.Errorf("hit[%d] = %q, want %q", i, got[i], want[i])
53+ }
54+ }
55+ if hits[1].Path != filepath.Join(root, "src", "main.go") || hits[1].Name != "main.go" {
56+ t.Errorf("hit = %+v, want absolute path and base name", hits[1])
57+ }
58+}
59+
60+func TestSearchFiltersCaseInsensitivelyAndRanksBaseNames(t *testing.T) {
61+ root, _ := searchWorkspace(t)
62+ cases := []struct {
63+ query string
64+ want []string
65+ }{
66+ {"MAIN", []string{"src/main.go", "src/server/main_test.go"}},
67+ // "src" matches only through the directory: still found.
68+ {"src/", []string{"src/main.go", "src/server/main_test.go"}},
69+ {"guide", []string{"docs/guide.md"}},
70+ {"nothing-here", []string{}},
71+ }
72+ for _, tc := range cases {
73+ got := relPaths(files.New(root).Search(tc.query, 10))
74+ if len(got) != len(tc.want) {
75+ t.Errorf("Search(%q) = %v, want %v", tc.query, got, tc.want)
76+ continue
77+ }
78+ for i := range tc.want {
79+ if got[i] != tc.want[i] {
80+ t.Errorf("Search(%q)[%d] = %q, want %q", tc.query, i, got[i], tc.want[i])
81+ }
82+ }
83+ }
84+}
85+
86+func TestSearchHonoursLimit(t *testing.T) {
87+ root, _ := searchWorkspace(t)
88+ if got := files.New(root).Search("", 2); len(got) != 2 {
89+ t.Errorf("limit 2 returned %d hits", len(got))
90+ }
91+ if got := files.New(root).Search("", 100000); len(got) != 4 {
92+ t.Errorf("oversized limit returned %d hits, want all 4", len(got))
93+ }
94+}
95+
96+func TestSearchEndpoint(t *testing.T) {
97+ _, h := searchWorkspace(t)
98+ rec := do(t, h, http.MethodGet, "/api/files/search?q=main&limit=1", "")
99+ if rec.Code != http.StatusOK {
100+ t.Fatalf("GET search = %d (%s)", rec.Code, rec.Body.String())
101+ }
102+ hits := decodeBody(t, rec)["files"].([]any)
103+ if len(hits) != 1 || hits[0].(map[string]any)["relPath"] != "src/main.go" {
104+ t.Errorf("files = %v, want [src/main.go]", hits)
105+ }
106+
107+ rec = do(t, h, http.MethodGet, "/api/files/search?q=zzz", "")
108+ if got := decodeBody(t, rec)["files"].([]any); len(got) != 0 {
109+ t.Errorf("no-match search = %v, want an empty array", got)
110+ }
111+}
112+
113+func TestSearchEndpointOnMissingRootIs404(t *testing.T) {
114+ mux := http.NewServeMux()
115+ for pattern, handler := range files.New(filepath.Join(t.TempDir(), "gone")).Routes() {
116+ mux.Handle(pattern, handler)
117+ }
118+ if rec := do(t, mux, http.MethodGet, "/api/files/search", ""); rec.Code != http.StatusNotFound {
119+ t.Errorf("search on missing root = %d, want 404", rec.Code)
120+ }
121+}
modified internal/httpserver/httpserver_test.go +1 -1
@@ -7,7 +7,7 @@ import (
77 "testing"
88 "testing/fstest"
99
10- "github.com/bots-garden/ori/internal/httpserver"
10+ "rickub.com/bots-garden/ori/internal/httpserver"
1111 )
1212
1313 // builtUI mimics a Vite production build.
@@ -7,7 +7,7 @@ import (
7 "testing"7 "testing"
8 "testing/fstest"8 "testing/fstest"
9 9
10- "github.com/bots-garden/ori/internal/httpserver"10+ "rickub.com/bots-garden/ori/internal/httpserver"
11 )11 )
12 12
13 // builtUI mimics a Vite production build.13 // builtUI mimics a Vite production build.
modified internal/mockagent/mockagent.go +28 -5
@@ -61,11 +61,30 @@ func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.Auth
6161 return acp.AuthenticateResponse{}, nil
6262 }
6363
64-func (a *Agent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
64+// NewSession opens a session and, like the Claude Code adapter, announces
65+// the slash commands the agent understands before answering, so the panel's
66+// "/" selector can be exercised without a real agent.
67+func (a *Agent) NewSession(ctx context.Context, _ acp.NewSessionRequest) (acp.NewSessionResponse, error) {
6568 a.mu.Lock()
66- defer a.mu.Unlock()
6769 a.sessions++
68- return acp.NewSessionResponse{SessionId: acp.SessionId("mock-" + strconv.Itoa(a.sessions))}, nil
70+ sessionId := acp.SessionId("mock-" + strconv.Itoa(a.sessions))
71+ a.mu.Unlock()
72+
73+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{SessionId: sessionId, Update: availableCommands()}); err != nil {
74+ return acp.NewSessionResponse{}, fmt.Errorf("announce commands: %w", err)
75+ }
76+ return acp.NewSessionResponse{SessionId: sessionId}, nil
77+}
78+
79+// availableCommands is the mock's available_commands_update payload.
80+func availableCommands() acp.SessionUpdate {
81+ return acp.SessionUpdate{AvailableCommandsUpdate: &acp.SessionAvailableCommandsUpdate{
82+ SessionUpdate: "available_commands_update",
83+ AvailableCommands: []acp.AvailableCommand{
84+ {Name: "review", Description: "Review the pending changes", Input: &acp.AvailableCommandInput{Unstructured: &acp.UnstructuredCommandInput{Hint: "optional focus"}}},
85+ {Name: "compact", Description: "Summarise the conversation so far"},
86+ },
87+ }}
6988 }
7089
7190 // Cancel is a no-op: the SDK already cancels the context of the running
@@ -85,12 +104,16 @@ func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Promp
85104 return acp.PromptResponse{StopReason: stop}, nil
86105 }
87106
88-// promptText joins the text blocks of the user's prompt.
107+// promptText joins the text blocks of the user's prompt and names the
108+// attached resource links, so a demo shows the @-mentions reached the agent.
89109 func promptText(params acp.PromptRequest) string {
90110 text := ""
91111 for _, block := range params.Prompt {
92- if block.Text != nil {
112+ switch {
113+ case block.Text != nil:
93114 text += block.Text.Text
115+ case block.ResourceLink != nil:
116+ text += " [attached: " + block.ResourceLink.Name + "]"
94117 }
95118 }
96119 return text
@@ -61,11 +61,30 @@ func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.Auth
61 return acp.AuthenticateResponse{}, nil61 return acp.AuthenticateResponse{}, nil
62 }62 }
63 63
64-func (a *Agent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {64+// NewSession opens a session and, like the Claude Code adapter, announces
65+// the slash commands the agent understands before answering, so the panel's
66+// "/" selector can be exercised without a real agent.
67+func (a *Agent) NewSession(ctx context.Context, _ acp.NewSessionRequest) (acp.NewSessionResponse, error) {
65 a.mu.Lock()68 a.mu.Lock()
66- defer a.mu.Unlock()
67 a.sessions++69 a.sessions++
68- return acp.NewSessionResponse{SessionId: acp.SessionId("mock-" + strconv.Itoa(a.sessions))}, nil70+ sessionId := acp.SessionId("mock-" + strconv.Itoa(a.sessions))
71+ a.mu.Unlock()
72+
73+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{SessionId: sessionId, Update: availableCommands()}); err != nil {
74+ return acp.NewSessionResponse{}, fmt.Errorf("announce commands: %w", err)
75+ }
76+ return acp.NewSessionResponse{SessionId: sessionId}, nil
77+}
78+
79+// availableCommands is the mock's available_commands_update payload.
80+func availableCommands() acp.SessionUpdate {
81+ return acp.SessionUpdate{AvailableCommandsUpdate: &acp.SessionAvailableCommandsUpdate{
82+ SessionUpdate: "available_commands_update",
83+ AvailableCommands: []acp.AvailableCommand{
84+ {Name: "review", Description: "Review the pending changes", Input: &acp.AvailableCommandInput{Unstructured: &acp.UnstructuredCommandInput{Hint: "optional focus"}}},
85+ {Name: "compact", Description: "Summarise the conversation so far"},
86+ },
87+ }}
69 }88 }
70 89
71 // Cancel is a no-op: the SDK already cancels the context of the running90 // Cancel is a no-op: the SDK already cancels the context of the running
@@ -85,12 +104,16 @@ func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Promp
85 return acp.PromptResponse{StopReason: stop}, nil104 return acp.PromptResponse{StopReason: stop}, nil
86 }105 }
87 106
88-// promptText joins the text blocks of the user's prompt.107+// promptText joins the text blocks of the user's prompt and names the
108+// attached resource links, so a demo shows the @-mentions reached the agent.
89 func promptText(params acp.PromptRequest) string {109 func promptText(params acp.PromptRequest) string {
90 text := ""110 text := ""
91 for _, block := range params.Prompt {111 for _, block := range params.Prompt {
92- if block.Text != nil {112+ switch {
113+ case block.Text != nil:
93 text += block.Text.Text114 text += block.Text.Text
115+ case block.ResourceLink != nil:
116+ text += " [attached: " + block.ResourceLink.Name + "]"
94 }117 }
95 }118 }
96 return text119 return text
modified internal/mockagent/mockagent_test.go +43 -2
@@ -10,8 +10,8 @@ import (
1010
1111 acp "github.com/coder/acp-go-sdk"
1212
13- "github.com/bots-garden/ori/internal/agent"
14- "github.com/bots-garden/ori/internal/mockagent"
13+ "rickub.com/bots-garden/ori/internal/agent"
14+ "rickub.com/bots-garden/ori/internal/mockagent"
1515 )
1616
1717 // panelHandler records everything the mock streams, and answers the
@@ -21,6 +21,7 @@ type panelHandler struct {
2121 optionId string
2222 texts []string
2323 updateKinds []string
24+ commands []string
2425 permissions int
2526 }
2627
@@ -41,6 +42,11 @@ func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotif
4142 h.updateKinds = append(h.updateKinds, "tool_call_update")
4243 case n.Update.Plan != nil:
4344 h.updateKinds = append(h.updateKinds, "plan")
45+ case n.Update.AvailableCommandsUpdate != nil:
46+ h.updateKinds = append(h.updateKinds, "available_commands")
47+ for _, c := range n.Update.AvailableCommandsUpdate.AvailableCommands {
48+ h.commands = append(h.commands, c.Name)
49+ }
4450 }
4551 }
4652
@@ -55,6 +61,11 @@ func (h *panelHandler) HandlePermissionRequest(_ context.Context, req acp.Reques
5561 func (h *panelHandler) kinds() map[string]int {
5662 h.mu.Lock()
5763 defer h.mu.Unlock()
64+ return h.kindsLocked()
65+}
66+
67+// kindsLocked is kinds for callers already holding h.mu.
68+func (h *panelHandler) kindsLocked() map[string]int {
5869 counts := map[string]int{}
5970 for _, k := range h.updateKinds {
6071 counts[k]++
@@ -109,6 +120,36 @@ func TestScenarioStreamsEveryPanelFeature(t *testing.T) {
109120 }
110121 }
111122
123+func TestSessionStartAnnouncesAvailableCommands(t *testing.T) {
124+ handler := &panelHandler{optionId: "allow-once"}
125+ connectPanel(t, handler)
126+
127+ handler.mu.Lock()
128+ defer handler.mu.Unlock()
129+ if handler.kindsLocked()["available_commands"] != 1 {
130+ t.Fatalf("available_commands updates = %d, want 1 (all: %v)", handler.kindsLocked()["available_commands"], handler.kindsLocked())
131+ }
132+ if len(handler.commands) != 2 || handler.commands[0] != "review" || handler.commands[1] != "compact" {
133+ t.Errorf("commands = %v, want [review compact]", handler.commands)
134+ }
135+}
136+
137+func TestPromptEchoesAttachedResourceLinks(t *testing.T) {
138+ handler := &panelHandler{optionId: "allow-once"}
139+ session := connectPanel(t, handler)
140+
141+ _, err := session.Prompt(context.Background(), []acp.ContentBlock{
142+ acp.TextBlock("look at @src/main.go"),
143+ acp.ResourceLinkBlock("src/main.go", "file:///work/src/main.go"),
144+ })
145+ if err != nil {
146+ t.Fatalf("Prompt returned an error: %v", err)
147+ }
148+ if all := strings.Join(handler.texts, ""); !strings.Contains(all, "[attached: src/main.go]") {
149+ t.Errorf("final message %q does not name the attachment", all)
150+ }
151+}
152+
112153 func TestScenarioStopsOnRejectedPermission(t *testing.T) {
113154 handler := &panelHandler{optionId: "reject-once"}
114155 session := connectPanel(t, handler)
@@ -10,8 +10,8 @@ import (
10 10
11 acp "github.com/coder/acp-go-sdk"11 acp "github.com/coder/acp-go-sdk"
12 12
13- "github.com/bots-garden/ori/internal/agent"13+ "rickub.com/bots-garden/ori/internal/agent"
14- "github.com/bots-garden/ori/internal/mockagent"14+ "rickub.com/bots-garden/ori/internal/mockagent"
15 )15 )
16 16
17 // panelHandler records everything the mock streams, and answers the17 // panelHandler records everything the mock streams, and answers the
@@ -21,6 +21,7 @@ type panelHandler struct {
21 optionId string21 optionId string
22 texts []string22 texts []string
23 updateKinds []string23 updateKinds []string
24+ commands []string
24 permissions int25 permissions int
25 }26 }
26 27
@@ -41,6 +42,11 @@ func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotif
41 h.updateKinds = append(h.updateKinds, "tool_call_update")42 h.updateKinds = append(h.updateKinds, "tool_call_update")
42 case n.Update.Plan != nil:43 case n.Update.Plan != nil:
43 h.updateKinds = append(h.updateKinds, "plan")44 h.updateKinds = append(h.updateKinds, "plan")
45+ case n.Update.AvailableCommandsUpdate != nil:
46+ h.updateKinds = append(h.updateKinds, "available_commands")
47+ for _, c := range n.Update.AvailableCommandsUpdate.AvailableCommands {
48+ h.commands = append(h.commands, c.Name)
49+ }
44 }50 }
45 }51 }
46 52
@@ -55,6 +61,11 @@ func (h *panelHandler) HandlePermissionRequest(_ context.Context, req acp.Reques
55 func (h *panelHandler) kinds() map[string]int {61 func (h *panelHandler) kinds() map[string]int {
56 h.mu.Lock()62 h.mu.Lock()
57 defer h.mu.Unlock()63 defer h.mu.Unlock()
64+ return h.kindsLocked()
65+}
66+
67+// kindsLocked is kinds for callers already holding h.mu.
68+func (h *panelHandler) kindsLocked() map[string]int {
58 counts := map[string]int{}69 counts := map[string]int{}
59 for _, k := range h.updateKinds {70 for _, k := range h.updateKinds {
60 counts[k]++71 counts[k]++
@@ -109,6 +120,36 @@ func TestScenarioStreamsEveryPanelFeature(t *testing.T) {
109 }120 }
110 }121 }
111 122
123+func TestSessionStartAnnouncesAvailableCommands(t *testing.T) {
124+ handler := &panelHandler{optionId: "allow-once"}
125+ connectPanel(t, handler)
126+
127+ handler.mu.Lock()
128+ defer handler.mu.Unlock()
129+ if handler.kindsLocked()["available_commands"] != 1 {
130+ t.Fatalf("available_commands updates = %d, want 1 (all: %v)", handler.kindsLocked()["available_commands"], handler.kindsLocked())
131+ }
132+ if len(handler.commands) != 2 || handler.commands[0] != "review" || handler.commands[1] != "compact" {
133+ t.Errorf("commands = %v, want [review compact]", handler.commands)
134+ }
135+}
136+
137+func TestPromptEchoesAttachedResourceLinks(t *testing.T) {
138+ handler := &panelHandler{optionId: "allow-once"}
139+ session := connectPanel(t, handler)
140+
141+ _, err := session.Prompt(context.Background(), []acp.ContentBlock{
142+ acp.TextBlock("look at @src/main.go"),
143+ acp.ResourceLinkBlock("src/main.go", "file:///work/src/main.go"),
144+ })
145+ if err != nil {
146+ t.Fatalf("Prompt returned an error: %v", err)
147+ }
148+ if all := strings.Join(handler.texts, ""); !strings.Contains(all, "[attached: src/main.go]") {
149+ t.Errorf("final message %q does not name the attachment", all)
150+ }
151+}
152+
112 func TestScenarioStopsOnRejectedPermission(t *testing.T) {153 func TestScenarioStopsOnRejectedPermission(t *testing.T) {
113 handler := &panelHandler{optionId: "reject-once"}154 handler := &panelHandler{optionId: "reject-once"}
114 session := connectPanel(t, handler)155 session := connectPanel(t, handler)
added internal/skills/skills.go +180 -0
new file mode 100644
@@ -0,0 +1,180 @@
1+// Package skills discovers the Claude Code skills available to the agent
2+// session and exposes them to the SPA's "/" selector.
3+//
4+// A skill is a directory holding a SKILL.md whose YAML frontmatter names and
5+// describes it. Claude Code looks in two places: the project's
6+// .claude/skills/ and the user's ~/.claude/skills/; a project skill shadows a
7+// user skill of the same name.
8+package skills
9+
10+import (
11+ "bufio"
12+ "encoding/json"
13+ "net/http"
14+ "os"
15+ "path/filepath"
16+ "sort"
17+ "strings"
18+)
19+
20+// Source tells where a skill was found.
21+type Source string
22+
23+const (
24+ // SourceProject is <cwd>/.claude/skills.
25+ SourceProject Source = "project"
26+ // SourceUser is ~/.claude/skills.
27+ SourceUser Source = "user"
28+)
29+
30+// Skill is one discovered skill.
31+type Skill struct {
32+ // Name is the frontmatter name, or the directory name when absent.
33+ Name string `json:"name"`
34+ // Description is the frontmatter description ("" when absent).
35+ Description string `json:"description"`
36+ // Path is the absolute path of the SKILL.md file.
37+ Path string `json:"path"`
38+ // Source is where the skill comes from.
39+ Source Source `json:"source"`
40+}
41+
42+// Service answers GET /api/skills for one project directory and one home
43+// directory.
44+type Service struct {
45+ projectDir string
46+ homeDir string
47+}
48+
49+// New creates a Service; cwd is the agent's working directory and home the
50+// user's home directory (an empty home disables user skills).
51+//
52+// Example:
53+//
54+// svc := skills.New("/work/project", os.Getenv("HOME"))
55+// for pattern, handler := range svc.Routes() {
56+// mux.Handle(pattern, handler)
57+// }
58+func New(cwd, home string) *Service {
59+ return &Service{projectDir: cwd, homeDir: home}
60+}
61+
62+// Routes returns the API endpoints, keyed by http.ServeMux pattern.
63+func (s *Service) Routes() map[string]http.Handler {
64+ return map[string]http.Handler{
65+ "GET /api/skills": http.HandlerFunc(s.listSkills),
66+ }
67+}
68+
69+// Discover returns the skills of both locations, sorted by name, project
70+// skills shadowing user skills of the same name.
71+func (s *Service) Discover() []Skill {
72+ byName := map[string]Skill{}
73+ if s.homeDir != "" {
74+ for _, skill := range scanDirectory(filepath.Join(s.homeDir, ".claude", "skills"), SourceUser) {
75+ byName[skill.Name] = skill
76+ }
77+ }
78+ for _, skill := range scanDirectory(filepath.Join(s.projectDir, ".claude", "skills"), SourceProject) {
79+ byName[skill.Name] = skill
80+ }
81+
82+ skills := make([]Skill, 0, len(byName))
83+ for _, skill := range byName {
84+ skills = append(skills, skill)
85+ }
86+ sort.Slice(skills, func(i, j int) bool { return skills[i].Name < skills[j].Name })
87+ return skills
88+}
89+
90+// scanDirectory lists the <dir>/*/SKILL.md skills; a missing directory
91+// yields nothing.
92+func scanDirectory(dir string, source Source) []Skill {
93+ entries, err := os.ReadDir(dir)
94+ if err != nil {
95+ return nil
96+ }
97+ var skills []Skill
98+ for _, entry := range entries {
99+ if !entry.IsDir() {
100+ continue
101+ }
102+ path := filepath.Join(dir, entry.Name(), "SKILL.md")
103+ file, err := os.Open(path)
104+ if err != nil {
105+ continue
106+ }
107+ meta := parseFrontmatter(file)
108+ _ = file.Close()
109+
110+ skill := Skill{Name: meta["name"], Description: meta["description"], Path: path, Source: source}
111+ if skill.Name == "" {
112+ skill.Name = entry.Name()
113+ }
114+ skills = append(skills, skill)
115+ }
116+ return skills
117+}
118+
119+// parseFrontmatter reads the leading "---" YAML block of a SKILL.md and
120+// returns its scalar key/values. It understands the subset skills use:
121+// "key: value" lines, quoted values, and folded/literal block scalars
122+// ("key: >" or "key: |" followed by indented lines).
123+func parseFrontmatter(r *os.File) map[string]string {
124+ meta := map[string]string{}
125+ scanner := bufio.NewScanner(r)
126+ scanner.Buffer(make([]byte, 0, 64*1024), 1<<20)
127+
128+ if !scanner.Scan() || strings.TrimSpace(scanner.Text()) != "---" {
129+ return meta
130+ }
131+ blockKey := ""
132+ var block []string
133+ flush := func() {
134+ if blockKey != "" {
135+ meta[blockKey] = strings.Join(block, " ")
136+ blockKey, block = "", nil
137+ }
138+ }
139+ for scanner.Scan() {
140+ line := scanner.Text()
141+ if strings.TrimSpace(line) == "---" {
142+ break
143+ }
144+ if blockKey != "" && (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) {
145+ block = append(block, strings.TrimSpace(line))
146+ continue
147+ }
148+ flush()
149+ key, value, ok := strings.Cut(line, ":")
150+ if !ok || strings.TrimSpace(key) == "" || strings.HasPrefix(line, " ") {
151+ continue
152+ }
153+ value = strings.TrimSpace(value)
154+ if value == ">" || value == "|" || value == ">-" || value == "|-" {
155+ blockKey = strings.TrimSpace(key)
156+ continue
157+ }
158+ meta[strings.TrimSpace(key)] = unquote(value)
159+ }
160+ flush()
161+ return meta
162+}
163+
164+// unquote strips one pair of matching single or double quotes.
165+func unquote(value string) string {
166+ if len(value) >= 2 {
167+ first, last := value[0], value[len(value)-1]
168+ if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
169+ return value[1 : len(value)-1]
170+ }
171+ }
172+ return value
173+}
174+
175+func (s *Service) listSkills(w http.ResponseWriter, _ *http.Request) {
176+ skills := s.Discover()
177+ w.Header().Set("Content-Type", "application/json")
178+ w.WriteHeader(http.StatusOK)
179+ _ = json.NewEncoder(w).Encode(map[string]any{"skills": skills})
180+}
new file mode 100644
@@ -0,0 +1,180 @@
1+// Package skills discovers the Claude Code skills available to the agent
2+// session and exposes them to the SPA's "/" selector.
3+//
4+// A skill is a directory holding a SKILL.md whose YAML frontmatter names and
5+// describes it. Claude Code looks in two places: the project's
6+// .claude/skills/ and the user's ~/.claude/skills/; a project skill shadows a
7+// user skill of the same name.
8+package skills
9+
10+import (
11+ "bufio"
12+ "encoding/json"
13+ "net/http"
14+ "os"
15+ "path/filepath"
16+ "sort"
17+ "strings"
18+)
19+
20+// Source tells where a skill was found.
21+type Source string
22+
23+const (
24+ // SourceProject is <cwd>/.claude/skills.
25+ SourceProject Source = "project"
26+ // SourceUser is ~/.claude/skills.
27+ SourceUser Source = "user"
28+)
29+
30+// Skill is one discovered skill.
31+type Skill struct {
32+ // Name is the frontmatter name, or the directory name when absent.
33+ Name string `json:"name"`
34+ // Description is the frontmatter description ("" when absent).
35+ Description string `json:"description"`
36+ // Path is the absolute path of the SKILL.md file.
37+ Path string `json:"path"`
38+ // Source is where the skill comes from.
39+ Source Source `json:"source"`
40+}
41+
42+// Service answers GET /api/skills for one project directory and one home
43+// directory.
44+type Service struct {
45+ projectDir string
46+ homeDir string
47+}
48+
49+// New creates a Service; cwd is the agent's working directory and home the
50+// user's home directory (an empty home disables user skills).
51+//
52+// Example:
53+//
54+// svc := skills.New("/work/project", os.Getenv("HOME"))
55+// for pattern, handler := range svc.Routes() {
56+// mux.Handle(pattern, handler)
57+// }
58+func New(cwd, home string) *Service {
59+ return &Service{projectDir: cwd, homeDir: home}
60+}
61+
62+// Routes returns the API endpoints, keyed by http.ServeMux pattern.
63+func (s *Service) Routes() map[string]http.Handler {
64+ return map[string]http.Handler{
65+ "GET /api/skills": http.HandlerFunc(s.listSkills),
66+ }
67+}
68+
69+// Discover returns the skills of both locations, sorted by name, project
70+// skills shadowing user skills of the same name.
71+func (s *Service) Discover() []Skill {
72+ byName := map[string]Skill{}
73+ if s.homeDir != "" {
74+ for _, skill := range scanDirectory(filepath.Join(s.homeDir, ".claude", "skills"), SourceUser) {
75+ byName[skill.Name] = skill
76+ }
77+ }
78+ for _, skill := range scanDirectory(filepath.Join(s.projectDir, ".claude", "skills"), SourceProject) {
79+ byName[skill.Name] = skill
80+ }
81+
82+ skills := make([]Skill, 0, len(byName))
83+ for _, skill := range byName {
84+ skills = append(skills, skill)
85+ }
86+ sort.Slice(skills, func(i, j int) bool { return skills[i].Name < skills[j].Name })
87+ return skills
88+}
89+
90+// scanDirectory lists the <dir>/*/SKILL.md skills; a missing directory
91+// yields nothing.
92+func scanDirectory(dir string, source Source) []Skill {
93+ entries, err := os.ReadDir(dir)
94+ if err != nil {
95+ return nil
96+ }
97+ var skills []Skill
98+ for _, entry := range entries {
99+ if !entry.IsDir() {
100+ continue
101+ }
102+ path := filepath.Join(dir, entry.Name(), "SKILL.md")
103+ file, err := os.Open(path)
104+ if err != nil {
105+ continue
106+ }
107+ meta := parseFrontmatter(file)
108+ _ = file.Close()
109+
110+ skill := Skill{Name: meta["name"], Description: meta["description"], Path: path, Source: source}
111+ if skill.Name == "" {
112+ skill.Name = entry.Name()
113+ }
114+ skills = append(skills, skill)
115+ }
116+ return skills
117+}
118+
119+// parseFrontmatter reads the leading "---" YAML block of a SKILL.md and
120+// returns its scalar key/values. It understands the subset skills use:
121+// "key: value" lines, quoted values, and folded/literal block scalars
122+// ("key: >" or "key: |" followed by indented lines).
123+func parseFrontmatter(r *os.File) map[string]string {
124+ meta := map[string]string{}
125+ scanner := bufio.NewScanner(r)
126+ scanner.Buffer(make([]byte, 0, 64*1024), 1<<20)
127+
128+ if !scanner.Scan() || strings.TrimSpace(scanner.Text()) != "---" {
129+ return meta
130+ }
131+ blockKey := ""
132+ var block []string
133+ flush := func() {
134+ if blockKey != "" {
135+ meta[blockKey] = strings.Join(block, " ")
136+ blockKey, block = "", nil
137+ }
138+ }
139+ for scanner.Scan() {
140+ line := scanner.Text()
141+ if strings.TrimSpace(line) == "---" {
142+ break
143+ }
144+ if blockKey != "" && (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) {
145+ block = append(block, strings.TrimSpace(line))
146+ continue
147+ }
148+ flush()
149+ key, value, ok := strings.Cut(line, ":")
150+ if !ok || strings.TrimSpace(key) == "" || strings.HasPrefix(line, " ") {
151+ continue
152+ }
153+ value = strings.TrimSpace(value)
154+ if value == ">" || value == "|" || value == ">-" || value == "|-" {
155+ blockKey = strings.TrimSpace(key)
156+ continue
157+ }
158+ meta[strings.TrimSpace(key)] = unquote(value)
159+ }
160+ flush()
161+ return meta
162+}
163+
164+// unquote strips one pair of matching single or double quotes.
165+func unquote(value string) string {
166+ if len(value) >= 2 {
167+ first, last := value[0], value[len(value)-1]
168+ if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
169+ return value[1 : len(value)-1]
170+ }
171+ }
172+ return value
173+}
174+
175+func (s *Service) listSkills(w http.ResponseWriter, _ *http.Request) {
176+ skills := s.Discover()
177+ w.Header().Set("Content-Type", "application/json")
178+ w.WriteHeader(http.StatusOK)
179+ _ = json.NewEncoder(w).Encode(map[string]any{"skills": skills})
180+}
added internal/skills/skills_test.go +119 -0
new file mode 100644
@@ -0,0 +1,119 @@
1+package skills_test
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+
11+ "rickub.com/bots-garden/ori/internal/skills"
12+)
13+
14+// writeSkill creates <base>/.claude/skills/<dir>/SKILL.md with content.
15+func writeSkill(t *testing.T, base, dir, content string) string {
16+ t.Helper()
17+ full := filepath.Join(base, ".claude", "skills", dir)
18+ if err := os.MkdirAll(full, 0o755); err != nil {
19+ t.Fatal(err)
20+ }
21+ path := filepath.Join(full, "SKILL.md")
22+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
23+ t.Fatal(err)
24+ }
25+ return path
26+}
27+
28+func TestDiscoverParsesFrontmatterAndFallsBackToDirectoryName(t *testing.T) {
29+ cwd := t.TempDir()
30+ writeSkill(t, cwd, "quality", "---\nname: quality\ndescription: Audit code quality with qlty.\n---\n# Quality\n")
31+ writeSkill(t, cwd, "bare-dir", "# No frontmatter here\n")
32+ writeSkill(t, cwd, "quoted", "---\nname: \"fancy-name\"\ndescription: 'Single quoted: with colon'\nother: ignored\n---\n")
33+ writeSkill(t, cwd, "folded", "---\nname: folded\ndescription: >\n First line of the\n description.\n---\n")
34+ // A stray file (not a directory) and a directory without SKILL.md are ignored.
35+ if err := os.WriteFile(filepath.Join(cwd, ".claude", "skills", "README.md"), []byte("x"), 0o644); err != nil {
36+ t.Fatal(err)
37+ }
38+ if err := os.MkdirAll(filepath.Join(cwd, ".claude", "skills", "empty"), 0o755); err != nil {
39+ t.Fatal(err)
40+ }
41+
42+ got := skills.New(cwd, "").Discover()
43+ want := []skills.Skill{
44+ {Name: "bare-dir", Description: ""},
45+ {Name: "fancy-name", Description: "Single quoted: with colon"},
46+ {Name: "folded", Description: "First line of the description."},
47+ {Name: "quality", Description: "Audit code quality with qlty."},
48+ }
49+ if len(got) != len(want) {
50+ t.Fatalf("Discover = %+v, want %d skills", got, len(want))
51+ }
52+ for i := range want {
53+ if got[i].Name != want[i].Name || got[i].Description != want[i].Description {
54+ t.Errorf("skill[%d] = %+v, want %+v", i, got[i], want[i])
55+ }
56+ if got[i].Source != skills.SourceProject || filepath.Base(got[i].Path) != "SKILL.md" {
57+ t.Errorf("skill[%d] source/path = %q %q", i, got[i].Source, got[i].Path)
58+ }
59+ }
60+}
61+
62+func TestDiscoverMergesUserSkillsAndProjectShadowsThem(t *testing.T) {
63+ cwd, home := t.TempDir(), t.TempDir()
64+ writeSkill(t, home, "shared", "---\nname: shared\ndescription: user version\n---\n")
65+ writeSkill(t, home, "user-only", "---\nname: user-only\ndescription: only at home\n---\n")
66+ writeSkill(t, cwd, "shared", "---\nname: shared\ndescription: project version\n---\n")
67+
68+ got := skills.New(cwd, home).Discover()
69+ if len(got) != 2 {
70+ t.Fatalf("Discover = %+v, want 2 skills", got)
71+ }
72+ if got[0].Name != "shared" || got[0].Description != "project version" || got[0].Source != skills.SourceProject {
73+ t.Errorf("shared = %+v, want the project version", got[0])
74+ }
75+ if got[1].Name != "user-only" || got[1].Source != skills.SourceUser {
76+ t.Errorf("user-only = %+v, want the user skill", got[1])
77+ }
78+}
79+
80+func TestDiscoverWithoutSkillDirectoriesIsEmpty(t *testing.T) {
81+ if got := skills.New(t.TempDir(), t.TempDir()).Discover(); len(got) != 0 {
82+ t.Errorf("Discover = %+v, want none", got)
83+ }
84+}
85+
86+func TestEndpointReturnsJSONArray(t *testing.T) {
87+ cwd := t.TempDir()
88+ writeSkill(t, cwd, "demo", "---\nname: demo\ndescription: A demo skill\n---\n")
89+ mux := http.NewServeMux()
90+ for pattern, handler := range skills.New(cwd, "").Routes() {
91+ mux.Handle(pattern, handler)
92+ }
93+
94+ rec := httptest.NewRecorder()
95+ mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
96+ if rec.Code != http.StatusOK {
97+ t.Fatalf("GET /api/skills = %d", rec.Code)
98+ }
99+ var payload struct {
100+ Skills []skills.Skill `json:"skills"`
101+ }
102+ if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
103+ t.Fatalf("not JSON: %v", err)
104+ }
105+ if len(payload.Skills) != 1 || payload.Skills[0].Name != "demo" || payload.Skills[0].Description != "A demo skill" {
106+ t.Errorf("skills = %+v", payload.Skills)
107+ }
108+
109+ // No skills anywhere: still a JSON array, never null.
110+ empty := http.NewServeMux()
111+ for pattern, handler := range skills.New(t.TempDir(), "").Routes() {
112+ empty.Handle(pattern, handler)
113+ }
114+ rec = httptest.NewRecorder()
115+ empty.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
116+ if body := rec.Body.String(); body != "{\"skills\":[]}\n" {
117+ t.Errorf("empty body = %q", body)
118+ }
119+}
new file mode 100644
@@ -0,0 +1,119 @@
1+package skills_test
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+
11+ "rickub.com/bots-garden/ori/internal/skills"
12+)
13+
14+// writeSkill creates <base>/.claude/skills/<dir>/SKILL.md with content.
15+func writeSkill(t *testing.T, base, dir, content string) string {
16+ t.Helper()
17+ full := filepath.Join(base, ".claude", "skills", dir)
18+ if err := os.MkdirAll(full, 0o755); err != nil {
19+ t.Fatal(err)
20+ }
21+ path := filepath.Join(full, "SKILL.md")
22+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
23+ t.Fatal(err)
24+ }
25+ return path
26+}
27+
28+func TestDiscoverParsesFrontmatterAndFallsBackToDirectoryName(t *testing.T) {
29+ cwd := t.TempDir()
30+ writeSkill(t, cwd, "quality", "---\nname: quality\ndescription: Audit code quality with qlty.\n---\n# Quality\n")
31+ writeSkill(t, cwd, "bare-dir", "# No frontmatter here\n")
32+ writeSkill(t, cwd, "quoted", "---\nname: \"fancy-name\"\ndescription: 'Single quoted: with colon'\nother: ignored\n---\n")
33+ writeSkill(t, cwd, "folded", "---\nname: folded\ndescription: >\n First line of the\n description.\n---\n")
34+ // A stray file (not a directory) and a directory without SKILL.md are ignored.
35+ if err := os.WriteFile(filepath.Join(cwd, ".claude", "skills", "README.md"), []byte("x"), 0o644); err != nil {
36+ t.Fatal(err)
37+ }
38+ if err := os.MkdirAll(filepath.Join(cwd, ".claude", "skills", "empty"), 0o755); err != nil {
39+ t.Fatal(err)
40+ }
41+
42+ got := skills.New(cwd, "").Discover()
43+ want := []skills.Skill{
44+ {Name: "bare-dir", Description: ""},
45+ {Name: "fancy-name", Description: "Single quoted: with colon"},
46+ {Name: "folded", Description: "First line of the description."},
47+ {Name: "quality", Description: "Audit code quality with qlty."},
48+ }
49+ if len(got) != len(want) {
50+ t.Fatalf("Discover = %+v, want %d skills", got, len(want))
51+ }
52+ for i := range want {
53+ if got[i].Name != want[i].Name || got[i].Description != want[i].Description {
54+ t.Errorf("skill[%d] = %+v, want %+v", i, got[i], want[i])
55+ }
56+ if got[i].Source != skills.SourceProject || filepath.Base(got[i].Path) != "SKILL.md" {
57+ t.Errorf("skill[%d] source/path = %q %q", i, got[i].Source, got[i].Path)
58+ }
59+ }
60+}
61+
62+func TestDiscoverMergesUserSkillsAndProjectShadowsThem(t *testing.T) {
63+ cwd, home := t.TempDir(), t.TempDir()
64+ writeSkill(t, home, "shared", "---\nname: shared\ndescription: user version\n---\n")
65+ writeSkill(t, home, "user-only", "---\nname: user-only\ndescription: only at home\n---\n")
66+ writeSkill(t, cwd, "shared", "---\nname: shared\ndescription: project version\n---\n")
67+
68+ got := skills.New(cwd, home).Discover()
69+ if len(got) != 2 {
70+ t.Fatalf("Discover = %+v, want 2 skills", got)
71+ }
72+ if got[0].Name != "shared" || got[0].Description != "project version" || got[0].Source != skills.SourceProject {
73+ t.Errorf("shared = %+v, want the project version", got[0])
74+ }
75+ if got[1].Name != "user-only" || got[1].Source != skills.SourceUser {
76+ t.Errorf("user-only = %+v, want the user skill", got[1])
77+ }
78+}
79+
80+func TestDiscoverWithoutSkillDirectoriesIsEmpty(t *testing.T) {
81+ if got := skills.New(t.TempDir(), t.TempDir()).Discover(); len(got) != 0 {
82+ t.Errorf("Discover = %+v, want none", got)
83+ }
84+}
85+
86+func TestEndpointReturnsJSONArray(t *testing.T) {
87+ cwd := t.TempDir()
88+ writeSkill(t, cwd, "demo", "---\nname: demo\ndescription: A demo skill\n---\n")
89+ mux := http.NewServeMux()
90+ for pattern, handler := range skills.New(cwd, "").Routes() {
91+ mux.Handle(pattern, handler)
92+ }
93+
94+ rec := httptest.NewRecorder()
95+ mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
96+ if rec.Code != http.StatusOK {
97+ t.Fatalf("GET /api/skills = %d", rec.Code)
98+ }
99+ var payload struct {
100+ Skills []skills.Skill `json:"skills"`
101+ }
102+ if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
103+ t.Fatalf("not JSON: %v", err)
104+ }
105+ if len(payload.Skills) != 1 || payload.Skills[0].Name != "demo" || payload.Skills[0].Description != "A demo skill" {
106+ t.Errorf("skills = %+v", payload.Skills)
107+ }
108+
109+ // No skills anywhere: still a JSON array, never null.
110+ empty := http.NewServeMux()
111+ for pattern, handler := range skills.New(t.TempDir(), "").Routes() {
112+ empty.Handle(pattern, handler)
113+ }
114+ rec = httptest.NewRecorder()
115+ empty.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
116+ if body := rec.Body.String(); body != "{\"skills\":[]}\n" {
117+ t.Errorf("empty body = %q", body)
118+ }
119+}
added internal/terminal/terminal.go +149 -0
new file mode 100644
@@ -0,0 +1,149 @@
1+// Package terminal gives the browser an interactive shell: each WebSocket
2+// connection to its handler spawns one shell inside a pseudo-terminal and
3+// pipes bytes both ways, which is exactly what xterm.js expects to drive.
4+//
5+// Wire protocol: the server sends raw terminal output as binary frames; the
6+// client sends JSON text frames — {"type":"input","data":"…"} for keystrokes
7+// and {"type":"resize","cols":N,"rows":N} when the viewport changes.
8+package terminal
9+
10+import (
11+ "context"
12+ "encoding/json"
13+ "log/slog"
14+ "net/http"
15+ "os"
16+ "os/exec"
17+
18+ "github.com/coder/websocket"
19+ "github.com/creack/pty"
20+)
21+
22+// Service spawns one shell per WebSocket connection.
23+type Service struct {
24+ // Shell is the command to run; when empty, $SHELL then bash then sh.
25+ Shell []string
26+ // Cwd is the directory each shell starts in.
27+ Cwd string
28+
29+ logger *slog.Logger
30+}
31+
32+// New creates a terminal service starting shells in cwd.
33+//
34+// Example:
35+//
36+// mux.Handle("GET /ws/terminal", terminal.New(cfg.Cwd, logger).WebSocketHandler())
37+func New(cwd string, logger *slog.Logger) *Service {
38+ if logger == nil {
39+ logger = slog.Default()
40+ }
41+ return &Service{Cwd: cwd, logger: logger}
42+}
43+
44+// controlMessage is what the client sends over text frames.
45+type controlMessage struct {
46+ Type string `json:"type"`
47+ Data string `json:"data,omitempty"`
48+ Cols uint16 `json:"cols,omitempty"`
49+ Rows uint16 `json:"rows,omitempty"`
50+}
51+
52+// WebSocketHandler upgrades the request and runs one shell session until the
53+// socket closes or the shell exits.
54+func (s *Service) WebSocketHandler() http.Handler {
55+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
57+ OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
58+ })
59+ if err != nil {
60+ s.logger.Warn("terminal websocket accept failed", "error", err)
61+ return
62+ }
63+ defer func() { _ = conn.CloseNow() }()
64+ s.serveShell(r.Context(), conn)
65+ })
66+}
67+
68+// serveShell runs the shell and the two pumps for one connection.
69+func (s *Service) serveShell(ctx context.Context, conn *websocket.Conn) {
70+ cmd := exec.Command(s.shellCommand()[0], s.shellCommand()[1:]...)
71+ cmd.Dir = s.Cwd
72+ cmd.Env = append(os.Environ(), "TERM=xterm-256color")
73+
74+ ptmx, err := pty.Start(cmd)
75+ if err != nil {
76+ s.logger.Error("pty start failed", "error", err)
77+ _ = conn.Close(websocket.StatusInternalError, "pty start failed")
78+ return
79+ }
80+ defer func() {
81+ _ = ptmx.Close()
82+ _ = cmd.Process.Kill()
83+ _ = cmd.Wait()
84+ }()
85+
86+ // Output pump: pty → socket, as binary frames.
87+ outputDone := make(chan struct{})
88+ go func() {
89+ defer close(outputDone)
90+ buffer := make([]byte, 8192)
91+ for {
92+ n, readErr := ptmx.Read(buffer)
93+ if n > 0 {
94+ if writeErr := conn.Write(ctx, websocket.MessageBinary, buffer[:n]); writeErr != nil {
95+ return
96+ }
97+ }
98+ if readErr != nil {
99+ // The shell exited (or the pty closed): tell the client.
100+ _ = conn.Close(websocket.StatusNormalClosure, "shell exited")
101+ return
102+ }
103+ }
104+ }()
105+
106+ // Input pump: socket → pty, until the client disconnects.
107+ s.readControlMessages(ctx, conn, ptmx)
108+ <-outputDone
109+}
110+
111+// readControlMessages feeds client frames into the pty.
112+func (s *Service) readControlMessages(ctx context.Context, conn *websocket.Conn, ptmx *os.File) {
113+ for {
114+ kind, data, err := conn.Read(ctx)
115+ if err != nil {
116+ return
117+ }
118+ if kind != websocket.MessageText {
119+ continue
120+ }
121+ var msg controlMessage
122+ if err := json.Unmarshal(data, &msg); err != nil {
123+ continue // a malformed frame must not kill the session
124+ }
125+ switch msg.Type {
126+ case "input":
127+ _, _ = ptmx.WriteString(msg.Data)
128+ case "resize":
129+ if msg.Cols > 0 && msg.Rows > 0 {
130+ _ = pty.Setsize(ptmx, &pty.Winsize{Cols: msg.Cols, Rows: msg.Rows})
131+ }
132+ }
133+ }
134+}
135+
136+// shellCommand picks the shell to run: the configured one, else $SHELL, else
137+// bash, else sh.
138+func (s *Service) shellCommand() []string {
139+ if len(s.Shell) > 0 {
140+ return s.Shell
141+ }
142+ if shell := os.Getenv("SHELL"); shell != "" {
143+ return []string{shell, "-l"}
144+ }
145+ if path, err := exec.LookPath("bash"); err == nil {
146+ return []string{path, "-l"}
147+ }
148+ return []string{"sh"}
149+}
new file mode 100644
@@ -0,0 +1,149 @@
1+// Package terminal gives the browser an interactive shell: each WebSocket
2+// connection to its handler spawns one shell inside a pseudo-terminal and
3+// pipes bytes both ways, which is exactly what xterm.js expects to drive.
4+//
5+// Wire protocol: the server sends raw terminal output as binary frames; the
6+// client sends JSON text frames — {"type":"input","data":"…"} for keystrokes
7+// and {"type":"resize","cols":N,"rows":N} when the viewport changes.
8+package terminal
9+
10+import (
11+ "context"
12+ "encoding/json"
13+ "log/slog"
14+ "net/http"
15+ "os"
16+ "os/exec"
17+
18+ "github.com/coder/websocket"
19+ "github.com/creack/pty"
20+)
21+
22+// Service spawns one shell per WebSocket connection.
23+type Service struct {
24+ // Shell is the command to run; when empty, $SHELL then bash then sh.
25+ Shell []string
26+ // Cwd is the directory each shell starts in.
27+ Cwd string
28+
29+ logger *slog.Logger
30+}
31+
32+// New creates a terminal service starting shells in cwd.
33+//
34+// Example:
35+//
36+// mux.Handle("GET /ws/terminal", terminal.New(cfg.Cwd, logger).WebSocketHandler())
37+func New(cwd string, logger *slog.Logger) *Service {
38+ if logger == nil {
39+ logger = slog.Default()
40+ }
41+ return &Service{Cwd: cwd, logger: logger}
42+}
43+
44+// controlMessage is what the client sends over text frames.
45+type controlMessage struct {
46+ Type string `json:"type"`
47+ Data string `json:"data,omitempty"`
48+ Cols uint16 `json:"cols,omitempty"`
49+ Rows uint16 `json:"rows,omitempty"`
50+}
51+
52+// WebSocketHandler upgrades the request and runs one shell session until the
53+// socket closes or the shell exits.
54+func (s *Service) WebSocketHandler() http.Handler {
55+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
57+ OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
58+ })
59+ if err != nil {
60+ s.logger.Warn("terminal websocket accept failed", "error", err)
61+ return
62+ }
63+ defer func() { _ = conn.CloseNow() }()
64+ s.serveShell(r.Context(), conn)
65+ })
66+}
67+
68+// serveShell runs the shell and the two pumps for one connection.
69+func (s *Service) serveShell(ctx context.Context, conn *websocket.Conn) {
70+ cmd := exec.Command(s.shellCommand()[0], s.shellCommand()[1:]...)
71+ cmd.Dir = s.Cwd
72+ cmd.Env = append(os.Environ(), "TERM=xterm-256color")
73+
74+ ptmx, err := pty.Start(cmd)
75+ if err != nil {
76+ s.logger.Error("pty start failed", "error", err)
77+ _ = conn.Close(websocket.StatusInternalError, "pty start failed")
78+ return
79+ }
80+ defer func() {
81+ _ = ptmx.Close()
82+ _ = cmd.Process.Kill()
83+ _ = cmd.Wait()
84+ }()
85+
86+ // Output pump: pty → socket, as binary frames.
87+ outputDone := make(chan struct{})
88+ go func() {
89+ defer close(outputDone)
90+ buffer := make([]byte, 8192)
91+ for {
92+ n, readErr := ptmx.Read(buffer)
93+ if n > 0 {
94+ if writeErr := conn.Write(ctx, websocket.MessageBinary, buffer[:n]); writeErr != nil {
95+ return
96+ }
97+ }
98+ if readErr != nil {
99+ // The shell exited (or the pty closed): tell the client.
100+ _ = conn.Close(websocket.StatusNormalClosure, "shell exited")
101+ return
102+ }
103+ }
104+ }()
105+
106+ // Input pump: socket → pty, until the client disconnects.
107+ s.readControlMessages(ctx, conn, ptmx)
108+ <-outputDone
109+}
110+
111+// readControlMessages feeds client frames into the pty.
112+func (s *Service) readControlMessages(ctx context.Context, conn *websocket.Conn, ptmx *os.File) {
113+ for {
114+ kind, data, err := conn.Read(ctx)
115+ if err != nil {
116+ return
117+ }
118+ if kind != websocket.MessageText {
119+ continue
120+ }
121+ var msg controlMessage
122+ if err := json.Unmarshal(data, &msg); err != nil {
123+ continue // a malformed frame must not kill the session
124+ }
125+ switch msg.Type {
126+ case "input":
127+ _, _ = ptmx.WriteString(msg.Data)
128+ case "resize":
129+ if msg.Cols > 0 && msg.Rows > 0 {
130+ _ = pty.Setsize(ptmx, &pty.Winsize{Cols: msg.Cols, Rows: msg.Rows})
131+ }
132+ }
133+ }
134+}
135+
136+// shellCommand picks the shell to run: the configured one, else $SHELL, else
137+// bash, else sh.
138+func (s *Service) shellCommand() []string {
139+ if len(s.Shell) > 0 {
140+ return s.Shell
141+ }
142+ if shell := os.Getenv("SHELL"); shell != "" {
143+ return []string{shell, "-l"}
144+ }
145+ if path, err := exec.LookPath("bash"); err == nil {
146+ return []string{path, "-l"}
147+ }
148+ return []string{"sh"}
149+}
added internal/terminal/terminal_test.go +124 -0
new file mode 100644
@@ -0,0 +1,124 @@
1+package terminal_test
2+
3+import (
4+ "context"
5+ "net/http/httptest"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/coder/websocket"
13+ "github.com/coder/websocket/wsjson"
14+
15+ "rickub.com/bots-garden/ori/internal/terminal"
16+)
17+
18+// dialTerminal starts the service on a test server, with plain sh for
19+// determinism, and connects a WebSocket client to it.
20+func dialTerminal(t *testing.T, cwd string) (*websocket.Conn, context.Context) {
21+ t.Helper()
22+ service := terminal.New(cwd, nil)
23+ service.Shell = []string{"/bin/sh"}
24+
25+ server := httptest.NewServer(service.WebSocketHandler())
26+ t.Cleanup(server.Close)
27+
28+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
29+ t.Cleanup(cancel)
30+
31+ conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
32+ if err != nil {
33+ t.Fatalf("websocket dial failed: %v", err)
34+ }
35+ t.Cleanup(func() { _ = conn.CloseNow() })
36+ return conn, ctx
37+}
38+
39+// readOutputUntil accumulates binary frames until the marker shows up.
40+func readOutputUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, marker string) string {
41+ t.Helper()
42+ var output strings.Builder
43+ for !strings.Contains(output.String(), marker) {
44+ kind, data, err := conn.Read(ctx)
45+ if err != nil {
46+ t.Fatalf("read failed while waiting for %q; got so far: %q (%v)", marker, output.String(), err)
47+ }
48+ if kind == websocket.MessageBinary {
49+ output.Write(data)
50+ }
51+ }
52+ return output.String()
53+}
54+
55+func send(t *testing.T, ctx context.Context, conn *websocket.Conn, msg map[string]any) {
56+ t.Helper()
57+ if err := wsjson.Write(ctx, conn, msg); err != nil {
58+ t.Fatalf("websocket write failed: %v", err)
59+ }
60+}
61+
62+func TestShellRunsCommandsAndStreamsOutput(t *testing.T) {
63+ conn, ctx := dialTerminal(t, t.TempDir())
64+
65+ send(t, ctx, conn, map[string]any{"type": "input", "data": "echo ori-$((20+22))\n"})
66+ output := readOutputUntil(t, ctx, conn, "ori-42")
67+ if output == "" {
68+ t.Fatal("no terminal output received")
69+ }
70+}
71+
72+func TestShellStartsInTheConfiguredDirectory(t *testing.T) {
73+ cwd := t.TempDir()
74+ resolved, err := filepath.EvalSymlinks(cwd)
75+ if err != nil {
76+ t.Fatal(err)
77+ }
78+ conn, ctx := dialTerminal(t, cwd)
79+
80+ send(t, ctx, conn, map[string]any{"type": "input", "data": "pwd\n"})
81+ readOutputUntil(t, ctx, conn, resolved)
82+}
83+
84+func TestResizeIsAcceptedAndSessionKeepsWorking(t *testing.T) {
85+ conn, ctx := dialTerminal(t, t.TempDir())
86+
87+ send(t, ctx, conn, map[string]any{"type": "resize", "cols": 120, "rows": 40})
88+ send(t, ctx, conn, map[string]any{"type": "input", "data": "stty size\n"})
89+ readOutputUntil(t, ctx, conn, "40 120")
90+}
91+
92+func TestMalformedFramesDoNotKillTheSession(t *testing.T) {
93+ conn, ctx := dialTerminal(t, t.TempDir())
94+
95+ if err := conn.Write(ctx, websocket.MessageText, []byte("{not json")); err != nil {
96+ t.Fatalf("write failed: %v", err)
97+ }
98+ send(t, ctx, conn, map[string]any{"type": "input", "data": "echo still-alive\n"})
99+ readOutputUntil(t, ctx, conn, "still-alive")
100+}
101+
102+func TestShellExitClosesTheSocket(t *testing.T) {
103+ conn, ctx := dialTerminal(t, t.TempDir())
104+
105+ send(t, ctx, conn, map[string]any{"type": "input", "data": "exit\n"})
106+
107+ deadline := time.Now().Add(10 * time.Second)
108+ for {
109+ if _, _, err := conn.Read(ctx); err != nil {
110+ return // closed, as expected
111+ }
112+ if time.Now().After(deadline) {
113+ t.Fatal("socket still open long after the shell exited")
114+ }
115+ }
116+}
117+
118+func TestMain(m *testing.M) {
119+ if _, err := os.Stat("/bin/sh"); err != nil {
120+ // No shell, no terminal tests (should not happen on linux).
121+ os.Exit(0)
122+ }
123+ os.Exit(m.Run())
124+}
new file mode 100644
@@ -0,0 +1,124 @@
1+package terminal_test
2+
3+import (
4+ "context"
5+ "net/http/httptest"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/coder/websocket"
13+ "github.com/coder/websocket/wsjson"
14+
15+ "rickub.com/bots-garden/ori/internal/terminal"
16+)
17+
18+// dialTerminal starts the service on a test server, with plain sh for
19+// determinism, and connects a WebSocket client to it.
20+func dialTerminal(t *testing.T, cwd string) (*websocket.Conn, context.Context) {
21+ t.Helper()
22+ service := terminal.New(cwd, nil)
23+ service.Shell = []string{"/bin/sh"}
24+
25+ server := httptest.NewServer(service.WebSocketHandler())
26+ t.Cleanup(server.Close)
27+
28+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
29+ t.Cleanup(cancel)
30+
31+ conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
32+ if err != nil {
33+ t.Fatalf("websocket dial failed: %v", err)
34+ }
35+ t.Cleanup(func() { _ = conn.CloseNow() })
36+ return conn, ctx
37+}
38+
39+// readOutputUntil accumulates binary frames until the marker shows up.
40+func readOutputUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, marker string) string {
41+ t.Helper()
42+ var output strings.Builder
43+ for !strings.Contains(output.String(), marker) {
44+ kind, data, err := conn.Read(ctx)
45+ if err != nil {
46+ t.Fatalf("read failed while waiting for %q; got so far: %q (%v)", marker, output.String(), err)
47+ }
48+ if kind == websocket.MessageBinary {
49+ output.Write(data)
50+ }
51+ }
52+ return output.String()
53+}
54+
55+func send(t *testing.T, ctx context.Context, conn *websocket.Conn, msg map[string]any) {
56+ t.Helper()
57+ if err := wsjson.Write(ctx, conn, msg); err != nil {
58+ t.Fatalf("websocket write failed: %v", err)
59+ }
60+}
61+
62+func TestShellRunsCommandsAndStreamsOutput(t *testing.T) {
63+ conn, ctx := dialTerminal(t, t.TempDir())
64+
65+ send(t, ctx, conn, map[string]any{"type": "input", "data": "echo ori-$((20+22))\n"})
66+ output := readOutputUntil(t, ctx, conn, "ori-42")
67+ if output == "" {
68+ t.Fatal("no terminal output received")
69+ }
70+}
71+
72+func TestShellStartsInTheConfiguredDirectory(t *testing.T) {
73+ cwd := t.TempDir()
74+ resolved, err := filepath.EvalSymlinks(cwd)
75+ if err != nil {
76+ t.Fatal(err)
77+ }
78+ conn, ctx := dialTerminal(t, cwd)
79+
80+ send(t, ctx, conn, map[string]any{"type": "input", "data": "pwd\n"})
81+ readOutputUntil(t, ctx, conn, resolved)
82+}
83+
84+func TestResizeIsAcceptedAndSessionKeepsWorking(t *testing.T) {
85+ conn, ctx := dialTerminal(t, t.TempDir())
86+
87+ send(t, ctx, conn, map[string]any{"type": "resize", "cols": 120, "rows": 40})
88+ send(t, ctx, conn, map[string]any{"type": "input", "data": "stty size\n"})
89+ readOutputUntil(t, ctx, conn, "40 120")
90+}
91+
92+func TestMalformedFramesDoNotKillTheSession(t *testing.T) {
93+ conn, ctx := dialTerminal(t, t.TempDir())
94+
95+ if err := conn.Write(ctx, websocket.MessageText, []byte("{not json")); err != nil {
96+ t.Fatalf("write failed: %v", err)
97+ }
98+ send(t, ctx, conn, map[string]any{"type": "input", "data": "echo still-alive\n"})
99+ readOutputUntil(t, ctx, conn, "still-alive")
100+}
101+
102+func TestShellExitClosesTheSocket(t *testing.T) {
103+ conn, ctx := dialTerminal(t, t.TempDir())
104+
105+ send(t, ctx, conn, map[string]any{"type": "input", "data": "exit\n"})
106+
107+ deadline := time.Now().Add(10 * time.Second)
108+ for {
109+ if _, _, err := conn.Read(ctx); err != nil {
110+ return // closed, as expected
111+ }
112+ if time.Now().After(deadline) {
113+ t.Fatal("socket still open long after the shell exited")
114+ }
115+ }
116+}
117+
118+func TestMain(m *testing.M) {
119+ if _, err := os.Stat("/bin/sh"); err != nil {
120+ // No shell, no terminal tests (should not happen on linux).
121+ os.Exit(0)
122+ }
123+ os.Exit(m.Run())
124+}
added kits/ori/README.md +57 -0
new file mode 100644
@@ -0,0 +1,57 @@
1+# ori kit
2+
3+A Docker Sandboxes **mixin kit** that starts the [ori](../../README.md) web server — a browser client for Claude Code over ACP — inside a sandbox built from the `k33g/ori` template image.
4+
5+## 1. Get the template image
6+
7+It is published on Docker Hub as `k33g/ori:0.0.0`. To (re)build and push it yourself from the ori repository root:
8+
9+```bash
10+./template/build.sh # multi-arch build, pushes k33g/ori:0.0.0 and :latest
11+# local-only alternative: make template (= docker build -t k33g/ori .)
12+```
13+
14+The image stacks ori (server, SPA and demo agent) and the `claude-code-acp` adapter on top of the official `docker/sandbox-templates:claude-code` template, so everything the claude sandbox normally provides is still there.
15+
16+If the image only exists on your host engine (never pushed to Docker Hub), hand it to the sandbox runtime's own image store:
17+
18+```bash
19+docker save k33g/ori -o /tmp/ori.tar
20+sbx template load /tmp/ori.tar
21+sbx template ls
22+```
23+
24+## 2. Create a sandbox with it
25+
26+```bash
27+sbx run -d claude ~/path/to/your/project \
28+ --template k33g/ori:0.0.0 \
29+ --kit /path/to/ori/kits/ori
30+```
31+
32+Use `sbx run -d` (`--detached`), **not** `sbx create`: sbx auto-stops a sandbox 30 s after its last CLI session closes, and a web server has no CLI session to hold it up. A detached sandbox is exempt from that auto-stop for its whole life; the mode is fixed at creation, so a sandbox made with `sbx create` has to be `sbx rm`'d and recreated.
33+
34+`--template` swaps the sandbox image for `k33g/ori:0.0.0`; `--kit` applies this mixin, whose startup command launches the ori server at every container start. The claude agent kit still handles Anthropic credentials, so the server's agent works without any login inside the sandbox.
35+
36+The listen port defaults to **8888** and is a kit argument: add `--kit-arg ori.port=9000` (or `--kit-arg port=9000`) to change it.
37+
38+## 3. Open the UI
39+
40+The kit declares port 8888, so the creation auto-publishes an ephemeral localhost port and prints it:
41+
42+```
43+ Published web: localhost:54321 -> 8888/tcp
44+```
45+
46+Browse to that address, or pin a fixed host port instead:
47+
48+```bash
49+sbx run -d claude … -p 8888:8888 # at creation
50+sbx ports <sandbox-name> --publish 8888:8888/tcp # afterwards
51+```
52+
53+## Notes
54+
55+- Server log inside the sandbox: `/home/agent/.ori.log`.
56+- To try the UI without consuming Claude usage, restart the server with the bundled demo agent: `pkill -x ori; nohup ori --addr :8888 --agent-cmd ori-mock-agent --cwd "$PWD" >> /home/agent/.ori.log 2>&1 &`.
57+- The image also runs standalone, outside sbx: `docker run --rm -p 8888:8888 k33g/ori` (bring your own `ANTHROPIC_API_KEY`, or switch to `ori-mock-agent`).
new file mode 100644
@@ -0,0 +1,57 @@
1+# ori kit
2+
3+A Docker Sandboxes **mixin kit** that starts the [ori](../../README.md) web server — a browser client for Claude Code over ACP — inside a sandbox built from the `k33g/ori` template image.
4+
5+## 1. Get the template image
6+
7+It is published on Docker Hub as `k33g/ori:0.0.0`. To (re)build and push it yourself from the ori repository root:
8+
9+```bash
10+./template/build.sh # multi-arch build, pushes k33g/ori:0.0.0 and :latest
11+# local-only alternative: make template (= docker build -t k33g/ori .)
12+```
13+
14+The image stacks ori (server, SPA and demo agent) and the `claude-code-acp` adapter on top of the official `docker/sandbox-templates:claude-code` template, so everything the claude sandbox normally provides is still there.
15+
16+If the image only exists on your host engine (never pushed to Docker Hub), hand it to the sandbox runtime's own image store:
17+
18+```bash
19+docker save k33g/ori -o /tmp/ori.tar
20+sbx template load /tmp/ori.tar
21+sbx template ls
22+```
23+
24+## 2. Create a sandbox with it
25+
26+```bash
27+sbx run -d claude ~/path/to/your/project \
28+ --template k33g/ori:0.0.0 \
29+ --kit /path/to/ori/kits/ori
30+```
31+
32+Use `sbx run -d` (`--detached`), **not** `sbx create`: sbx auto-stops a sandbox 30 s after its last CLI session closes, and a web server has no CLI session to hold it up. A detached sandbox is exempt from that auto-stop for its whole life; the mode is fixed at creation, so a sandbox made with `sbx create` has to be `sbx rm`'d and recreated.
33+
34+`--template` swaps the sandbox image for `k33g/ori:0.0.0`; `--kit` applies this mixin, whose startup command launches the ori server at every container start. The claude agent kit still handles Anthropic credentials, so the server's agent works without any login inside the sandbox.
35+
36+The listen port defaults to **8888** and is a kit argument: add `--kit-arg ori.port=9000` (or `--kit-arg port=9000`) to change it.
37+
38+## 3. Open the UI
39+
40+The kit declares port 8888, so the creation auto-publishes an ephemeral localhost port and prints it:
41+
42+```
43+ Published web: localhost:54321 -> 8888/tcp
44+```
45+
46+Browse to that address, or pin a fixed host port instead:
47+
48+```bash
49+sbx run -d claude … -p 8888:8888 # at creation
50+sbx ports <sandbox-name> --publish 8888:8888/tcp # afterwards
51+```
52+
53+## Notes
54+
55+- Server log inside the sandbox: `/home/agent/.ori.log`.
56+- To try the UI without consuming Claude usage, restart the server with the bundled demo agent: `pkill -x ori; nohup ori --addr :8888 --agent-cmd ori-mock-agent --cwd "$PWD" >> /home/agent/.ori.log 2>&1 &`.
57+- The image also runs standalone, outside sbx: `docker run --rm -p 8888:8888 k33g/ori` (bring your own `ANTHROPIC_API_KEY`, or switch to `ori-mock-agent`).
added kits/ori/spec.yaml +88 -0
new file mode 100644
@@ -0,0 +1,88 @@
1+schemaVersion: "2"
2+kind: mixin
3+name: ori
4+displayName: Ori — ACP web client for Claude Code
5+description: >
6+ Starts the ori web server inside the sandbox. Ori is a browser client for
7+ ACP (Agent Client Protocol) code agents: a Go backend serves a React SPA
8+ (Zed-style agent panel: streamed answers, thoughts, tool calls, plans,
9+ permission prompts) plus a workspace panel (file tree with Material icons,
10+ Monaco preview/editor with rendered Markdown and AsciiDoc, interactive
11+ terminal). This kit only launches and describes the server; the binaries
12+ ship in the k33g/ori template image (template/Dockerfile in the ori
13+ repository), so use it together with `--template k33g/ori:0.0.1`.
14+licenses:
15+ - MIT
16+
17+# This mixin only makes sense composed with the claude agent kit: the server
18+# it starts drives Claude Code through the claude-code-acp adapter, and the
19+# Anthropic credentials/egress come from that kit.
20+requires:
21+ agent: claude
22+
23+args:
24+ port:
25+ default: "8888"
26+ description: TCP port the ori server listens on inside the sandbox
27+ pattern: "^[0-9]{2,5}$"
28+
29+# No extra network permissions: the Anthropic endpoints come from the claude
30+# agent kit this mixin composes with, and the ACP adapter is baked into the
31+# template image, so nothing is downloaded at runtime.
32+
33+# Auto-publishes an ephemeral localhost port at create (the create output
34+# prints it: "Published web: localhost:<port> -> <port>/tcp"). Pin a fixed
35+# host port instead with `-p 8888:8888` at create or `sbx ports … --publish`.
36+# The unquoted arg reference decodes as the integer the schema expects.
37+ports:
38+ - container: ${{ kit.args.port }}
39+ protocol: tcp
40+ name: web
41+
42+setup:
43+ startup:
44+ # Startup commands run on every container start through the detached
45+ # dispatcher, so the pgrep guard makes this idempotent. The server binds
46+ # 0.0.0.0 (empty host in --addr) so published ports reach it from the
47+ # host's browser. `user` is omitted: startup commands default to uid 1000,
48+ # the agent user.
49+ #
50+ # The PATH export is load-bearing: the dispatcher runs this via
51+ # `su -s /bin/sh -c … agent`, and su resets PATH to the login.defs
52+ # default — which does not contain the npm prefix where claude-code-acp
53+ # lives, nor ~/.local/bin where claude does.
54+ - command:
55+ - "sh"
56+ - "-c"
57+ - |
58+ export PATH="/usr/local/share/npm-global/bin:/home/agent/.local/bin:$PATH"
59+ pgrep -x ori >/dev/null 2>&1 && exit 0
60+ nohup /usr/local/bin/ori \
61+ --addr ":${{ kit.args.port }}" \
62+ --agent-cmd claude-code-acp \
63+ --cwd "${WORKSPACE_DIR:-/home/agent/workspace}" \
64+ >> /home/agent/.ori.log 2>&1 &
65+ description: Start the ori web server (idempotent)
66+
67+agentInstructions:
68+ content: |
69+ ## Ori web server (kit `ori`)
70+
71+ This sandbox runs **ori**, a web client for Claude Code over ACP. It was
72+ started automatically and serves on **port ${{ kit.args.port }}** (all
73+ interfaces).
74+
75+ - Binaries: `/usr/local/bin/ori` (server) and `/usr/local/bin/ori-mock-agent`
76+ (a deterministic demo ACP agent that needs no credentials).
77+ - Log file: `/home/agent/.ori.log`.
78+ - The server drives its own Claude Code session through the
79+ `claude-code-acp` adapter (preinstalled); credentials are injected by the
80+ sandbox proxy, so no login is needed inside the container.
81+ - Restart it with:
82+ `pkill -x ori; nohup ori --addr ":${{ kit.args.port }}" --agent-cmd claude-code-acp --cwd "$PWD" >> /home/agent/.ori.log 2>&1 &`
83+ - To demo without consuming Claude usage, restart it with
84+ `--agent-cmd ori-mock-agent` instead.
85+
86+ A localhost port for the UI was published on the host at create time (see
87+ the create output); the human can pin a different one with
88+ `sbx ports <sandbox-name> --publish <host-port>:${{ kit.args.port }}/tcp`.
new file mode 100644
@@ -0,0 +1,88 @@
1+schemaVersion: "2"
2+kind: mixin
3+name: ori
4+displayName: Ori — ACP web client for Claude Code
5+description: >
6+ Starts the ori web server inside the sandbox. Ori is a browser client for
7+ ACP (Agent Client Protocol) code agents: a Go backend serves a React SPA
8+ (Zed-style agent panel: streamed answers, thoughts, tool calls, plans,
9+ permission prompts) plus a workspace panel (file tree with Material icons,
10+ Monaco preview/editor with rendered Markdown and AsciiDoc, interactive
11+ terminal). This kit only launches and describes the server; the binaries
12+ ship in the k33g/ori template image (template/Dockerfile in the ori
13+ repository), so use it together with `--template k33g/ori:0.0.1`.
14+licenses:
15+ - MIT
16+
17+# This mixin only makes sense composed with the claude agent kit: the server
18+# it starts drives Claude Code through the claude-code-acp adapter, and the
19+# Anthropic credentials/egress come from that kit.
20+requires:
21+ agent: claude
22+
23+args:
24+ port:
25+ default: "8888"
26+ description: TCP port the ori server listens on inside the sandbox
27+ pattern: "^[0-9]{2,5}$"
28+
29+# No extra network permissions: the Anthropic endpoints come from the claude
30+# agent kit this mixin composes with, and the ACP adapter is baked into the
31+# template image, so nothing is downloaded at runtime.
32+
33+# Auto-publishes an ephemeral localhost port at create (the create output
34+# prints it: "Published web: localhost:<port> -> <port>/tcp"). Pin a fixed
35+# host port instead with `-p 8888:8888` at create or `sbx ports … --publish`.
36+# The unquoted arg reference decodes as the integer the schema expects.
37+ports:
38+ - container: ${{ kit.args.port }}
39+ protocol: tcp
40+ name: web
41+
42+setup:
43+ startup:
44+ # Startup commands run on every container start through the detached
45+ # dispatcher, so the pgrep guard makes this idempotent. The server binds
46+ # 0.0.0.0 (empty host in --addr) so published ports reach it from the
47+ # host's browser. `user` is omitted: startup commands default to uid 1000,
48+ # the agent user.
49+ #
50+ # The PATH export is load-bearing: the dispatcher runs this via
51+ # `su -s /bin/sh -c … agent`, and su resets PATH to the login.defs
52+ # default — which does not contain the npm prefix where claude-code-acp
53+ # lives, nor ~/.local/bin where claude does.
54+ - command:
55+ - "sh"
56+ - "-c"
57+ - |
58+ export PATH="/usr/local/share/npm-global/bin:/home/agent/.local/bin:$PATH"
59+ pgrep -x ori >/dev/null 2>&1 && exit 0
60+ nohup /usr/local/bin/ori \
61+ --addr ":${{ kit.args.port }}" \
62+ --agent-cmd claude-code-acp \
63+ --cwd "${WORKSPACE_DIR:-/home/agent/workspace}" \
64+ >> /home/agent/.ori.log 2>&1 &
65+ description: Start the ori web server (idempotent)
66+
67+agentInstructions:
68+ content: |
69+ ## Ori web server (kit `ori`)
70+
71+ This sandbox runs **ori**, a web client for Claude Code over ACP. It was
72+ started automatically and serves on **port ${{ kit.args.port }}** (all
73+ interfaces).
74+
75+ - Binaries: `/usr/local/bin/ori` (server) and `/usr/local/bin/ori-mock-agent`
76+ (a deterministic demo ACP agent that needs no credentials).
77+ - Log file: `/home/agent/.ori.log`.
78+ - The server drives its own Claude Code session through the
79+ `claude-code-acp` adapter (preinstalled); credentials are injected by the
80+ sandbox proxy, so no login is needed inside the container.
81+ - Restart it with:
82+ `pkill -x ori; nohup ori --addr ":${{ kit.args.port }}" --agent-cmd claude-code-acp --cwd "$PWD" >> /home/agent/.ori.log 2>&1 &`
83+ - To demo without consuming Claude usage, restart it with
84+ `--agent-cmd ori-mock-agent` instead.
85+
86+ A localhost port for the UI was published on the host at create time (see
87+ the create output); the human can pin a different one with
88+ `sbx ports <sandbox-name> --publish <host-port>:${{ kit.args.port }}/tcp`.
added ori-desktop/README.fr.md +144 -0
new file mode 100644
@@ -0,0 +1,144 @@
1+# ori-desktop — une coquille de bureau Wails pour ori
2+
3+`ori-desktop` enveloppe le client web [ori](../README.md) dans une fenêtre native construite
4+avec [Wails v2](https://wails.io) (Go + la webview de la plateforme). Il ne réimplémente **pas**
5+ori : le serveur ori continue de servir la SPA React et les points d'entrée `/ws`, `/api/files`,
6+`/ws/terminal`. L'application de bureau se contente de
7+
8+1. afficher un petit **écran de connexion** avec l'URL du serveur ori (par défaut
9+ `http://localhost:8888`), mémorisée d'une exécution à l'autre dans un fichier JSON ;
10+2. vérifier le serveur avec **`GET /healthz`** via une méthode Go exposée au frontend ;
11+3. puis afficher **la webapp ori elle-même** dans la fenêtre.
12+
13+C'est un module Go séparé (`rickub.com/bots-garden/ori-desktop`) : le `go test ./...` et le
14+`make build` du dépôt parent ne sont pas affectés.
15+
16+## Prérequis
17+
18+- Go 1.25+ (exigence de Wails v2.16, reprise par le module).
19+- La CLI Wails v2 : `go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0`
20+ (puis vérifier que `$(go env GOPATH)/bin` est dans le `PATH`).
21+- La chaîne d'outils graphique attendue par Wails — `wails doctor` liste ce qui manque :
22+ - **Linux** : `gcc`, `pkg-config`, `libgtk-3-dev`, `libwebkit2gtk-4.0-dev` (ou `-4.1-dev`
23+ avec `-tags webkit2_41`) ;
24+ - **macOS** : les Xcode command line tools ;
25+ - **Windows** : le runtime WebView2 (préinstallé sur Windows 10/11) ; NSIS seulement pour
26+ l'installeur.
27+- Un serveur ori en marche. Depuis la racine du dépôt : `make run` ou `make run-mock`.
28+
29+Aucun Node/npm n'est nécessaire : le frontend est du HTML/CSS/JS pur embarqué tel quel (pas de
30+bundler).
31+
32+## Lancer en mode développement
33+
34+```bash
35+cd ori-desktop
36+wails dev
37+```
38+
39+`wails dev` compile la partie Go, ouvre la fenêtre et recharge le frontend à chaque modification
40+sous `frontend/src/` (`assetdir` dans `wails.json`). Il sert aussi l'application sur
41+http://localhost:34115 pour l'ouvrir dans un navigateur classique avec les devtools tout en
42+appelant les méthodes Go.
43+
44+## Construire un binaire distribuable
45+
46+```bash
47+cd ori-desktop
48+wails build # → build/bin/ori-desktop (ou .app / .exe)
49+# ou, depuis la racine du dépôt :
50+make desktop
51+```
52+
53+La compilation croisée vers Windows depuis Linux/macOS fonctionne sans chaîne C :
54+`wails build -platform windows/amd64``build/bin/ori-desktop.exe`.
55+
56+## Tests
57+
58+La logique de connexion est en Go pur et testée sans aucune dépendance graphique :
59+
60+```bash
61+cd ori-desktop && go test ./internal/...
62+# ou, depuis la racine du dépôt :
63+make desktop-test
64+```
65+
66+`internal/settings` couvre les allers-retours chargement/sauvegarde, les valeurs par défaut au
67+premier lancement, les fichiers invalides et la normalisation d'URL ; `internal/health` couvre
68+`/healthz` face à un serveur `httptest` (sain, non-ori, injoignable, contexte annulé).
69+
70+## Comment il se connecte à ori
71+
72+```
73+┌──────────────── ori-desktop (fenêtre Wails) ───────────────┐
74+│ frontend/src (embarqué, origine wails://) │
75+│ écran de connexion ──► window.go.main.App.Connect(url) │
76+│ │ Go : NormalizeURL → GET /healthz → sauvegarde des réglages
77+│ ▼ │
78+│ <iframe src="http://localhost:8888/"> ◄── le serveur ori sert la SPA,
79+│ la SPA ouvre ws://localhost:8888/ws, /ws/terminal, /api/files comme d'habitude
80+└─────────────────────────────────────────────────────────────┘
81+```
82+
83+**Pourquoi une iframe ?** Wails v2 n'offre aucun appel runtime pour faire naviguer la webview
84+principale vers une URL externe ; la fenêtre affiche toujours le frontend embarqué
85+(`runtime.BrowserOpenURL` ouvre le navigateur système, proposé comme solution de repli « Open in
86+browser »). Charger ori dans une `<iframe>` conserve une fine barre native (URL, Reload, Open in
87+browser, Disconnect) autour de la SPA ori intacte. Cela fonctionne parce qu'ori n'envoie ni
88+en-tête `X-Frame-Options` ni CSP `frame-ancestors`, et parce que la SPA dérive ses URL
89+WebSocket de `window.location` — dans l'iframe c'est l'origine ori elle-même, donc le contrôle
90+d'origine `localhost:*` des WebSockets d'ori est satisfait.
91+
92+Sur macOS, `build/darwin/Info.plist` positionne `NSAppTransportSecurity/NSAllowsLocalNetworking`
93+pour qu'App Transport Security autorise le chargement HTTP en clair sur la boucle locale.
94+
95+### Méthodes Go exposées au frontend (`app.go`)
96+
97+| Méthode | Rôle |
98+| --- | --- |
99+| `GetConfig() → settings.Settings` | Réglages mémorisés (valeurs par défaut au premier lancement). |
100+| `SaveConfig(settings.Settings)` | Normalise l'URL puis persiste. |
101+| `CheckHealth(url) → health.Report` | `GET <url>/healthz` ; ne rejette jamais — `ok`, `statusCode`, `detail`. |
102+| `Connect(url) → string` | Normalise, vérifie la santé, mémorise l'URL, renvoie l'URL de base à charger. |
103+| `OpenInBrowser(url)` | Repli `runtime.BrowserOpenURL`. |
104+| `SettingsPath() → string` | Emplacement du fichier de réglages (affiché à l'écran). |
105+
106+Le frontend les appelle via `window.go.main.App.<Méthode>()` (Promesses). Les wrappers typés
107+générés par `wails dev`/`wails build` sous `frontend/wailsjs/` sont ignorés par git car rien ne
108+les importe ; `wails generate module` les régénère si l'on veut les fichiers `.d.ts`.
109+
110+### Fichier de réglages
111+
112+`<répertoire de configuration utilisateur>/ori-desktop/settings.json`, soit
113+`~/.config/ori-desktop/settings.json` sous Linux, `~/Library/Application
114+Support/ori-desktop/settings.json` sous macOS, `%AppData%\ori-desktop\settings.json` sous
115+Windows :
116+
117+```json
118+{
119+ "serverUrl": "http://localhost:8888"
120+}
121+```
122+
123+## Arborescence
124+
125+```
126+ori-desktop/
127+├── main.go amorçage Wails (options de fenêtre, assets embarqués, bindings)
128+├── app.go méthodes Go exposées au frontend
129+├── wails.json configuration du projet Wails
130+├── internal/settings/ fichier de réglages (chargement/sauvegarde/normalisation) + tests
131+├── internal/health/ sonde GET /healthz + tests
132+├── frontend/src/ index.html, main.css, main.js (module ES embarqué, sans bundler)
133+└── build/ assets de build Wails (icône, Info.plist, manifeste Windows) ; build/bin est la sortie
134+```
135+
136+## État / limites connues
137+
138+- Vérifié à ce jour : tests unitaires, `go vet`, et une compilation croisée Windows
139+ (`wails build -platform windows/amd64`) depuis un bac à sable Linux sans bibliothèques
140+ graphiques. La fenêtre elle-même n'a pas encore été lancée (pas de WebKitGTK dans ce bac à
141+ sable) — voir les notes `.memory/` du dépôt.
142+- ori est chargé en HTTP clair ; à réserver à des serveurs locaux ou de confiance.
143+- Un ori lancé avec un autre `--addr` que `:8888` se rejoint simplement en saisissant l'URL
144+ correspondante sur l'écran de connexion.
new file mode 100644
@@ -0,0 +1,144 @@
1+# ori-desktop — une coquille de bureau Wails pour ori
2+
3+`ori-desktop` enveloppe le client web [ori](../README.md) dans une fenêtre native construite
4+avec [Wails v2](https://wails.io) (Go + la webview de la plateforme). Il ne réimplémente **pas**
5+ori : le serveur ori continue de servir la SPA React et les points d'entrée `/ws`, `/api/files`,
6+`/ws/terminal`. L'application de bureau se contente de
7+
8+1. afficher un petit **écran de connexion** avec l'URL du serveur ori (par défaut
9+ `http://localhost:8888`), mémorisée d'une exécution à l'autre dans un fichier JSON ;
10+2. vérifier le serveur avec **`GET /healthz`** via une méthode Go exposée au frontend ;
11+3. puis afficher **la webapp ori elle-même** dans la fenêtre.
12+
13+C'est un module Go séparé (`rickub.com/bots-garden/ori-desktop`) : le `go test ./...` et le
14+`make build` du dépôt parent ne sont pas affectés.
15+
16+## Prérequis
17+
18+- Go 1.25+ (exigence de Wails v2.16, reprise par le module).
19+- La CLI Wails v2 : `go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0`
20+ (puis vérifier que `$(go env GOPATH)/bin` est dans le `PATH`).
21+- La chaîne d'outils graphique attendue par Wails — `wails doctor` liste ce qui manque :
22+ - **Linux** : `gcc`, `pkg-config`, `libgtk-3-dev`, `libwebkit2gtk-4.0-dev` (ou `-4.1-dev`
23+ avec `-tags webkit2_41`) ;
24+ - **macOS** : les Xcode command line tools ;
25+ - **Windows** : le runtime WebView2 (préinstallé sur Windows 10/11) ; NSIS seulement pour
26+ l'installeur.
27+- Un serveur ori en marche. Depuis la racine du dépôt : `make run` ou `make run-mock`.
28+
29+Aucun Node/npm n'est nécessaire : le frontend est du HTML/CSS/JS pur embarqué tel quel (pas de
30+bundler).
31+
32+## Lancer en mode développement
33+
34+```bash
35+cd ori-desktop
36+wails dev
37+```
38+
39+`wails dev` compile la partie Go, ouvre la fenêtre et recharge le frontend à chaque modification
40+sous `frontend/src/` (`assetdir` dans `wails.json`). Il sert aussi l'application sur
41+http://localhost:34115 pour l'ouvrir dans un navigateur classique avec les devtools tout en
42+appelant les méthodes Go.
43+
44+## Construire un binaire distribuable
45+
46+```bash
47+cd ori-desktop
48+wails build # → build/bin/ori-desktop (ou .app / .exe)
49+# ou, depuis la racine du dépôt :
50+make desktop
51+```
52+
53+La compilation croisée vers Windows depuis Linux/macOS fonctionne sans chaîne C :
54+`wails build -platform windows/amd64``build/bin/ori-desktop.exe`.
55+
56+## Tests
57+
58+La logique de connexion est en Go pur et testée sans aucune dépendance graphique :
59+
60+```bash
61+cd ori-desktop && go test ./internal/...
62+# ou, depuis la racine du dépôt :
63+make desktop-test
64+```
65+
66+`internal/settings` couvre les allers-retours chargement/sauvegarde, les valeurs par défaut au
67+premier lancement, les fichiers invalides et la normalisation d'URL ; `internal/health` couvre
68+`/healthz` face à un serveur `httptest` (sain, non-ori, injoignable, contexte annulé).
69+
70+## Comment il se connecte à ori
71+
72+```
73+┌──────────────── ori-desktop (fenêtre Wails) ───────────────┐
74+│ frontend/src (embarqué, origine wails://) │
75+│ écran de connexion ──► window.go.main.App.Connect(url) │
76+│ │ Go : NormalizeURL → GET /healthz → sauvegarde des réglages
77+│ ▼ │
78+│ <iframe src="http://localhost:8888/"> ◄── le serveur ori sert la SPA,
79+│ la SPA ouvre ws://localhost:8888/ws, /ws/terminal, /api/files comme d'habitude
80+└─────────────────────────────────────────────────────────────┘
81+```
82+
83+**Pourquoi une iframe ?** Wails v2 n'offre aucun appel runtime pour faire naviguer la webview
84+principale vers une URL externe ; la fenêtre affiche toujours le frontend embarqué
85+(`runtime.BrowserOpenURL` ouvre le navigateur système, proposé comme solution de repli « Open in
86+browser »). Charger ori dans une `<iframe>` conserve une fine barre native (URL, Reload, Open in
87+browser, Disconnect) autour de la SPA ori intacte. Cela fonctionne parce qu'ori n'envoie ni
88+en-tête `X-Frame-Options` ni CSP `frame-ancestors`, et parce que la SPA dérive ses URL
89+WebSocket de `window.location` — dans l'iframe c'est l'origine ori elle-même, donc le contrôle
90+d'origine `localhost:*` des WebSockets d'ori est satisfait.
91+
92+Sur macOS, `build/darwin/Info.plist` positionne `NSAppTransportSecurity/NSAllowsLocalNetworking`
93+pour qu'App Transport Security autorise le chargement HTTP en clair sur la boucle locale.
94+
95+### Méthodes Go exposées au frontend (`app.go`)
96+
97+| Méthode | Rôle |
98+| --- | --- |
99+| `GetConfig() → settings.Settings` | Réglages mémorisés (valeurs par défaut au premier lancement). |
100+| `SaveConfig(settings.Settings)` | Normalise l'URL puis persiste. |
101+| `CheckHealth(url) → health.Report` | `GET <url>/healthz` ; ne rejette jamais — `ok`, `statusCode`, `detail`. |
102+| `Connect(url) → string` | Normalise, vérifie la santé, mémorise l'URL, renvoie l'URL de base à charger. |
103+| `OpenInBrowser(url)` | Repli `runtime.BrowserOpenURL`. |
104+| `SettingsPath() → string` | Emplacement du fichier de réglages (affiché à l'écran). |
105+
106+Le frontend les appelle via `window.go.main.App.<Méthode>()` (Promesses). Les wrappers typés
107+générés par `wails dev`/`wails build` sous `frontend/wailsjs/` sont ignorés par git car rien ne
108+les importe ; `wails generate module` les régénère si l'on veut les fichiers `.d.ts`.
109+
110+### Fichier de réglages
111+
112+`<répertoire de configuration utilisateur>/ori-desktop/settings.json`, soit
113+`~/.config/ori-desktop/settings.json` sous Linux, `~/Library/Application
114+Support/ori-desktop/settings.json` sous macOS, `%AppData%\ori-desktop\settings.json` sous
115+Windows :
116+
117+```json
118+{
119+ "serverUrl": "http://localhost:8888"
120+}
121+```
122+
123+## Arborescence
124+
125+```
126+ori-desktop/
127+├── main.go amorçage Wails (options de fenêtre, assets embarqués, bindings)
128+├── app.go méthodes Go exposées au frontend
129+├── wails.json configuration du projet Wails
130+├── internal/settings/ fichier de réglages (chargement/sauvegarde/normalisation) + tests
131+├── internal/health/ sonde GET /healthz + tests
132+├── frontend/src/ index.html, main.css, main.js (module ES embarqué, sans bundler)
133+└── build/ assets de build Wails (icône, Info.plist, manifeste Windows) ; build/bin est la sortie
134+```
135+
136+## État / limites connues
137+
138+- Vérifié à ce jour : tests unitaires, `go vet`, et une compilation croisée Windows
139+ (`wails build -platform windows/amd64`) depuis un bac à sable Linux sans bibliothèques
140+ graphiques. La fenêtre elle-même n'a pas encore été lancée (pas de WebKitGTK dans ce bac à
141+ sable) — voir les notes `.memory/` du dépôt.
142+- ori est chargé en HTTP clair ; à réserver à des serveurs locaux ou de confiance.
143+- Un ori lancé avec un autre `--addr` que `:8888` se rejoint simplement en saisissant l'URL
144+ correspondante sur l'écran de connexion.
added ori-desktop/README.md +138 -0
new file mode 100644
@@ -0,0 +1,138 @@
1+# ori-desktop — a Wails desktop shell for ori
2+
3+`ori-desktop` wraps the [ori](../README.md) web client in a native window built with
4+[Wails v2](https://wails.io) (Go + the platform's webview). It does **not** reimplement ori:
5+the ori server keeps serving the React SPA and the `/ws`, `/api/files`, `/ws/terminal`
6+endpoints. The desktop app only
7+
8+1. shows a small **connection screen** with the ori server URL (default `http://localhost:8888`),
9+ remembered between runs in a JSON settings file;
10+2. checks the server with **`GET /healthz`** through a Go-bound method;
11+3. then displays the **ori webapp itself** inside the window.
12+
13+It is a separate Go module (`rickub.com/bots-garden/ori-desktop`), so the parent
14+repository's `go test ./...` and `make build` are unaffected.
15+
16+## Prerequisites
17+
18+- Go 1.25+ (the module follows Wails v2.16's requirement).
19+- The Wails v2 CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0`
20+ (then make sure `$(go env GOPATH)/bin` is on your `PATH`).
21+- The platform GUI toolchain Wails needs — run `wails doctor` to see what is missing:
22+ - **Linux**: `gcc`, `pkg-config`, `libgtk-3-dev`, `libwebkit2gtk-4.0-dev` (or `-4.1-dev`
23+ with `-tags webkit2_41`);
24+ - **macOS**: Xcode command line tools;
25+ - **Windows**: WebView2 runtime (preinstalled on Windows 10/11); NSIS only for the installer.
26+- A running ori server. From the repository root: `make run` or `make run-mock`.
27+
28+No Node/npm is needed: the frontend is plain HTML/CSS/JS embedded as-is (no bundler).
29+
30+## Run in development mode
31+
32+```bash
33+cd ori-desktop
34+wails dev
35+```
36+
37+`wails dev` builds the Go side, opens the window, and reloads the frontend when a file under
38+`frontend/src/` changes (`assetdir` in `wails.json`). It also serves the app at
39+http://localhost:34115 so you can open it in a normal browser with devtools and still call the
40+Go methods.
41+
42+## Build a redistributable binary
43+
44+```bash
45+cd ori-desktop
46+wails build # → build/bin/ori-desktop (or .app / .exe)
47+# or, from the repository root:
48+make desktop
49+```
50+
51+Cross-compiling for Windows from Linux/macOS works without any C toolchain:
52+`wails build -platform windows/amd64``build/bin/ori-desktop.exe`.
53+
54+## Tests
55+
56+The connection logic is pure Go and tested without any GUI dependency:
57+
58+```bash
59+cd ori-desktop && go test ./internal/...
60+# or, from the repository root:
61+make desktop-test
62+```
63+
64+`internal/settings` covers load/save round trips, defaults on a first run, invalid files and URL
65+normalisation; `internal/health` covers `/healthz` against an `httptest` server (healthy,
66+non-ori, unreachable, cancelled).
67+
68+## How it connects to ori
69+
70+```
71+┌──────────────── ori-desktop (Wails window) ────────────────┐
72+│ frontend/src (embedded, wails:// origin) │
73+│ connection screen ──► window.go.main.App.Connect(url) │
74+│ │ Go: NormalizeURL → GET /healthz → Save settings
75+│ ▼ │
76+│ <iframe src="http://localhost:8888/"> ◄── ori server serves the SPA,
77+│ SPA opens ws://localhost:8888/ws, /ws/terminal, /api/files as usual
78+└─────────────────────────────────────────────────────────────┘
79+```
80+
81+**Why an iframe?** Wails v2 gives no runtime call to navigate the main webview to an external
82+URL; the window always shows the embedded frontend (`runtime.BrowserOpenURL` opens the system
83+browser instead, which is offered as the "Open in browser" fallback). Loading ori in an
84+`<iframe>` keeps a thin native bar (URL, Reload, Open in browser, Disconnect) around the
85+untouched ori SPA. This works because ori sends no `X-Frame-Options`/CSP `frame-ancestors`
86+header, and because the SPA derives its WebSocket URLs from `window.location` — inside the
87+iframe that is the ori origin itself, so ori's `localhost:*` WebSocket origin check passes.
88+
89+On macOS, `build/darwin/Info.plist` sets `NSAppTransportSecurity/NSAllowsLocalNetworking` so
90+App Transport Security allows the plain-HTTP loopback load.
91+
92+### Go methods bound to the frontend (`app.go`)
93+
94+| Method | Purpose |
95+| --- | --- |
96+| `GetConfig() → settings.Settings` | Remembered settings (defaults on a first run). |
97+| `SaveConfig(settings.Settings)` | Normalise the URL and persist. |
98+| `CheckHealth(url) → health.Report` | `GET <url>/healthz`; never rejects — `ok`, `statusCode`, `detail`. |
99+| `Connect(url) → string` | Normalise, check health, remember the URL, return the base URL to load. |
100+| `OpenInBrowser(url)` | `runtime.BrowserOpenURL` fallback. |
101+| `SettingsPath() → string` | Where the settings file lives (displayed on the screen). |
102+
103+The frontend calls them as `window.go.main.App.<Method>()` (Promises). The typed wrappers that
104+`wails dev`/`wails build` generate under `frontend/wailsjs/` are gitignored because nothing
105+imports them; regenerate them with `wails generate module` if you want the `.d.ts` files.
106+
107+### Settings file
108+
109+`<user config dir>/ori-desktop/settings.json`, i.e. `~/.config/ori-desktop/settings.json` on
110+Linux, `~/Library/Application Support/ori-desktop/settings.json` on macOS,
111+`%AppData%\ori-desktop\settings.json` on Windows:
112+
113+```json
114+{
115+ "serverUrl": "http://localhost:8888"
116+}
117+```
118+
119+## Layout
120+
121+```
122+ori-desktop/
123+├── main.go Wails bootstrap (window options, embedded assets, bindings)
124+├── app.go Go methods bound to the frontend
125+├── wails.json Wails project configuration
126+├── internal/settings/ settings file (load/save/normalise) + tests
127+├── internal/health/ GET /healthz probe + tests
128+├── frontend/src/ index.html, main.css, main.js (embedded ES module, no bundler)
129+└── build/ Wails build assets (icon, Info.plist, Windows manifest); build/bin is the output
130+```
131+
132+## Status / known limitations
133+
134+- Verified so far: unit tests, `go vet`, and a Windows cross-build (`wails build -platform
135+ windows/amd64`) from a Linux sandbox without GUI libraries. The window itself has not yet been
136+ launched (no WebKitGTK in that sandbox) — see the repository's `.memory/` notes.
137+- ori is loaded over plain HTTP; use it against local or trusted servers only.
138+- Ori's `--addr` other than `:8888` simply means typing the matching URL on the connection screen.
new file mode 100644
@@ -0,0 +1,138 @@
1+# ori-desktop — a Wails desktop shell for ori
2+
3+`ori-desktop` wraps the [ori](../README.md) web client in a native window built with
4+[Wails v2](https://wails.io) (Go + the platform's webview). It does **not** reimplement ori:
5+the ori server keeps serving the React SPA and the `/ws`, `/api/files`, `/ws/terminal`
6+endpoints. The desktop app only
7+
8+1. shows a small **connection screen** with the ori server URL (default `http://localhost:8888`),
9+ remembered between runs in a JSON settings file;
10+2. checks the server with **`GET /healthz`** through a Go-bound method;
11+3. then displays the **ori webapp itself** inside the window.
12+
13+It is a separate Go module (`rickub.com/bots-garden/ori-desktop`), so the parent
14+repository's `go test ./...` and `make build` are unaffected.
15+
16+## Prerequisites
17+
18+- Go 1.25+ (the module follows Wails v2.16's requirement).
19+- The Wails v2 CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0`
20+ (then make sure `$(go env GOPATH)/bin` is on your `PATH`).
21+- The platform GUI toolchain Wails needs — run `wails doctor` to see what is missing:
22+ - **Linux**: `gcc`, `pkg-config`, `libgtk-3-dev`, `libwebkit2gtk-4.0-dev` (or `-4.1-dev`
23+ with `-tags webkit2_41`);
24+ - **macOS**: Xcode command line tools;
25+ - **Windows**: WebView2 runtime (preinstalled on Windows 10/11); NSIS only for the installer.
26+- A running ori server. From the repository root: `make run` or `make run-mock`.
27+
28+No Node/npm is needed: the frontend is plain HTML/CSS/JS embedded as-is (no bundler).
29+
30+## Run in development mode
31+
32+```bash
33+cd ori-desktop
34+wails dev
35+```
36+
37+`wails dev` builds the Go side, opens the window, and reloads the frontend when a file under
38+`frontend/src/` changes (`assetdir` in `wails.json`). It also serves the app at
39+http://localhost:34115 so you can open it in a normal browser with devtools and still call the
40+Go methods.
41+
42+## Build a redistributable binary
43+
44+```bash
45+cd ori-desktop
46+wails build # → build/bin/ori-desktop (or .app / .exe)
47+# or, from the repository root:
48+make desktop
49+```
50+
51+Cross-compiling for Windows from Linux/macOS works without any C toolchain:
52+`wails build -platform windows/amd64``build/bin/ori-desktop.exe`.
53+
54+## Tests
55+
56+The connection logic is pure Go and tested without any GUI dependency:
57+
58+```bash
59+cd ori-desktop && go test ./internal/...
60+# or, from the repository root:
61+make desktop-test
62+```
63+
64+`internal/settings` covers load/save round trips, defaults on a first run, invalid files and URL
65+normalisation; `internal/health` covers `/healthz` against an `httptest` server (healthy,
66+non-ori, unreachable, cancelled).
67+
68+## How it connects to ori
69+
70+```
71+┌──────────────── ori-desktop (Wails window) ────────────────┐
72+│ frontend/src (embedded, wails:// origin) │
73+│ connection screen ──► window.go.main.App.Connect(url) │
74+│ │ Go: NormalizeURL → GET /healthz → Save settings
75+│ ▼ │
76+│ <iframe src="http://localhost:8888/"> ◄── ori server serves the SPA,
77+│ SPA opens ws://localhost:8888/ws, /ws/terminal, /api/files as usual
78+└─────────────────────────────────────────────────────────────┘
79+```
80+
81+**Why an iframe?** Wails v2 gives no runtime call to navigate the main webview to an external
82+URL; the window always shows the embedded frontend (`runtime.BrowserOpenURL` opens the system
83+browser instead, which is offered as the "Open in browser" fallback). Loading ori in an
84+`<iframe>` keeps a thin native bar (URL, Reload, Open in browser, Disconnect) around the
85+untouched ori SPA. This works because ori sends no `X-Frame-Options`/CSP `frame-ancestors`
86+header, and because the SPA derives its WebSocket URLs from `window.location` — inside the
87+iframe that is the ori origin itself, so ori's `localhost:*` WebSocket origin check passes.
88+
89+On macOS, `build/darwin/Info.plist` sets `NSAppTransportSecurity/NSAllowsLocalNetworking` so
90+App Transport Security allows the plain-HTTP loopback load.
91+
92+### Go methods bound to the frontend (`app.go`)
93+
94+| Method | Purpose |
95+| --- | --- |
96+| `GetConfig() → settings.Settings` | Remembered settings (defaults on a first run). |
97+| `SaveConfig(settings.Settings)` | Normalise the URL and persist. |
98+| `CheckHealth(url) → health.Report` | `GET <url>/healthz`; never rejects — `ok`, `statusCode`, `detail`. |
99+| `Connect(url) → string` | Normalise, check health, remember the URL, return the base URL to load. |
100+| `OpenInBrowser(url)` | `runtime.BrowserOpenURL` fallback. |
101+| `SettingsPath() → string` | Where the settings file lives (displayed on the screen). |
102+
103+The frontend calls them as `window.go.main.App.<Method>()` (Promises). The typed wrappers that
104+`wails dev`/`wails build` generate under `frontend/wailsjs/` are gitignored because nothing
105+imports them; regenerate them with `wails generate module` if you want the `.d.ts` files.
106+
107+### Settings file
108+
109+`<user config dir>/ori-desktop/settings.json`, i.e. `~/.config/ori-desktop/settings.json` on
110+Linux, `~/Library/Application Support/ori-desktop/settings.json` on macOS,
111+`%AppData%\ori-desktop\settings.json` on Windows:
112+
113+```json
114+{
115+ "serverUrl": "http://localhost:8888"
116+}
117+```
118+
119+## Layout
120+
121+```
122+ori-desktop/
123+├── main.go Wails bootstrap (window options, embedded assets, bindings)
124+├── app.go Go methods bound to the frontend
125+├── wails.json Wails project configuration
126+├── internal/settings/ settings file (load/save/normalise) + tests
127+├── internal/health/ GET /healthz probe + tests
128+├── frontend/src/ index.html, main.css, main.js (embedded ES module, no bundler)
129+└── build/ Wails build assets (icon, Info.plist, Windows manifest); build/bin is the output
130+```
131+
132+## Status / known limitations
133+
134+- Verified so far: unit tests, `go vet`, and a Windows cross-build (`wails build -platform
135+ windows/amd64`) from a Linux sandbox without GUI libraries. The window itself has not yet been
136+ launched (no WebKitGTK in that sandbox) — see the repository's `.memory/` notes.
137+- ori is loaded over plain HTTP; use it against local or trusted servers only.
138+- Ori's `--addr` other than `:8888` simply means typing the matching URL on the connection screen.
added ori-desktop/app.go +125 -0
new file mode 100644
@@ -0,0 +1,125 @@
1+package main
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/wailsapp/wails/v2/pkg/runtime"
9+
10+ "rickub.com/bots-garden/ori-desktop/internal/health"
11+ "rickub.com/bots-garden/ori-desktop/internal/settings"
12+)
13+
14+// App holds the state shared by the Go-bound methods exposed to the frontend.
15+// Every exported method on App is callable from JavaScript as
16+// window.go.main.App.<Method>(...) and returns a Promise.
17+type App struct {
18+ ctx context.Context
19+ settingsPath string
20+ client *http.Client
21+}
22+
23+// NewApp creates the application state. The settings location is resolved
24+// once here; if the platform exposes no user config directory the path is
25+// left empty and SaveConfig reports the problem instead of guessing.
26+func NewApp() *App {
27+ path, err := settings.DefaultPath()
28+ if err != nil {
29+ path = ""
30+ }
31+ return &App{
32+ settingsPath: path,
33+ client: &http.Client{Timeout: health.DefaultTimeout},
34+ }
35+}
36+
37+// startup is the Wails OnStartup hook; it keeps the runtime context needed by
38+// runtime.* calls such as BrowserOpenURL.
39+func (a *App) startup(ctx context.Context) {
40+ a.ctx = ctx
41+}
42+
43+// GetConfig returns the remembered settings (defaults on a first run).
44+func (a *App) GetConfig() (settings.Settings, error) {
45+ if a.settingsPath == "" {
46+ return settings.Default(), nil
47+ }
48+ return settings.Load(a.settingsPath)
49+}
50+
51+// SaveConfig normalises the server URL and persists the settings.
52+func (a *App) SaveConfig(s settings.Settings) error {
53+ if a.settingsPath == "" {
54+ return fmt.Errorf("no user configuration directory available on this platform")
55+ }
56+ normalized, err := settings.NormalizeURL(s.ServerURL)
57+ if err != nil {
58+ return err
59+ }
60+ s.ServerURL = normalized
61+ return settings.Save(a.settingsPath, s)
62+}
63+
64+// CheckHealth probes GET <url>/healthz and reports the outcome. It never
65+// fails the Promise: a malformed URL or an unreachable server both come back
66+// as a Report with OK=false and an explanatory Detail.
67+func (a *App) CheckHealth(rawURL string) health.Report {
68+ base, err := settings.NormalizeURL(rawURL)
69+ if err != nil {
70+ return health.Report{URL: rawURL, Detail: err.Error()}
71+ }
72+ return health.Check(a.context(), a.client, base)
73+}
74+
75+// Connect is the one-call happy path used by the connection screen: it
76+// normalises the URL, checks /healthz, remembers the URL on success and
77+// returns the normalised base URL the frontend should load. On failure the
78+// Promise rejects with the health Detail so the screen can show it.
79+func (a *App) Connect(rawURL string) (string, error) {
80+ base, err := settings.NormalizeURL(rawURL)
81+ if err != nil {
82+ return "", err
83+ }
84+ rep := health.Check(a.context(), a.client, base)
85+ if !rep.OK {
86+ return "", fmt.Errorf("%s", rep.Detail)
87+ }
88+ if a.settingsPath != "" {
89+ if err := settings.Save(a.settingsPath, settings.Settings{ServerURL: base}); err != nil {
90+ // Not fatal for the session: the user is connected, only the
91+ // memory of the URL is lost. Surface it in the returned error?
92+ // No — the frontend treats an error as "not connected"; log it.
93+ fmt.Println("ori-desktop: warning:", err)
94+ }
95+ }
96+ return base, nil
97+}
98+
99+// OpenInBrowser opens the given URL (normalised first) in the system's
100+// default browser — the fallback when embedding is not wanted.
101+func (a *App) OpenInBrowser(rawURL string) error {
102+ base, err := settings.NormalizeURL(rawURL)
103+ if err != nil {
104+ return err
105+ }
106+ if a.ctx == nil {
107+ return fmt.Errorf("application runtime not ready")
108+ }
109+ runtime.BrowserOpenURL(a.ctx, base)
110+ return nil
111+}
112+
113+// SettingsPath tells the frontend where the settings live, for display.
114+func (a *App) SettingsPath() string {
115+ return a.settingsPath
116+}
117+
118+// context returns the runtime context, or a background one before startup
119+// (bound methods are only reachable after startup, this is belt and braces).
120+func (a *App) context() context.Context {
121+ if a.ctx != nil {
122+ return a.ctx
123+ }
124+ return context.Background()
125+}
new file mode 100644
@@ -0,0 +1,125 @@
1+package main
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/wailsapp/wails/v2/pkg/runtime"
9+
10+ "rickub.com/bots-garden/ori-desktop/internal/health"
11+ "rickub.com/bots-garden/ori-desktop/internal/settings"
12+)
13+
14+// App holds the state shared by the Go-bound methods exposed to the frontend.
15+// Every exported method on App is callable from JavaScript as
16+// window.go.main.App.<Method>(...) and returns a Promise.
17+type App struct {
18+ ctx context.Context
19+ settingsPath string
20+ client *http.Client
21+}
22+
23+// NewApp creates the application state. The settings location is resolved
24+// once here; if the platform exposes no user config directory the path is
25+// left empty and SaveConfig reports the problem instead of guessing.
26+func NewApp() *App {
27+ path, err := settings.DefaultPath()
28+ if err != nil {
29+ path = ""
30+ }
31+ return &App{
32+ settingsPath: path,
33+ client: &http.Client{Timeout: health.DefaultTimeout},
34+ }
35+}
36+
37+// startup is the Wails OnStartup hook; it keeps the runtime context needed by
38+// runtime.* calls such as BrowserOpenURL.
39+func (a *App) startup(ctx context.Context) {
40+ a.ctx = ctx
41+}
42+
43+// GetConfig returns the remembered settings (defaults on a first run).
44+func (a *App) GetConfig() (settings.Settings, error) {
45+ if a.settingsPath == "" {
46+ return settings.Default(), nil
47+ }
48+ return settings.Load(a.settingsPath)
49+}
50+
51+// SaveConfig normalises the server URL and persists the settings.
52+func (a *App) SaveConfig(s settings.Settings) error {
53+ if a.settingsPath == "" {
54+ return fmt.Errorf("no user configuration directory available on this platform")
55+ }
56+ normalized, err := settings.NormalizeURL(s.ServerURL)
57+ if err != nil {
58+ return err
59+ }
60+ s.ServerURL = normalized
61+ return settings.Save(a.settingsPath, s)
62+}
63+
64+// CheckHealth probes GET <url>/healthz and reports the outcome. It never
65+// fails the Promise: a malformed URL or an unreachable server both come back
66+// as a Report with OK=false and an explanatory Detail.
67+func (a *App) CheckHealth(rawURL string) health.Report {
68+ base, err := settings.NormalizeURL(rawURL)
69+ if err != nil {
70+ return health.Report{URL: rawURL, Detail: err.Error()}
71+ }
72+ return health.Check(a.context(), a.client, base)
73+}
74+
75+// Connect is the one-call happy path used by the connection screen: it
76+// normalises the URL, checks /healthz, remembers the URL on success and
77+// returns the normalised base URL the frontend should load. On failure the
78+// Promise rejects with the health Detail so the screen can show it.
79+func (a *App) Connect(rawURL string) (string, error) {
80+ base, err := settings.NormalizeURL(rawURL)
81+ if err != nil {
82+ return "", err
83+ }
84+ rep := health.Check(a.context(), a.client, base)
85+ if !rep.OK {
86+ return "", fmt.Errorf("%s", rep.Detail)
87+ }
88+ if a.settingsPath != "" {
89+ if err := settings.Save(a.settingsPath, settings.Settings{ServerURL: base}); err != nil {
90+ // Not fatal for the session: the user is connected, only the
91+ // memory of the URL is lost. Surface it in the returned error?
92+ // No — the frontend treats an error as "not connected"; log it.
93+ fmt.Println("ori-desktop: warning:", err)
94+ }
95+ }
96+ return base, nil
97+}
98+
99+// OpenInBrowser opens the given URL (normalised first) in the system's
100+// default browser — the fallback when embedding is not wanted.
101+func (a *App) OpenInBrowser(rawURL string) error {
102+ base, err := settings.NormalizeURL(rawURL)
103+ if err != nil {
104+ return err
105+ }
106+ if a.ctx == nil {
107+ return fmt.Errorf("application runtime not ready")
108+ }
109+ runtime.BrowserOpenURL(a.ctx, base)
110+ return nil
111+}
112+
113+// SettingsPath tells the frontend where the settings live, for display.
114+func (a *App) SettingsPath() string {
115+ return a.settingsPath
116+}
117+
118+// context returns the runtime context, or a background one before startup
119+// (bound methods are only reachable after startup, this is belt and braces).
120+func (a *App) context() context.Context {
121+ if a.ctx != nil {
122+ return a.ctx
123+ }
124+ return context.Background()
125+}
added ori-desktop/build/README.md +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+# Build Directory
2+
3+The build directory is used to house all the build files and assets for your application.
4+
5+The structure is:
6+
7+* bin - Output directory
8+* darwin - macOS specific files
9+* windows - Windows specific files
10+
11+## Mac
12+
13+The `darwin` directory holds files specific to Mac builds.
14+These may be customised and used as part of the build. To return these files to the default state, simply delete them
15+and
16+build with `wails build`.
17+
18+The directory contains the following files:
19+
20+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
21+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
22+
23+## Windows
24+
25+The `windows` directory contains the manifest and rc files used when building with `wails build`.
26+These may be customised for your application. To return these files to the default state, simply delete them and
27+build with `wails build`.
28+
29+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
30+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
31+ will be created using the `appicon.png` file in the build directory.
32+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
33+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
34+ as well as the application itself (right click the exe -> properties -> details)
35+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,35 @@
1+# Build Directory
2+
3+The build directory is used to house all the build files and assets for your application.
4+
5+The structure is:
6+
7+* bin - Output directory
8+* darwin - macOS specific files
9+* windows - Windows specific files
10+
11+## Mac
12+
13+The `darwin` directory holds files specific to Mac builds.
14+These may be customised and used as part of the build. To return these files to the default state, simply delete them
15+and
16+build with `wails build`.
17+
18+The directory contains the following files:
19+
20+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
21+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
22+
23+## Windows
24+
25+The `windows` directory contains the manifest and rc files used when building with `wails build`.
26+These may be customised for your application. To return these files to the default state, simply delete them and
27+build with `wails build`.
28+
29+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
30+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
31+ will be created using the `appicon.png` file in the build directory.
32+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
33+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
34+ as well as the application itself (right click the exe -> properties -> details)
35+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file\ No newline at end of file
added ori-desktop/build/appicon.png +0 -0
new file mode 100644
Binary files /dev/null and b/ori-desktop/build/appicon.png differ
new file mode 100644
Binary files /dev/null and b/ori-desktop/build/appicon.png differBinary files /dev/null and b/ori-desktop/build/appicon.png differ
added ori-desktop/build/darwin/Info.dev.plist +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>{{.Info.ProductName}}</string>
8+ <key>CFBundleExecutable</key>
9+ <string>{{.OutputFilename}}</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.wails.{{safeBundleID .Name}}</string>
12+ <key>CFBundleVersion</key>
13+ <string>{{.Info.ProductVersion}}</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>{{.Info.Comments}}</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>{{.Info.ProductVersion}}</string>
18+ <key>CFBundleIconFile</key>
19+ <string>iconfile</string>
20+ <key>LSMinimumSystemVersion</key>
21+ <string>10.13.0</string>
22+ <key>NSHighResolutionCapable</key>
23+ <string>true</string>
24+ <key>NSHumanReadableCopyright</key>
25+ <string>{{.Info.Copyright}}</string>
26+ {{if .Info.FileAssociations}}
27+ <key>CFBundleDocumentTypes</key>
28+ <array>
29+ {{range .Info.FileAssociations}}
30+ <dict>
31+ <key>CFBundleTypeExtensions</key>
32+ <array>
33+ <string>{{.Ext}}</string>
34+ </array>
35+ <key>CFBundleTypeName</key>
36+ <string>{{.Name}}</string>
37+ <key>CFBundleTypeRole</key>
38+ <string>{{.Role}}</string>
39+ <key>CFBundleTypeIconFile</key>
40+ <string>{{.IconName}}</string>
41+ </dict>
42+ {{end}}
43+ </array>
44+ {{end}}
45+ {{if .Info.Protocols}}
46+ <key>CFBundleURLTypes</key>
47+ <array>
48+ {{range .Info.Protocols}}
49+ <dict>
50+ <key>CFBundleURLName</key>
51+ <string>com.wails.{{.Scheme}}</string>
52+ <key>CFBundleURLSchemes</key>
53+ <array>
54+ <string>{{.Scheme}}</string>
55+ </array>
56+ <key>CFBundleTypeRole</key>
57+ <string>{{.Role}}</string>
58+ </dict>
59+ {{end}}
60+ </array>
61+ {{end}}
62+ <key>NSAppTransportSecurity</key>
63+ <dict>
64+ <key>NSAllowsLocalNetworking</key>
65+ <true/>
66+ </dict>
67+ </dict>
68+</plist>
new file mode 100644
@@ -0,0 +1,68 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>{{.Info.ProductName}}</string>
8+ <key>CFBundleExecutable</key>
9+ <string>{{.OutputFilename}}</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.wails.{{safeBundleID .Name}}</string>
12+ <key>CFBundleVersion</key>
13+ <string>{{.Info.ProductVersion}}</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>{{.Info.Comments}}</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>{{.Info.ProductVersion}}</string>
18+ <key>CFBundleIconFile</key>
19+ <string>iconfile</string>
20+ <key>LSMinimumSystemVersion</key>
21+ <string>10.13.0</string>
22+ <key>NSHighResolutionCapable</key>
23+ <string>true</string>
24+ <key>NSHumanReadableCopyright</key>
25+ <string>{{.Info.Copyright}}</string>
26+ {{if .Info.FileAssociations}}
27+ <key>CFBundleDocumentTypes</key>
28+ <array>
29+ {{range .Info.FileAssociations}}
30+ <dict>
31+ <key>CFBundleTypeExtensions</key>
32+ <array>
33+ <string>{{.Ext}}</string>
34+ </array>
35+ <key>CFBundleTypeName</key>
36+ <string>{{.Name}}</string>
37+ <key>CFBundleTypeRole</key>
38+ <string>{{.Role}}</string>
39+ <key>CFBundleTypeIconFile</key>
40+ <string>{{.IconName}}</string>
41+ </dict>
42+ {{end}}
43+ </array>
44+ {{end}}
45+ {{if .Info.Protocols}}
46+ <key>CFBundleURLTypes</key>
47+ <array>
48+ {{range .Info.Protocols}}
49+ <dict>
50+ <key>CFBundleURLName</key>
51+ <string>com.wails.{{.Scheme}}</string>
52+ <key>CFBundleURLSchemes</key>
53+ <array>
54+ <string>{{.Scheme}}</string>
55+ </array>
56+ <key>CFBundleTypeRole</key>
57+ <string>{{.Role}}</string>
58+ </dict>
59+ {{end}}
60+ </array>
61+ {{end}}
62+ <key>NSAppTransportSecurity</key>
63+ <dict>
64+ <key>NSAllowsLocalNetworking</key>
65+ <true/>
66+ </dict>
67+ </dict>
68+</plist>
added ori-desktop/build/darwin/Info.plist +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>{{.Info.ProductName}}</string>
8+ <key>CFBundleExecutable</key>
9+ <string>{{.OutputFilename}}</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.wails.{{safeBundleID .Name}}</string>
12+ <key>CFBundleVersion</key>
13+ <string>{{.Info.ProductVersion}}</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>{{.Info.Comments}}</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>{{.Info.ProductVersion}}</string>
18+ <key>CFBundleIconFile</key>
19+ <string>iconfile</string>
20+ <key>LSMinimumSystemVersion</key>
21+ <string>10.13.0</string>
22+ <key>NSAppTransportSecurity</key>
23+ <dict>
24+ <key>NSAllowsLocalNetworking</key>
25+ <true/>
26+ </dict>
27+ <key>NSHighResolutionCapable</key>
28+ <string>true</string>
29+ <key>NSHumanReadableCopyright</key>
30+ <string>{{.Info.Copyright}}</string>
31+ {{if .Info.FileAssociations}}
32+ <key>CFBundleDocumentTypes</key>
33+ <array>
34+ {{range .Info.FileAssociations}}
35+ <dict>
36+ <key>CFBundleTypeExtensions</key>
37+ <array>
38+ <string>{{.Ext}}</string>
39+ </array>
40+ <key>CFBundleTypeName</key>
41+ <string>{{.Name}}</string>
42+ <key>CFBundleTypeRole</key>
43+ <string>{{.Role}}</string>
44+ <key>CFBundleTypeIconFile</key>
45+ <string>{{.IconName}}</string>
46+ </dict>
47+ {{end}}
48+ </array>
49+ {{end}}
50+ {{if .Info.Protocols}}
51+ <key>CFBundleURLTypes</key>
52+ <array>
53+ {{range .Info.Protocols}}
54+ <dict>
55+ <key>CFBundleURLName</key>
56+ <string>com.wails.{{.Scheme}}</string>
57+ <key>CFBundleURLSchemes</key>
58+ <array>
59+ <string>{{.Scheme}}</string>
60+ </array>
61+ <key>CFBundleTypeRole</key>
62+ <string>{{.Role}}</string>
63+ </dict>
64+ {{end}}
65+ </array>
66+ {{end}}
67+ </dict>
68+</plist>
new file mode 100644
@@ -0,0 +1,68 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>{{.Info.ProductName}}</string>
8+ <key>CFBundleExecutable</key>
9+ <string>{{.OutputFilename}}</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.wails.{{safeBundleID .Name}}</string>
12+ <key>CFBundleVersion</key>
13+ <string>{{.Info.ProductVersion}}</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>{{.Info.Comments}}</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>{{.Info.ProductVersion}}</string>
18+ <key>CFBundleIconFile</key>
19+ <string>iconfile</string>
20+ <key>LSMinimumSystemVersion</key>
21+ <string>10.13.0</string>
22+ <key>NSAppTransportSecurity</key>
23+ <dict>
24+ <key>NSAllowsLocalNetworking</key>
25+ <true/>
26+ </dict>
27+ <key>NSHighResolutionCapable</key>
28+ <string>true</string>
29+ <key>NSHumanReadableCopyright</key>
30+ <string>{{.Info.Copyright}}</string>
31+ {{if .Info.FileAssociations}}
32+ <key>CFBundleDocumentTypes</key>
33+ <array>
34+ {{range .Info.FileAssociations}}
35+ <dict>
36+ <key>CFBundleTypeExtensions</key>
37+ <array>
38+ <string>{{.Ext}}</string>
39+ </array>
40+ <key>CFBundleTypeName</key>
41+ <string>{{.Name}}</string>
42+ <key>CFBundleTypeRole</key>
43+ <string>{{.Role}}</string>
44+ <key>CFBundleTypeIconFile</key>
45+ <string>{{.IconName}}</string>
46+ </dict>
47+ {{end}}
48+ </array>
49+ {{end}}
50+ {{if .Info.Protocols}}
51+ <key>CFBundleURLTypes</key>
52+ <array>
53+ {{range .Info.Protocols}}
54+ <dict>
55+ <key>CFBundleURLName</key>
56+ <string>com.wails.{{.Scheme}}</string>
57+ <key>CFBundleURLSchemes</key>
58+ <array>
59+ <string>{{.Scheme}}</string>
60+ </array>
61+ <key>CFBundleTypeRole</key>
62+ <string>{{.Role}}</string>
63+ </dict>
64+ {{end}}
65+ </array>
66+ {{end}}
67+ </dict>
68+</plist>
added ori-desktop/build/windows/icon.ico +0 -0
new file mode 100644
Binary files /dev/null and b/ori-desktop/build/windows/icon.ico differ
new file mode 100644
Binary files /dev/null and b/ori-desktop/build/windows/icon.ico differBinary files /dev/null and b/ori-desktop/build/windows/icon.ico differ
added ori-desktop/build/windows/info.json +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "fixed": {
3+ "file_version": "{{.Info.ProductVersion}}"
4+ },
5+ "info": {
6+ "0000": {
7+ "ProductVersion": "{{.Info.ProductVersion}}",
8+ "CompanyName": "{{.Info.CompanyName}}",
9+ "FileDescription": "{{.Info.ProductName}}",
10+ "LegalCopyright": "{{.Info.Copyright}}",
11+ "ProductName": "{{.Info.ProductName}}",
12+ "Comments": "{{.Info.Comments}}"
13+ }
14+ }
15+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "fixed": {
3+ "file_version": "{{.Info.ProductVersion}}"
4+ },
5+ "info": {
6+ "0000": {
7+ "ProductVersion": "{{.Info.ProductVersion}}",
8+ "CompanyName": "{{.Info.CompanyName}}",
9+ "FileDescription": "{{.Info.ProductName}}",
10+ "LegalCopyright": "{{.Info.Copyright}}",
11+ "ProductName": "{{.Info.ProductName}}",
12+ "Comments": "{{.Info.Comments}}"
13+ }
14+ }
15+}
\ No newline at end of file\ No newline at end of file
added ori-desktop/build/windows/installer/project.nsi +122 -0
new file mode 100644
@@ -0,0 +1,122 @@
1+Unicode true
2+
3+####
4+## Please note: Template replacements don't work in this file. They are provided with default defines like
5+## mentioned underneath.
6+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
7+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
8+## from outside of Wails for debugging and development of the installer.
9+##
10+## For development first make a wails nsis build to populate the "wails_tools.nsh":
11+## > wails build --target windows/amd64 --nsis
12+## Then you can call makensis on this file with specifying the path to your binary:
13+## For a AMD64 only installer:
14+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15+## For a ARM64 only installer:
16+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17+## For a installer with both architectures:
18+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19+####
20+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
21+####
22+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
23+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
24+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
25+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
26+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
27+###
28+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30+####
31+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32+####
33+## Include the wails tools
34+####
35+!include "wails_tools.nsh"
36+
37+# The version information for this two must consist of 4 parts
38+VIProductVersion "${INFO_PRODUCTVERSION}.0"
39+VIFileVersion "${INFO_PRODUCTVERSION}.0"
40+
41+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
42+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
43+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
44+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
45+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
46+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
47+
48+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
49+ManifestDPIAware true
50+
51+!include "MUI.nsh"
52+
53+!define MUI_ICON "..\icon.ico"
54+!define MUI_UNICON "..\icon.ico"
55+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
56+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
57+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
58+
59+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
60+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
61+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
62+!insertmacro MUI_PAGE_INSTFILES # Installing page.
63+!insertmacro MUI_PAGE_FINISH # Finished installation page.
64+
65+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
66+
67+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
68+
69+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
70+#!uninstfinalize 'signtool --file "%1"'
71+#!finalize 'signtool --file "%1"'
72+
73+Name "${INFO_PRODUCTNAME}"
74+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
75+!ifdef WAILS_INSTALL_SCOPE
76+ !if "${WAILS_INSTALL_SCOPE}" == "user"
77+ InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
78+ !else
79+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
80+ !endif
81+!else
82+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
83+!endif # Default installing folder ($PROGRAMFILES is Program Files folder).
84+ShowInstDetails show # This will always show the installation details.
85+
86+Function .onInit
87+ !insertmacro wails.checkArchitecture
88+FunctionEnd
89+
90+Section
91+ !insertmacro wails.setShellContext
92+
93+ !insertmacro wails.webview2runtime
94+
95+ SetOutPath $INSTDIR
96+
97+ !insertmacro wails.files
98+
99+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
100+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
101+
102+ !insertmacro wails.associateFiles
103+ !insertmacro wails.associateCustomProtocols
104+
105+ !insertmacro wails.writeUninstaller
106+SectionEnd
107+
108+Section "uninstall"
109+ !insertmacro wails.setShellContext
110+
111+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
112+
113+ RMDir /r $INSTDIR
114+
115+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
116+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
117+
118+ !insertmacro wails.unassociateFiles
119+ !insertmacro wails.unassociateCustomProtocols
120+
121+ !insertmacro wails.deleteUninstaller
122+SectionEnd
new file mode 100644
@@ -0,0 +1,122 @@
1+Unicode true
2+
3+####
4+## Please note: Template replacements don't work in this file. They are provided with default defines like
5+## mentioned underneath.
6+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
7+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
8+## from outside of Wails for debugging and development of the installer.
9+##
10+## For development first make a wails nsis build to populate the "wails_tools.nsh":
11+## > wails build --target windows/amd64 --nsis
12+## Then you can call makensis on this file with specifying the path to your binary:
13+## For a AMD64 only installer:
14+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15+## For a ARM64 only installer:
16+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17+## For a installer with both architectures:
18+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19+####
20+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
21+####
22+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
23+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
24+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
25+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
26+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
27+###
28+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30+####
31+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32+####
33+## Include the wails tools
34+####
35+!include "wails_tools.nsh"
36+
37+# The version information for this two must consist of 4 parts
38+VIProductVersion "${INFO_PRODUCTVERSION}.0"
39+VIFileVersion "${INFO_PRODUCTVERSION}.0"
40+
41+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
42+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
43+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
44+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
45+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
46+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
47+
48+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
49+ManifestDPIAware true
50+
51+!include "MUI.nsh"
52+
53+!define MUI_ICON "..\icon.ico"
54+!define MUI_UNICON "..\icon.ico"
55+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
56+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
57+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
58+
59+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
60+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
61+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
62+!insertmacro MUI_PAGE_INSTFILES # Installing page.
63+!insertmacro MUI_PAGE_FINISH # Finished installation page.
64+
65+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
66+
67+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
68+
69+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
70+#!uninstfinalize 'signtool --file "%1"'
71+#!finalize 'signtool --file "%1"'
72+
73+Name "${INFO_PRODUCTNAME}"
74+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
75+!ifdef WAILS_INSTALL_SCOPE
76+ !if "${WAILS_INSTALL_SCOPE}" == "user"
77+ InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
78+ !else
79+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
80+ !endif
81+!else
82+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
83+!endif # Default installing folder ($PROGRAMFILES is Program Files folder).
84+ShowInstDetails show # This will always show the installation details.
85+
86+Function .onInit
87+ !insertmacro wails.checkArchitecture
88+FunctionEnd
89+
90+Section
91+ !insertmacro wails.setShellContext
92+
93+ !insertmacro wails.webview2runtime
94+
95+ SetOutPath $INSTDIR
96+
97+ !insertmacro wails.files
98+
99+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
100+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
101+
102+ !insertmacro wails.associateFiles
103+ !insertmacro wails.associateCustomProtocols
104+
105+ !insertmacro wails.writeUninstaller
106+SectionEnd
107+
108+Section "uninstall"
109+ !insertmacro wails.setShellContext
110+
111+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
112+
113+ RMDir /r $INSTDIR
114+
115+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
116+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
117+
118+ !insertmacro wails.unassociateFiles
119+ !insertmacro wails.unassociateCustomProtocols
120+
121+ !insertmacro wails.deleteUninstaller
122+SectionEnd
added ori-desktop/build/windows/wails.exe.manifest +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3+ <assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
4+ <dependency>
5+ <dependentAssembly>
6+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
7+ </dependentAssembly>
8+ </dependency>
9+ <asmv3:application>
10+ <asmv3:windowsSettings>
11+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
12+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
13+ </asmv3:windowsSettings>
14+ </asmv3:application>
15+</assembly>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,15 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3+ <assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
4+ <dependency>
5+ <dependentAssembly>
6+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
7+ </dependentAssembly>
8+ </dependency>
9+ <asmv3:application>
10+ <asmv3:windowsSettings>
11+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
12+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
13+ </asmv3:windowsSettings>
14+ </asmv3:application>
15+</assembly>
\ No newline at end of file\ No newline at end of file
added ori-desktop/frontend/src/index.html +41 -0
new file mode 100644
@@ -0,0 +1,41 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+ <meta charset="UTF-8"/>
5+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6+ <title>ori</title>
7+ <link href="/main.css" rel="stylesheet"/>
8+</head>
9+<body>
10+<!-- Screen 1: connection. Shown at startup and after "Disconnect". -->
11+<section class="screen connect" id="connect-screen">
12+ <form class="card" id="connect-form" autocomplete="off">
13+ <h1>ori</h1>
14+ <p class="hint">Address of the ori server (the Go server that serves the web client).</p>
15+ <label for="server-url">Server URL</label>
16+ <input id="server-url" type="text" placeholder="http://localhost:8888" spellcheck="false"/>
17+ <div class="actions">
18+ <button type="button" class="secondary" id="check-btn">Check</button>
19+ <button type="submit" class="primary" id="connect-btn">Connect</button>
20+ </div>
21+ <p class="status" id="status" role="status" aria-live="polite"></p>
22+ <p class="fineprint" id="settings-path"></p>
23+ </form>
24+</section>
25+
26+<!-- Screen 2: the ori webapp itself, embedded in an iframe served by ori. -->
27+<section class="screen viewer" id="viewer-screen" hidden>
28+ <header class="bar">
29+ <span class="dot" aria-hidden="true"></span>
30+ <span class="url" id="viewer-url"></span>
31+ <span class="spacer"></span>
32+ <button type="button" class="ghost" id="reload-btn" title="Reload the ori page">Reload</button>
33+ <button type="button" class="ghost" id="browser-btn" title="Open in the system browser">Open in browser</button>
34+ <button type="button" class="ghost" id="disconnect-btn" title="Back to the connection screen">Disconnect</button>
35+ </header>
36+ <iframe id="ori-frame" title="ori web client" allow="clipboard-read; clipboard-write"></iframe>
37+</section>
38+
39+<script type="module" src="/main.js"></script>
40+</body>
41+</html>
new file mode 100644
@@ -0,0 +1,41 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+ <meta charset="UTF-8"/>
5+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6+ <title>ori</title>
7+ <link href="/main.css" rel="stylesheet"/>
8+</head>
9+<body>
10+<!-- Screen 1: connection. Shown at startup and after "Disconnect". -->
11+<section class="screen connect" id="connect-screen">
12+ <form class="card" id="connect-form" autocomplete="off">
13+ <h1>ori</h1>
14+ <p class="hint">Address of the ori server (the Go server that serves the web client).</p>
15+ <label for="server-url">Server URL</label>
16+ <input id="server-url" type="text" placeholder="http://localhost:8888" spellcheck="false"/>
17+ <div class="actions">
18+ <button type="button" class="secondary" id="check-btn">Check</button>
19+ <button type="submit" class="primary" id="connect-btn">Connect</button>
20+ </div>
21+ <p class="status" id="status" role="status" aria-live="polite"></p>
22+ <p class="fineprint" id="settings-path"></p>
23+ </form>
24+</section>
25+
26+<!-- Screen 2: the ori webapp itself, embedded in an iframe served by ori. -->
27+<section class="screen viewer" id="viewer-screen" hidden>
28+ <header class="bar">
29+ <span class="dot" aria-hidden="true"></span>
30+ <span class="url" id="viewer-url"></span>
31+ <span class="spacer"></span>
32+ <button type="button" class="ghost" id="reload-btn" title="Reload the ori page">Reload</button>
33+ <button type="button" class="ghost" id="browser-btn" title="Open in the system browser">Open in browser</button>
34+ <button type="button" class="ghost" id="disconnect-btn" title="Back to the connection screen">Disconnect</button>
35+ </header>
36+ <iframe id="ori-frame" title="ori web client" allow="clipboard-read; clipboard-write"></iframe>
37+</section>
38+
39+<script type="module" src="/main.js"></script>
40+</body>
41+</html>
added ori-desktop/frontend/src/main.css +185 -0
new file mode 100644
@@ -0,0 +1,185 @@
1+/* ori-desktop — minimal dark shell around the ori webapp. */
2+:root {
3+ --bg: #18181b;
4+ --panel: #232327;
5+ --border: #34343a;
6+ --text: #e4e4e7;
7+ --muted: #9ca3af;
8+ --accent: #7c9cff;
9+ --ok: #34d399;
10+ --err: #f87171;
11+ color-scheme: dark;
12+}
13+
14+* {
15+ box-sizing: border-box;
16+}
17+
18+html,
19+body {
20+ margin: 0;
21+ height: 100%;
22+ background: var(--bg);
23+ color: var(--text);
24+ font: 14px / 1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
25+ -webkit-user-select: none;
26+ user-select: none;
27+}
28+
29+.screen {
30+ height: 100%;
31+}
32+/* Class + attribute selectors only: an id selector such as #connect-screen
33+ would outrank .screen[hidden] and keep the screen visible for ever. */
34+.screen[hidden] {
35+ display: none !important;
36+}
37+
38+/* ---- connection screen ---------------------------------------------- */
39+.connect {
40+ display: grid;
41+ place-items: center;
42+ padding: 24px;
43+}
44+
45+.card {
46+ width: min(460px, 100%);
47+ background: var(--panel);
48+ border: 1px solid var(--border);
49+ border-radius: 12px;
50+ padding: 28px;
51+ display: flex;
52+ flex-direction: column;
53+ gap: 10px;
54+}
55+
56+h1 {
57+ margin: 0 0 4px;
58+ font-size: 28px;
59+ font-weight: 600;
60+ letter-spacing: 0.02em;
61+}
62+
63+.hint {
64+ margin: 0 0 8px;
65+ color: var(--muted);
66+}
67+
68+label {
69+ font-size: 12px;
70+ color: var(--muted);
71+ text-transform: uppercase;
72+ letter-spacing: 0.06em;
73+}
74+
75+input {
76+ -webkit-user-select: text;
77+ user-select: text;
78+ width: 100%;
79+ padding: 10px 12px;
80+ border-radius: 8px;
81+ border: 1px solid var(--border);
82+ background: var(--bg);
83+ color: var(--text);
84+ font: inherit;
85+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
86+}
87+input:focus {
88+ outline: 2px solid var(--accent);
89+ outline-offset: 1px;
90+}
91+
92+.actions {
93+ display: flex;
94+ gap: 8px;
95+ justify-content: flex-end;
96+ margin-top: 6px;
97+}
98+
99+button {
100+ font: inherit;
101+ padding: 8px 14px;
102+ border-radius: 8px;
103+ border: 1px solid var(--border);
104+ background: var(--panel);
105+ color: var(--text);
106+ cursor: pointer;
107+}
108+button:hover {
109+ border-color: var(--accent);
110+}
111+button:disabled {
112+ opacity: 0.5;
113+ cursor: progress;
114+}
115+button.primary {
116+ background: var(--accent);
117+ border-color: var(--accent);
118+ color: #0b1020;
119+ font-weight: 600;
120+}
121+button.ghost {
122+ background: transparent;
123+ padding: 4px 10px;
124+ font-size: 13px;
125+}
126+
127+.status {
128+ min-height: 1.4em;
129+ margin: 4px 0 0;
130+ -webkit-user-select: text;
131+ user-select: text;
132+}
133+.status.ok {
134+ color: var(--ok);
135+}
136+.status.err {
137+ color: var(--err);
138+}
139+.fineprint {
140+ margin: 0;
141+ font-size: 11px;
142+ color: var(--muted);
143+ word-break: break-all;
144+}
145+
146+/* ---- viewer -------------------------------------------------------- */
147+.viewer {
148+ display: flex;
149+ flex-direction: column;
150+}
151+.viewer[hidden] {
152+ display: none;
153+}
154+
155+.bar {
156+ display: flex;
157+ align-items: center;
158+ gap: 8px;
159+ padding: 4px 8px;
160+ border-bottom: 1px solid var(--border);
161+ background: var(--panel);
162+ font-size: 13px;
163+}
164+.dot {
165+ width: 8px;
166+ height: 8px;
167+ border-radius: 50%;
168+ background: var(--ok);
169+}
170+.url {
171+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
172+ color: var(--muted);
173+ -webkit-user-select: text;
174+ user-select: text;
175+}
176+.spacer {
177+ flex: 1;
178+}
179+
180+iframe {
181+ flex: 1;
182+ width: 100%;
183+ border: 0;
184+ background: var(--bg);
185+}
new file mode 100644
@@ -0,0 +1,185 @@
1+/* ori-desktop — minimal dark shell around the ori webapp. */
2+:root {
3+ --bg: #18181b;
4+ --panel: #232327;
5+ --border: #34343a;
6+ --text: #e4e4e7;
7+ --muted: #9ca3af;
8+ --accent: #7c9cff;
9+ --ok: #34d399;
10+ --err: #f87171;
11+ color-scheme: dark;
12+}
13+
14+* {
15+ box-sizing: border-box;
16+}
17+
18+html,
19+body {
20+ margin: 0;
21+ height: 100%;
22+ background: var(--bg);
23+ color: var(--text);
24+ font: 14px / 1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
25+ -webkit-user-select: none;
26+ user-select: none;
27+}
28+
29+.screen {
30+ height: 100%;
31+}
32+/* Class + attribute selectors only: an id selector such as #connect-screen
33+ would outrank .screen[hidden] and keep the screen visible for ever. */
34+.screen[hidden] {
35+ display: none !important;
36+}
37+
38+/* ---- connection screen ---------------------------------------------- */
39+.connect {
40+ display: grid;
41+ place-items: center;
42+ padding: 24px;
43+}
44+
45+.card {
46+ width: min(460px, 100%);
47+ background: var(--panel);
48+ border: 1px solid var(--border);
49+ border-radius: 12px;
50+ padding: 28px;
51+ display: flex;
52+ flex-direction: column;
53+ gap: 10px;
54+}
55+
56+h1 {
57+ margin: 0 0 4px;
58+ font-size: 28px;
59+ font-weight: 600;
60+ letter-spacing: 0.02em;
61+}
62+
63+.hint {
64+ margin: 0 0 8px;
65+ color: var(--muted);
66+}
67+
68+label {
69+ font-size: 12px;
70+ color: var(--muted);
71+ text-transform: uppercase;
72+ letter-spacing: 0.06em;
73+}
74+
75+input {
76+ -webkit-user-select: text;
77+ user-select: text;
78+ width: 100%;
79+ padding: 10px 12px;
80+ border-radius: 8px;
81+ border: 1px solid var(--border);
82+ background: var(--bg);
83+ color: var(--text);
84+ font: inherit;
85+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
86+}
87+input:focus {
88+ outline: 2px solid var(--accent);
89+ outline-offset: 1px;
90+}
91+
92+.actions {
93+ display: flex;
94+ gap: 8px;
95+ justify-content: flex-end;
96+ margin-top: 6px;
97+}
98+
99+button {
100+ font: inherit;
101+ padding: 8px 14px;
102+ border-radius: 8px;
103+ border: 1px solid var(--border);
104+ background: var(--panel);
105+ color: var(--text);
106+ cursor: pointer;
107+}
108+button:hover {
109+ border-color: var(--accent);
110+}
111+button:disabled {
112+ opacity: 0.5;
113+ cursor: progress;
114+}
115+button.primary {
116+ background: var(--accent);
117+ border-color: var(--accent);
118+ color: #0b1020;
119+ font-weight: 600;
120+}
121+button.ghost {
122+ background: transparent;
123+ padding: 4px 10px;
124+ font-size: 13px;
125+}
126+
127+.status {
128+ min-height: 1.4em;
129+ margin: 4px 0 0;
130+ -webkit-user-select: text;
131+ user-select: text;
132+}
133+.status.ok {
134+ color: var(--ok);
135+}
136+.status.err {
137+ color: var(--err);
138+}
139+.fineprint {
140+ margin: 0;
141+ font-size: 11px;
142+ color: var(--muted);
143+ word-break: break-all;
144+}
145+
146+/* ---- viewer -------------------------------------------------------- */
147+.viewer {
148+ display: flex;
149+ flex-direction: column;
150+}
151+.viewer[hidden] {
152+ display: none;
153+}
154+
155+.bar {
156+ display: flex;
157+ align-items: center;
158+ gap: 8px;
159+ padding: 4px 8px;
160+ border-bottom: 1px solid var(--border);
161+ background: var(--panel);
162+ font-size: 13px;
163+}
164+.dot {
165+ width: 8px;
166+ height: 8px;
167+ border-radius: 50%;
168+ background: var(--ok);
169+}
170+.url {
171+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
172+ color: var(--muted);
173+ -webkit-user-select: text;
174+ user-select: text;
175+}
176+.spacer {
177+ flex: 1;
178+}
179+
180+iframe {
181+ flex: 1;
182+ width: 100%;
183+ border: 0;
184+ background: var(--bg);
185+}
added ori-desktop/frontend/src/main.js +165 -0
new file mode 100644
@@ -0,0 +1,165 @@
1+// ori-desktop frontend — plain JavaScript, no bundler.
2+//
3+// The Wails runtime injects two globals into this page:
4+// window.go.main.App.* — the Go methods bound in main.go (each returns a Promise)
5+// window.runtime.* — the Wails runtime API (window title, browser, ...)
6+// Calling them directly (rather than through the generated frontend/wailsjs
7+// wrappers) keeps this frontend free of generated files.
8+//
9+// Loaded as an ES module (see index.html): strict mode and a private scope
10+// come for free, and the script runs once the document has been parsed.
11+
12+const $ = (id) => document.getElementById(id);
13+const els = {
14+ connectScreen: $("connect-screen"),
15+ viewerScreen: $("viewer-screen"),
16+ form: $("connect-form"),
17+ url: $("server-url"),
18+ check: $("check-btn"),
19+ connect: $("connect-btn"),
20+ status: $("status"),
21+ settingsPath: $("settings-path"),
22+ viewerUrl: $("viewer-url"),
23+ reload: $("reload-btn"),
24+ browser: $("browser-btn"),
25+ disconnect: $("disconnect-btn"),
26+ frame: $("ori-frame"),
27+};
28+
29+/** The Go-bound App, or null when the page is opened outside Wails. */
30+function app() {
31+ return window.go?.main?.App || null;
32+}
33+
34+function setStatus(text, kind) {
35+ els.status.textContent = text || "";
36+ els.status.className = kind ? `status ${kind}` : "status";
37+}
38+
39+function setBusy(busy) {
40+ els.check.disabled = busy;
41+ els.connect.disabled = busy;
42+ els.url.disabled = busy;
43+}
44+
45+function setTitle(title) {
46+ if (window.runtime?.WindowSetTitle) {
47+ window.runtime.WindowSetTitle(title);
48+ }
49+ document.title = title;
50+}
51+
52+// ---- screens --------------------------------------------------------
53+
54+function showViewer(baseURL) {
55+ els.viewerUrl.textContent = baseURL;
56+ els.frame.src = `${baseURL}/`;
57+ els.connectScreen.hidden = true;
58+ els.viewerScreen.hidden = false;
59+ setTitle(`ori — ${baseURL}`);
60+}
61+
62+function showConnect(message, kind) {
63+ els.frame.src = "about:blank";
64+ els.viewerScreen.hidden = true;
65+ els.connectScreen.hidden = false;
66+ setTitle("ori");
67+ setStatus(message, kind);
68+ els.url.focus();
69+ els.url.select();
70+}
71+
72+// ---- actions --------------------------------------------------------
73+
74+async function check() {
75+ const a = app();
76+ if (!a) return;
77+ setBusy(true);
78+ setStatus(`Checking ${els.url.value.trim()}/healthz …`);
79+ try {
80+ const rep = await a.CheckHealth(els.url.value);
81+ setStatus(rep.detail, rep.ok ? "ok" : "err");
82+ } catch (err) {
83+ setStatus(String(err), "err");
84+ } finally {
85+ setBusy(false);
86+ }
87+}
88+
89+async function connect() {
90+ const a = app();
91+ if (!a) return;
92+ setBusy(true);
93+ setStatus("Connecting …");
94+ try {
95+ const base = await a.Connect(els.url.value);
96+ els.url.value = base;
97+ setStatus("");
98+ showViewer(base);
99+ } catch (err) {
100+ setStatus(String(err), "err");
101+ } finally {
102+ setBusy(false);
103+ }
104+}
105+
106+async function openInBrowser() {
107+ const a = app();
108+ if (!a) return;
109+ try {
110+ await a.OpenInBrowser(els.viewerUrl.textContent || els.url.value);
111+ } catch (err) {
112+ console.error(err);
113+ }
114+}
115+
116+/** Reloads the embedded ori page by re-assigning the frame's URL. */
117+function reloadFrame() {
118+ const current = els.frame.src;
119+ els.frame.src = current;
120+}
121+
122+// ---- startup --------------------------------------------------------
123+
124+async function init() {
125+ els.form.addEventListener("submit", (e) => {
126+ e.preventDefault();
127+ connect();
128+ });
129+ els.check.addEventListener("click", check);
130+ els.reload.addEventListener("click", reloadFrame);
131+ els.browser.addEventListener("click", openInBrowser);
132+ els.disconnect.addEventListener("click", () =>
133+ showConnect("Disconnected.", ""),
134+ );
135+
136+ const a = app();
137+ if (!a) {
138+ setStatus(
139+ "Wails runtime not found — open this page from the ori-desktop app (wails dev / wails build).",
140+ "err",
141+ );
142+ setBusy(true);
143+ return;
144+ }
145+
146+ try {
147+ const cfg = await a.GetConfig();
148+ els.url.value = cfg.serverUrl || "http://localhost:8888";
149+ } catch (err) {
150+ els.url.value = "http://localhost:8888";
151+ setStatus(`Could not read saved settings: ${err}`, "err");
152+ }
153+ try {
154+ const path = await a.SettingsPath();
155+ els.settingsPath.textContent = path ? `Remembered in ${path}` : "";
156+ } catch (_) {
157+ /* cosmetic only */
158+ }
159+
160+ // Auto-connect when the remembered server already answers; otherwise
161+ // stay on this screen with the reason, so the user can fix the URL.
162+ await connect();
163+}
164+
165+document.addEventListener("DOMContentLoaded", init);
new file mode 100644
@@ -0,0 +1,165 @@
1+// ori-desktop frontend — plain JavaScript, no bundler.
2+//
3+// The Wails runtime injects two globals into this page:
4+// window.go.main.App.* — the Go methods bound in main.go (each returns a Promise)
5+// window.runtime.* — the Wails runtime API (window title, browser, ...)
6+// Calling them directly (rather than through the generated frontend/wailsjs
7+// wrappers) keeps this frontend free of generated files.
8+//
9+// Loaded as an ES module (see index.html): strict mode and a private scope
10+// come for free, and the script runs once the document has been parsed.
11+
12+const $ = (id) => document.getElementById(id);
13+const els = {
14+ connectScreen: $("connect-screen"),
15+ viewerScreen: $("viewer-screen"),
16+ form: $("connect-form"),
17+ url: $("server-url"),
18+ check: $("check-btn"),
19+ connect: $("connect-btn"),
20+ status: $("status"),
21+ settingsPath: $("settings-path"),
22+ viewerUrl: $("viewer-url"),
23+ reload: $("reload-btn"),
24+ browser: $("browser-btn"),
25+ disconnect: $("disconnect-btn"),
26+ frame: $("ori-frame"),
27+};
28+
29+/** The Go-bound App, or null when the page is opened outside Wails. */
30+function app() {
31+ return window.go?.main?.App || null;
32+}
33+
34+function setStatus(text, kind) {
35+ els.status.textContent = text || "";
36+ els.status.className = kind ? `status ${kind}` : "status";
37+}
38+
39+function setBusy(busy) {
40+ els.check.disabled = busy;
41+ els.connect.disabled = busy;
42+ els.url.disabled = busy;
43+}
44+
45+function setTitle(title) {
46+ if (window.runtime?.WindowSetTitle) {
47+ window.runtime.WindowSetTitle(title);
48+ }
49+ document.title = title;
50+}
51+
52+// ---- screens --------------------------------------------------------
53+
54+function showViewer(baseURL) {
55+ els.viewerUrl.textContent = baseURL;
56+ els.frame.src = `${baseURL}/`;
57+ els.connectScreen.hidden = true;
58+ els.viewerScreen.hidden = false;
59+ setTitle(`ori — ${baseURL}`);
60+}
61+
62+function showConnect(message, kind) {
63+ els.frame.src = "about:blank";
64+ els.viewerScreen.hidden = true;
65+ els.connectScreen.hidden = false;
66+ setTitle("ori");
67+ setStatus(message, kind);
68+ els.url.focus();
69+ els.url.select();
70+}
71+
72+// ---- actions --------------------------------------------------------
73+
74+async function check() {
75+ const a = app();
76+ if (!a) return;
77+ setBusy(true);
78+ setStatus(`Checking ${els.url.value.trim()}/healthz …`);
79+ try {
80+ const rep = await a.CheckHealth(els.url.value);
81+ setStatus(rep.detail, rep.ok ? "ok" : "err");
82+ } catch (err) {
83+ setStatus(String(err), "err");
84+ } finally {
85+ setBusy(false);
86+ }
87+}
88+
89+async function connect() {
90+ const a = app();
91+ if (!a) return;
92+ setBusy(true);
93+ setStatus("Connecting …");
94+ try {
95+ const base = await a.Connect(els.url.value);
96+ els.url.value = base;
97+ setStatus("");
98+ showViewer(base);
99+ } catch (err) {
100+ setStatus(String(err), "err");
101+ } finally {
102+ setBusy(false);
103+ }
104+}
105+
106+async function openInBrowser() {
107+ const a = app();
108+ if (!a) return;
109+ try {
110+ await a.OpenInBrowser(els.viewerUrl.textContent || els.url.value);
111+ } catch (err) {
112+ console.error(err);
113+ }
114+}
115+
116+/** Reloads the embedded ori page by re-assigning the frame's URL. */
117+function reloadFrame() {
118+ const current = els.frame.src;
119+ els.frame.src = current;
120+}
121+
122+// ---- startup --------------------------------------------------------
123+
124+async function init() {
125+ els.form.addEventListener("submit", (e) => {
126+ e.preventDefault();
127+ connect();
128+ });
129+ els.check.addEventListener("click", check);
130+ els.reload.addEventListener("click", reloadFrame);
131+ els.browser.addEventListener("click", openInBrowser);
132+ els.disconnect.addEventListener("click", () =>
133+ showConnect("Disconnected.", ""),
134+ );
135+
136+ const a = app();
137+ if (!a) {
138+ setStatus(
139+ "Wails runtime not found — open this page from the ori-desktop app (wails dev / wails build).",
140+ "err",
141+ );
142+ setBusy(true);
143+ return;
144+ }
145+
146+ try {
147+ const cfg = await a.GetConfig();
148+ els.url.value = cfg.serverUrl || "http://localhost:8888";
149+ } catch (err) {
150+ els.url.value = "http://localhost:8888";
151+ setStatus(`Could not read saved settings: ${err}`, "err");
152+ }
153+ try {
154+ const path = await a.SettingsPath();
155+ els.settingsPath.textContent = path ? `Remembered in ${path}` : "";
156+ } catch (_) {
157+ /* cosmetic only */
158+ }
159+
160+ // Auto-connect when the remembered server already answers; otherwise
161+ // stay on this screen with the reason, so the user can fix the URL.
162+ await connect();
163+}
164+
165+document.addEventListener("DOMContentLoaded", init);
added ori-desktop/go.mod +36 -0
new file mode 100644
@@ -0,0 +1,36 @@
1+module rickub.com/bots-garden/ori-desktop
2+
3+go 1.25.0
4+
5+require github.com/wailsapp/wails/v2 v2.16.0
6+
7+require (
8+ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
9+ github.com/bep/debounce v1.2.1 // indirect
10+ github.com/go-ole/go-ole v1.3.0 // indirect
11+ github.com/godbus/dbus/v5 v5.1.0 // indirect
12+ github.com/google/uuid v1.6.0 // indirect
13+ github.com/gorilla/websocket v1.5.3 // indirect
14+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
15+ github.com/labstack/echo/v4 v4.13.3 // indirect
16+ github.com/labstack/gommon v0.4.2 // indirect
17+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
18+ github.com/leaanthony/gosod v1.0.4 // indirect
19+ github.com/leaanthony/slicer v1.6.0 // indirect
20+ github.com/leaanthony/u v1.1.1 // indirect
21+ github.com/mattn/go-colorable v0.1.13 // indirect
22+ github.com/mattn/go-isatty v0.0.20 // indirect
23+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
24+ github.com/pkg/errors v0.9.1 // indirect
25+ github.com/rivo/uniseg v0.4.7 // indirect
26+ github.com/samber/lo v1.49.1 // indirect
27+ github.com/tkrajina/go-reflector v0.5.8 // indirect
28+ github.com/valyala/bytebufferpool v1.0.0 // indirect
29+ github.com/valyala/fasttemplate v1.2.2 // indirect
30+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
31+ github.com/wailsapp/mimetype v1.4.1 // indirect
32+ golang.org/x/crypto v0.53.0 // indirect
33+ golang.org/x/net v0.56.0 // indirect
34+ golang.org/x/sys v0.46.0 // indirect
35+ golang.org/x/text v0.39.0 // indirect
36+)
new file mode 100644
@@ -0,0 +1,36 @@
1+module rickub.com/bots-garden/ori-desktop
2+
3+go 1.25.0
4+
5+require github.com/wailsapp/wails/v2 v2.16.0
6+
7+require (
8+ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
9+ github.com/bep/debounce v1.2.1 // indirect
10+ github.com/go-ole/go-ole v1.3.0 // indirect
11+ github.com/godbus/dbus/v5 v5.1.0 // indirect
12+ github.com/google/uuid v1.6.0 // indirect
13+ github.com/gorilla/websocket v1.5.3 // indirect
14+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
15+ github.com/labstack/echo/v4 v4.13.3 // indirect
16+ github.com/labstack/gommon v0.4.2 // indirect
17+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
18+ github.com/leaanthony/gosod v1.0.4 // indirect
19+ github.com/leaanthony/slicer v1.6.0 // indirect
20+ github.com/leaanthony/u v1.1.1 // indirect
21+ github.com/mattn/go-colorable v0.1.13 // indirect
22+ github.com/mattn/go-isatty v0.0.20 // indirect
23+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
24+ github.com/pkg/errors v0.9.1 // indirect
25+ github.com/rivo/uniseg v0.4.7 // indirect
26+ github.com/samber/lo v1.49.1 // indirect
27+ github.com/tkrajina/go-reflector v0.5.8 // indirect
28+ github.com/valyala/bytebufferpool v1.0.0 // indirect
29+ github.com/valyala/fasttemplate v1.2.2 // indirect
30+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
31+ github.com/wailsapp/mimetype v1.4.1 // indirect
32+ golang.org/x/crypto v0.53.0 // indirect
33+ golang.org/x/net v0.56.0 // indirect
34+ golang.org/x/sys v0.46.0 // indirect
35+ golang.org/x/text v0.39.0 // indirect
36+)
added ori-desktop/go.sum +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
2+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
3+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
4+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
5+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
8+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
9+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
10+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
11+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
14+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
15+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
16+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
17+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
18+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
19+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
20+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
21+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
22+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
23+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
24+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
25+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
26+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
27+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
28+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
29+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
30+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
31+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
32+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
33+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
34+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
35+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
36+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
37+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
38+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
39+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
40+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
41+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
42+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
43+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
45+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
46+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
47+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
48+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
49+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
50+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
51+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
52+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
53+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
54+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
55+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
56+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
57+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
58+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
59+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
60+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
61+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
62+github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJzxyw=
63+github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
64+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
65+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
66+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
67+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
68+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
69+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
70+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
76+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
77+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
78+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
79+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
80+golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
81+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
82+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
83+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
new file mode 100644
@@ -0,0 +1,83 @@
1+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
2+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
3+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
4+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
5+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
8+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
9+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
10+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
11+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
14+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
15+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
16+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
17+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
18+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
19+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
20+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
21+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
22+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
23+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
24+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
25+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
26+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
27+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
28+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
29+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
30+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
31+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
32+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
33+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
34+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
35+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
36+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
37+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
38+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
39+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
40+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
41+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
42+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
43+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
45+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
46+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
47+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
48+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
49+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
50+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
51+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
52+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
53+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
54+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
55+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
56+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
57+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
58+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
59+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
60+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
61+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
62+github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJzxyw=
63+github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
64+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
65+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
66+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
67+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
68+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
69+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
70+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
76+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
77+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
78+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
79+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
80+golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
81+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
82+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
83+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
added ori-desktop/internal/health/health.go +77 -0
new file mode 100644
@@ -0,0 +1,77 @@
1+// Package health probes an ori server's GET /healthz endpoint. It is pure Go
2+// (no GUI dependency) so the connection logic of the desktop client can be
3+// unit-tested against an httptest server.
4+package health
5+
6+import (
7+ "context"
8+ "encoding/json"
9+ "fmt"
10+ "io"
11+ "net/http"
12+ "time"
13+)
14+
15+// DefaultTimeout bounds a single probe; a desktop connection screen must fail
16+// fast when the server is down.
17+const DefaultTimeout = 3 * time.Second
18+
19+// Report is the outcome of one probe, shaped to cross the Wails bridge as a
20+// plain JSON object (no error type, so the JavaScript side never has to
21+// distinguish a rejected promise from a negative answer).
22+type Report struct {
23+ // OK is true when /healthz answered with a 2xx status.
24+ OK bool `json:"ok"`
25+ // URL is the exact URL that was probed.
26+ URL string `json:"url"`
27+ // StatusCode is the HTTP status received, or 0 when no response arrived.
28+ StatusCode int `json:"statusCode"`
29+ // Detail is a short human-readable explanation ("ori is reachable",
30+ // the transport error, or the unexpected status).
31+ Detail string `json:"detail"`
32+}
33+
34+// Check performs GET <baseURL>/healthz. baseURL must already be normalised
35+// (scheme + host, no trailing slash). A nil client uses http.DefaultTransport
36+// with DefaultTimeout.
37+func Check(ctx context.Context, client *http.Client, baseURL string) Report {
38+ if client == nil {
39+ client = &http.Client{Timeout: DefaultTimeout}
40+ }
41+ target := baseURL + "/healthz"
42+ rep := Report{URL: target}
43+
44+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
45+ if err != nil {
46+ rep.Detail = fmt.Sprintf("invalid URL: %v", err)
47+ return rep
48+ }
49+ resp, err := client.Do(req)
50+ if err != nil {
51+ rep.Detail = fmt.Sprintf("cannot reach ori: %v", err)
52+ return rep
53+ }
54+ defer resp.Body.Close()
55+ rep.StatusCode = resp.StatusCode
56+
57+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
58+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
59+ rep.Detail = fmt.Sprintf("unexpected status %s from /healthz (is this really an ori server?)", resp.Status)
60+ return rep
61+ }
62+ rep.OK = true
63+ rep.Detail = describe(body)
64+ return rep
65+}
66+
67+// describe turns ori's `{"status":"ok"}` body into a one-liner, tolerating
68+// any other 2xx body so a future ori version cannot break the client.
69+func describe(body []byte) string {
70+ var payload struct {
71+ Status string `json:"status"`
72+ }
73+ if err := json.Unmarshal(body, &payload); err == nil && payload.Status != "" {
74+ return "ori is reachable (status: " + payload.Status + ")"
75+ }
76+ return "ori is reachable"
77+}
new file mode 100644
@@ -0,0 +1,77 @@
1+// Package health probes an ori server's GET /healthz endpoint. It is pure Go
2+// (no GUI dependency) so the connection logic of the desktop client can be
3+// unit-tested against an httptest server.
4+package health
5+
6+import (
7+ "context"
8+ "encoding/json"
9+ "fmt"
10+ "io"
11+ "net/http"
12+ "time"
13+)
14+
15+// DefaultTimeout bounds a single probe; a desktop connection screen must fail
16+// fast when the server is down.
17+const DefaultTimeout = 3 * time.Second
18+
19+// Report is the outcome of one probe, shaped to cross the Wails bridge as a
20+// plain JSON object (no error type, so the JavaScript side never has to
21+// distinguish a rejected promise from a negative answer).
22+type Report struct {
23+ // OK is true when /healthz answered with a 2xx status.
24+ OK bool `json:"ok"`
25+ // URL is the exact URL that was probed.
26+ URL string `json:"url"`
27+ // StatusCode is the HTTP status received, or 0 when no response arrived.
28+ StatusCode int `json:"statusCode"`
29+ // Detail is a short human-readable explanation ("ori is reachable",
30+ // the transport error, or the unexpected status).
31+ Detail string `json:"detail"`
32+}
33+
34+// Check performs GET <baseURL>/healthz. baseURL must already be normalised
35+// (scheme + host, no trailing slash). A nil client uses http.DefaultTransport
36+// with DefaultTimeout.
37+func Check(ctx context.Context, client *http.Client, baseURL string) Report {
38+ if client == nil {
39+ client = &http.Client{Timeout: DefaultTimeout}
40+ }
41+ target := baseURL + "/healthz"
42+ rep := Report{URL: target}
43+
44+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
45+ if err != nil {
46+ rep.Detail = fmt.Sprintf("invalid URL: %v", err)
47+ return rep
48+ }
49+ resp, err := client.Do(req)
50+ if err != nil {
51+ rep.Detail = fmt.Sprintf("cannot reach ori: %v", err)
52+ return rep
53+ }
54+ defer resp.Body.Close()
55+ rep.StatusCode = resp.StatusCode
56+
57+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
58+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
59+ rep.Detail = fmt.Sprintf("unexpected status %s from /healthz (is this really an ori server?)", resp.Status)
60+ return rep
61+ }
62+ rep.OK = true
63+ rep.Detail = describe(body)
64+ return rep
65+}
66+
67+// describe turns ori's `{"status":"ok"}` body into a one-liner, tolerating
68+// any other 2xx body so a future ori version cannot break the client.
69+func describe(body []byte) string {
70+ var payload struct {
71+ Status string `json:"status"`
72+ }
73+ if err := json.Unmarshal(body, &payload); err == nil && payload.Status != "" {
74+ return "ori is reachable (status: " + payload.Status + ")"
75+ }
76+ return "ori is reachable"
77+}
added ori-desktop/internal/health/health_test.go +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+package health
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestCheckHealthyServer(t *testing.T) {
12+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+ if r.URL.Path != "/healthz" || r.Method != http.MethodGet {
14+ http.NotFound(w, r)
15+ return
16+ }
17+ w.Header().Set("Content-Type", "application/json")
18+ _, _ = w.Write([]byte(`{"status":"ok"}`))
19+ }))
20+ defer srv.Close()
21+
22+ rep := Check(context.Background(), srv.Client(), srv.URL)
23+ if !rep.OK {
24+ t.Fatalf("expected OK, got %+v", rep)
25+ }
26+ if rep.StatusCode != http.StatusOK || rep.URL != srv.URL+"/healthz" {
27+ t.Fatalf("unexpected report %+v", rep)
28+ }
29+ if !strings.Contains(rep.Detail, "status: ok") {
30+ t.Fatalf("detail should quote the status, got %q", rep.Detail)
31+ }
32+}
33+
34+func TestCheckNonOriServer(t *testing.T) {
35+ srv := httptest.NewServer(http.NotFoundHandler())
36+ defer srv.Close()
37+
38+ rep := Check(context.Background(), srv.Client(), srv.URL)
39+ if rep.OK {
40+ t.Fatalf("expected failure for 404, got %+v", rep)
41+ }
42+ if rep.StatusCode != http.StatusNotFound || !strings.Contains(rep.Detail, "404") {
43+ t.Fatalf("unexpected report %+v", rep)
44+ }
45+}
46+
47+func TestCheckUnreachableServer(t *testing.T) {
48+ srv := httptest.NewServer(http.NotFoundHandler())
49+ addr := srv.URL
50+ srv.Close() // nothing listens there any more
51+
52+ rep := Check(context.Background(), nil, addr)
53+ if rep.OK || rep.StatusCode != 0 {
54+ t.Fatalf("expected transport failure, got %+v", rep)
55+ }
56+ if !strings.Contains(rep.Detail, "cannot reach ori") {
57+ t.Fatalf("unexpected detail %q", rep.Detail)
58+ }
59+}
60+
61+func TestCheckPlainOKBody(t *testing.T) {
62+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
63+ _, _ = w.Write([]byte("OK"))
64+ }))
65+ defer srv.Close()
66+
67+ rep := Check(context.Background(), srv.Client(), srv.URL)
68+ if !rep.OK || rep.Detail != "ori is reachable" {
69+ t.Fatalf("a 2xx with a non-JSON body must still count as healthy, got %+v", rep)
70+ }
71+}
72+
73+func TestCheckCancelledContext(t *testing.T) {
74+ ctx, cancel := context.WithCancel(context.Background())
75+ cancel()
76+ rep := Check(ctx, nil, "http://127.0.0.1:1")
77+ if rep.OK {
78+ t.Fatalf("expected failure with a cancelled context, got %+v", rep)
79+ }
80+}
new file mode 100644
@@ -0,0 +1,80 @@
1+package health
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestCheckHealthyServer(t *testing.T) {
12+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+ if r.URL.Path != "/healthz" || r.Method != http.MethodGet {
14+ http.NotFound(w, r)
15+ return
16+ }
17+ w.Header().Set("Content-Type", "application/json")
18+ _, _ = w.Write([]byte(`{"status":"ok"}`))
19+ }))
20+ defer srv.Close()
21+
22+ rep := Check(context.Background(), srv.Client(), srv.URL)
23+ if !rep.OK {
24+ t.Fatalf("expected OK, got %+v", rep)
25+ }
26+ if rep.StatusCode != http.StatusOK || rep.URL != srv.URL+"/healthz" {
27+ t.Fatalf("unexpected report %+v", rep)
28+ }
29+ if !strings.Contains(rep.Detail, "status: ok") {
30+ t.Fatalf("detail should quote the status, got %q", rep.Detail)
31+ }
32+}
33+
34+func TestCheckNonOriServer(t *testing.T) {
35+ srv := httptest.NewServer(http.NotFoundHandler())
36+ defer srv.Close()
37+
38+ rep := Check(context.Background(), srv.Client(), srv.URL)
39+ if rep.OK {
40+ t.Fatalf("expected failure for 404, got %+v", rep)
41+ }
42+ if rep.StatusCode != http.StatusNotFound || !strings.Contains(rep.Detail, "404") {
43+ t.Fatalf("unexpected report %+v", rep)
44+ }
45+}
46+
47+func TestCheckUnreachableServer(t *testing.T) {
48+ srv := httptest.NewServer(http.NotFoundHandler())
49+ addr := srv.URL
50+ srv.Close() // nothing listens there any more
51+
52+ rep := Check(context.Background(), nil, addr)
53+ if rep.OK || rep.StatusCode != 0 {
54+ t.Fatalf("expected transport failure, got %+v", rep)
55+ }
56+ if !strings.Contains(rep.Detail, "cannot reach ori") {
57+ t.Fatalf("unexpected detail %q", rep.Detail)
58+ }
59+}
60+
61+func TestCheckPlainOKBody(t *testing.T) {
62+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
63+ _, _ = w.Write([]byte("OK"))
64+ }))
65+ defer srv.Close()
66+
67+ rep := Check(context.Background(), srv.Client(), srv.URL)
68+ if !rep.OK || rep.Detail != "ori is reachable" {
69+ t.Fatalf("a 2xx with a non-JSON body must still count as healthy, got %+v", rep)
70+ }
71+}
72+
73+func TestCheckCancelledContext(t *testing.T) {
74+ ctx, cancel := context.WithCancel(context.Background())
75+ cancel()
76+ rep := Check(ctx, nil, "http://127.0.0.1:1")
77+ if rep.OK {
78+ t.Fatalf("expected failure with a cancelled context, got %+v", rep)
79+ }
80+}
added ori-desktop/internal/settings/settings.go +114 -0
new file mode 100644
@@ -0,0 +1,114 @@
1+// Package settings persists the desktop client's preferences — today only the
2+// URL of the ori server — as a small JSON file in the user's configuration
3+// directory (see DefaultPath). It is pure Go with no GUI dependency so it can
4+// be unit-tested anywhere.
5+package settings
6+
7+import (
8+ "encoding/json"
9+ "errors"
10+ "fmt"
11+ "net/url"
12+ "os"
13+ "path/filepath"
14+ "strings"
15+)
16+
17+// DefaultServerURL is the address ori listens on when started without --addr.
18+const DefaultServerURL = "http://localhost:8888"
19+
20+// Settings is what the desktop client remembers between runs.
21+type Settings struct {
22+ // ServerURL is the base URL of the ori server (scheme + host[:port]).
23+ ServerURL string `json:"serverUrl"`
24+}
25+
26+// Default returns the settings used when nothing has been saved yet.
27+func Default() Settings {
28+ return Settings{ServerURL: DefaultServerURL}
29+}
30+
31+// DefaultPath returns the per-user location of the settings file:
32+// <user config dir>/ori-desktop/settings.json (for example
33+// ~/.config/ori-desktop/settings.json on Linux, ~/Library/Application
34+// Support/ori-desktop/settings.json on macOS, %AppData%\ori-desktop\settings.json
35+// on Windows).
36+func DefaultPath() (string, error) {
37+ dir, err := os.UserConfigDir()
38+ if err != nil {
39+ return "", fmt.Errorf("settings: locate user config dir: %w", err)
40+ }
41+ return filepath.Join(dir, "ori-desktop", "settings.json"), nil
42+}
43+
44+// Load reads the settings stored at path. A missing file is not an error: the
45+// defaults are returned so a first run behaves sensibly. A file that exists
46+// but cannot be read or parsed is reported as an error, never silently
47+// replaced. An empty ServerURL in the file falls back to DefaultServerURL.
48+func Load(path string) (Settings, error) {
49+ data, err := os.ReadFile(path)
50+ if errors.Is(err, os.ErrNotExist) {
51+ return Default(), nil
52+ }
53+ if err != nil {
54+ return Settings{}, fmt.Errorf("settings: read %s: %w", path, err)
55+ }
56+ var s Settings
57+ if err := json.Unmarshal(data, &s); err != nil {
58+ return Settings{}, fmt.Errorf("settings: parse %s: %w", path, err)
59+ }
60+ if strings.TrimSpace(s.ServerURL) == "" {
61+ s.ServerURL = DefaultServerURL
62+ }
63+ return s, nil
64+}
65+
66+// Save writes s to path as indented JSON, creating the parent directory if
67+// needed. The write goes through a temporary file renamed into place so a
68+// crash mid-write cannot leave a truncated settings file behind.
69+func Save(path string, s Settings) error {
70+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
71+ return fmt.Errorf("settings: create config dir: %w", err)
72+ }
73+ data, err := json.MarshalIndent(s, "", " ")
74+ if err != nil {
75+ return fmt.Errorf("settings: encode: %w", err)
76+ }
77+ tmp := path + ".tmp"
78+ if err := os.WriteFile(tmp, append(data, '\n'), 0o600); err != nil {
79+ return fmt.Errorf("settings: write %s: %w", tmp, err)
80+ }
81+ if err := os.Rename(tmp, path); err != nil {
82+ _ = os.Remove(tmp)
83+ return fmt.Errorf("settings: replace %s: %w", path, err)
84+ }
85+ return nil
86+}
87+
88+// NormalizeURL turns what a user typed into a canonical base URL: surrounding
89+// whitespace is dropped, a missing scheme defaults to http://, trailing
90+// slashes are removed, and the query string and fragment are discarded. Only
91+// http and https URLs with a host are accepted.
92+func NormalizeURL(raw string) (string, error) {
93+ raw = strings.TrimSpace(raw)
94+ if raw == "" {
95+ return "", errors.New("server URL is empty")
96+ }
97+ if !strings.Contains(raw, "://") {
98+ raw = "http://" + raw
99+ }
100+ u, err := url.Parse(raw)
101+ if err != nil {
102+ return "", fmt.Errorf("invalid server URL: %w", err)
103+ }
104+ if u.Scheme != "http" && u.Scheme != "https" {
105+ return "", fmt.Errorf("unsupported scheme %q (use http or https)", u.Scheme)
106+ }
107+ if u.Host == "" {
108+ return "", errors.New("server URL has no host")
109+ }
110+ u.Path = strings.TrimRight(u.Path, "/")
111+ u.RawQuery = ""
112+ u.Fragment = ""
113+ return u.String(), nil
114+}
new file mode 100644
@@ -0,0 +1,114 @@
1+// Package settings persists the desktop client's preferences — today only the
2+// URL of the ori server — as a small JSON file in the user's configuration
3+// directory (see DefaultPath). It is pure Go with no GUI dependency so it can
4+// be unit-tested anywhere.
5+package settings
6+
7+import (
8+ "encoding/json"
9+ "errors"
10+ "fmt"
11+ "net/url"
12+ "os"
13+ "path/filepath"
14+ "strings"
15+)
16+
17+// DefaultServerURL is the address ori listens on when started without --addr.
18+const DefaultServerURL = "http://localhost:8888"
19+
20+// Settings is what the desktop client remembers between runs.
21+type Settings struct {
22+ // ServerURL is the base URL of the ori server (scheme + host[:port]).
23+ ServerURL string `json:"serverUrl"`
24+}
25+
26+// Default returns the settings used when nothing has been saved yet.
27+func Default() Settings {
28+ return Settings{ServerURL: DefaultServerURL}
29+}
30+
31+// DefaultPath returns the per-user location of the settings file:
32+// <user config dir>/ori-desktop/settings.json (for example
33+// ~/.config/ori-desktop/settings.json on Linux, ~/Library/Application
34+// Support/ori-desktop/settings.json on macOS, %AppData%\ori-desktop\settings.json
35+// on Windows).
36+func DefaultPath() (string, error) {
37+ dir, err := os.UserConfigDir()
38+ if err != nil {
39+ return "", fmt.Errorf("settings: locate user config dir: %w", err)
40+ }
41+ return filepath.Join(dir, "ori-desktop", "settings.json"), nil
42+}
43+
44+// Load reads the settings stored at path. A missing file is not an error: the
45+// defaults are returned so a first run behaves sensibly. A file that exists
46+// but cannot be read or parsed is reported as an error, never silently
47+// replaced. An empty ServerURL in the file falls back to DefaultServerURL.
48+func Load(path string) (Settings, error) {
49+ data, err := os.ReadFile(path)
50+ if errors.Is(err, os.ErrNotExist) {
51+ return Default(), nil
52+ }
53+ if err != nil {
54+ return Settings{}, fmt.Errorf("settings: read %s: %w", path, err)
55+ }
56+ var s Settings
57+ if err := json.Unmarshal(data, &s); err != nil {
58+ return Settings{}, fmt.Errorf("settings: parse %s: %w", path, err)
59+ }
60+ if strings.TrimSpace(s.ServerURL) == "" {
61+ s.ServerURL = DefaultServerURL
62+ }
63+ return s, nil
64+}
65+
66+// Save writes s to path as indented JSON, creating the parent directory if
67+// needed. The write goes through a temporary file renamed into place so a
68+// crash mid-write cannot leave a truncated settings file behind.
69+func Save(path string, s Settings) error {
70+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
71+ return fmt.Errorf("settings: create config dir: %w", err)
72+ }
73+ data, err := json.MarshalIndent(s, "", " ")
74+ if err != nil {
75+ return fmt.Errorf("settings: encode: %w", err)
76+ }
77+ tmp := path + ".tmp"
78+ if err := os.WriteFile(tmp, append(data, '\n'), 0o600); err != nil {
79+ return fmt.Errorf("settings: write %s: %w", tmp, err)
80+ }
81+ if err := os.Rename(tmp, path); err != nil {
82+ _ = os.Remove(tmp)
83+ return fmt.Errorf("settings: replace %s: %w", path, err)
84+ }
85+ return nil
86+}
87+
88+// NormalizeURL turns what a user typed into a canonical base URL: surrounding
89+// whitespace is dropped, a missing scheme defaults to http://, trailing
90+// slashes are removed, and the query string and fragment are discarded. Only
91+// http and https URLs with a host are accepted.
92+func NormalizeURL(raw string) (string, error) {
93+ raw = strings.TrimSpace(raw)
94+ if raw == "" {
95+ return "", errors.New("server URL is empty")
96+ }
97+ if !strings.Contains(raw, "://") {
98+ raw = "http://" + raw
99+ }
100+ u, err := url.Parse(raw)
101+ if err != nil {
102+ return "", fmt.Errorf("invalid server URL: %w", err)
103+ }
104+ if u.Scheme != "http" && u.Scheme != "https" {
105+ return "", fmt.Errorf("unsupported scheme %q (use http or https)", u.Scheme)
106+ }
107+ if u.Host == "" {
108+ return "", errors.New("server URL has no host")
109+ }
110+ u.Path = strings.TrimRight(u.Path, "/")
111+ u.RawQuery = ""
112+ u.Fragment = ""
113+ return u.String(), nil
114+}
added ori-desktop/internal/settings/settings_test.go +120 -0
new file mode 100644
@@ -0,0 +1,120 @@
1+package settings
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+)
8+
9+func TestLoadMissingFileReturnsDefaults(t *testing.T) {
10+ s, err := Load(filepath.Join(t.TempDir(), "nope", "settings.json"))
11+ if err != nil {
12+ t.Fatalf("Load: unexpected error %v", err)
13+ }
14+ if s != Default() {
15+ t.Fatalf("Load: got %+v, want defaults %+v", s, Default())
16+ }
17+}
18+
19+func TestSaveThenLoadRoundTrip(t *testing.T) {
20+ path := filepath.Join(t.TempDir(), "sub", "dir", "settings.json")
21+ want := Settings{ServerURL: "http://127.0.0.1:9999"}
22+ if err := Save(path, want); err != nil {
23+ t.Fatalf("Save: %v", err)
24+ }
25+ if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
26+ t.Fatalf("temporary file left behind: %v", err)
27+ }
28+ got, err := Load(path)
29+ if err != nil {
30+ t.Fatalf("Load: %v", err)
31+ }
32+ if got != want {
33+ t.Fatalf("round trip: got %+v, want %+v", got, want)
34+ }
35+}
36+
37+func TestSaveOverwritesPreviousValue(t *testing.T) {
38+ path := filepath.Join(t.TempDir(), "settings.json")
39+ for _, u := range []string{"http://a:1", "http://b:2"} {
40+ if err := Save(path, Settings{ServerURL: u}); err != nil {
41+ t.Fatalf("Save %s: %v", u, err)
42+ }
43+ }
44+ got, err := Load(path)
45+ if err != nil {
46+ t.Fatalf("Load: %v", err)
47+ }
48+ if got.ServerURL != "http://b:2" {
49+ t.Fatalf("Load after two saves: got %q, want the last value", got.ServerURL)
50+ }
51+}
52+
53+func TestLoadEmptyURLFallsBackToDefault(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "settings.json")
55+ if err := os.WriteFile(path, []byte(`{"serverUrl": " "}`), 0o600); err != nil {
56+ t.Fatal(err)
57+ }
58+ s, err := Load(path)
59+ if err != nil {
60+ t.Fatalf("Load: %v", err)
61+ }
62+ if s.ServerURL != DefaultServerURL {
63+ t.Fatalf("got %q, want %q", s.ServerURL, DefaultServerURL)
64+ }
65+}
66+
67+func TestLoadInvalidJSONIsAnError(t *testing.T) {
68+ path := filepath.Join(t.TempDir(), "settings.json")
69+ if err := os.WriteFile(path, []byte(`{not json`), 0o600); err != nil {
70+ t.Fatal(err)
71+ }
72+ if _, err := Load(path); err == nil {
73+ t.Fatal("Load: expected an error for invalid JSON, got nil")
74+ }
75+}
76+
77+func TestDefaultPathEndsWithAppFile(t *testing.T) {
78+ t.Setenv("XDG_CONFIG_HOME", t.TempDir()) // honoured on Linux; harmless elsewhere
79+ p, err := DefaultPath()
80+ if err != nil {
81+ t.Skipf("no user config dir on this platform: %v", err)
82+ }
83+ if filepath.Base(p) != "settings.json" || filepath.Base(filepath.Dir(p)) != "ori-desktop" {
84+ t.Fatalf("DefaultPath = %q, want .../ori-desktop/settings.json", p)
85+ }
86+}
87+
88+func TestNormalizeURL(t *testing.T) {
89+ cases := []struct {
90+ in, want string
91+ wantErr bool
92+ }{
93+ {in: "http://localhost:8888", want: "http://localhost:8888"},
94+ {in: " localhost:8888 ", want: "http://localhost:8888"},
95+ {in: "https://ori.example.com/", want: "https://ori.example.com"},
96+ {in: "http://host:1/prefix//", want: "http://host:1/prefix"},
97+ {in: "http://host:1/?x=1#frag", want: "http://host:1"},
98+ {in: "", wantErr: true},
99+ {in: " ", wantErr: true},
100+ {in: "ftp://host", wantErr: true},
101+ {in: "http://", wantErr: true},
102+ {in: "http://bad host", wantErr: true},
103+ }
104+ for _, c := range cases {
105+ got, err := NormalizeURL(c.in)
106+ if c.wantErr {
107+ if err == nil {
108+ t.Errorf("NormalizeURL(%q): expected error, got %q", c.in, got)
109+ }
110+ continue
111+ }
112+ if err != nil {
113+ t.Errorf("NormalizeURL(%q): unexpected error %v", c.in, err)
114+ continue
115+ }
116+ if got != c.want {
117+ t.Errorf("NormalizeURL(%q) = %q, want %q", c.in, got, c.want)
118+ }
119+ }
120+}
new file mode 100644
@@ -0,0 +1,120 @@
1+package settings
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+)
8+
9+func TestLoadMissingFileReturnsDefaults(t *testing.T) {
10+ s, err := Load(filepath.Join(t.TempDir(), "nope", "settings.json"))
11+ if err != nil {
12+ t.Fatalf("Load: unexpected error %v", err)
13+ }
14+ if s != Default() {
15+ t.Fatalf("Load: got %+v, want defaults %+v", s, Default())
16+ }
17+}
18+
19+func TestSaveThenLoadRoundTrip(t *testing.T) {
20+ path := filepath.Join(t.TempDir(), "sub", "dir", "settings.json")
21+ want := Settings{ServerURL: "http://127.0.0.1:9999"}
22+ if err := Save(path, want); err != nil {
23+ t.Fatalf("Save: %v", err)
24+ }
25+ if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
26+ t.Fatalf("temporary file left behind: %v", err)
27+ }
28+ got, err := Load(path)
29+ if err != nil {
30+ t.Fatalf("Load: %v", err)
31+ }
32+ if got != want {
33+ t.Fatalf("round trip: got %+v, want %+v", got, want)
34+ }
35+}
36+
37+func TestSaveOverwritesPreviousValue(t *testing.T) {
38+ path := filepath.Join(t.TempDir(), "settings.json")
39+ for _, u := range []string{"http://a:1", "http://b:2"} {
40+ if err := Save(path, Settings{ServerURL: u}); err != nil {
41+ t.Fatalf("Save %s: %v", u, err)
42+ }
43+ }
44+ got, err := Load(path)
45+ if err != nil {
46+ t.Fatalf("Load: %v", err)
47+ }
48+ if got.ServerURL != "http://b:2" {
49+ t.Fatalf("Load after two saves: got %q, want the last value", got.ServerURL)
50+ }
51+}
52+
53+func TestLoadEmptyURLFallsBackToDefault(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "settings.json")
55+ if err := os.WriteFile(path, []byte(`{"serverUrl": " "}`), 0o600); err != nil {
56+ t.Fatal(err)
57+ }
58+ s, err := Load(path)
59+ if err != nil {
60+ t.Fatalf("Load: %v", err)
61+ }
62+ if s.ServerURL != DefaultServerURL {
63+ t.Fatalf("got %q, want %q", s.ServerURL, DefaultServerURL)
64+ }
65+}
66+
67+func TestLoadInvalidJSONIsAnError(t *testing.T) {
68+ path := filepath.Join(t.TempDir(), "settings.json")
69+ if err := os.WriteFile(path, []byte(`{not json`), 0o600); err != nil {
70+ t.Fatal(err)
71+ }
72+ if _, err := Load(path); err == nil {
73+ t.Fatal("Load: expected an error for invalid JSON, got nil")
74+ }
75+}
76+
77+func TestDefaultPathEndsWithAppFile(t *testing.T) {
78+ t.Setenv("XDG_CONFIG_HOME", t.TempDir()) // honoured on Linux; harmless elsewhere
79+ p, err := DefaultPath()
80+ if err != nil {
81+ t.Skipf("no user config dir on this platform: %v", err)
82+ }
83+ if filepath.Base(p) != "settings.json" || filepath.Base(filepath.Dir(p)) != "ori-desktop" {
84+ t.Fatalf("DefaultPath = %q, want .../ori-desktop/settings.json", p)
85+ }
86+}
87+
88+func TestNormalizeURL(t *testing.T) {
89+ cases := []struct {
90+ in, want string
91+ wantErr bool
92+ }{
93+ {in: "http://localhost:8888", want: "http://localhost:8888"},
94+ {in: " localhost:8888 ", want: "http://localhost:8888"},
95+ {in: "https://ori.example.com/", want: "https://ori.example.com"},
96+ {in: "http://host:1/prefix//", want: "http://host:1/prefix"},
97+ {in: "http://host:1/?x=1#frag", want: "http://host:1"},
98+ {in: "", wantErr: true},
99+ {in: " ", wantErr: true},
100+ {in: "ftp://host", wantErr: true},
101+ {in: "http://", wantErr: true},
102+ {in: "http://bad host", wantErr: true},
103+ }
104+ for _, c := range cases {
105+ got, err := NormalizeURL(c.in)
106+ if c.wantErr {
107+ if err == nil {
108+ t.Errorf("NormalizeURL(%q): expected error, got %q", c.in, got)
109+ }
110+ continue
111+ }
112+ if err != nil {
113+ t.Errorf("NormalizeURL(%q): unexpected error %v", c.in, err)
114+ continue
115+ }
116+ if got != c.want {
117+ t.Errorf("NormalizeURL(%q) = %q, want %q", c.in, got, c.want)
118+ }
119+ }
120+}
added ori-desktop/main.go +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+// Command ori-desktop is a Wails v2 desktop shell around the ori web client.
2+//
3+// It does not reimplement ori: the ori server keeps serving the React SPA and
4+// the WebSocket/file/terminal endpoints. The desktop app only remembers the
5+// server URL, checks GET /healthz through a Go-bound method, then shows the
6+// ori webapp inside the native window (an <iframe> in the embedded frontend —
7+// Wails v2 exposes no runtime call to navigate the main webview to an
8+// external URL, see README.md).
9+package main
10+
11+import (
12+ "embed"
13+ "log"
14+
15+ "github.com/wailsapp/wails/v2"
16+ "github.com/wailsapp/wails/v2/pkg/options"
17+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
18+)
19+
20+// assets is the plain HTML/CSS/JS frontend, embedded as-is (no bundler).
21+//
22+//go:embed all:frontend/src
23+var assets embed.FS
24+
25+func main() {
26+ app := NewApp()
27+
28+ err := wails.Run(&options.App{
29+ Title: "ori",
30+ Width: 1280,
31+ Height: 860,
32+ MinWidth: 640,
33+ MinHeight: 400,
34+ AssetServer: &assetserver.Options{
35+ Assets: assets,
36+ },
37+ BackgroundColour: &options.RGBA{R: 24, G: 24, B: 27, A: 1},
38+ OnStartup: app.startup,
39+ Bind: []interface{}{
40+ app,
41+ },
42+ })
43+ if err != nil {
44+ log.Fatalf("ori-desktop: %v", err)
45+ }
46+}
new file mode 100644
@@ -0,0 +1,46 @@
1+// Command ori-desktop is a Wails v2 desktop shell around the ori web client.
2+//
3+// It does not reimplement ori: the ori server keeps serving the React SPA and
4+// the WebSocket/file/terminal endpoints. The desktop app only remembers the
5+// server URL, checks GET /healthz through a Go-bound method, then shows the
6+// ori webapp inside the native window (an <iframe> in the embedded frontend —
7+// Wails v2 exposes no runtime call to navigate the main webview to an
8+// external URL, see README.md).
9+package main
10+
11+import (
12+ "embed"
13+ "log"
14+
15+ "github.com/wailsapp/wails/v2"
16+ "github.com/wailsapp/wails/v2/pkg/options"
17+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
18+)
19+
20+// assets is the plain HTML/CSS/JS frontend, embedded as-is (no bundler).
21+//
22+//go:embed all:frontend/src
23+var assets embed.FS
24+
25+func main() {
26+ app := NewApp()
27+
28+ err := wails.Run(&options.App{
29+ Title: "ori",
30+ Width: 1280,
31+ Height: 860,
32+ MinWidth: 640,
33+ MinHeight: 400,
34+ AssetServer: &assetserver.Options{
35+ Assets: assets,
36+ },
37+ BackgroundColour: &options.RGBA{R: 24, G: 24, B: 27, A: 1},
38+ OnStartup: app.startup,
39+ Bind: []interface{}{
40+ app,
41+ },
42+ })
43+ if err != nil {
44+ log.Fatalf("ori-desktop: %v", err)
45+ }
46+}
added ori-desktop/wails.json +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+{
2+ "$schema": "https://wails.io/schemas/config.v2.json",
3+ "name": "ori-desktop",
4+ "outputfilename": "ori-desktop",
5+ "assetdir": "frontend/src",
6+ "wailsjsdir": "./frontend",
7+ "frontend:install": "",
8+ "frontend:build": "",
9+ "frontend:dev:watcher": "",
10+ "frontend:dev:serverUrl": "",
11+ "author": {
12+ "name": "k33g",
13+ "email": "ph.charriere@gmail.com"
14+ },
15+ "info": {
16+ "companyName": "bots-garden",
17+ "productName": "ori",
18+ "productVersion": "0.1.0",
19+ "copyright": "Copyright © 2026 k33g",
20+ "comments": "Desktop shell for the ori ACP web client"
21+ }
22+}
new file mode 100644
@@ -0,0 +1,22 @@
1+{
2+ "$schema": "https://wails.io/schemas/config.v2.json",
3+ "name": "ori-desktop",
4+ "outputfilename": "ori-desktop",
5+ "assetdir": "frontend/src",
6+ "wailsjsdir": "./frontend",
7+ "frontend:install": "",
8+ "frontend:build": "",
9+ "frontend:dev:watcher": "",
10+ "frontend:dev:serverUrl": "",
11+ "author": {
12+ "name": "k33g",
13+ "email": "ph.charriere@gmail.com"
14+ },
15+ "info": {
16+ "companyName": "bots-garden",
17+ "productName": "ori",
18+ "productVersion": "0.1.0",
19+ "copyright": "Copyright © 2026 k33g",
20+ "comments": "Desktop shell for the ori ACP web client"
21+ }
22+}
added quickstart.md +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+# Ori — quickstart
2+
3+Launch a Docker sandbox running [Ori](README.md): Claude Code plus its web UI (chat panel, file tree, Monaco editor, terminal), in your browser.
4+
5+## Prerequisites
6+
7+- Docker Desktop with the `sbx` CLI, and your Anthropic credentials configured for sandboxes (`sbx secret set anthropic …` or an OAuth login — whatever you already use for `sbx run claude`).
8+- The ori kit: clone this repository (only `kits/ori/` is needed).
9+
10+## Create the sandbox
11+
12+From the directory where the kit lives, pointing at the project you want the agent to work on:
13+
14+```bash
15+sbx run -d claude ~/path/to/your/project \
16+ --template k33g/ori:0.0.0 \
17+ --kit ./kits/ori \
18+ --name ori \
19+ -p 8888:8888
20+```
21+
22+Then open **http://localhost:8888** — the panel connects to a Claude Code session already running in the sandbox, no login needed.
23+
24+> **Why `sbx run -d` and not `sbx create`?** sbx stops a sandbox 30 seconds after the last CLI session on it closes — browser traffic does not count. `sbx create` holds such a session only while it runs, so a sandbox created that way goes `stopped` half a minute after the prompt returns. `-d` (`--detached`) marks the sandbox as detached at creation, which exempts it from that auto-stop for good; it then runs until you `sbx stop` or `sbx rm` it. The flag only exists on `sbx run`, and the mode cannot be changed afterwards: a sandbox created with `sbx create` must be removed and recreated.
25+
26+Without `-p`, the kit still publishes an ephemeral port; the command output prints it:
27+
28+```
29+ Published web: localhost:54321 -> 8888/tcp
30+```
31+
32+## Everyday commands
33+
34+| Action | Command |
35+| --- | --- |
36+| Use another host port (e.g. http://localhost:5555) | `-p 5555:8888` at creation, or `sbx ports ori --publish 5555:8888/tcp` |
37+| Change the internal port | `--kit-arg ori.port=9000` (then `-p 9000:9000`) |
38+| Publish / unpublish a port later | `sbx ports ori --publish 8888:8888/tcp` / `--unpublish 8888:8888/tcp` |
39+| Follow the server log | `sbx exec ori tail -f /home/agent/.ori.log` |
40+| Inspect why the kit's startup ran or failed | `sbx exec ori cat /var/log/sbx-kit-startup.log` |
41+| Open a shell in the sandbox | `sbx exec -it ori bash` |
42+| Attach the terminal Claude session (starts a stopped sandbox) | `sbx attach ori` |
43+| Restart a stopped sandbox without attaching | `sbx run -d --name ori` |
44+| Stop / remove | `sbx stop ori` · `sbx rm ori` |
45+| Demo without Claude usage | `sbx exec ori sh -c 'pkill -x ori; nohup ori --addr :8888 --agent-cmd ori-mock-agent --cwd /home/agent/workspace >> /home/agent/.ori.log 2>&1 &'` |
46+
47+## Building the image yourself
48+
49+Only needed if you maintain the template: `./template/build.sh` builds and pushes `k33g/ori:0.0.0` + `:latest` (multi-arch). For a never-pushed local image: `make template && docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
50+
51+Full guide: [docs/en/how-to/run-in-a-sandbox.md](docs/en/how-to/run-in-a-sandbox.md) · [docs/fr/how-to/run-in-a-sandbox.md](docs/fr/how-to/run-in-a-sandbox.md)
52+
53+
54+
55+```
56+sbx run -d claude . \
57+ --template k33g/ori:0.0.0 \
58+ --kit ./kits/ori \
59+ --name ori \
60+ -p 8888:8888
61+
62+sbx run -d claude . \
63+ --template k33g/ori:0.0.0 \
64+ --kit ./kits/ori \
65+ --name ori2 \
66+ -p 5555:8888
67+```
68+
69+cd ori-desktop
70+wails build
71+ xattr -cr build/bin/ori-desktop.app
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,71 @@
1+# Ori — quickstart
2+
3+Launch a Docker sandbox running [Ori](README.md): Claude Code plus its web UI (chat panel, file tree, Monaco editor, terminal), in your browser.
4+
5+## Prerequisites
6+
7+- Docker Desktop with the `sbx` CLI, and your Anthropic credentials configured for sandboxes (`sbx secret set anthropic …` or an OAuth login — whatever you already use for `sbx run claude`).
8+- The ori kit: clone this repository (only `kits/ori/` is needed).
9+
10+## Create the sandbox
11+
12+From the directory where the kit lives, pointing at the project you want the agent to work on:
13+
14+```bash
15+sbx run -d claude ~/path/to/your/project \
16+ --template k33g/ori:0.0.0 \
17+ --kit ./kits/ori \
18+ --name ori \
19+ -p 8888:8888
20+```
21+
22+Then open **http://localhost:8888** — the panel connects to a Claude Code session already running in the sandbox, no login needed.
23+
24+> **Why `sbx run -d` and not `sbx create`?** sbx stops a sandbox 30 seconds after the last CLI session on it closes — browser traffic does not count. `sbx create` holds such a session only while it runs, so a sandbox created that way goes `stopped` half a minute after the prompt returns. `-d` (`--detached`) marks the sandbox as detached at creation, which exempts it from that auto-stop for good; it then runs until you `sbx stop` or `sbx rm` it. The flag only exists on `sbx run`, and the mode cannot be changed afterwards: a sandbox created with `sbx create` must be removed and recreated.
25+
26+Without `-p`, the kit still publishes an ephemeral port; the command output prints it:
27+
28+```
29+ Published web: localhost:54321 -> 8888/tcp
30+```
31+
32+## Everyday commands
33+
34+| Action | Command |
35+| --- | --- |
36+| Use another host port (e.g. http://localhost:5555) | `-p 5555:8888` at creation, or `sbx ports ori --publish 5555:8888/tcp` |
37+| Change the internal port | `--kit-arg ori.port=9000` (then `-p 9000:9000`) |
38+| Publish / unpublish a port later | `sbx ports ori --publish 8888:8888/tcp` / `--unpublish 8888:8888/tcp` |
39+| Follow the server log | `sbx exec ori tail -f /home/agent/.ori.log` |
40+| Inspect why the kit's startup ran or failed | `sbx exec ori cat /var/log/sbx-kit-startup.log` |
41+| Open a shell in the sandbox | `sbx exec -it ori bash` |
42+| Attach the terminal Claude session (starts a stopped sandbox) | `sbx attach ori` |
43+| Restart a stopped sandbox without attaching | `sbx run -d --name ori` |
44+| Stop / remove | `sbx stop ori` · `sbx rm ori` |
45+| Demo without Claude usage | `sbx exec ori sh -c 'pkill -x ori; nohup ori --addr :8888 --agent-cmd ori-mock-agent --cwd /home/agent/workspace >> /home/agent/.ori.log 2>&1 &'` |
46+
47+## Building the image yourself
48+
49+Only needed if you maintain the template: `./template/build.sh` builds and pushes `k33g/ori:0.0.0` + `:latest` (multi-arch). For a never-pushed local image: `make template && docker save k33g/ori -o /tmp/ori.tar && sbx template load /tmp/ori.tar`.
50+
51+Full guide: [docs/en/how-to/run-in-a-sandbox.md](docs/en/how-to/run-in-a-sandbox.md) · [docs/fr/how-to/run-in-a-sandbox.md](docs/fr/how-to/run-in-a-sandbox.md)
52+
53+
54+
55+```
56+sbx run -d claude . \
57+ --template k33g/ori:0.0.0 \
58+ --kit ./kits/ori \
59+ --name ori \
60+ -p 8888:8888
61+
62+sbx run -d claude . \
63+ --template k33g/ori:0.0.0 \
64+ --kit ./kits/ori \
65+ --name ori2 \
66+ -p 5555:8888
67+```
68+
69+cd ori-desktop
70+wails build
71+ xattr -cr build/bin/ori-desktop.app
\ No newline at end of file\ No newline at end of file
added template/Dockerfile +76 -0
new file mode 100644
@@ -0,0 +1,76 @@
1+# syntax=docker/dockerfile:1
2+#
3+# Ori sandbox template: the ori web application packaged on top of the
4+# official Docker Sandboxes claude-code template, so the sandbox ships both
5+# Claude Code and the ori server that fronts it over ACP.
6+#
7+# Build from the repository root (the ui and Go sources are the context):
8+#
9+# docker build -f template/Dockerfile -t k33g/ori .
10+#
11+# In a sandbox the runtime replaces CMD with its own process manager; the ori
12+# kit (kits/ori/spec.yaml) is what starts the server. The CMD below makes the
13+# image also work standalone (docker run -p 8888:8888 k33g/ori).
14+
15+ARG GO_VERSION=1.26
16+ARG NODE_VERSION=24
17+
18+#############################
19+# Frontend build (Vite SPA) #
20+#############################
21+# --platform=$BUILDPLATFORM: the SPA build is platform-independent, so it runs
22+# once, natively. Without this, the amd64 half of a multi-arch build runs Vite
23+# under qemu on an arm64 host — where bundling Monaco exhausts Node's heap.
24+FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm AS ui-builder
25+WORKDIR /src/ui
26+COPY ui/package.json ui/package-lock.json ./
27+RUN npm ci
28+COPY ui/ ./
29+# prebuild copies the Material icons into public/, then Vite builds to dist/
30+RUN npm run build
31+
32+####################
33+# Go backend build #
34+####################
35+# Also native: Go cross-compiles to the target with GOOS/GOARCH, no qemu.
36+FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS go-builder
37+ARG TARGETOS
38+ARG TARGETARCH
39+WORKDIR /src
40+COPY go.mod go.sum ./
41+RUN go mod download
42+COPY cmd/ ./cmd/
43+COPY internal/ ./internal/
44+COPY ui/embed.go ./ui/
45+COPY --from=ui-builder /src/ui/dist ./ui/dist
46+RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -o /out/ori ./cmd/ori \
47+ && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -o /out/ori-mock-agent ./cmd/ori-mock-agent
48+
49+##############################################################
50+# Claude Code ACP adapter, fetched natively then copied over #
51+##############################################################
52+# Installed in a $BUILDPLATFORM stage (the package is JavaScript, and its
53+# vendored binaries ship for every platform) so the final stage needs no RUN
54+# at all — a multi-arch build never touches qemu.
55+FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm AS adapter
56+RUN npm install -g --prefix /adapter @zed-industries/claude-code-acp
57+
58+###################################################
59+# Final image: claude-code sandbox template + ori #
60+###################################################
61+FROM docker/sandbox-templates:claude-code
62+
63+# The adapter lands in the template's npm prefix, which is agent-owned and on
64+# PATH — so `claude-code-acp` resolves and the sandbox never needs npx to
65+# download it at runtime.
66+COPY --from=adapter --chown=agent:agent /adapter/ /usr/local/share/npm-global/
67+
68+COPY --from=go-builder /out/ori /out/ori-mock-agent /usr/local/bin/
69+
70+# ori listens here; publish it from the host with:
71+# sbx ports <sandbox-name> --publish 8888:8888/tcp
72+EXPOSE 8888
73+
74+# Standalone-run default. Inside a sandbox the ori kit starts the server
75+# instead (the runtime overrides CMD with its own supervisor).
76+CMD [ "ori", "--addr", ":8888", "--agent-cmd", "claude-code-acp", "--cwd", "/home/agent/workspace" ]
new file mode 100644
@@ -0,0 +1,76 @@
1+# syntax=docker/dockerfile:1
2+#
3+# Ori sandbox template: the ori web application packaged on top of the
4+# official Docker Sandboxes claude-code template, so the sandbox ships both
5+# Claude Code and the ori server that fronts it over ACP.
6+#
7+# Build from the repository root (the ui and Go sources are the context):
8+#
9+# docker build -f template/Dockerfile -t k33g/ori .
10+#
11+# In a sandbox the runtime replaces CMD with its own process manager; the ori
12+# kit (kits/ori/spec.yaml) is what starts the server. The CMD below makes the
13+# image also work standalone (docker run -p 8888:8888 k33g/ori).
14+
15+ARG GO_VERSION=1.26
16+ARG NODE_VERSION=24
17+
18+#############################
19+# Frontend build (Vite SPA) #
20+#############################
21+# --platform=$BUILDPLATFORM: the SPA build is platform-independent, so it runs
22+# once, natively. Without this, the amd64 half of a multi-arch build runs Vite
23+# under qemu on an arm64 host — where bundling Monaco exhausts Node's heap.
24+FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm AS ui-builder
25+WORKDIR /src/ui
26+COPY ui/package.json ui/package-lock.json ./
27+RUN npm ci
28+COPY ui/ ./
29+# prebuild copies the Material icons into public/, then Vite builds to dist/
30+RUN npm run build
31+
32+####################
33+# Go backend build #
34+####################
35+# Also native: Go cross-compiles to the target with GOOS/GOARCH, no qemu.
36+FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS go-builder
37+ARG TARGETOS
38+ARG TARGETARCH
39+WORKDIR /src
40+COPY go.mod go.sum ./
41+RUN go mod download
42+COPY cmd/ ./cmd/
43+COPY internal/ ./internal/
44+COPY ui/embed.go ./ui/
45+COPY --from=ui-builder /src/ui/dist ./ui/dist
46+RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -o /out/ori ./cmd/ori \
47+ && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -o /out/ori-mock-agent ./cmd/ori-mock-agent
48+
49+##############################################################
50+# Claude Code ACP adapter, fetched natively then copied over #
51+##############################################################
52+# Installed in a $BUILDPLATFORM stage (the package is JavaScript, and its
53+# vendored binaries ship for every platform) so the final stage needs no RUN
54+# at all — a multi-arch build never touches qemu.
55+FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm AS adapter
56+RUN npm install -g --prefix /adapter @zed-industries/claude-code-acp
57+
58+###################################################
59+# Final image: claude-code sandbox template + ori #
60+###################################################
61+FROM docker/sandbox-templates:claude-code
62+
63+# The adapter lands in the template's npm prefix, which is agent-owned and on
64+# PATH — so `claude-code-acp` resolves and the sandbox never needs npx to
65+# download it at runtime.
66+COPY --from=adapter --chown=agent:agent /adapter/ /usr/local/share/npm-global/
67+
68+COPY --from=go-builder /out/ori /out/ori-mock-agent /usr/local/bin/
69+
70+# ori listens here; publish it from the host with:
71+# sbx ports <sandbox-name> --publish 8888:8888/tcp
72+EXPOSE 8888
73+
74+# Standalone-run default. Inside a sandbox the ori kit starts the server
75+# instead (the runtime overrides CMD with its own supervisor).
76+CMD [ "ori", "--addr", ":8888", "--agent-cmd", "claude-code-acp", "--cwd", "/home/agent/workspace" ]
added template/build.sh +31 -0
new file mode 100755
@@ -0,0 +1,31 @@
1+#!/usr/bin/env bash
2+# Builds the ori sandbox template image for amd64+arm64 and pushes it to
3+# Docker Hub as k33g/ori:<version> and k33g/ori:latest.
4+#
5+# Usage:
6+# ./template/build.sh # version 0.0.0
7+# ./template/build.sh 0.1.0 # another version
8+# PLATFORMS=linux/arm64 ./template/build.sh # single-arch build
9+#
10+# Requires: docker login (hub account k33g), and a buildx builder able to do
11+# multi-platform builds. If the default builder refuses, create one with:
12+# docker buildx create --name ori-builder --driver docker-container --use
13+set -euo pipefail
14+
15+VERSION="${1:-0.0.1}"
16+IMAGE="k33g/ori"
17+PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
18+
19+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20+CONTEXT="${SCRIPT_DIR}/.."
21+
22+echo "Building ${IMAGE}:${VERSION} (+latest) for ${PLATFORMS} and pushing to Docker Hub…"
23+docker buildx build \
24+ --platform "${PLATFORMS}" \
25+ --file "${SCRIPT_DIR}/Dockerfile" \
26+ --tag "${IMAGE}:${VERSION}" \
27+ --tag "${IMAGE}:latest" \
28+ --push \
29+ "${CONTEXT}"
30+
31+echo "Done: ${IMAGE}:${VERSION} and ${IMAGE}:latest pushed."
new file mode 100755
@@ -0,0 +1,31 @@
1+#!/usr/bin/env bash
2+# Builds the ori sandbox template image for amd64+arm64 and pushes it to
3+# Docker Hub as k33g/ori:<version> and k33g/ori:latest.
4+#
5+# Usage:
6+# ./template/build.sh # version 0.0.0
7+# ./template/build.sh 0.1.0 # another version
8+# PLATFORMS=linux/arm64 ./template/build.sh # single-arch build
9+#
10+# Requires: docker login (hub account k33g), and a buildx builder able to do
11+# multi-platform builds. If the default builder refuses, create one with:
12+# docker buildx create --name ori-builder --driver docker-container --use
13+set -euo pipefail
14+
15+VERSION="${1:-0.0.1}"
16+IMAGE="k33g/ori"
17+PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
18+
19+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20+CONTEXT="${SCRIPT_DIR}/.."
21+
22+echo "Building ${IMAGE}:${VERSION} (+latest) for ${PLATFORMS} and pushing to Docker Hub…"
23+docker buildx build \
24+ --platform "${PLATFORMS}" \
25+ --file "${SCRIPT_DIR}/Dockerfile" \
26+ --tag "${IMAGE}:${VERSION}" \
27+ --tag "${IMAGE}:latest" \
28+ --push \
29+ "${CONTEXT}"
30+
31+echo "Done: ${IMAGE}:${VERSION} and ${IMAGE}:latest pushed."
modified ui/package-lock.json +4581 -4478
@@ -1,4480 +1,4583 @@
11 {
2- "name": "ori-ui",
3- "version": "0.1.0",
4- "lockfileVersion": 3,
5- "requires": true,
6- "packages": {
7- "": {
8- "name": "ori-ui",
9- "version": "0.1.0",
10- "dependencies": {
11- "react": "^19.0.0",
12- "react-dom": "^19.0.0",
13- "react-markdown": "^10.1.0",
14- "remark-gfm": "^4.0.1",
15- "zustand": "^5.0.15"
16- },
17- "devDependencies": {
18- "@testing-library/react": "^16.0.0",
19- "@types/react": "^19.0.0",
20- "@types/react-dom": "^19.0.0",
21- "@vitejs/plugin-react": "^5.0.0",
22- "jsdom": "^26.0.0",
23- "typescript": "^5.6.0",
24- "vite": "^7.0.0",
25- "vitest": "^3.0.0"
26- }
27- },
28- "node_modules/@asamuzakjp/css-color": {
29- "version": "3.2.0",
30- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
31- "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
32- "dev": true,
33- "license": "MIT",
34- "dependencies": {
35- "@csstools/css-calc": "^2.1.3",
36- "@csstools/css-color-parser": "^3.0.9",
37- "@csstools/css-parser-algorithms": "^3.0.4",
38- "@csstools/css-tokenizer": "^3.0.3",
39- "lru-cache": "^10.4.3"
40- }
41- },
42- "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
43- "version": "10.4.3",
44- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
45- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
46- "dev": true,
47- "license": "ISC"
48- },
49- "node_modules/@babel/code-frame": {
50- "version": "7.29.7",
51- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
52- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
53- "dev": true,
54- "license": "MIT",
55- "dependencies": {
56- "@babel/helper-validator-identifier": "^7.29.7",
57- "js-tokens": "^4.0.0",
58- "picocolors": "^1.1.1"
59- },
60- "engines": {
61- "node": ">=6.9.0"
62- }
63- },
64- "node_modules/@babel/compat-data": {
65- "version": "7.29.7",
66- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
67- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
68- "dev": true,
69- "license": "MIT",
70- "engines": {
71- "node": ">=6.9.0"
72- }
73- },
74- "node_modules/@babel/core": {
75- "version": "7.29.7",
76- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
77- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
78- "dev": true,
79- "license": "MIT",
80- "dependencies": {
81- "@babel/code-frame": "^7.29.7",
82- "@babel/generator": "^7.29.7",
83- "@babel/helper-compilation-targets": "^7.29.7",
84- "@babel/helper-module-transforms": "^7.29.7",
85- "@babel/helpers": "^7.29.7",
86- "@babel/parser": "^7.29.7",
87- "@babel/template": "^7.29.7",
88- "@babel/traverse": "^7.29.7",
89- "@babel/types": "^7.29.7",
90- "@jridgewell/remapping": "^2.3.5",
91- "convert-source-map": "^2.0.0",
92- "debug": "^4.1.0",
93- "gensync": "^1.0.0-beta.2",
94- "json5": "^2.2.3",
95- "semver": "^6.3.1"
96- },
97- "engines": {
98- "node": ">=6.9.0"
99- },
100- "funding": {
101- "type": "opencollective",
102- "url": "https://opencollective.com/babel"
103- }
104- },
105- "node_modules/@babel/generator": {
106- "version": "7.29.8",
107- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
108- "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
109- "dev": true,
110- "license": "MIT",
111- "dependencies": {
112- "@babel/parser": "^7.29.8",
113- "@babel/types": "^7.29.8",
114- "@jridgewell/gen-mapping": "^0.3.12",
115- "@jridgewell/trace-mapping": "^0.3.28",
116- "jsesc": "^3.0.2"
117- },
118- "engines": {
119- "node": ">=6.9.0"
120- }
121- },
122- "node_modules/@babel/helper-compilation-targets": {
123- "version": "7.29.7",
124- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
125- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
126- "dev": true,
127- "license": "MIT",
128- "dependencies": {
129- "@babel/compat-data": "^7.29.7",
130- "@babel/helper-validator-option": "^7.29.7",
131- "browserslist": "^4.24.0",
132- "lru-cache": "^5.1.1",
133- "semver": "^6.3.1"
134- },
135- "engines": {
136- "node": ">=6.9.0"
137- }
138- },
139- "node_modules/@babel/helper-globals": {
140- "version": "7.29.7",
141- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
142- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
143- "dev": true,
144- "license": "MIT",
145- "engines": {
146- "node": ">=6.9.0"
147- }
148- },
149- "node_modules/@babel/helper-module-imports": {
150- "version": "7.29.7",
151- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
152- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
153- "dev": true,
154- "license": "MIT",
155- "dependencies": {
156- "@babel/traverse": "^7.29.7",
157- "@babel/types": "^7.29.7"
158- },
159- "engines": {
160- "node": ">=6.9.0"
161- }
162- },
163- "node_modules/@babel/helper-module-transforms": {
164- "version": "7.29.7",
165- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
166- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
167- "dev": true,
168- "license": "MIT",
169- "dependencies": {
170- "@babel/helper-module-imports": "^7.29.7",
171- "@babel/helper-validator-identifier": "^7.29.7",
172- "@babel/traverse": "^7.29.7"
173- },
174- "engines": {
175- "node": ">=6.9.0"
176- },
177- "peerDependencies": {
178- "@babel/core": "^7.0.0"
179- }
180- },
181- "node_modules/@babel/helper-plugin-utils": {
182- "version": "7.29.7",
183- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
184- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
185- "dev": true,
186- "license": "MIT",
187- "engines": {
188- "node": ">=6.9.0"
189- }
190- },
191- "node_modules/@babel/helper-string-parser": {
192- "version": "7.29.7",
193- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
194- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
195- "dev": true,
196- "license": "MIT",
197- "engines": {
198- "node": ">=6.9.0"
199- }
200- },
201- "node_modules/@babel/helper-validator-identifier": {
202- "version": "7.29.7",
203- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
204- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
205- "dev": true,
206- "license": "MIT",
207- "engines": {
208- "node": ">=6.9.0"
209- }
210- },
211- "node_modules/@babel/helper-validator-option": {
212- "version": "7.29.7",
213- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
214- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
215- "dev": true,
216- "license": "MIT",
217- "engines": {
218- "node": ">=6.9.0"
219- }
220- },
221- "node_modules/@babel/helpers": {
222- "version": "7.29.7",
223- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
224- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
225- "dev": true,
226- "license": "MIT",
227- "dependencies": {
228- "@babel/template": "^7.29.7",
229- "@babel/types": "^7.29.7"
230- },
231- "engines": {
232- "node": ">=6.9.0"
233- }
234- },
235- "node_modules/@babel/parser": {
236- "version": "7.29.8",
237- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
238- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
239- "dev": true,
240- "license": "MIT",
241- "dependencies": {
242- "@babel/types": "^7.29.8"
243- },
244- "bin": {
245- "parser": "bin/babel-parser.js"
246- },
247- "engines": {
248- "node": ">=6.0.0"
249- }
250- },
251- "node_modules/@babel/plugin-transform-react-jsx-self": {
252- "version": "7.29.7",
253- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
254- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
255- "dev": true,
256- "license": "MIT",
257- "dependencies": {
258- "@babel/helper-plugin-utils": "^7.29.7"
259- },
260- "engines": {
261- "node": ">=6.9.0"
262- },
263- "peerDependencies": {
264- "@babel/core": "^7.0.0-0"
265- }
266- },
267- "node_modules/@babel/plugin-transform-react-jsx-source": {
268- "version": "7.29.7",
269- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
270- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
271- "dev": true,
272- "license": "MIT",
273- "dependencies": {
274- "@babel/helper-plugin-utils": "^7.29.7"
275- },
276- "engines": {
277- "node": ">=6.9.0"
278- },
279- "peerDependencies": {
280- "@babel/core": "^7.0.0-0"
281- }
282- },
283- "node_modules/@babel/runtime": {
284- "version": "7.29.7",
285- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
286- "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
287- "dev": true,
288- "license": "MIT",
289- "engines": {
290- "node": ">=6.9.0"
291- }
292- },
293- "node_modules/@babel/template": {
294- "version": "7.29.7",
295- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
296- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
297- "dev": true,
298- "license": "MIT",
299- "dependencies": {
300- "@babel/code-frame": "^7.29.7",
301- "@babel/parser": "^7.29.7",
302- "@babel/types": "^7.29.7"
303- },
304- "engines": {
305- "node": ">=6.9.0"
306- }
307- },
308- "node_modules/@babel/traverse": {
309- "version": "7.29.8",
310- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
311- "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
312- "dev": true,
313- "license": "MIT",
314- "dependencies": {
315- "@babel/code-frame": "^7.29.7",
316- "@babel/generator": "^7.29.8",
317- "@babel/helper-globals": "^7.29.7",
318- "@babel/parser": "^7.29.8",
319- "@babel/template": "^7.29.7",
320- "@babel/types": "^7.29.8",
321- "debug": "^4.3.1"
322- },
323- "engines": {
324- "node": ">=6.9.0"
325- }
326- },
327- "node_modules/@babel/types": {
328- "version": "7.29.8",
329- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
330- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
331- "dev": true,
332- "license": "MIT",
333- "dependencies": {
334- "@babel/helper-string-parser": "^7.29.7",
335- "@babel/helper-validator-identifier": "^7.29.7"
336- },
337- "engines": {
338- "node": ">=6.9.0"
339- }
340- },
341- "node_modules/@csstools/color-helpers": {
342- "version": "5.1.0",
343- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
344- "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
345- "dev": true,
346- "funding": [
347- {
348- "type": "github",
349- "url": "https://github.com/sponsors/csstools"
350- },
351- {
352- "type": "opencollective",
353- "url": "https://opencollective.com/csstools"
354- }
355- ],
356- "license": "MIT-0",
357- "engines": {
358- "node": ">=18"
359- }
360- },
361- "node_modules/@csstools/css-calc": {
362- "version": "2.1.4",
363- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
364- "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
365- "dev": true,
366- "funding": [
367- {
368- "type": "github",
369- "url": "https://github.com/sponsors/csstools"
370- },
371- {
372- "type": "opencollective",
373- "url": "https://opencollective.com/csstools"
374- }
375- ],
376- "license": "MIT",
377- "engines": {
378- "node": ">=18"
379- },
380- "peerDependencies": {
381- "@csstools/css-parser-algorithms": "^3.0.5",
382- "@csstools/css-tokenizer": "^3.0.4"
383- }
384- },
385- "node_modules/@csstools/css-color-parser": {
386- "version": "3.1.0",
387- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
388- "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
389- "dev": true,
390- "funding": [
391- {
392- "type": "github",
393- "url": "https://github.com/sponsors/csstools"
394- },
395- {
396- "type": "opencollective",
397- "url": "https://opencollective.com/csstools"
398- }
399- ],
400- "license": "MIT",
401- "dependencies": {
402- "@csstools/color-helpers": "^5.1.0",
403- "@csstools/css-calc": "^2.1.4"
404- },
405- "engines": {
406- "node": ">=18"
407- },
408- "peerDependencies": {
409- "@csstools/css-parser-algorithms": "^3.0.5",
410- "@csstools/css-tokenizer": "^3.0.4"
411- }
412- },
413- "node_modules/@csstools/css-parser-algorithms": {
414- "version": "3.0.5",
415- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
416- "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
417- "dev": true,
418- "funding": [
419- {
420- "type": "github",
421- "url": "https://github.com/sponsors/csstools"
422- },
423- {
424- "type": "opencollective",
425- "url": "https://opencollective.com/csstools"
426- }
427- ],
428- "license": "MIT",
429- "engines": {
430- "node": ">=18"
431- },
432- "peerDependencies": {
433- "@csstools/css-tokenizer": "^3.0.4"
434- }
435- },
436- "node_modules/@csstools/css-tokenizer": {
437- "version": "3.0.4",
438- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
439- "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
440- "dev": true,
441- "funding": [
442- {
443- "type": "github",
444- "url": "https://github.com/sponsors/csstools"
445- },
446- {
447- "type": "opencollective",
448- "url": "https://opencollective.com/csstools"
449- }
450- ],
451- "license": "MIT",
452- "engines": {
453- "node": ">=18"
454- }
455- },
456- "node_modules/@esbuild/aix-ppc64": {
457- "version": "0.28.2",
458- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
459- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
460- "cpu": [
461- "ppc64"
462- ],
463- "dev": true,
464- "license": "MIT",
465- "optional": true,
466- "os": [
467- "aix"
468- ],
469- "engines": {
470- "node": ">=18"
471- }
472- },
473- "node_modules/@esbuild/android-arm": {
474- "version": "0.28.2",
475- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
476- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
477- "cpu": [
478- "arm"
479- ],
480- "dev": true,
481- "license": "MIT",
482- "optional": true,
483- "os": [
484- "android"
485- ],
486- "engines": {
487- "node": ">=18"
488- }
489- },
490- "node_modules/@esbuild/android-arm64": {
491- "version": "0.28.2",
492- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
493- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
494- "cpu": [
495- "arm64"
496- ],
497- "dev": true,
498- "license": "MIT",
499- "optional": true,
500- "os": [
501- "android"
502- ],
503- "engines": {
504- "node": ">=18"
505- }
506- },
507- "node_modules/@esbuild/android-x64": {
508- "version": "0.28.2",
509- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
510- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
511- "cpu": [
512- "x64"
513- ],
514- "dev": true,
515- "license": "MIT",
516- "optional": true,
517- "os": [
518- "android"
519- ],
520- "engines": {
521- "node": ">=18"
522- }
523- },
524- "node_modules/@esbuild/darwin-arm64": {
525- "version": "0.28.2",
526- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
527- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
528- "cpu": [
529- "arm64"
530- ],
531- "dev": true,
532- "license": "MIT",
533- "optional": true,
534- "os": [
535- "darwin"
536- ],
537- "engines": {
538- "node": ">=18"
539- }
540- },
541- "node_modules/@esbuild/darwin-x64": {
542- "version": "0.28.2",
543- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
544- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
545- "cpu": [
546- "x64"
547- ],
548- "dev": true,
549- "license": "MIT",
550- "optional": true,
551- "os": [
552- "darwin"
553- ],
554- "engines": {
555- "node": ">=18"
556- }
557- },
558- "node_modules/@esbuild/freebsd-arm64": {
559- "version": "0.28.2",
560- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
561- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
562- "cpu": [
563- "arm64"
564- ],
565- "dev": true,
566- "license": "MIT",
567- "optional": true,
568- "os": [
569- "freebsd"
570- ],
571- "engines": {
572- "node": ">=18"
573- }
574- },
575- "node_modules/@esbuild/freebsd-x64": {
576- "version": "0.28.2",
577- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
578- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
579- "cpu": [
580- "x64"
581- ],
582- "dev": true,
583- "license": "MIT",
584- "optional": true,
585- "os": [
586- "freebsd"
587- ],
588- "engines": {
589- "node": ">=18"
590- }
591- },
592- "node_modules/@esbuild/linux-arm": {
593- "version": "0.28.2",
594- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
595- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
596- "cpu": [
597- "arm"
598- ],
599- "dev": true,
600- "license": "MIT",
601- "optional": true,
602- "os": [
603- "linux"
604- ],
605- "engines": {
606- "node": ">=18"
607- }
608- },
609- "node_modules/@esbuild/linux-arm64": {
610- "version": "0.28.2",
611- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
612- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
613- "cpu": [
614- "arm64"
615- ],
616- "dev": true,
617- "license": "MIT",
618- "optional": true,
619- "os": [
620- "linux"
621- ],
622- "engines": {
623- "node": ">=18"
624- }
625- },
626- "node_modules/@esbuild/linux-ia32": {
627- "version": "0.28.2",
628- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
629- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
630- "cpu": [
631- "ia32"
632- ],
633- "dev": true,
634- "license": "MIT",
635- "optional": true,
636- "os": [
637- "linux"
638- ],
639- "engines": {
640- "node": ">=18"
641- }
642- },
643- "node_modules/@esbuild/linux-loong64": {
644- "version": "0.28.2",
645- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
646- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
647- "cpu": [
648- "loong64"
649- ],
650- "dev": true,
651- "license": "MIT",
652- "optional": true,
653- "os": [
654- "linux"
655- ],
656- "engines": {
657- "node": ">=18"
658- }
659- },
660- "node_modules/@esbuild/linux-mips64el": {
661- "version": "0.28.2",
662- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
663- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
664- "cpu": [
665- "mips64el"
666- ],
667- "dev": true,
668- "license": "MIT",
669- "optional": true,
670- "os": [
671- "linux"
672- ],
673- "engines": {
674- "node": ">=18"
675- }
676- },
677- "node_modules/@esbuild/linux-ppc64": {
678- "version": "0.28.2",
679- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
680- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
681- "cpu": [
682- "ppc64"
683- ],
684- "dev": true,
685- "license": "MIT",
686- "optional": true,
687- "os": [
688- "linux"
689- ],
690- "engines": {
691- "node": ">=18"
692- }
693- },
694- "node_modules/@esbuild/linux-riscv64": {
695- "version": "0.28.2",
696- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
697- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
698- "cpu": [
699- "riscv64"
700- ],
701- "dev": true,
702- "license": "MIT",
703- "optional": true,
704- "os": [
705- "linux"
706- ],
707- "engines": {
708- "node": ">=18"
709- }
710- },
711- "node_modules/@esbuild/linux-s390x": {
712- "version": "0.28.2",
713- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
714- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
715- "cpu": [
716- "s390x"
717- ],
718- "dev": true,
719- "license": "MIT",
720- "optional": true,
721- "os": [
722- "linux"
723- ],
724- "engines": {
725- "node": ">=18"
726- }
727- },
728- "node_modules/@esbuild/linux-x64": {
729- "version": "0.28.2",
730- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
731- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
732- "cpu": [
733- "x64"
734- ],
735- "dev": true,
736- "license": "MIT",
737- "optional": true,
738- "os": [
739- "linux"
740- ],
741- "engines": {
742- "node": ">=18"
743- }
744- },
745- "node_modules/@esbuild/netbsd-arm64": {
746- "version": "0.28.2",
747- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
748- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
749- "cpu": [
750- "arm64"
751- ],
752- "dev": true,
753- "license": "MIT",
754- "optional": true,
755- "os": [
756- "netbsd"
757- ],
758- "engines": {
759- "node": ">=18"
760- }
761- },
762- "node_modules/@esbuild/netbsd-x64": {
763- "version": "0.28.2",
764- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
765- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
766- "cpu": [
767- "x64"
768- ],
769- "dev": true,
770- "license": "MIT",
771- "optional": true,
772- "os": [
773- "netbsd"
774- ],
775- "engines": {
776- "node": ">=18"
777- }
778- },
779- "node_modules/@esbuild/openbsd-arm64": {
780- "version": "0.28.2",
781- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
782- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
783- "cpu": [
784- "arm64"
785- ],
786- "dev": true,
787- "license": "MIT",
788- "optional": true,
789- "os": [
790- "openbsd"
791- ],
792- "engines": {
793- "node": ">=18"
794- }
795- },
796- "node_modules/@esbuild/openbsd-x64": {
797- "version": "0.28.2",
798- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
799- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
800- "cpu": [
801- "x64"
802- ],
803- "dev": true,
804- "license": "MIT",
805- "optional": true,
806- "os": [
807- "openbsd"
808- ],
809- "engines": {
810- "node": ">=18"
811- }
812- },
813- "node_modules/@esbuild/openharmony-arm64": {
814- "version": "0.28.2",
815- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
816- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
817- "cpu": [
818- "arm64"
819- ],
820- "dev": true,
821- "license": "MIT",
822- "optional": true,
823- "os": [
824- "openharmony"
825- ],
826- "engines": {
827- "node": ">=18"
828- }
829- },
830- "node_modules/@esbuild/sunos-x64": {
831- "version": "0.28.2",
832- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
833- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
834- "cpu": [
835- "x64"
836- ],
837- "dev": true,
838- "license": "MIT",
839- "optional": true,
840- "os": [
841- "sunos"
842- ],
843- "engines": {
844- "node": ">=18"
845- }
846- },
847- "node_modules/@esbuild/win32-arm64": {
848- "version": "0.28.2",
849- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
850- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
851- "cpu": [
852- "arm64"
853- ],
854- "dev": true,
855- "license": "MIT",
856- "optional": true,
857- "os": [
858- "win32"
859- ],
860- "engines": {
861- "node": ">=18"
862- }
863- },
864- "node_modules/@esbuild/win32-ia32": {
865- "version": "0.28.2",
866- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
867- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
868- "cpu": [
869- "ia32"
870- ],
871- "dev": true,
872- "license": "MIT",
873- "optional": true,
874- "os": [
875- "win32"
876- ],
877- "engines": {
878- "node": ">=18"
879- }
880- },
881- "node_modules/@esbuild/win32-x64": {
882- "version": "0.28.2",
883- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
884- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
885- "cpu": [
886- "x64"
887- ],
888- "dev": true,
889- "license": "MIT",
890- "optional": true,
891- "os": [
892- "win32"
893- ],
894- "engines": {
895- "node": ">=18"
896- }
897- },
898- "node_modules/@jridgewell/gen-mapping": {
899- "version": "0.3.13",
900- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
901- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
902- "dev": true,
903- "license": "MIT",
904- "dependencies": {
905- "@jridgewell/sourcemap-codec": "^1.5.0",
906- "@jridgewell/trace-mapping": "^0.3.24"
907- }
908- },
909- "node_modules/@jridgewell/remapping": {
910- "version": "2.3.5",
911- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
912- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
913- "dev": true,
914- "license": "MIT",
915- "dependencies": {
916- "@jridgewell/gen-mapping": "^0.3.5",
917- "@jridgewell/trace-mapping": "^0.3.24"
918- }
919- },
920- "node_modules/@jridgewell/resolve-uri": {
921- "version": "3.1.2",
922- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
923- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
924- "dev": true,
925- "license": "MIT",
926- "engines": {
927- "node": ">=6.0.0"
928- }
929- },
930- "node_modules/@jridgewell/sourcemap-codec": {
931- "version": "1.6.0",
932- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
933- "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
934- "dev": true,
935- "license": "MIT"
936- },
937- "node_modules/@jridgewell/trace-mapping": {
938- "version": "0.3.31",
939- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
940- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
941- "dev": true,
942- "license": "MIT",
943- "dependencies": {
944- "@jridgewell/resolve-uri": "^3.1.0",
945- "@jridgewell/sourcemap-codec": "^1.4.14"
946- }
947- },
948- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
949- "version": "1.5.1",
950- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
951- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
952- "cpu": [
953- "x64"
954- ],
955- "dev": true,
956- "libc": [
957- "glibc"
958- ],
959- "license": "MIT",
960- "optional": true,
961- "os": [
962- "linux"
963- ],
964- "engines": {
965- "node": "^22.20 || ^24.12 || >=25"
966- }
967- },
968- "node_modules/@rolldown/pluginutils": {
969- "version": "1.0.0-rc.3",
970- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
971- "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
972- "dev": true,
973- "license": "MIT"
974- },
975- "node_modules/@rollup/rollup-android-arm-eabi": {
976- "version": "4.63.3",
977- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",
978- "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==",
979- "cpu": [
980- "arm"
981- ],
982- "dev": true,
983- "license": "MIT",
984- "optional": true,
985- "os": [
986- "android"
987- ]
988- },
989- "node_modules/@rollup/rollup-android-arm64": {
990- "version": "4.63.3",
991- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz",
992- "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==",
993- "cpu": [
994- "arm64"
995- ],
996- "dev": true,
997- "license": "MIT",
998- "optional": true,
999- "os": [
1000- "android"
1001- ]
1002- },
1003- "node_modules/@rollup/rollup-darwin-arm64": {
1004- "version": "4.63.3",
1005- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz",
1006- "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==",
1007- "cpu": [
1008- "arm64"
1009- ],
1010- "dev": true,
1011- "license": "MIT",
1012- "optional": true,
1013- "os": [
1014- "darwin"
1015- ]
1016- },
1017- "node_modules/@rollup/rollup-darwin-x64": {
1018- "version": "4.63.3",
1019- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz",
1020- "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==",
1021- "cpu": [
1022- "x64"
1023- ],
1024- "dev": true,
1025- "license": "MIT",
1026- "optional": true,
1027- "os": [
1028- "darwin"
1029- ]
1030- },
1031- "node_modules/@rollup/rollup-freebsd-arm64": {
1032- "version": "4.63.3",
1033- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz",
1034- "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==",
1035- "cpu": [
1036- "arm64"
1037- ],
1038- "dev": true,
1039- "license": "MIT",
1040- "optional": true,
1041- "os": [
1042- "freebsd"
1043- ]
1044- },
1045- "node_modules/@rollup/rollup-freebsd-x64": {
1046- "version": "4.63.3",
1047- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz",
1048- "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==",
1049- "cpu": [
1050- "x64"
1051- ],
1052- "dev": true,
1053- "license": "MIT",
1054- "optional": true,
1055- "os": [
1056- "freebsd"
1057- ]
1058- },
1059- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1060- "version": "4.63.3",
1061- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz",
1062- "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==",
1063- "cpu": [
1064- "arm"
1065- ],
1066- "dev": true,
1067- "libc": [
1068- "glibc"
1069- ],
1070- "license": "MIT",
1071- "optional": true,
1072- "os": [
1073- "linux"
1074- ]
1075- },
1076- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1077- "version": "4.63.3",
1078- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz",
1079- "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==",
1080- "cpu": [
1081- "arm"
1082- ],
1083- "dev": true,
1084- "libc": [
1085- "musl"
1086- ],
1087- "license": "MIT",
1088- "optional": true,
1089- "os": [
1090- "linux"
1091- ]
1092- },
1093- "node_modules/@rollup/rollup-linux-arm64-gnu": {
1094- "version": "4.63.3",
1095- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz",
1096- "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==",
1097- "cpu": [
1098- "arm64"
1099- ],
1100- "dev": true,
1101- "libc": [
1102- "glibc"
1103- ],
1104- "license": "MIT",
1105- "optional": true,
1106- "os": [
1107- "linux"
1108- ]
1109- },
1110- "node_modules/@rollup/rollup-linux-arm64-musl": {
1111- "version": "4.63.3",
1112- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz",
1113- "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==",
1114- "cpu": [
1115- "arm64"
1116- ],
1117- "dev": true,
1118- "libc": [
1119- "musl"
1120- ],
1121- "license": "MIT",
1122- "optional": true,
1123- "os": [
1124- "linux"
1125- ]
1126- },
1127- "node_modules/@rollup/rollup-linux-loong64-gnu": {
1128- "version": "4.63.3",
1129- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz",
1130- "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==",
1131- "cpu": [
1132- "loong64"
1133- ],
1134- "dev": true,
1135- "libc": [
1136- "glibc"
1137- ],
1138- "license": "MIT",
1139- "optional": true,
1140- "os": [
1141- "linux"
1142- ]
1143- },
1144- "node_modules/@rollup/rollup-linux-loong64-musl": {
1145- "version": "4.63.3",
1146- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz",
1147- "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==",
1148- "cpu": [
1149- "loong64"
1150- ],
1151- "dev": true,
1152- "libc": [
1153- "musl"
1154- ],
1155- "license": "MIT",
1156- "optional": true,
1157- "os": [
1158- "linux"
1159- ]
1160- },
1161- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1162- "version": "4.63.3",
1163- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz",
1164- "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==",
1165- "cpu": [
1166- "ppc64"
1167- ],
1168- "dev": true,
1169- "libc": [
1170- "glibc"
1171- ],
1172- "license": "MIT",
1173- "optional": true,
1174- "os": [
1175- "linux"
1176- ]
1177- },
1178- "node_modules/@rollup/rollup-linux-ppc64-musl": {
1179- "version": "4.63.3",
1180- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz",
1181- "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==",
1182- "cpu": [
1183- "ppc64"
1184- ],
1185- "dev": true,
1186- "libc": [
1187- "musl"
1188- ],
1189- "license": "MIT",
1190- "optional": true,
1191- "os": [
1192- "linux"
1193- ]
1194- },
1195- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1196- "version": "4.63.3",
1197- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz",
1198- "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==",
1199- "cpu": [
1200- "riscv64"
1201- ],
1202- "dev": true,
1203- "libc": [
1204- "glibc"
1205- ],
1206- "license": "MIT",
1207- "optional": true,
1208- "os": [
1209- "linux"
1210- ]
1211- },
1212- "node_modules/@rollup/rollup-linux-riscv64-musl": {
1213- "version": "4.63.3",
1214- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz",
1215- "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==",
1216- "cpu": [
1217- "riscv64"
1218- ],
1219- "dev": true,
1220- "libc": [
1221- "musl"
1222- ],
1223- "license": "MIT",
1224- "optional": true,
1225- "os": [
1226- "linux"
1227- ]
1228- },
1229- "node_modules/@rollup/rollup-linux-s390x-gnu": {
1230- "version": "4.63.3",
1231- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz",
1232- "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==",
1233- "cpu": [
1234- "s390x"
1235- ],
1236- "dev": true,
1237- "libc": [
1238- "glibc"
1239- ],
1240- "license": "MIT",
1241- "optional": true,
1242- "os": [
1243- "linux"
1244- ]
1245- },
1246- "node_modules/@rollup/rollup-linux-x64-gnu": {
1247- "version": "4.63.3",
1248- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz",
1249- "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==",
1250- "cpu": [
1251- "x64"
1252- ],
1253- "dev": true,
1254- "libc": [
1255- "glibc"
1256- ],
1257- "license": "MIT",
1258- "optional": true,
1259- "os": [
1260- "linux"
1261- ]
1262- },
1263- "node_modules/@rollup/rollup-linux-x64-musl": {
1264- "version": "4.63.3",
1265- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz",
1266- "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==",
1267- "cpu": [
1268- "x64"
1269- ],
1270- "dev": true,
1271- "libc": [
1272- "musl"
1273- ],
1274- "license": "MIT",
1275- "optional": true,
1276- "os": [
1277- "linux"
1278- ]
1279- },
1280- "node_modules/@rollup/rollup-openbsd-x64": {
1281- "version": "4.63.3",
1282- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz",
1283- "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==",
1284- "cpu": [
1285- "x64"
1286- ],
1287- "dev": true,
1288- "license": "MIT",
1289- "optional": true,
1290- "os": [
1291- "openbsd"
1292- ]
1293- },
1294- "node_modules/@rollup/rollup-openharmony-arm64": {
1295- "version": "4.63.3",
1296- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz",
1297- "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==",
1298- "cpu": [
1299- "arm64"
1300- ],
1301- "dev": true,
1302- "license": "MIT",
1303- "optional": true,
1304- "os": [
1305- "openharmony"
1306- ]
1307- },
1308- "node_modules/@rollup/rollup-win32-arm64-msvc": {
1309- "version": "4.63.3",
1310- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz",
1311- "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==",
1312- "cpu": [
1313- "arm64"
1314- ],
1315- "dev": true,
1316- "license": "MIT",
1317- "optional": true,
1318- "os": [
1319- "win32"
1320- ]
1321- },
1322- "node_modules/@rollup/rollup-win32-ia32-msvc": {
1323- "version": "4.63.3",
1324- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz",
1325- "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==",
1326- "cpu": [
1327- "ia32"
1328- ],
1329- "dev": true,
1330- "license": "MIT",
1331- "optional": true,
1332- "os": [
1333- "win32"
1334- ]
1335- },
1336- "node_modules/@rollup/rollup-win32-x64-gnu": {
1337- "version": "4.63.3",
1338- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz",
1339- "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==",
1340- "cpu": [
1341- "x64"
1342- ],
1343- "dev": true,
1344- "license": "MIT",
1345- "optional": true,
1346- "os": [
1347- "win32"
1348- ]
1349- },
1350- "node_modules/@rollup/rollup-win32-x64-msvc": {
1351- "version": "4.63.3",
1352- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz",
1353- "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==",
1354- "cpu": [
1355- "x64"
1356- ],
1357- "dev": true,
1358- "license": "MIT",
1359- "optional": true,
1360- "os": [
1361- "win32"
1362- ]
1363- },
1364- "node_modules/@testing-library/dom": {
1365- "version": "10.4.2",
1366- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
1367- "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
1368- "dev": true,
1369- "license": "MIT",
1370- "peer": true,
1371- "dependencies": {
1372- "@babel/code-frame": "^7.10.4",
1373- "@babel/runtime": "^7.12.5",
1374- "@types/aria-query": "^5.0.1",
1375- "aria-query": "5.3.0",
1376- "dom-accessibility-api": "^0.5.9",
1377- "lz-string": "^1.5.0",
1378- "picocolors": "1.1.1",
1379- "pretty-format": "^27.0.2"
1380- },
1381- "engines": {
1382- "node": ">=18"
1383- }
1384- },
1385- "node_modules/@testing-library/react": {
1386- "version": "16.3.3",
1387- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
1388- "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
1389- "dev": true,
1390- "license": "MIT",
1391- "dependencies": {
1392- "@babel/runtime": "^7.12.5"
1393- },
1394- "engines": {
1395- "node": ">=18"
1396- },
1397- "peerDependencies": {
1398- "@testing-library/dom": "^10.0.0",
1399- "@types/react": "^18.0.0 || ^19.0.0",
1400- "@types/react-dom": "^18.0.0 || ^19.0.0",
1401- "react": "^18.0.0 || ^19.0.0",
1402- "react-dom": "^18.0.0 || ^19.0.0"
1403- },
1404- "peerDependenciesMeta": {
1405- "@types/react": {
1406- "optional": true
1407- },
1408- "@types/react-dom": {
1409- "optional": true
1410- }
1411- }
1412- },
1413- "node_modules/@types/aria-query": {
1414- "version": "5.0.4",
1415- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
1416- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
1417- "dev": true,
1418- "license": "MIT",
1419- "peer": true
1420- },
1421- "node_modules/@types/babel__core": {
1422- "version": "7.20.5",
1423- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1424- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1425- "dev": true,
1426- "license": "MIT",
1427- "dependencies": {
1428- "@babel/parser": "^7.20.7",
1429- "@babel/types": "^7.20.7",
1430- "@types/babel__generator": "*",
1431- "@types/babel__template": "*",
1432- "@types/babel__traverse": "*"
1433- }
1434- },
1435- "node_modules/@types/babel__generator": {
1436- "version": "7.27.0",
1437- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1438- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1439- "dev": true,
1440- "license": "MIT",
1441- "dependencies": {
1442- "@babel/types": "^7.0.0"
1443- }
1444- },
1445- "node_modules/@types/babel__template": {
1446- "version": "7.4.4",
1447- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1448- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1449- "dev": true,
1450- "license": "MIT",
1451- "dependencies": {
1452- "@babel/parser": "^7.1.0",
1453- "@babel/types": "^7.0.0"
1454- }
1455- },
1456- "node_modules/@types/babel__traverse": {
1457- "version": "7.28.0",
1458- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1459- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1460- "dev": true,
1461- "license": "MIT",
1462- "dependencies": {
1463- "@babel/types": "^7.28.2"
1464- }
1465- },
1466- "node_modules/@types/chai": {
1467- "version": "5.2.3",
1468- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
1469- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
1470- "dev": true,
1471- "license": "MIT",
1472- "dependencies": {
1473- "@types/deep-eql": "*",
1474- "assertion-error": "^2.0.1"
1475- }
1476- },
1477- "node_modules/@types/debug": {
1478- "version": "4.1.13",
1479- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
1480- "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
1481- "license": "MIT",
1482- "dependencies": {
1483- "@types/ms": "*"
1484- }
1485- },
1486- "node_modules/@types/deep-eql": {
1487- "version": "4.0.2",
1488- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
1489- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
1490- "dev": true,
1491- "license": "MIT"
1492- },
1493- "node_modules/@types/estree": {
1494- "version": "1.0.9",
1495- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1496- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1497- "license": "MIT"
1498- },
1499- "node_modules/@types/estree-jsx": {
1500- "version": "1.0.5",
1501- "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
1502- "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
1503- "license": "MIT",
1504- "dependencies": {
1505- "@types/estree": "*"
1506- }
1507- },
1508- "node_modules/@types/hast": {
1509- "version": "3.0.5",
1510- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
1511- "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
1512- "license": "MIT",
1513- "dependencies": {
1514- "@types/unist": "*"
1515- }
1516- },
1517- "node_modules/@types/mdast": {
1518- "version": "4.0.4",
1519- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1520- "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1521- "license": "MIT",
1522- "dependencies": {
1523- "@types/unist": "*"
1524- }
1525- },
1526- "node_modules/@types/ms": {
1527- "version": "2.1.0",
1528- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1529- "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1530- "license": "MIT"
1531- },
1532- "node_modules/@types/react": {
1533- "version": "19.3.0",
1534- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
1535- "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
1536- "license": "MIT",
1537- "dependencies": {
1538- "csstype": "^3.2.2"
1539- }
1540- },
1541- "node_modules/@types/react-dom": {
1542- "version": "19.3.0",
1543- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
1544- "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
1545- "dev": true,
1546- "license": "MIT",
1547- "peerDependencies": {
1548- "@types/react": "^19.3.0"
1549- }
1550- },
1551- "node_modules/@types/unist": {
1552- "version": "3.0.3",
1553- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1554- "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1555- "license": "MIT"
1556- },
1557- "node_modules/@ungap/structured-clone": {
1558- "version": "1.4.0",
1559- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
1560- "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
1561- "license": "ISC"
1562- },
1563- "node_modules/@vitejs/plugin-react": {
1564- "version": "5.2.0",
1565- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1566- "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1567- "dev": true,
1568- "license": "MIT",
1569- "dependencies": {
1570- "@babel/core": "^7.29.0",
1571- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1572- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1573- "@rolldown/pluginutils": "1.0.0-rc.3",
1574- "@types/babel__core": "^7.20.5",
1575- "react-refresh": "^0.18.0"
1576- },
1577- "engines": {
1578- "node": "^20.19.0 || >=22.12.0"
1579- },
1580- "peerDependencies": {
1581- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1582- }
1583- },
1584- "node_modules/@vitest/expect": {
1585- "version": "3.2.7",
1586- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
1587- "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
1588- "dev": true,
1589- "license": "MIT",
1590- "dependencies": {
1591- "@types/chai": "^5.2.2",
1592- "@vitest/spy": "3.2.7",
1593- "@vitest/utils": "3.2.7",
1594- "chai": "^5.2.0",
1595- "tinyrainbow": "^2.0.0"
1596- },
1597- "funding": {
1598- "url": "https://opencollective.com/vitest"
1599- }
1600- },
1601- "node_modules/@vitest/mocker": {
1602- "version": "3.2.7",
1603- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
1604- "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
1605- "dev": true,
1606- "license": "MIT",
1607- "dependencies": {
1608- "@vitest/spy": "3.2.7",
1609- "estree-walker": "^3.0.3",
1610- "magic-string": "^0.30.17"
1611- },
1612- "funding": {
1613- "url": "https://opencollective.com/vitest"
1614- },
1615- "peerDependencies": {
1616- "msw": "^2.4.9",
1617- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
1618- },
1619- "peerDependenciesMeta": {
1620- "msw": {
1621- "optional": true
1622- },
1623- "vite": {
1624- "optional": true
1625- }
1626- }
1627- },
1628- "node_modules/@vitest/pretty-format": {
1629- "version": "3.2.7",
1630- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
1631- "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
1632- "dev": true,
1633- "license": "MIT",
1634- "dependencies": {
1635- "tinyrainbow": "^2.0.0"
1636- },
1637- "funding": {
1638- "url": "https://opencollective.com/vitest"
1639- }
1640- },
1641- "node_modules/@vitest/runner": {
1642- "version": "3.2.7",
1643- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
1644- "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
1645- "dev": true,
1646- "license": "MIT",
1647- "dependencies": {
1648- "@vitest/utils": "3.2.7",
1649- "pathe": "^2.0.3",
1650- "strip-literal": "^3.0.0"
1651- },
1652- "funding": {
1653- "url": "https://opencollective.com/vitest"
1654- }
1655- },
1656- "node_modules/@vitest/snapshot": {
1657- "version": "3.2.7",
1658- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
1659- "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
1660- "dev": true,
1661- "license": "MIT",
1662- "dependencies": {
1663- "@vitest/pretty-format": "3.2.7",
1664- "magic-string": "^0.30.17",
1665- "pathe": "^2.0.3"
1666- },
1667- "funding": {
1668- "url": "https://opencollective.com/vitest"
1669- }
1670- },
1671- "node_modules/@vitest/spy": {
1672- "version": "3.2.7",
1673- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
1674- "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
1675- "dev": true,
1676- "license": "MIT",
1677- "dependencies": {
1678- "tinyspy": "^4.0.3"
1679- },
1680- "funding": {
1681- "url": "https://opencollective.com/vitest"
1682- }
1683- },
1684- "node_modules/@vitest/utils": {
1685- "version": "3.2.7",
1686- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
1687- "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
1688- "dev": true,
1689- "license": "MIT",
1690- "dependencies": {
1691- "@vitest/pretty-format": "3.2.7",
1692- "loupe": "^3.1.4",
1693- "tinyrainbow": "^2.0.0"
1694- },
1695- "funding": {
1696- "url": "https://opencollective.com/vitest"
1697- }
1698- },
1699- "node_modules/agent-base": {
1700- "version": "7.1.4",
1701- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1702- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1703- "dev": true,
1704- "license": "MIT",
1705- "engines": {
1706- "node": ">= 14"
1707- }
1708- },
1709- "node_modules/ansi-regex": {
1710- "version": "5.0.1",
1711- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1712- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1713- "dev": true,
1714- "license": "MIT",
1715- "peer": true,
1716- "engines": {
1717- "node": ">=8"
1718- }
1719- },
1720- "node_modules/ansi-styles": {
1721- "version": "5.2.0",
1722- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
1723- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
1724- "dev": true,
1725- "license": "MIT",
1726- "peer": true,
1727- "engines": {
1728- "node": ">=10"
1729- },
1730- "funding": {
1731- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1732- }
1733- },
1734- "node_modules/aria-query": {
1735- "version": "5.3.0",
1736- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
1737- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
1738- "dev": true,
1739- "license": "Apache-2.0",
1740- "peer": true,
1741- "dependencies": {
1742- "dequal": "^2.0.3"
1743- }
1744- },
1745- "node_modules/assertion-error": {
1746- "version": "2.0.1",
1747- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1748- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1749- "dev": true,
1750- "license": "MIT",
1751- "engines": {
1752- "node": ">=12"
1753- }
1754- },
1755- "node_modules/bail": {
1756- "version": "2.0.2",
1757- "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
1758- "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
1759- "license": "MIT",
1760- "funding": {
1761- "type": "github",
1762- "url": "https://github.com/sponsors/wooorm"
1763- }
1764- },
1765- "node_modules/baseline-browser-mapping": {
1766- "version": "2.11.25",
1767- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz",
1768- "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==",
1769- "dev": true,
1770- "license": "Apache-2.0",
1771- "bin": {
1772- "baseline-browser-mapping": "dist/cli.cjs"
1773- },
1774- "engines": {
1775- "node": ">=6.0.0"
1776- }
1777- },
1778- "node_modules/browserslist": {
1779- "version": "4.29.0",
1780- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",
1781- "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",
1782- "dev": true,
1783- "funding": [
1784- {
1785- "type": "opencollective",
1786- "url": "https://opencollective.com/browserslist"
1787- },
1788- {
1789- "type": "tidelift",
1790- "url": "https://tidelift.com/funding/github/npm/browserslist"
1791- },
1792- {
1793- "type": "github",
1794- "url": "https://github.com/sponsors/ai"
1795- }
1796- ],
1797- "license": "MIT",
1798- "dependencies": {
1799- "baseline-browser-mapping": "^2.11.23",
1800- "caniuse-lite": "^1.0.30001810",
1801- "electron-to-chromium": "^1.5.427",
1802- "node-releases": "^2.0.55",
1803- "update-browserslist-db": "^1.3.3"
1804- },
1805- "bin": {
1806- "browserslist": "cli.js"
1807- },
1808- "engines": {
1809- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1810- }
1811- },
1812- "node_modules/cac": {
1813- "version": "6.7.14",
1814- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1815- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1816- "dev": true,
1817- "license": "MIT",
1818- "engines": {
1819- "node": ">=8"
1820- }
1821- },
1822- "node_modules/caniuse-lite": {
1823- "version": "1.0.30001810",
1824- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
1825- "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
1826- "dev": true,
1827- "funding": [
1828- {
1829- "type": "opencollective",
1830- "url": "https://opencollective.com/browserslist"
1831- },
1832- {
1833- "type": "tidelift",
1834- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1835- },
1836- {
1837- "type": "github",
1838- "url": "https://github.com/sponsors/ai"
1839- }
1840- ],
1841- "license": "CC-BY-4.0"
1842- },
1843- "node_modules/ccount": {
1844- "version": "2.0.1",
1845- "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1846- "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1847- "license": "MIT",
1848- "funding": {
1849- "type": "github",
1850- "url": "https://github.com/sponsors/wooorm"
1851- }
1852- },
1853- "node_modules/chai": {
1854- "version": "5.3.3",
1855- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
1856- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
1857- "dev": true,
1858- "license": "MIT",
1859- "dependencies": {
1860- "assertion-error": "^2.0.1",
1861- "check-error": "^2.1.1",
1862- "deep-eql": "^5.0.1",
1863- "loupe": "^3.1.0",
1864- "pathval": "^2.0.0"
1865- },
1866- "engines": {
1867- "node": ">=18"
1868- }
1869- },
1870- "node_modules/character-entities": {
1871- "version": "2.0.2",
1872- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
1873- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
1874- "license": "MIT",
1875- "funding": {
1876- "type": "github",
1877- "url": "https://github.com/sponsors/wooorm"
1878- }
1879- },
1880- "node_modules/character-entities-html4": {
1881- "version": "2.1.0",
1882- "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
1883- "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
1884- "license": "MIT",
1885- "funding": {
1886- "type": "github",
1887- "url": "https://github.com/sponsors/wooorm"
1888- }
1889- },
1890- "node_modules/character-entities-legacy": {
1891- "version": "3.0.0",
1892- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
1893- "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
1894- "license": "MIT",
1895- "funding": {
1896- "type": "github",
1897- "url": "https://github.com/sponsors/wooorm"
1898- }
1899- },
1900- "node_modules/character-reference-invalid": {
1901- "version": "2.0.1",
1902- "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
1903- "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
1904- "license": "MIT",
1905- "funding": {
1906- "type": "github",
1907- "url": "https://github.com/sponsors/wooorm"
1908- }
1909- },
1910- "node_modules/check-error": {
1911- "version": "2.1.3",
1912- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
1913- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
1914- "dev": true,
1915- "license": "MIT",
1916- "engines": {
1917- "node": ">= 16"
1918- }
1919- },
1920- "node_modules/comma-separated-tokens": {
1921- "version": "2.0.3",
1922- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
1923- "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
1924- "license": "MIT",
1925- "funding": {
1926- "type": "github",
1927- "url": "https://github.com/sponsors/wooorm"
1928- }
1929- },
1930- "node_modules/convert-source-map": {
1931- "version": "2.0.0",
1932- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1933- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1934- "dev": true,
1935- "license": "MIT"
1936- },
1937- "node_modules/cssstyle": {
1938- "version": "4.6.0",
1939- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
1940- "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
1941- "dev": true,
1942- "license": "MIT",
1943- "dependencies": {
1944- "@asamuzakjp/css-color": "^3.2.0",
1945- "rrweb-cssom": "^0.8.0"
1946- },
1947- "engines": {
1948- "node": ">=18"
1949- }
1950- },
1951- "node_modules/csstype": {
1952- "version": "3.2.3",
1953- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1954- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1955- "license": "MIT"
1956- },
1957- "node_modules/data-urls": {
1958- "version": "5.0.0",
1959- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
1960- "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
1961- "dev": true,
1962- "license": "MIT",
1963- "dependencies": {
1964- "whatwg-mimetype": "^4.0.0",
1965- "whatwg-url": "^14.0.0"
1966- },
1967- "engines": {
1968- "node": ">=18"
1969- }
1970- },
1971- "node_modules/debug": {
1972- "version": "4.4.3",
1973- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1974- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1975- "license": "MIT",
1976- "dependencies": {
1977- "ms": "^2.1.3"
1978- },
1979- "engines": {
1980- "node": ">=6.0"
1981- },
1982- "peerDependenciesMeta": {
1983- "supports-color": {
1984- "optional": true
1985- }
1986- }
1987- },
1988- "node_modules/decimal.js": {
1989- "version": "10.6.0",
1990- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
1991- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
1992- "dev": true,
1993- "license": "MIT"
1994- },
1995- "node_modules/decode-named-character-reference": {
1996- "version": "1.3.0",
1997- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
1998- "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
1999- "license": "MIT",
2000- "dependencies": {
2001- "character-entities": "^2.0.0"
2002- },
2003- "funding": {
2004- "type": "github",
2005- "url": "https://github.com/sponsors/wooorm"
2006- }
2007- },
2008- "node_modules/deep-eql": {
2009- "version": "5.0.2",
2010- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
2011- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
2012- "dev": true,
2013- "license": "MIT",
2014- "engines": {
2015- "node": ">=6"
2016- }
2017- },
2018- "node_modules/dequal": {
2019- "version": "2.0.3",
2020- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
2021- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2022- "license": "MIT",
2023- "engines": {
2024- "node": ">=6"
2025- }
2026- },
2027- "node_modules/devlop": {
2028- "version": "1.1.0",
2029- "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
2030- "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
2031- "license": "MIT",
2032- "dependencies": {
2033- "dequal": "^2.0.0"
2034- },
2035- "funding": {
2036- "type": "github",
2037- "url": "https://github.com/sponsors/wooorm"
2038- }
2039- },
2040- "node_modules/dom-accessibility-api": {
2041- "version": "0.5.16",
2042- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
2043- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
2044- "dev": true,
2045- "license": "MIT",
2046- "peer": true
2047- },
2048- "node_modules/electron-to-chromium": {
2049- "version": "1.5.430",
2050- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz",
2051- "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==",
2052- "dev": true,
2053- "license": "ISC"
2054- },
2055- "node_modules/entities": {
2056- "version": "6.0.1",
2057- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
2058- "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
2059- "dev": true,
2060- "license": "BSD-2-Clause",
2061- "engines": {
2062- "node": ">=0.12"
2063- },
2064- "funding": {
2065- "url": "https://github.com/fb55/entities?sponsor=1"
2066- }
2067- },
2068- "node_modules/es-module-lexer": {
2069- "version": "1.7.0",
2070- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
2071- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
2072- "dev": true,
2073- "license": "MIT"
2074- },
2075- "node_modules/esbuild": {
2076- "version": "0.28.2",
2077- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
2078- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
2079- "dev": true,
2080- "hasInstallScript": true,
2081- "license": "MIT",
2082- "bin": {
2083- "esbuild": "bin/esbuild"
2084- },
2085- "engines": {
2086- "node": ">=18"
2087- },
2088- "optionalDependencies": {
2089- "@esbuild/aix-ppc64": "0.28.2",
2090- "@esbuild/android-arm": "0.28.2",
2091- "@esbuild/android-arm64": "0.28.2",
2092- "@esbuild/android-x64": "0.28.2",
2093- "@esbuild/darwin-arm64": "0.28.2",
2094- "@esbuild/darwin-x64": "0.28.2",
2095- "@esbuild/freebsd-arm64": "0.28.2",
2096- "@esbuild/freebsd-x64": "0.28.2",
2097- "@esbuild/linux-arm": "0.28.2",
2098- "@esbuild/linux-arm64": "0.28.2",
2099- "@esbuild/linux-ia32": "0.28.2",
2100- "@esbuild/linux-loong64": "0.28.2",
2101- "@esbuild/linux-mips64el": "0.28.2",
2102- "@esbuild/linux-ppc64": "0.28.2",
2103- "@esbuild/linux-riscv64": "0.28.2",
2104- "@esbuild/linux-s390x": "0.28.2",
2105- "@esbuild/linux-x64": "0.28.2",
2106- "@esbuild/netbsd-arm64": "0.28.2",
2107- "@esbuild/netbsd-x64": "0.28.2",
2108- "@esbuild/openbsd-arm64": "0.28.2",
2109- "@esbuild/openbsd-x64": "0.28.2",
2110- "@esbuild/openharmony-arm64": "0.28.2",
2111- "@esbuild/sunos-x64": "0.28.2",
2112- "@esbuild/win32-arm64": "0.28.2",
2113- "@esbuild/win32-ia32": "0.28.2",
2114- "@esbuild/win32-x64": "0.28.2"
2115- }
2116- },
2117- "node_modules/escalade": {
2118- "version": "3.2.0",
2119- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
2120- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
2121- "dev": true,
2122- "license": "MIT",
2123- "engines": {
2124- "node": ">=6"
2125- }
2126- },
2127- "node_modules/escape-string-regexp": {
2128- "version": "5.0.0",
2129- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
2130- "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
2131- "license": "MIT",
2132- "engines": {
2133- "node": ">=12"
2134- },
2135- "funding": {
2136- "url": "https://github.com/sponsors/sindresorhus"
2137- }
2138- },
2139- "node_modules/estree-util-is-identifier-name": {
2140- "version": "3.0.0",
2141- "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
2142- "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
2143- "license": "MIT",
2144- "funding": {
2145- "type": "opencollective",
2146- "url": "https://opencollective.com/unified"
2147- }
2148- },
2149- "node_modules/estree-walker": {
2150- "version": "3.0.3",
2151- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
2152- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
2153- "dev": true,
2154- "license": "MIT",
2155- "dependencies": {
2156- "@types/estree": "^1.0.0"
2157- }
2158- },
2159- "node_modules/expect-type": {
2160- "version": "1.4.0",
2161- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
2162- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
2163- "dev": true,
2164- "license": "Apache-2.0",
2165- "engines": {
2166- "node": ">=12.0.0"
2167- }
2168- },
2169- "node_modules/extend": {
2170- "version": "3.0.2",
2171- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
2172- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
2173- "license": "MIT"
2174- },
2175- "node_modules/fdir": {
2176- "version": "6.5.0",
2177- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2178- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2179- "dev": true,
2180- "license": "MIT",
2181- "engines": {
2182- "node": ">=12.0.0"
2183- },
2184- "peerDependencies": {
2185- "picomatch": "^3 || ^4"
2186- },
2187- "peerDependenciesMeta": {
2188- "picomatch": {
2189- "optional": true
2190- }
2191- }
2192- },
2193- "node_modules/fsevents": {
2194- "version": "2.3.3",
2195- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
2196- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2197- "dev": true,
2198- "hasInstallScript": true,
2199- "license": "MIT",
2200- "optional": true,
2201- "os": [
2202- "darwin"
2203- ],
2204- "engines": {
2205- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2206- }
2207- },
2208- "node_modules/gensync": {
2209- "version": "1.0.0-beta.2",
2210- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
2211- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
2212- "dev": true,
2213- "license": "MIT",
2214- "engines": {
2215- "node": ">=6.9.0"
2216- }
2217- },
2218- "node_modules/hast-util-to-jsx-runtime": {
2219- "version": "2.3.6",
2220- "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
2221- "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
2222- "license": "MIT",
2223- "dependencies": {
2224- "@types/estree": "^1.0.0",
2225- "@types/hast": "^3.0.0",
2226- "@types/unist": "^3.0.0",
2227- "comma-separated-tokens": "^2.0.0",
2228- "devlop": "^1.0.0",
2229- "estree-util-is-identifier-name": "^3.0.0",
2230- "hast-util-whitespace": "^3.0.0",
2231- "mdast-util-mdx-expression": "^2.0.0",
2232- "mdast-util-mdx-jsx": "^3.0.0",
2233- "mdast-util-mdxjs-esm": "^2.0.0",
2234- "property-information": "^7.0.0",
2235- "space-separated-tokens": "^2.0.0",
2236- "style-to-js": "^1.0.0",
2237- "unist-util-position": "^5.0.0",
2238- "vfile-message": "^4.0.0"
2239- },
2240- "funding": {
2241- "type": "opencollective",
2242- "url": "https://opencollective.com/unified"
2243- }
2244- },
2245- "node_modules/hast-util-whitespace": {
2246- "version": "3.0.0",
2247- "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
2248- "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
2249- "license": "MIT",
2250- "dependencies": {
2251- "@types/hast": "^3.0.0"
2252- },
2253- "funding": {
2254- "type": "opencollective",
2255- "url": "https://opencollective.com/unified"
2256- }
2257- },
2258- "node_modules/html-encoding-sniffer": {
2259- "version": "4.0.0",
2260- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
2261- "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
2262- "dev": true,
2263- "license": "MIT",
2264- "dependencies": {
2265- "whatwg-encoding": "^3.1.1"
2266- },
2267- "engines": {
2268- "node": ">=18"
2269- }
2270- },
2271- "node_modules/html-url-attributes": {
2272- "version": "3.0.1",
2273- "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
2274- "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
2275- "license": "MIT",
2276- "funding": {
2277- "type": "opencollective",
2278- "url": "https://opencollective.com/unified"
2279- }
2280- },
2281- "node_modules/http-proxy-agent": {
2282- "version": "7.0.2",
2283- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
2284- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
2285- "dev": true,
2286- "license": "MIT",
2287- "dependencies": {
2288- "agent-base": "^7.1.0",
2289- "debug": "^4.3.4"
2290- },
2291- "engines": {
2292- "node": ">= 14"
2293- }
2294- },
2295- "node_modules/https-proxy-agent": {
2296- "version": "7.0.6",
2297- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
2298- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
2299- "dev": true,
2300- "license": "MIT",
2301- "dependencies": {
2302- "agent-base": "^7.1.2",
2303- "debug": "4"
2304- },
2305- "engines": {
2306- "node": ">= 14"
2307- }
2308- },
2309- "node_modules/iconv-lite": {
2310- "version": "0.6.3",
2311- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
2312- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
2313- "dev": true,
2314- "license": "MIT",
2315- "dependencies": {
2316- "safer-buffer": ">= 2.1.2 < 3.0.0"
2317- },
2318- "engines": {
2319- "node": ">=0.10.0"
2320- }
2321- },
2322- "node_modules/inline-style-parser": {
2323- "version": "0.2.7",
2324- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
2325- "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
2326- "license": "MIT"
2327- },
2328- "node_modules/is-alphabetical": {
2329- "version": "2.0.1",
2330- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
2331- "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
2332- "license": "MIT",
2333- "funding": {
2334- "type": "github",
2335- "url": "https://github.com/sponsors/wooorm"
2336- }
2337- },
2338- "node_modules/is-alphanumerical": {
2339- "version": "2.0.1",
2340- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
2341- "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
2342- "license": "MIT",
2343- "dependencies": {
2344- "is-alphabetical": "^2.0.0",
2345- "is-decimal": "^2.0.0"
2346- },
2347- "funding": {
2348- "type": "github",
2349- "url": "https://github.com/sponsors/wooorm"
2350- }
2351- },
2352- "node_modules/is-decimal": {
2353- "version": "2.0.1",
2354- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
2355- "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
2356- "license": "MIT",
2357- "funding": {
2358- "type": "github",
2359- "url": "https://github.com/sponsors/wooorm"
2360- }
2361- },
2362- "node_modules/is-hexadecimal": {
2363- "version": "2.0.1",
2364- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
2365- "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
2366- "license": "MIT",
2367- "funding": {
2368- "type": "github",
2369- "url": "https://github.com/sponsors/wooorm"
2370- }
2371- },
2372- "node_modules/is-plain-obj": {
2373- "version": "4.1.0",
2374- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
2375- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
2376- "license": "MIT",
2377- "engines": {
2378- "node": ">=12"
2379- },
2380- "funding": {
2381- "url": "https://github.com/sponsors/sindresorhus"
2382- }
2383- },
2384- "node_modules/is-potential-custom-element-name": {
2385- "version": "1.0.1",
2386- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2387- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
2388- "dev": true,
2389- "license": "MIT"
2390- },
2391- "node_modules/js-tokens": {
2392- "version": "4.0.0",
2393- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
2394- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
2395- "dev": true,
2396- "license": "MIT"
2397- },
2398- "node_modules/jsdom": {
2399- "version": "26.1.0",
2400- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
2401- "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
2402- "dev": true,
2403- "license": "MIT",
2404- "dependencies": {
2405- "cssstyle": "^4.2.1",
2406- "data-urls": "^5.0.0",
2407- "decimal.js": "^10.5.0",
2408- "html-encoding-sniffer": "^4.0.0",
2409- "http-proxy-agent": "^7.0.2",
2410- "https-proxy-agent": "^7.0.6",
2411- "is-potential-custom-element-name": "^1.0.1",
2412- "nwsapi": "^2.2.16",
2413- "parse5": "^7.2.1",
2414- "rrweb-cssom": "^0.8.0",
2415- "saxes": "^6.0.0",
2416- "symbol-tree": "^3.2.4",
2417- "tough-cookie": "^5.1.1",
2418- "w3c-xmlserializer": "^5.0.0",
2419- "webidl-conversions": "^7.0.0",
2420- "whatwg-encoding": "^3.1.1",
2421- "whatwg-mimetype": "^4.0.0",
2422- "whatwg-url": "^14.1.1",
2423- "ws": "^8.18.0",
2424- "xml-name-validator": "^5.0.0"
2425- },
2426- "engines": {
2427- "node": ">=18"
2428- },
2429- "peerDependencies": {
2430- "canvas": "^3.0.0"
2431- },
2432- "peerDependenciesMeta": {
2433- "canvas": {
2434- "optional": true
2435- }
2436- }
2437- },
2438- "node_modules/jsesc": {
2439- "version": "3.1.0",
2440- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
2441- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
2442- "dev": true,
2443- "license": "MIT",
2444- "bin": {
2445- "jsesc": "bin/jsesc"
2446- },
2447- "engines": {
2448- "node": ">=6"
2449- }
2450- },
2451- "node_modules/json5": {
2452- "version": "2.2.3",
2453- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
2454- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
2455- "dev": true,
2456- "license": "MIT",
2457- "bin": {
2458- "json5": "lib/cli.js"
2459- },
2460- "engines": {
2461- "node": ">=6"
2462- }
2463- },
2464- "node_modules/longest-streak": {
2465- "version": "3.1.0",
2466- "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
2467- "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
2468- "license": "MIT",
2469- "funding": {
2470- "type": "github",
2471- "url": "https://github.com/sponsors/wooorm"
2472- }
2473- },
2474- "node_modules/loupe": {
2475- "version": "3.2.1",
2476- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
2477- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
2478- "dev": true,
2479- "license": "MIT"
2480- },
2481- "node_modules/lru-cache": {
2482- "version": "5.1.1",
2483- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2484- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2485- "dev": true,
2486- "license": "ISC",
2487- "dependencies": {
2488- "yallist": "^3.0.2"
2489- }
2490- },
2491- "node_modules/lz-string": {
2492- "version": "1.5.0",
2493- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
2494- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
2495- "dev": true,
2496- "license": "MIT",
2497- "peer": true,
2498- "bin": {
2499- "lz-string": "bin/bin.js"
2500- }
2501- },
2502- "node_modules/magic-string": {
2503- "version": "0.30.21",
2504- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2505- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2506- "dev": true,
2507- "license": "MIT",
2508- "dependencies": {
2509- "@jridgewell/sourcemap-codec": "^1.5.5"
2510- }
2511- },
2512- "node_modules/markdown-table": {
2513- "version": "3.0.4",
2514- "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
2515- "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
2516- "license": "MIT",
2517- "funding": {
2518- "type": "github",
2519- "url": "https://github.com/sponsors/wooorm"
2520- }
2521- },
2522- "node_modules/mdast-util-find-and-replace": {
2523- "version": "3.0.2",
2524- "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
2525- "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
2526- "license": "MIT",
2527- "dependencies": {
2528- "@types/mdast": "^4.0.0",
2529- "escape-string-regexp": "^5.0.0",
2530- "unist-util-is": "^6.0.0",
2531- "unist-util-visit-parents": "^6.0.0"
2532- },
2533- "funding": {
2534- "type": "opencollective",
2535- "url": "https://opencollective.com/unified"
2536- }
2537- },
2538- "node_modules/mdast-util-from-markdown": {
2539- "version": "2.0.3",
2540- "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
2541- "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
2542- "license": "MIT",
2543- "dependencies": {
2544- "@types/mdast": "^4.0.0",
2545- "@types/unist": "^3.0.0",
2546- "decode-named-character-reference": "^1.0.0",
2547- "devlop": "^1.0.0",
2548- "mdast-util-to-string": "^4.0.0",
2549- "micromark": "^4.0.0",
2550- "micromark-util-decode-numeric-character-reference": "^2.0.0",
2551- "micromark-util-decode-string": "^2.0.0",
2552- "micromark-util-normalize-identifier": "^2.0.0",
2553- "micromark-util-symbol": "^2.0.0",
2554- "micromark-util-types": "^2.0.0",
2555- "unist-util-stringify-position": "^4.0.0"
2556- },
2557- "funding": {
2558- "type": "opencollective",
2559- "url": "https://opencollective.com/unified"
2560- }
2561- },
2562- "node_modules/mdast-util-gfm": {
2563- "version": "3.1.0",
2564- "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
2565- "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
2566- "license": "MIT",
2567- "dependencies": {
2568- "mdast-util-from-markdown": "^2.0.0",
2569- "mdast-util-gfm-autolink-literal": "^2.0.0",
2570- "mdast-util-gfm-footnote": "^2.0.0",
2571- "mdast-util-gfm-strikethrough": "^2.0.0",
2572- "mdast-util-gfm-table": "^2.0.0",
2573- "mdast-util-gfm-task-list-item": "^2.0.0",
2574- "mdast-util-to-markdown": "^2.0.0"
2575- },
2576- "funding": {
2577- "type": "opencollective",
2578- "url": "https://opencollective.com/unified"
2579- }
2580- },
2581- "node_modules/mdast-util-gfm-autolink-literal": {
2582- "version": "2.0.1",
2583- "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
2584- "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
2585- "license": "MIT",
2586- "dependencies": {
2587- "@types/mdast": "^4.0.0",
2588- "ccount": "^2.0.0",
2589- "devlop": "^1.0.0",
2590- "mdast-util-find-and-replace": "^3.0.0",
2591- "micromark-util-character": "^2.0.0"
2592- },
2593- "funding": {
2594- "type": "opencollective",
2595- "url": "https://opencollective.com/unified"
2596- }
2597- },
2598- "node_modules/mdast-util-gfm-footnote": {
2599- "version": "2.1.0",
2600- "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
2601- "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
2602- "license": "MIT",
2603- "dependencies": {
2604- "@types/mdast": "^4.0.0",
2605- "devlop": "^1.1.0",
2606- "mdast-util-from-markdown": "^2.0.0",
2607- "mdast-util-to-markdown": "^2.0.0",
2608- "micromark-util-normalize-identifier": "^2.0.0"
2609- },
2610- "funding": {
2611- "type": "opencollective",
2612- "url": "https://opencollective.com/unified"
2613- }
2614- },
2615- "node_modules/mdast-util-gfm-strikethrough": {
2616- "version": "2.0.0",
2617- "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
2618- "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
2619- "license": "MIT",
2620- "dependencies": {
2621- "@types/mdast": "^4.0.0",
2622- "mdast-util-from-markdown": "^2.0.0",
2623- "mdast-util-to-markdown": "^2.0.0"
2624- },
2625- "funding": {
2626- "type": "opencollective",
2627- "url": "https://opencollective.com/unified"
2628- }
2629- },
2630- "node_modules/mdast-util-gfm-table": {
2631- "version": "2.0.0",
2632- "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
2633- "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
2634- "license": "MIT",
2635- "dependencies": {
2636- "@types/mdast": "^4.0.0",
2637- "devlop": "^1.0.0",
2638- "markdown-table": "^3.0.0",
2639- "mdast-util-from-markdown": "^2.0.0",
2640- "mdast-util-to-markdown": "^2.0.0"
2641- },
2642- "funding": {
2643- "type": "opencollective",
2644- "url": "https://opencollective.com/unified"
2645- }
2646- },
2647- "node_modules/mdast-util-gfm-task-list-item": {
2648- "version": "2.0.0",
2649- "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
2650- "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
2651- "license": "MIT",
2652- "dependencies": {
2653- "@types/mdast": "^4.0.0",
2654- "devlop": "^1.0.0",
2655- "mdast-util-from-markdown": "^2.0.0",
2656- "mdast-util-to-markdown": "^2.0.0"
2657- },
2658- "funding": {
2659- "type": "opencollective",
2660- "url": "https://opencollective.com/unified"
2661- }
2662- },
2663- "node_modules/mdast-util-mdx-expression": {
2664- "version": "2.0.1",
2665- "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
2666- "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
2667- "license": "MIT",
2668- "dependencies": {
2669- "@types/estree-jsx": "^1.0.0",
2670- "@types/hast": "^3.0.0",
2671- "@types/mdast": "^4.0.0",
2672- "devlop": "^1.0.0",
2673- "mdast-util-from-markdown": "^2.0.0",
2674- "mdast-util-to-markdown": "^2.0.0"
2675- },
2676- "funding": {
2677- "type": "opencollective",
2678- "url": "https://opencollective.com/unified"
2679- }
2680- },
2681- "node_modules/mdast-util-mdx-jsx": {
2682- "version": "3.2.0",
2683- "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
2684- "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
2685- "license": "MIT",
2686- "dependencies": {
2687- "@types/estree-jsx": "^1.0.0",
2688- "@types/hast": "^3.0.0",
2689- "@types/mdast": "^4.0.0",
2690- "@types/unist": "^3.0.0",
2691- "ccount": "^2.0.0",
2692- "devlop": "^1.1.0",
2693- "mdast-util-from-markdown": "^2.0.0",
2694- "mdast-util-to-markdown": "^2.0.0",
2695- "parse-entities": "^4.0.0",
2696- "stringify-entities": "^4.0.0",
2697- "unist-util-stringify-position": "^4.0.0",
2698- "vfile-message": "^4.0.0"
2699- },
2700- "funding": {
2701- "type": "opencollective",
2702- "url": "https://opencollective.com/unified"
2703- }
2704- },
2705- "node_modules/mdast-util-mdxjs-esm": {
2706- "version": "2.0.1",
2707- "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
2708- "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
2709- "license": "MIT",
2710- "dependencies": {
2711- "@types/estree-jsx": "^1.0.0",
2712- "@types/hast": "^3.0.0",
2713- "@types/mdast": "^4.0.0",
2714- "devlop": "^1.0.0",
2715- "mdast-util-from-markdown": "^2.0.0",
2716- "mdast-util-to-markdown": "^2.0.0"
2717- },
2718- "funding": {
2719- "type": "opencollective",
2720- "url": "https://opencollective.com/unified"
2721- }
2722- },
2723- "node_modules/mdast-util-phrasing": {
2724- "version": "4.1.0",
2725- "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
2726- "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
2727- "license": "MIT",
2728- "dependencies": {
2729- "@types/mdast": "^4.0.0",
2730- "unist-util-is": "^6.0.0"
2731- },
2732- "funding": {
2733- "type": "opencollective",
2734- "url": "https://opencollective.com/unified"
2735- }
2736- },
2737- "node_modules/mdast-util-to-hast": {
2738- "version": "13.2.1",
2739- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
2740- "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
2741- "license": "MIT",
2742- "dependencies": {
2743- "@types/hast": "^3.0.0",
2744- "@types/mdast": "^4.0.0",
2745- "@ungap/structured-clone": "^1.0.0",
2746- "devlop": "^1.0.0",
2747- "micromark-util-sanitize-uri": "^2.0.0",
2748- "trim-lines": "^3.0.0",
2749- "unist-util-position": "^5.0.0",
2750- "unist-util-visit": "^5.0.0",
2751- "vfile": "^6.0.0"
2752- },
2753- "funding": {
2754- "type": "opencollective",
2755- "url": "https://opencollective.com/unified"
2756- }
2757- },
2758- "node_modules/mdast-util-to-markdown": {
2759- "version": "2.1.2",
2760- "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
2761- "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
2762- "license": "MIT",
2763- "dependencies": {
2764- "@types/mdast": "^4.0.0",
2765- "@types/unist": "^3.0.0",
2766- "longest-streak": "^3.0.0",
2767- "mdast-util-phrasing": "^4.0.0",
2768- "mdast-util-to-string": "^4.0.0",
2769- "micromark-util-classify-character": "^2.0.0",
2770- "micromark-util-decode-string": "^2.0.0",
2771- "unist-util-visit": "^5.0.0",
2772- "zwitch": "^2.0.0"
2773- },
2774- "funding": {
2775- "type": "opencollective",
2776- "url": "https://opencollective.com/unified"
2777- }
2778- },
2779- "node_modules/mdast-util-to-string": {
2780- "version": "4.0.0",
2781- "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
2782- "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
2783- "license": "MIT",
2784- "dependencies": {
2785- "@types/mdast": "^4.0.0"
2786- },
2787- "funding": {
2788- "type": "opencollective",
2789- "url": "https://opencollective.com/unified"
2790- }
2791- },
2792- "node_modules/micromark": {
2793- "version": "4.0.2",
2794- "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
2795- "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
2796- "funding": [
2797- {
2798- "type": "GitHub Sponsors",
2799- "url": "https://github.com/sponsors/unifiedjs"
2800- },
2801- {
2802- "type": "OpenCollective",
2803- "url": "https://opencollective.com/unified"
2804- }
2805- ],
2806- "license": "MIT",
2807- "dependencies": {
2808- "@types/debug": "^4.0.0",
2809- "debug": "^4.0.0",
2810- "decode-named-character-reference": "^1.0.0",
2811- "devlop": "^1.0.0",
2812- "micromark-core-commonmark": "^2.0.0",
2813- "micromark-factory-space": "^2.0.0",
2814- "micromark-util-character": "^2.0.0",
2815- "micromark-util-chunked": "^2.0.0",
2816- "micromark-util-combine-extensions": "^2.0.0",
2817- "micromark-util-decode-numeric-character-reference": "^2.0.0",
2818- "micromark-util-encode": "^2.0.0",
2819- "micromark-util-normalize-identifier": "^2.0.0",
2820- "micromark-util-resolve-all": "^2.0.0",
2821- "micromark-util-sanitize-uri": "^2.0.0",
2822- "micromark-util-subtokenize": "^2.0.0",
2823- "micromark-util-symbol": "^2.0.0",
2824- "micromark-util-types": "^2.0.0"
2825- }
2826- },
2827- "node_modules/micromark-core-commonmark": {
2828- "version": "2.0.3",
2829- "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
2830- "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
2831- "funding": [
2832- {
2833- "type": "GitHub Sponsors",
2834- "url": "https://github.com/sponsors/unifiedjs"
2835- },
2836- {
2837- "type": "OpenCollective",
2838- "url": "https://opencollective.com/unified"
2839- }
2840- ],
2841- "license": "MIT",
2842- "dependencies": {
2843- "decode-named-character-reference": "^1.0.0",
2844- "devlop": "^1.0.0",
2845- "micromark-factory-destination": "^2.0.0",
2846- "micromark-factory-label": "^2.0.0",
2847- "micromark-factory-space": "^2.0.0",
2848- "micromark-factory-title": "^2.0.0",
2849- "micromark-factory-whitespace": "^2.0.0",
2850- "micromark-util-character": "^2.0.0",
2851- "micromark-util-chunked": "^2.0.0",
2852- "micromark-util-classify-character": "^2.0.0",
2853- "micromark-util-html-tag-name": "^2.0.0",
2854- "micromark-util-normalize-identifier": "^2.0.0",
2855- "micromark-util-resolve-all": "^2.0.0",
2856- "micromark-util-subtokenize": "^2.0.0",
2857- "micromark-util-symbol": "^2.0.0",
2858- "micromark-util-types": "^2.0.0"
2859- }
2860- },
2861- "node_modules/micromark-extension-gfm": {
2862- "version": "3.0.0",
2863- "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
2864- "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
2865- "license": "MIT",
2866- "dependencies": {
2867- "micromark-extension-gfm-autolink-literal": "^2.0.0",
2868- "micromark-extension-gfm-footnote": "^2.0.0",
2869- "micromark-extension-gfm-strikethrough": "^2.0.0",
2870- "micromark-extension-gfm-table": "^2.0.0",
2871- "micromark-extension-gfm-tagfilter": "^2.0.0",
2872- "micromark-extension-gfm-task-list-item": "^2.0.0",
2873- "micromark-util-combine-extensions": "^2.0.0",
2874- "micromark-util-types": "^2.0.0"
2875- },
2876- "funding": {
2877- "type": "opencollective",
2878- "url": "https://opencollective.com/unified"
2879- }
2880- },
2881- "node_modules/micromark-extension-gfm-autolink-literal": {
2882- "version": "2.1.0",
2883- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
2884- "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
2885- "license": "MIT",
2886- "dependencies": {
2887- "micromark-util-character": "^2.0.0",
2888- "micromark-util-sanitize-uri": "^2.0.0",
2889- "micromark-util-symbol": "^2.0.0",
2890- "micromark-util-types": "^2.0.0"
2891- },
2892- "funding": {
2893- "type": "opencollective",
2894- "url": "https://opencollective.com/unified"
2895- }
2896- },
2897- "node_modules/micromark-extension-gfm-footnote": {
2898- "version": "2.1.0",
2899- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
2900- "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
2901- "license": "MIT",
2902- "dependencies": {
2903- "devlop": "^1.0.0",
2904- "micromark-core-commonmark": "^2.0.0",
2905- "micromark-factory-space": "^2.0.0",
2906- "micromark-util-character": "^2.0.0",
2907- "micromark-util-normalize-identifier": "^2.0.0",
2908- "micromark-util-sanitize-uri": "^2.0.0",
2909- "micromark-util-symbol": "^2.0.0",
2910- "micromark-util-types": "^2.0.0"
2911- },
2912- "funding": {
2913- "type": "opencollective",
2914- "url": "https://opencollective.com/unified"
2915- }
2916- },
2917- "node_modules/micromark-extension-gfm-strikethrough": {
2918- "version": "2.1.0",
2919- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
2920- "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
2921- "license": "MIT",
2922- "dependencies": {
2923- "devlop": "^1.0.0",
2924- "micromark-util-chunked": "^2.0.0",
2925- "micromark-util-classify-character": "^2.0.0",
2926- "micromark-util-resolve-all": "^2.0.0",
2927- "micromark-util-symbol": "^2.0.0",
2928- "micromark-util-types": "^2.0.0"
2929- },
2930- "funding": {
2931- "type": "opencollective",
2932- "url": "https://opencollective.com/unified"
2933- }
2934- },
2935- "node_modules/micromark-extension-gfm-table": {
2936- "version": "2.1.2",
2937- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz",
2938- "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",
2939- "license": "MIT",
2940- "dependencies": {
2941- "devlop": "^1.0.0",
2942- "micromark-factory-space": "^2.0.0",
2943- "micromark-util-character": "^2.0.0",
2944- "micromark-util-symbol": "^2.0.0",
2945- "micromark-util-types": "^2.0.0"
2946- },
2947- "funding": {
2948- "type": "opencollective",
2949- "url": "https://opencollective.com/unified"
2950- }
2951- },
2952- "node_modules/micromark-extension-gfm-tagfilter": {
2953- "version": "2.0.0",
2954- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
2955- "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
2956- "license": "MIT",
2957- "dependencies": {
2958- "micromark-util-types": "^2.0.0"
2959- },
2960- "funding": {
2961- "type": "opencollective",
2962- "url": "https://opencollective.com/unified"
2963- }
2964- },
2965- "node_modules/micromark-extension-gfm-task-list-item": {
2966- "version": "2.1.0",
2967- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
2968- "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
2969- "license": "MIT",
2970- "dependencies": {
2971- "devlop": "^1.0.0",
2972- "micromark-factory-space": "^2.0.0",
2973- "micromark-util-character": "^2.0.0",
2974- "micromark-util-symbol": "^2.0.0",
2975- "micromark-util-types": "^2.0.0"
2976- },
2977- "funding": {
2978- "type": "opencollective",
2979- "url": "https://opencollective.com/unified"
2980- }
2981- },
2982- "node_modules/micromark-factory-destination": {
2983- "version": "2.0.1",
2984- "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
2985- "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
2986- "funding": [
2987- {
2988- "type": "GitHub Sponsors",
2989- "url": "https://github.com/sponsors/unifiedjs"
2990- },
2991- {
2992- "type": "OpenCollective",
2993- "url": "https://opencollective.com/unified"
2994- }
2995- ],
2996- "license": "MIT",
2997- "dependencies": {
2998- "micromark-util-character": "^2.0.0",
2999- "micromark-util-symbol": "^2.0.0",
3000- "micromark-util-types": "^2.0.0"
3001- }
3002- },
3003- "node_modules/micromark-factory-label": {
3004- "version": "2.0.1",
3005- "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
3006- "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
3007- "funding": [
3008- {
3009- "type": "GitHub Sponsors",
3010- "url": "https://github.com/sponsors/unifiedjs"
3011- },
3012- {
3013- "type": "OpenCollective",
3014- "url": "https://opencollective.com/unified"
3015- }
3016- ],
3017- "license": "MIT",
3018- "dependencies": {
3019- "devlop": "^1.0.0",
3020- "micromark-util-character": "^2.0.0",
3021- "micromark-util-symbol": "^2.0.0",
3022- "micromark-util-types": "^2.0.0"
3023- }
3024- },
3025- "node_modules/micromark-factory-space": {
3026- "version": "2.0.1",
3027- "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
3028- "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
3029- "funding": [
3030- {
3031- "type": "GitHub Sponsors",
3032- "url": "https://github.com/sponsors/unifiedjs"
3033- },
3034- {
3035- "type": "OpenCollective",
3036- "url": "https://opencollective.com/unified"
3037- }
3038- ],
3039- "license": "MIT",
3040- "dependencies": {
3041- "micromark-util-character": "^2.0.0",
3042- "micromark-util-types": "^2.0.0"
3043- }
3044- },
3045- "node_modules/micromark-factory-title": {
3046- "version": "2.0.1",
3047- "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
3048- "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
3049- "funding": [
3050- {
3051- "type": "GitHub Sponsors",
3052- "url": "https://github.com/sponsors/unifiedjs"
3053- },
3054- {
3055- "type": "OpenCollective",
3056- "url": "https://opencollective.com/unified"
3057- }
3058- ],
3059- "license": "MIT",
3060- "dependencies": {
3061- "micromark-factory-space": "^2.0.0",
3062- "micromark-util-character": "^2.0.0",
3063- "micromark-util-symbol": "^2.0.0",
3064- "micromark-util-types": "^2.0.0"
3065- }
3066- },
3067- "node_modules/micromark-factory-whitespace": {
3068- "version": "2.0.1",
3069- "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
3070- "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
3071- "funding": [
3072- {
3073- "type": "GitHub Sponsors",
3074- "url": "https://github.com/sponsors/unifiedjs"
3075- },
3076- {
3077- "type": "OpenCollective",
3078- "url": "https://opencollective.com/unified"
3079- }
3080- ],
3081- "license": "MIT",
3082- "dependencies": {
3083- "micromark-factory-space": "^2.0.0",
3084- "micromark-util-character": "^2.0.0",
3085- "micromark-util-symbol": "^2.0.0",
3086- "micromark-util-types": "^2.0.0"
3087- }
3088- },
3089- "node_modules/micromark-util-character": {
3090- "version": "2.1.1",
3091- "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
3092- "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
3093- "funding": [
3094- {
3095- "type": "GitHub Sponsors",
3096- "url": "https://github.com/sponsors/unifiedjs"
3097- },
3098- {
3099- "type": "OpenCollective",
3100- "url": "https://opencollective.com/unified"
3101- }
3102- ],
3103- "license": "MIT",
3104- "dependencies": {
3105- "micromark-util-symbol": "^2.0.0",
3106- "micromark-util-types": "^2.0.0"
3107- }
3108- },
3109- "node_modules/micromark-util-chunked": {
3110- "version": "2.0.1",
3111- "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
3112- "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
3113- "funding": [
3114- {
3115- "type": "GitHub Sponsors",
3116- "url": "https://github.com/sponsors/unifiedjs"
3117- },
3118- {
3119- "type": "OpenCollective",
3120- "url": "https://opencollective.com/unified"
3121- }
3122- ],
3123- "license": "MIT",
3124- "dependencies": {
3125- "micromark-util-symbol": "^2.0.0"
3126- }
3127- },
3128- "node_modules/micromark-util-classify-character": {
3129- "version": "2.0.1",
3130- "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
3131- "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
3132- "funding": [
3133- {
3134- "type": "GitHub Sponsors",
3135- "url": "https://github.com/sponsors/unifiedjs"
3136- },
3137- {
3138- "type": "OpenCollective",
3139- "url": "https://opencollective.com/unified"
3140- }
3141- ],
3142- "license": "MIT",
3143- "dependencies": {
3144- "micromark-util-character": "^2.0.0",
3145- "micromark-util-symbol": "^2.0.0",
3146- "micromark-util-types": "^2.0.0"
3147- }
3148- },
3149- "node_modules/micromark-util-combine-extensions": {
3150- "version": "2.0.1",
3151- "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
3152- "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
3153- "funding": [
3154- {
3155- "type": "GitHub Sponsors",
3156- "url": "https://github.com/sponsors/unifiedjs"
3157- },
3158- {
3159- "type": "OpenCollective",
3160- "url": "https://opencollective.com/unified"
3161- }
3162- ],
3163- "license": "MIT",
3164- "dependencies": {
3165- "micromark-util-chunked": "^2.0.0",
3166- "micromark-util-types": "^2.0.0"
3167- }
3168- },
3169- "node_modules/micromark-util-decode-numeric-character-reference": {
3170- "version": "2.0.2",
3171- "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
3172- "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
3173- "funding": [
3174- {
3175- "type": "GitHub Sponsors",
3176- "url": "https://github.com/sponsors/unifiedjs"
3177- },
3178- {
3179- "type": "OpenCollective",
3180- "url": "https://opencollective.com/unified"
3181- }
3182- ],
3183- "license": "MIT",
3184- "dependencies": {
3185- "micromark-util-symbol": "^2.0.0"
3186- }
3187- },
3188- "node_modules/micromark-util-decode-string": {
3189- "version": "2.0.1",
3190- "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
3191- "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
3192- "funding": [
3193- {
3194- "type": "GitHub Sponsors",
3195- "url": "https://github.com/sponsors/unifiedjs"
3196- },
3197- {
3198- "type": "OpenCollective",
3199- "url": "https://opencollective.com/unified"
3200- }
3201- ],
3202- "license": "MIT",
3203- "dependencies": {
3204- "decode-named-character-reference": "^1.0.0",
3205- "micromark-util-character": "^2.0.0",
3206- "micromark-util-decode-numeric-character-reference": "^2.0.0",
3207- "micromark-util-symbol": "^2.0.0"
3208- }
3209- },
3210- "node_modules/micromark-util-encode": {
3211- "version": "2.0.1",
3212- "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
3213- "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
3214- "funding": [
3215- {
3216- "type": "GitHub Sponsors",
3217- "url": "https://github.com/sponsors/unifiedjs"
3218- },
3219- {
3220- "type": "OpenCollective",
3221- "url": "https://opencollective.com/unified"
3222- }
3223- ],
3224- "license": "MIT"
3225- },
3226- "node_modules/micromark-util-html-tag-name": {
3227- "version": "2.0.1",
3228- "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
3229- "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
3230- "funding": [
3231- {
3232- "type": "GitHub Sponsors",
3233- "url": "https://github.com/sponsors/unifiedjs"
3234- },
3235- {
3236- "type": "OpenCollective",
3237- "url": "https://opencollective.com/unified"
3238- }
3239- ],
3240- "license": "MIT"
3241- },
3242- "node_modules/micromark-util-normalize-identifier": {
3243- "version": "2.0.1",
3244- "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
3245- "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
3246- "funding": [
3247- {
3248- "type": "GitHub Sponsors",
3249- "url": "https://github.com/sponsors/unifiedjs"
3250- },
3251- {
3252- "type": "OpenCollective",
3253- "url": "https://opencollective.com/unified"
3254- }
3255- ],
3256- "license": "MIT",
3257- "dependencies": {
3258- "micromark-util-symbol": "^2.0.0"
3259- }
3260- },
3261- "node_modules/micromark-util-resolve-all": {
3262- "version": "2.0.1",
3263- "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
3264- "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
3265- "funding": [
3266- {
3267- "type": "GitHub Sponsors",
3268- "url": "https://github.com/sponsors/unifiedjs"
3269- },
3270- {
3271- "type": "OpenCollective",
3272- "url": "https://opencollective.com/unified"
3273- }
3274- ],
3275- "license": "MIT",
3276- "dependencies": {
3277- "micromark-util-types": "^2.0.0"
3278- }
3279- },
3280- "node_modules/micromark-util-sanitize-uri": {
3281- "version": "2.0.1",
3282- "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
3283- "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
3284- "funding": [
3285- {
3286- "type": "GitHub Sponsors",
3287- "url": "https://github.com/sponsors/unifiedjs"
3288- },
3289- {
3290- "type": "OpenCollective",
3291- "url": "https://opencollective.com/unified"
3292- }
3293- ],
3294- "license": "MIT",
3295- "dependencies": {
3296- "micromark-util-character": "^2.0.0",
3297- "micromark-util-encode": "^2.0.0",
3298- "micromark-util-symbol": "^2.0.0"
3299- }
3300- },
3301- "node_modules/micromark-util-subtokenize": {
3302- "version": "2.1.0",
3303- "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
3304- "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
3305- "funding": [
3306- {
3307- "type": "GitHub Sponsors",
3308- "url": "https://github.com/sponsors/unifiedjs"
3309- },
3310- {
3311- "type": "OpenCollective",
3312- "url": "https://opencollective.com/unified"
3313- }
3314- ],
3315- "license": "MIT",
3316- "dependencies": {
3317- "devlop": "^1.0.0",
3318- "micromark-util-chunked": "^2.0.0",
3319- "micromark-util-symbol": "^2.0.0",
3320- "micromark-util-types": "^2.0.0"
3321- }
3322- },
3323- "node_modules/micromark-util-symbol": {
3324- "version": "2.0.1",
3325- "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
3326- "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
3327- "funding": [
3328- {
3329- "type": "GitHub Sponsors",
3330- "url": "https://github.com/sponsors/unifiedjs"
3331- },
3332- {
3333- "type": "OpenCollective",
3334- "url": "https://opencollective.com/unified"
3335- }
3336- ],
3337- "license": "MIT"
3338- },
3339- "node_modules/micromark-util-types": {
3340- "version": "2.0.2",
3341- "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
3342- "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
3343- "funding": [
3344- {
3345- "type": "GitHub Sponsors",
3346- "url": "https://github.com/sponsors/unifiedjs"
3347- },
3348- {
3349- "type": "OpenCollective",
3350- "url": "https://opencollective.com/unified"
3351- }
3352- ],
3353- "license": "MIT"
3354- },
3355- "node_modules/ms": {
3356- "version": "2.1.3",
3357- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
3358- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
3359- "license": "MIT"
3360- },
3361- "node_modules/nanoid": {
3362- "version": "3.3.19",
3363- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
3364- "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
3365- "dev": true,
3366- "funding": [
3367- {
3368- "type": "github",
3369- "url": "https://github.com/sponsors/ai"
3370- }
3371- ],
3372- "license": "MIT",
3373- "bin": {
3374- "nanoid": "bin/nanoid.cjs"
3375- },
3376- "engines": {
3377- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
3378- }
3379- },
3380- "node_modules/node-releases": {
3381- "version": "2.0.56",
3382- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz",
3383- "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",
3384- "dev": true,
3385- "license": "MIT",
3386- "engines": {
3387- "node": ">=18"
3388- }
3389- },
3390- "node_modules/nwsapi": {
3391- "version": "2.2.27",
3392- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
3393- "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
3394- "dev": true,
3395- "license": "MIT"
3396- },
3397- "node_modules/parse-entities": {
3398- "version": "4.0.2",
3399- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
3400- "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
3401- "license": "MIT",
3402- "dependencies": {
3403- "@types/unist": "^2.0.0",
3404- "character-entities-legacy": "^3.0.0",
3405- "character-reference-invalid": "^2.0.0",
3406- "decode-named-character-reference": "^1.0.0",
3407- "is-alphanumerical": "^2.0.0",
3408- "is-decimal": "^2.0.0",
3409- "is-hexadecimal": "^2.0.0"
3410- },
3411- "funding": {
3412- "type": "github",
3413- "url": "https://github.com/sponsors/wooorm"
3414- }
3415- },
3416- "node_modules/parse-entities/node_modules/@types/unist": {
3417- "version": "2.0.11",
3418- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
3419- "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
3420- "license": "MIT"
3421- },
3422- "node_modules/parse5": {
3423- "version": "7.3.0",
3424- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
3425- "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
3426- "dev": true,
3427- "license": "MIT",
3428- "dependencies": {
3429- "entities": "^6.0.0"
3430- },
3431- "funding": {
3432- "url": "https://github.com/inikulin/parse5?sponsor=1"
3433- }
3434- },
3435- "node_modules/pathe": {
3436- "version": "2.0.3",
3437- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
3438- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
3439- "dev": true,
3440- "license": "MIT"
3441- },
3442- "node_modules/pathval": {
3443- "version": "2.0.1",
3444- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
3445- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
3446- "dev": true,
3447- "license": "MIT",
3448- "engines": {
3449- "node": ">= 14.16"
3450- }
3451- },
3452- "node_modules/picocolors": {
3453- "version": "1.1.1",
3454- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
3455- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
3456- "dev": true,
3457- "license": "ISC"
3458- },
3459- "node_modules/picomatch": {
3460- "version": "4.0.7",
3461- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
3462- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
3463- "dev": true,
3464- "license": "MIT",
3465- "engines": {
3466- "node": ">=12"
3467- },
3468- "funding": {
3469- "url": "https://github.com/sponsors/jonschlinkert"
3470- }
3471- },
3472- "node_modules/postcss": {
3473- "version": "8.5.28",
3474- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
3475- "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
3476- "dev": true,
3477- "funding": [
3478- {
3479- "type": "opencollective",
3480- "url": "https://opencollective.com/postcss/"
3481- },
3482- {
3483- "type": "tidelift",
3484- "url": "https://tidelift.com/funding/github/npm/postcss"
3485- },
3486- {
3487- "type": "github",
3488- "url": "https://github.com/sponsors/ai"
3489- }
3490- ],
3491- "license": "MIT",
3492- "dependencies": {
3493- "nanoid": "^3.3.18",
3494- "picocolors": "^1.1.1",
3495- "source-map-js": "^1.2.1"
3496- },
3497- "engines": {
3498- "node": "^10 || ^12 || >=14"
3499- }
3500- },
3501- "node_modules/pretty-format": {
3502- "version": "27.5.1",
3503- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
3504- "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
3505- "dev": true,
3506- "license": "MIT",
3507- "peer": true,
3508- "dependencies": {
3509- "ansi-regex": "^5.0.1",
3510- "ansi-styles": "^5.0.0",
3511- "react-is": "^17.0.1"
3512- },
3513- "engines": {
3514- "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
3515- }
3516- },
3517- "node_modules/property-information": {
3518- "version": "7.2.0",
3519- "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
3520- "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
3521- "license": "MIT",
3522- "funding": {
3523- "type": "github",
3524- "url": "https://github.com/sponsors/wooorm"
3525- }
3526- },
3527- "node_modules/punycode": {
3528- "version": "2.3.1",
3529- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
3530- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
3531- "dev": true,
3532- "license": "MIT",
3533- "engines": {
3534- "node": ">=6"
3535- }
3536- },
3537- "node_modules/react": {
3538- "version": "19.3.0",
3539- "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
3540- "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
3541- "license": "MIT",
3542- "engines": {
3543- "node": ">=0.10.0"
3544- }
3545- },
3546- "node_modules/react-dom": {
3547- "version": "19.3.0",
3548- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
3549- "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
3550- "license": "MIT",
3551- "dependencies": {
3552- "scheduler": "^0.28.0"
3553- },
3554- "peerDependencies": {
3555- "react": "^19.3.0"
3556- }
3557- },
3558- "node_modules/react-is": {
3559- "version": "17.0.2",
3560- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
3561- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
3562- "dev": true,
3563- "license": "MIT",
3564- "peer": true
3565- },
3566- "node_modules/react-markdown": {
3567- "version": "10.1.0",
3568- "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
3569- "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
3570- "license": "MIT",
3571- "dependencies": {
3572- "@types/hast": "^3.0.0",
3573- "@types/mdast": "^4.0.0",
3574- "devlop": "^1.0.0",
3575- "hast-util-to-jsx-runtime": "^2.0.0",
3576- "html-url-attributes": "^3.0.0",
3577- "mdast-util-to-hast": "^13.0.0",
3578- "remark-parse": "^11.0.0",
3579- "remark-rehype": "^11.0.0",
3580- "unified": "^11.0.0",
3581- "unist-util-visit": "^5.0.0",
3582- "vfile": "^6.0.0"
3583- },
3584- "funding": {
3585- "type": "opencollective",
3586- "url": "https://opencollective.com/unified"
3587- },
3588- "peerDependencies": {
3589- "@types/react": ">=18",
3590- "react": ">=18"
3591- }
3592- },
3593- "node_modules/react-refresh": {
3594- "version": "0.18.0",
3595- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
3596- "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
3597- "dev": true,
3598- "license": "MIT",
3599- "engines": {
3600- "node": ">=0.10.0"
3601- }
3602- },
3603- "node_modules/remark-gfm": {
3604- "version": "4.0.1",
3605- "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
3606- "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
3607- "license": "MIT",
3608- "dependencies": {
3609- "@types/mdast": "^4.0.0",
3610- "mdast-util-gfm": "^3.0.0",
3611- "micromark-extension-gfm": "^3.0.0",
3612- "remark-parse": "^11.0.0",
3613- "remark-stringify": "^11.0.0",
3614- "unified": "^11.0.0"
3615- },
3616- "funding": {
3617- "type": "opencollective",
3618- "url": "https://opencollective.com/unified"
3619- }
3620- },
3621- "node_modules/remark-parse": {
3622- "version": "11.0.0",
3623- "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
3624- "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
3625- "license": "MIT",
3626- "dependencies": {
3627- "@types/mdast": "^4.0.0",
3628- "mdast-util-from-markdown": "^2.0.0",
3629- "micromark-util-types": "^2.0.0",
3630- "unified": "^11.0.0"
3631- },
3632- "funding": {
3633- "type": "opencollective",
3634- "url": "https://opencollective.com/unified"
3635- }
3636- },
3637- "node_modules/remark-rehype": {
3638- "version": "11.1.2",
3639- "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
3640- "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
3641- "license": "MIT",
3642- "dependencies": {
3643- "@types/hast": "^3.0.0",
3644- "@types/mdast": "^4.0.0",
3645- "mdast-util-to-hast": "^13.0.0",
3646- "unified": "^11.0.0",
3647- "vfile": "^6.0.0"
3648- },
3649- "funding": {
3650- "type": "opencollective",
3651- "url": "https://opencollective.com/unified"
3652- }
3653- },
3654- "node_modules/remark-stringify": {
3655- "version": "11.0.0",
3656- "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
3657- "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
3658- "license": "MIT",
3659- "dependencies": {
3660- "@types/mdast": "^4.0.0",
3661- "mdast-util-to-markdown": "^2.0.0",
3662- "unified": "^11.0.0"
3663- },
3664- "funding": {
3665- "type": "opencollective",
3666- "url": "https://opencollective.com/unified"
3667- }
3668- },
3669- "node_modules/rollup": {
3670- "version": "4.63.3",
3671- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz",
3672- "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==",
3673- "dev": true,
3674- "license": "MIT",
3675- "dependencies": {
3676- "@types/estree": "1.0.9"
3677- },
3678- "bin": {
3679- "rollup": "dist/bin/rollup"
3680- },
3681- "engines": {
3682- "node": ">=18.0.0",
3683- "npm": ">=8.0.0"
3684- },
3685- "optionalDependencies": {
3686- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
3687- "@rollup/rollup-android-arm-eabi": "4.63.3",
3688- "@rollup/rollup-android-arm64": "4.63.3",
3689- "@rollup/rollup-darwin-arm64": "4.63.3",
3690- "@rollup/rollup-darwin-x64": "4.63.3",
3691- "@rollup/rollup-freebsd-arm64": "4.63.3",
3692- "@rollup/rollup-freebsd-x64": "4.63.3",
3693- "@rollup/rollup-linux-arm-gnueabihf": "4.63.3",
3694- "@rollup/rollup-linux-arm-musleabihf": "4.63.3",
3695- "@rollup/rollup-linux-arm64-gnu": "4.63.3",
3696- "@rollup/rollup-linux-arm64-musl": "4.63.3",
3697- "@rollup/rollup-linux-loong64-gnu": "4.63.3",
3698- "@rollup/rollup-linux-loong64-musl": "4.63.3",
3699- "@rollup/rollup-linux-ppc64-gnu": "4.63.3",
3700- "@rollup/rollup-linux-ppc64-musl": "4.63.3",
3701- "@rollup/rollup-linux-riscv64-gnu": "4.63.3",
3702- "@rollup/rollup-linux-riscv64-musl": "4.63.3",
3703- "@rollup/rollup-linux-s390x-gnu": "4.63.3",
3704- "@rollup/rollup-linux-x64-gnu": "4.63.3",
3705- "@rollup/rollup-linux-x64-musl": "4.63.3",
3706- "@rollup/rollup-openbsd-x64": "4.63.3",
3707- "@rollup/rollup-openharmony-arm64": "4.63.3",
3708- "@rollup/rollup-win32-arm64-msvc": "4.63.3",
3709- "@rollup/rollup-win32-ia32-msvc": "4.63.3",
3710- "@rollup/rollup-win32-x64-gnu": "4.63.3",
3711- "@rollup/rollup-win32-x64-msvc": "4.63.3",
3712- "fsevents": "~2.3.2"
3713- }
3714- },
3715- "node_modules/rrweb-cssom": {
3716- "version": "0.8.0",
3717- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
3718- "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
3719- "dev": true,
3720- "license": "MIT"
3721- },
3722- "node_modules/safer-buffer": {
3723- "version": "2.1.2",
3724- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
3725- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
3726- "dev": true,
3727- "license": "MIT"
3728- },
3729- "node_modules/saxes": {
3730- "version": "6.0.0",
3731- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
3732- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
3733- "dev": true,
3734- "license": "ISC",
3735- "dependencies": {
3736- "xmlchars": "^2.2.0"
3737- },
3738- "engines": {
3739- "node": ">=v12.22.7"
3740- }
3741- },
3742- "node_modules/scheduler": {
3743- "version": "0.28.0",
3744- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
3745- "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
3746- "license": "MIT"
3747- },
3748- "node_modules/semver": {
3749- "version": "6.3.1",
3750- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
3751- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
3752- "dev": true,
3753- "license": "ISC",
3754- "bin": {
3755- "semver": "bin/semver.js"
3756- }
3757- },
3758- "node_modules/siginfo": {
3759- "version": "2.0.0",
3760- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
3761- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
3762- "dev": true,
3763- "license": "ISC"
3764- },
3765- "node_modules/source-map-js": {
3766- "version": "1.2.1",
3767- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
3768- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
3769- "dev": true,
3770- "license": "BSD-3-Clause",
3771- "engines": {
3772- "node": ">=0.10.0"
3773- }
3774- },
3775- "node_modules/space-separated-tokens": {
3776- "version": "2.0.2",
3777- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
3778- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
3779- "license": "MIT",
3780- "funding": {
3781- "type": "github",
3782- "url": "https://github.com/sponsors/wooorm"
3783- }
3784- },
3785- "node_modules/stackback": {
3786- "version": "0.0.2",
3787- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
3788- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
3789- "dev": true,
3790- "license": "MIT"
3791- },
3792- "node_modules/std-env": {
3793- "version": "3.10.0",
3794- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
3795- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
3796- "dev": true,
3797- "license": "MIT"
3798- },
3799- "node_modules/stringify-entities": {
3800- "version": "4.0.4",
3801- "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
3802- "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
3803- "license": "MIT",
3804- "dependencies": {
3805- "character-entities-html4": "^2.0.0",
3806- "character-entities-legacy": "^3.0.0"
3807- },
3808- "funding": {
3809- "type": "github",
3810- "url": "https://github.com/sponsors/wooorm"
3811- }
3812- },
3813- "node_modules/strip-literal": {
3814- "version": "3.1.0",
3815- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
3816- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
3817- "dev": true,
3818- "license": "MIT",
3819- "dependencies": {
3820- "js-tokens": "^9.0.1"
3821- },
3822- "funding": {
3823- "url": "https://github.com/sponsors/antfu"
3824- }
3825- },
3826- "node_modules/strip-literal/node_modules/js-tokens": {
3827- "version": "9.0.1",
3828- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
3829- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
3830- "dev": true,
3831- "license": "MIT"
3832- },
3833- "node_modules/style-to-js": {
3834- "version": "1.1.21",
3835- "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
3836- "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
3837- "license": "MIT",
3838- "dependencies": {
3839- "style-to-object": "1.0.14"
3840- }
3841- },
3842- "node_modules/style-to-object": {
3843- "version": "1.0.14",
3844- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
3845- "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
3846- "license": "MIT",
3847- "dependencies": {
3848- "inline-style-parser": "0.2.7"
3849- }
3850- },
3851- "node_modules/symbol-tree": {
3852- "version": "3.2.4",
3853- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
3854- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
3855- "dev": true,
3856- "license": "MIT"
3857- },
3858- "node_modules/tinybench": {
3859- "version": "2.9.0",
3860- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
3861- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
3862- "dev": true,
3863- "license": "MIT"
3864- },
3865- "node_modules/tinyexec": {
3866- "version": "0.3.2",
3867- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
3868- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
3869- "dev": true,
3870- "license": "MIT"
3871- },
3872- "node_modules/tinyglobby": {
3873- "version": "0.2.17",
3874- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
3875- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
3876- "dev": true,
3877- "license": "MIT",
3878- "dependencies": {
3879- "fdir": "^6.5.0",
3880- "picomatch": "^4.0.4"
3881- },
3882- "engines": {
3883- "node": ">=12.0.0"
3884- },
3885- "funding": {
3886- "url": "https://github.com/sponsors/SuperchupuDev"
3887- }
3888- },
3889- "node_modules/tinypool": {
3890- "version": "1.1.1",
3891- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
3892- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
3893- "dev": true,
3894- "license": "MIT",
3895- "engines": {
3896- "node": "^18.0.0 || >=20.0.0"
3897- }
3898- },
3899- "node_modules/tinyrainbow": {
3900- "version": "2.0.0",
3901- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
3902- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
3903- "dev": true,
3904- "license": "MIT",
3905- "engines": {
3906- "node": ">=14.0.0"
3907- }
3908- },
3909- "node_modules/tinyspy": {
3910- "version": "4.0.6",
3911- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
3912- "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
3913- "dev": true,
3914- "license": "MIT",
3915- "engines": {
3916- "node": ">=14.0.0"
3917- }
3918- },
3919- "node_modules/tldts": {
3920- "version": "6.1.86",
3921- "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
3922- "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
3923- "dev": true,
3924- "license": "MIT",
3925- "dependencies": {
3926- "tldts-core": "^6.1.86"
3927- },
3928- "bin": {
3929- "tldts": "bin/cli.js"
3930- }
3931- },
3932- "node_modules/tldts-core": {
3933- "version": "6.1.86",
3934- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
3935- "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
3936- "dev": true,
3937- "license": "MIT"
3938- },
3939- "node_modules/tough-cookie": {
3940- "version": "5.1.2",
3941- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
3942- "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
3943- "dev": true,
3944- "license": "BSD-3-Clause",
3945- "dependencies": {
3946- "tldts": "^6.1.32"
3947- },
3948- "engines": {
3949- "node": ">=16"
3950- }
3951- },
3952- "node_modules/tr46": {
3953- "version": "5.1.1",
3954- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
3955- "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
3956- "dev": true,
3957- "license": "MIT",
3958- "dependencies": {
3959- "punycode": "^2.3.1"
3960- },
3961- "engines": {
3962- "node": ">=18"
3963- }
3964- },
3965- "node_modules/trim-lines": {
3966- "version": "3.0.1",
3967- "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
3968- "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
3969- "license": "MIT",
3970- "funding": {
3971- "type": "github",
3972- "url": "https://github.com/sponsors/wooorm"
3973- }
3974- },
3975- "node_modules/trough": {
3976- "version": "2.2.0",
3977- "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
3978- "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
3979- "license": "MIT",
3980- "funding": {
3981- "type": "github",
3982- "url": "https://github.com/sponsors/wooorm"
3983- }
3984- },
3985- "node_modules/typescript": {
3986- "version": "5.9.3",
3987- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
3988- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
3989- "dev": true,
3990- "license": "Apache-2.0",
3991- "bin": {
3992- "tsc": "bin/tsc",
3993- "tsserver": "bin/tsserver"
3994- },
3995- "engines": {
3996- "node": ">=14.17"
3997- }
3998- },
3999- "node_modules/unified": {
4000- "version": "11.0.5",
4001- "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
4002- "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
4003- "license": "MIT",
4004- "dependencies": {
4005- "@types/unist": "^3.0.0",
4006- "bail": "^2.0.0",
4007- "devlop": "^1.0.0",
4008- "extend": "^3.0.0",
4009- "is-plain-obj": "^4.0.0",
4010- "trough": "^2.0.0",
4011- "vfile": "^6.0.0"
4012- },
4013- "funding": {
4014- "type": "opencollective",
4015- "url": "https://opencollective.com/unified"
4016- }
4017- },
4018- "node_modules/unist-util-is": {
4019- "version": "6.0.1",
4020- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
4021- "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
4022- "license": "MIT",
4023- "dependencies": {
4024- "@types/unist": "^3.0.0"
4025- },
4026- "funding": {
4027- "type": "opencollective",
4028- "url": "https://opencollective.com/unified"
4029- }
4030- },
4031- "node_modules/unist-util-position": {
4032- "version": "5.0.0",
4033- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
4034- "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
4035- "license": "MIT",
4036- "dependencies": {
4037- "@types/unist": "^3.0.0"
4038- },
4039- "funding": {
4040- "type": "opencollective",
4041- "url": "https://opencollective.com/unified"
4042- }
4043- },
4044- "node_modules/unist-util-stringify-position": {
4045- "version": "4.0.0",
4046- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
4047- "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
4048- "license": "MIT",
4049- "dependencies": {
4050- "@types/unist": "^3.0.0"
4051- },
4052- "funding": {
4053- "type": "opencollective",
4054- "url": "https://opencollective.com/unified"
4055- }
4056- },
4057- "node_modules/unist-util-visit": {
4058- "version": "5.1.0",
4059- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
4060- "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
4061- "license": "MIT",
4062- "dependencies": {
4063- "@types/unist": "^3.0.0",
4064- "unist-util-is": "^6.0.0",
4065- "unist-util-visit-parents": "^6.0.0"
4066- },
4067- "funding": {
4068- "type": "opencollective",
4069- "url": "https://opencollective.com/unified"
4070- }
4071- },
4072- "node_modules/unist-util-visit-parents": {
4073- "version": "6.0.2",
4074- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
4075- "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
4076- "license": "MIT",
4077- "dependencies": {
4078- "@types/unist": "^3.0.0",
4079- "unist-util-is": "^6.0.0"
4080- },
4081- "funding": {
4082- "type": "opencollective",
4083- "url": "https://opencollective.com/unified"
4084- }
4085- },
4086- "node_modules/update-browserslist-db": {
4087- "version": "1.3.3",
4088- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
4089- "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
4090- "dev": true,
4091- "funding": [
4092- {
4093- "type": "opencollective",
4094- "url": "https://opencollective.com/browserslist"
4095- },
4096- {
4097- "type": "tidelift",
4098- "url": "https://tidelift.com/funding/github/npm/browserslist"
4099- },
4100- {
4101- "type": "github",
4102- "url": "https://github.com/sponsors/ai"
4103- }
4104- ],
4105- "license": "MIT",
4106- "dependencies": {
4107- "escalade": "^3.2.0",
4108- "picocolors": "^1.1.1"
4109- },
4110- "bin": {
4111- "update-browserslist-db": "cli.js"
4112- },
4113- "peerDependencies": {
4114- "browserslist": ">= 4.21.0"
4115- }
4116- },
4117- "node_modules/vfile": {
4118- "version": "6.0.3",
4119- "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
4120- "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
4121- "license": "MIT",
4122- "dependencies": {
4123- "@types/unist": "^3.0.0",
4124- "vfile-message": "^4.0.0"
4125- },
4126- "funding": {
4127- "type": "opencollective",
4128- "url": "https://opencollective.com/unified"
4129- }
4130- },
4131- "node_modules/vfile-message": {
4132- "version": "4.0.3",
4133- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
4134- "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
4135- "license": "MIT",
4136- "dependencies": {
4137- "@types/unist": "^3.0.0",
4138- "unist-util-stringify-position": "^4.0.0"
4139- },
4140- "funding": {
4141- "type": "opencollective",
4142- "url": "https://opencollective.com/unified"
4143- }
4144- },
4145- "node_modules/vite": {
4146- "version": "7.3.6",
4147- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
4148- "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
4149- "dev": true,
4150- "license": "MIT",
4151- "dependencies": {
4152- "esbuild": "^0.27.0 || ^0.28.0",
4153- "fdir": "^6.5.0",
4154- "picomatch": "^4.0.3",
4155- "postcss": "^8.5.6",
4156- "rollup": "^4.43.0",
4157- "tinyglobby": "^0.2.15"
4158- },
4159- "bin": {
4160- "vite": "bin/vite.js"
4161- },
4162- "engines": {
4163- "node": "^20.19.0 || >=22.12.0"
4164- },
4165- "funding": {
4166- "url": "https://github.com/vitejs/vite?sponsor=1"
4167- },
4168- "optionalDependencies": {
4169- "fsevents": "~2.3.3"
4170- },
4171- "peerDependencies": {
4172- "@types/node": "^20.19.0 || >=22.12.0",
4173- "jiti": ">=1.21.0",
4174- "less": "^4.0.0",
4175- "lightningcss": "^1.21.0",
4176- "sass": "^1.70.0",
4177- "sass-embedded": "^1.70.0",
4178- "stylus": ">=0.54.8",
4179- "sugarss": "^5.0.0",
4180- "terser": "^5.16.0",
4181- "tsx": "^4.8.1",
4182- "yaml": "^2.4.2"
4183- },
4184- "peerDependenciesMeta": {
4185- "@types/node": {
4186- "optional": true
4187- },
4188- "jiti": {
4189- "optional": true
4190- },
4191- "less": {
4192- "optional": true
4193- },
4194- "lightningcss": {
4195- "optional": true
4196- },
4197- "sass": {
4198- "optional": true
4199- },
4200- "sass-embedded": {
4201- "optional": true
4202- },
4203- "stylus": {
4204- "optional": true
4205- },
4206- "sugarss": {
4207- "optional": true
4208- },
4209- "terser": {
4210- "optional": true
4211- },
4212- "tsx": {
4213- "optional": true
4214- },
4215- "yaml": {
4216- "optional": true
4217- }
4218- }
4219- },
4220- "node_modules/vite-node": {
4221- "version": "3.2.4",
4222- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
4223- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
4224- "dev": true,
4225- "license": "MIT",
4226- "dependencies": {
4227- "cac": "^6.7.14",
4228- "debug": "^4.4.1",
4229- "es-module-lexer": "^1.7.0",
4230- "pathe": "^2.0.3",
4231- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
4232- },
4233- "bin": {
4234- "vite-node": "vite-node.mjs"
4235- },
4236- "engines": {
4237- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4238- },
4239- "funding": {
4240- "url": "https://opencollective.com/vitest"
4241- }
4242- },
4243- "node_modules/vitest": {
4244- "version": "3.2.7",
4245- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
4246- "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
4247- "dev": true,
4248- "license": "MIT",
4249- "dependencies": {
4250- "@types/chai": "^5.2.2",
4251- "@vitest/expect": "3.2.7",
4252- "@vitest/mocker": "3.2.7",
4253- "@vitest/pretty-format": "^3.2.7",
4254- "@vitest/runner": "3.2.7",
4255- "@vitest/snapshot": "3.2.7",
4256- "@vitest/spy": "3.2.7",
4257- "@vitest/utils": "3.2.7",
4258- "chai": "^5.2.0",
4259- "debug": "^4.4.1",
4260- "expect-type": "^1.2.1",
4261- "magic-string": "^0.30.17",
4262- "pathe": "^2.0.3",
4263- "picomatch": "^4.0.2",
4264- "std-env": "^3.9.0",
4265- "tinybench": "^2.9.0",
4266- "tinyexec": "^0.3.2",
4267- "tinyglobby": "^0.2.14",
4268- "tinypool": "^1.1.1",
4269- "tinyrainbow": "^2.0.0",
4270- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
4271- "vite-node": "3.2.4",
4272- "why-is-node-running": "^2.3.0"
4273- },
4274- "bin": {
4275- "vitest": "vitest.mjs"
4276- },
4277- "engines": {
4278- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4279- },
4280- "funding": {
4281- "url": "https://opencollective.com/vitest"
4282- },
4283- "peerDependencies": {
4284- "@edge-runtime/vm": "*",
4285- "@types/debug": "^4.1.12",
4286- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
4287- "@vitest/browser": "3.2.7",
4288- "@vitest/ui": "3.2.7",
4289- "happy-dom": "*",
4290- "jsdom": "*"
4291- },
4292- "peerDependenciesMeta": {
4293- "@edge-runtime/vm": {
4294- "optional": true
4295- },
4296- "@types/debug": {
4297- "optional": true
4298- },
4299- "@types/node": {
4300- "optional": true
4301- },
4302- "@vitest/browser": {
4303- "optional": true
4304- },
4305- "@vitest/ui": {
4306- "optional": true
4307- },
4308- "happy-dom": {
4309- "optional": true
4310- },
4311- "jsdom": {
4312- "optional": true
4313- }
4314- }
4315- },
4316- "node_modules/w3c-xmlserializer": {
4317- "version": "5.0.0",
4318- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
4319- "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
4320- "dev": true,
4321- "license": "MIT",
4322- "dependencies": {
4323- "xml-name-validator": "^5.0.0"
4324- },
4325- "engines": {
4326- "node": ">=18"
4327- }
4328- },
4329- "node_modules/webidl-conversions": {
4330- "version": "7.0.0",
4331- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
4332- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
4333- "dev": true,
4334- "license": "BSD-2-Clause",
4335- "engines": {
4336- "node": ">=12"
4337- }
4338- },
4339- "node_modules/whatwg-encoding": {
4340- "version": "3.1.1",
4341- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
4342- "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
4343- "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
4344- "dev": true,
4345- "license": "MIT",
4346- "dependencies": {
4347- "iconv-lite": "0.6.3"
4348- },
4349- "engines": {
4350- "node": ">=18"
4351- }
4352- },
4353- "node_modules/whatwg-mimetype": {
4354- "version": "4.0.0",
4355- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
4356- "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
4357- "dev": true,
4358- "license": "MIT",
4359- "engines": {
4360- "node": ">=18"
4361- }
4362- },
4363- "node_modules/whatwg-url": {
4364- "version": "14.2.0",
4365- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
4366- "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
4367- "dev": true,
4368- "license": "MIT",
4369- "dependencies": {
4370- "tr46": "^5.1.0",
4371- "webidl-conversions": "^7.0.0"
4372- },
4373- "engines": {
4374- "node": ">=18"
4375- }
4376- },
4377- "node_modules/why-is-node-running": {
4378- "version": "2.3.0",
4379- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
4380- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
4381- "dev": true,
4382- "license": "MIT",
4383- "dependencies": {
4384- "siginfo": "^2.0.0",
4385- "stackback": "0.0.2"
4386- },
4387- "bin": {
4388- "why-is-node-running": "cli.js"
4389- },
4390- "engines": {
4391- "node": ">=8"
4392- }
4393- },
4394- "node_modules/ws": {
4395- "version": "8.21.3",
4396- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
4397- "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
4398- "dev": true,
4399- "license": "MIT",
4400- "engines": {
4401- "node": ">=10.0.0"
4402- },
4403- "peerDependencies": {
4404- "bufferutil": "^4.0.1",
4405- "utf-8-validate": ">=5.0.2"
4406- },
4407- "peerDependenciesMeta": {
4408- "bufferutil": {
4409- "optional": true
4410- },
4411- "utf-8-validate": {
4412- "optional": true
4413- }
4414- }
4415- },
4416- "node_modules/xml-name-validator": {
4417- "version": "5.0.0",
4418- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
4419- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
4420- "dev": true,
4421- "license": "Apache-2.0",
4422- "engines": {
4423- "node": ">=18"
4424- }
4425- },
4426- "node_modules/xmlchars": {
4427- "version": "2.2.0",
4428- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
4429- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
4430- "dev": true,
4431- "license": "MIT"
4432- },
4433- "node_modules/yallist": {
4434- "version": "3.1.1",
4435- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
4436- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
4437- "dev": true,
4438- "license": "ISC"
4439- },
4440- "node_modules/zustand": {
4441- "version": "5.0.15",
4442- "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
4443- "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
4444- "license": "MIT",
4445- "engines": {
4446- "node": ">=12.20.0"
4447- },
4448- "peerDependencies": {
4449- "@types/react": ">=18.0.0",
4450- "immer": ">=9.0.6",
4451- "react": ">=18.0.0",
4452- "use-sync-external-store": ">=1.2.0"
4453- },
4454- "peerDependenciesMeta": {
4455- "@types/react": {
4456- "optional": true
4457- },
4458- "immer": {
4459- "optional": true
4460- },
4461- "react": {
4462- "optional": true
4463- },
4464- "use-sync-external-store": {
4465- "optional": true
4466- }
4467- }
4468- },
4469- "node_modules/zwitch": {
4470- "version": "2.0.4",
4471- "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
4472- "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
4473- "license": "MIT",
4474- "funding": {
4475- "type": "github",
4476- "url": "https://github.com/sponsors/wooorm"
4477- }
4478- }
4479- }
2+ "name": "ori-ui",
3+ "version": "0.1.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "ori-ui",
9+ "version": "0.1.0",
10+ "dependencies": {
11+ "@asciidoctor/core": "^4.0.11",
12+ "@monaco-editor/react": "^4.7.0",
13+ "@xterm/addon-fit": "^0.11.0",
14+ "@xterm/xterm": "^6.0.0",
15+ "monaco-editor": "^0.56.0",
16+ "react": "^19.0.0",
17+ "react-dom": "^19.0.0",
18+ "react-markdown": "^10.1.0",
19+ "remark-gfm": "^4.0.1",
20+ "vscode-material-icons": "^0.1.1",
21+ "zustand": "^5.0.15"
22+ },
23+ "devDependencies": {
24+ "@testing-library/react": "^16.0.0",
25+ "@types/react": "^19.0.0",
26+ "@types/react-dom": "^19.0.0",
27+ "@vitejs/plugin-react": "^5.0.0",
28+ "jsdom": "^26.0.0",
29+ "typescript": "^5.6.0",
30+ "vite": "^7.0.0",
31+ "vitest": "^3.0.0"
32+ }
33+ },
34+ "node_modules/@asamuzakjp/css-color": {
35+ "version": "3.2.0",
36+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
37+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
38+ "dev": true,
39+ "license": "MIT",
40+ "dependencies": {
41+ "@csstools/css-calc": "^2.1.3",
42+ "@csstools/css-color-parser": "^3.0.9",
43+ "@csstools/css-parser-algorithms": "^3.0.4",
44+ "@csstools/css-tokenizer": "^3.0.3",
45+ "lru-cache": "^10.4.3"
46+ }
47+ },
48+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
49+ "version": "10.4.3",
50+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
51+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
52+ "dev": true,
53+ "license": "ISC"
54+ },
55+ "node_modules/@asciidoctor/core": {
56+ "version": "4.0.11",
57+ "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-4.0.11.tgz",
58+ "integrity": "sha512-5uWPTLTiPJVQp9q4MllSROM2avdECzoT7oeVw/onT9H2PIdCCtQwKrSTvztwxMbSu/8cqHqZox/2qTBrQN454g==",
59+ "license": "MIT",
60+ "engines": {
61+ "node": ">=20"
62+ }
63+ },
64+ "node_modules/@babel/code-frame": {
65+ "version": "7.29.7",
66+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
67+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
68+ "dev": true,
69+ "license": "MIT",
70+ "dependencies": {
71+ "@babel/helper-validator-identifier": "^7.29.7",
72+ "js-tokens": "^4.0.0",
73+ "picocolors": "^1.1.1"
74+ },
75+ "engines": {
76+ "node": ">=6.9.0"
77+ }
78+ },
79+ "node_modules/@babel/compat-data": {
80+ "version": "7.29.7",
81+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
82+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
83+ "dev": true,
84+ "license": "MIT",
85+ "engines": {
86+ "node": ">=6.9.0"
87+ }
88+ },
89+ "node_modules/@babel/core": {
90+ "version": "7.29.7",
91+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
92+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
93+ "dev": true,
94+ "license": "MIT",
95+ "dependencies": {
96+ "@babel/code-frame": "^7.29.7",
97+ "@babel/generator": "^7.29.7",
98+ "@babel/helper-compilation-targets": "^7.29.7",
99+ "@babel/helper-module-transforms": "^7.29.7",
100+ "@babel/helpers": "^7.29.7",
101+ "@babel/parser": "^7.29.7",
102+ "@babel/template": "^7.29.7",
103+ "@babel/traverse": "^7.29.7",
104+ "@babel/types": "^7.29.7",
105+ "@jridgewell/remapping": "^2.3.5",
106+ "convert-source-map": "^2.0.0",
107+ "debug": "^4.1.0",
108+ "gensync": "^1.0.0-beta.2",
109+ "json5": "^2.2.3",
110+ "semver": "^6.3.1"
111+ },
112+ "engines": {
113+ "node": ">=6.9.0"
114+ },
115+ "funding": {
116+ "type": "opencollective",
117+ "url": "https://opencollective.com/babel"
118+ }
119+ },
120+ "node_modules/@babel/generator": {
121+ "version": "7.29.8",
122+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
123+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
124+ "dev": true,
125+ "license": "MIT",
126+ "dependencies": {
127+ "@babel/parser": "^7.29.8",
128+ "@babel/types": "^7.29.8",
129+ "@jridgewell/gen-mapping": "^0.3.12",
130+ "@jridgewell/trace-mapping": "^0.3.28",
131+ "jsesc": "^3.0.2"
132+ },
133+ "engines": {
134+ "node": ">=6.9.0"
135+ }
136+ },
137+ "node_modules/@babel/helper-compilation-targets": {
138+ "version": "7.29.7",
139+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
140+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
141+ "dev": true,
142+ "license": "MIT",
143+ "dependencies": {
144+ "@babel/compat-data": "^7.29.7",
145+ "@babel/helper-validator-option": "^7.29.7",
146+ "browserslist": "^4.24.0",
147+ "lru-cache": "^5.1.1",
148+ "semver": "^6.3.1"
149+ },
150+ "engines": {
151+ "node": ">=6.9.0"
152+ }
153+ },
154+ "node_modules/@babel/helper-globals": {
155+ "version": "7.29.7",
156+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
157+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
158+ "dev": true,
159+ "license": "MIT",
160+ "engines": {
161+ "node": ">=6.9.0"
162+ }
163+ },
164+ "node_modules/@babel/helper-module-imports": {
165+ "version": "7.29.7",
166+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
167+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
168+ "dev": true,
169+ "license": "MIT",
170+ "dependencies": {
171+ "@babel/traverse": "^7.29.7",
172+ "@babel/types": "^7.29.7"
173+ },
174+ "engines": {
175+ "node": ">=6.9.0"
176+ }
177+ },
178+ "node_modules/@babel/helper-module-transforms": {
179+ "version": "7.29.7",
180+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
181+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
182+ "dev": true,
183+ "license": "MIT",
184+ "dependencies": {
185+ "@babel/helper-module-imports": "^7.29.7",
186+ "@babel/helper-validator-identifier": "^7.29.7",
187+ "@babel/traverse": "^7.29.7"
188+ },
189+ "engines": {
190+ "node": ">=6.9.0"
191+ },
192+ "peerDependencies": {
193+ "@babel/core": "^7.0.0"
194+ }
195+ },
196+ "node_modules/@babel/helper-plugin-utils": {
197+ "version": "7.29.7",
198+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
199+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
200+ "dev": true,
201+ "license": "MIT",
202+ "engines": {
203+ "node": ">=6.9.0"
204+ }
205+ },
206+ "node_modules/@babel/helper-string-parser": {
207+ "version": "7.29.7",
208+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
209+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
210+ "dev": true,
211+ "license": "MIT",
212+ "engines": {
213+ "node": ">=6.9.0"
214+ }
215+ },
216+ "node_modules/@babel/helper-validator-identifier": {
217+ "version": "7.29.7",
218+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
219+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
220+ "dev": true,
221+ "license": "MIT",
222+ "engines": {
223+ "node": ">=6.9.0"
224+ }
225+ },
226+ "node_modules/@babel/helper-validator-option": {
227+ "version": "7.29.7",
228+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
229+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
230+ "dev": true,
231+ "license": "MIT",
232+ "engines": {
233+ "node": ">=6.9.0"
234+ }
235+ },
236+ "node_modules/@babel/helpers": {
237+ "version": "7.29.7",
238+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
239+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
240+ "dev": true,
241+ "license": "MIT",
242+ "dependencies": {
243+ "@babel/template": "^7.29.7",
244+ "@babel/types": "^7.29.7"
245+ },
246+ "engines": {
247+ "node": ">=6.9.0"
248+ }
249+ },
250+ "node_modules/@babel/parser": {
251+ "version": "7.29.8",
252+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
253+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
254+ "dev": true,
255+ "license": "MIT",
256+ "dependencies": {
257+ "@babel/types": "^7.29.8"
258+ },
259+ "bin": {
260+ "parser": "bin/babel-parser.js"
261+ },
262+ "engines": {
263+ "node": ">=6.0.0"
264+ }
265+ },
266+ "node_modules/@babel/plugin-transform-react-jsx-self": {
267+ "version": "7.29.7",
268+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
269+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
270+ "dev": true,
271+ "license": "MIT",
272+ "dependencies": {
273+ "@babel/helper-plugin-utils": "^7.29.7"
274+ },
275+ "engines": {
276+ "node": ">=6.9.0"
277+ },
278+ "peerDependencies": {
279+ "@babel/core": "^7.0.0-0"
280+ }
281+ },
282+ "node_modules/@babel/plugin-transform-react-jsx-source": {
283+ "version": "7.29.7",
284+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
285+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
286+ "dev": true,
287+ "license": "MIT",
288+ "dependencies": {
289+ "@babel/helper-plugin-utils": "^7.29.7"
290+ },
291+ "engines": {
292+ "node": ">=6.9.0"
293+ },
294+ "peerDependencies": {
295+ "@babel/core": "^7.0.0-0"
296+ }
297+ },
298+ "node_modules/@babel/runtime": {
299+ "version": "7.29.7",
300+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
301+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
302+ "dev": true,
303+ "license": "MIT",
304+ "engines": {
305+ "node": ">=6.9.0"
306+ }
307+ },
308+ "node_modules/@babel/template": {
309+ "version": "7.29.7",
310+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
311+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
312+ "dev": true,
313+ "license": "MIT",
314+ "dependencies": {
315+ "@babel/code-frame": "^7.29.7",
316+ "@babel/parser": "^7.29.7",
317+ "@babel/types": "^7.29.7"
318+ },
319+ "engines": {
320+ "node": ">=6.9.0"
321+ }
322+ },
323+ "node_modules/@babel/traverse": {
324+ "version": "7.29.8",
325+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
326+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
327+ "dev": true,
328+ "license": "MIT",
329+ "dependencies": {
330+ "@babel/code-frame": "^7.29.7",
331+ "@babel/generator": "^7.29.8",
332+ "@babel/helper-globals": "^7.29.7",
333+ "@babel/parser": "^7.29.8",
334+ "@babel/template": "^7.29.7",
335+ "@babel/types": "^7.29.8",
336+ "debug": "^4.3.1"
337+ },
338+ "engines": {
339+ "node": ">=6.9.0"
340+ }
341+ },
342+ "node_modules/@babel/types": {
343+ "version": "7.29.8",
344+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
345+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
346+ "dev": true,
347+ "license": "MIT",
348+ "dependencies": {
349+ "@babel/helper-string-parser": "^7.29.7",
350+ "@babel/helper-validator-identifier": "^7.29.7"
351+ },
352+ "engines": {
353+ "node": ">=6.9.0"
354+ }
355+ },
356+ "node_modules/@csstools/color-helpers": {
357+ "version": "5.1.0",
358+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
359+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
360+ "dev": true,
361+ "funding": [
362+ {
363+ "type": "github",
364+ "url": "https://github.com/sponsors/csstools"
365+ },
366+ {
367+ "type": "opencollective",
368+ "url": "https://opencollective.com/csstools"
369+ }
370+ ],
371+ "license": "MIT-0",
372+ "engines": {
373+ "node": ">=18"
374+ }
375+ },
376+ "node_modules/@csstools/css-calc": {
377+ "version": "2.1.4",
378+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
379+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
380+ "dev": true,
381+ "funding": [
382+ {
383+ "type": "github",
384+ "url": "https://github.com/sponsors/csstools"
385+ },
386+ {
387+ "type": "opencollective",
388+ "url": "https://opencollective.com/csstools"
389+ }
390+ ],
391+ "license": "MIT",
392+ "engines": {
393+ "node": ">=18"
394+ },
395+ "peerDependencies": {
396+ "@csstools/css-parser-algorithms": "^3.0.5",
397+ "@csstools/css-tokenizer": "^3.0.4"
398+ }
399+ },
400+ "node_modules/@csstools/css-color-parser": {
401+ "version": "3.1.0",
402+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
403+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
404+ "dev": true,
405+ "funding": [
406+ {
407+ "type": "github",
408+ "url": "https://github.com/sponsors/csstools"
409+ },
410+ {
411+ "type": "opencollective",
412+ "url": "https://opencollective.com/csstools"
413+ }
414+ ],
415+ "license": "MIT",
416+ "dependencies": {
417+ "@csstools/color-helpers": "^5.1.0",
418+ "@csstools/css-calc": "^2.1.4"
419+ },
420+ "engines": {
421+ "node": ">=18"
422+ },
423+ "peerDependencies": {
424+ "@csstools/css-parser-algorithms": "^3.0.5",
425+ "@csstools/css-tokenizer": "^3.0.4"
426+ }
427+ },
428+ "node_modules/@csstools/css-parser-algorithms": {
429+ "version": "3.0.5",
430+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
431+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
432+ "dev": true,
433+ "funding": [
434+ {
435+ "type": "github",
436+ "url": "https://github.com/sponsors/csstools"
437+ },
438+ {
439+ "type": "opencollective",
440+ "url": "https://opencollective.com/csstools"
441+ }
442+ ],
443+ "license": "MIT",
444+ "engines": {
445+ "node": ">=18"
446+ },
447+ "peerDependencies": {
448+ "@csstools/css-tokenizer": "^3.0.4"
449+ }
450+ },
451+ "node_modules/@csstools/css-tokenizer": {
452+ "version": "3.0.4",
453+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
454+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
455+ "dev": true,
456+ "funding": [
457+ {
458+ "type": "github",
459+ "url": "https://github.com/sponsors/csstools"
460+ },
461+ {
462+ "type": "opencollective",
463+ "url": "https://opencollective.com/csstools"
464+ }
465+ ],
466+ "license": "MIT",
467+ "engines": {
468+ "node": ">=18"
469+ }
470+ },
471+ "node_modules/@esbuild/aix-ppc64": {
472+ "version": "0.28.2",
473+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
474+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
475+ "cpu": [
476+ "ppc64"
477+ ],
478+ "dev": true,
479+ "license": "MIT",
480+ "optional": true,
481+ "os": [
482+ "aix"
483+ ],
484+ "engines": {
485+ "node": ">=18"
486+ }
487+ },
488+ "node_modules/@esbuild/android-arm": {
489+ "version": "0.28.2",
490+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
491+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
492+ "cpu": [
493+ "arm"
494+ ],
495+ "dev": true,
496+ "license": "MIT",
497+ "optional": true,
498+ "os": [
499+ "android"
500+ ],
501+ "engines": {
502+ "node": ">=18"
503+ }
504+ },
505+ "node_modules/@esbuild/android-arm64": {
506+ "version": "0.28.2",
507+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
508+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
509+ "cpu": [
510+ "arm64"
511+ ],
512+ "dev": true,
513+ "license": "MIT",
514+ "optional": true,
515+ "os": [
516+ "android"
517+ ],
518+ "engines": {
519+ "node": ">=18"
520+ }
521+ },
522+ "node_modules/@esbuild/android-x64": {
523+ "version": "0.28.2",
524+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
525+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
526+ "cpu": [
527+ "x64"
528+ ],
529+ "dev": true,
530+ "license": "MIT",
531+ "optional": true,
532+ "os": [
533+ "android"
534+ ],
535+ "engines": {
536+ "node": ">=18"
537+ }
538+ },
539+ "node_modules/@esbuild/darwin-arm64": {
540+ "version": "0.28.2",
541+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
542+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
543+ "cpu": [
544+ "arm64"
545+ ],
546+ "dev": true,
547+ "license": "MIT",
548+ "optional": true,
549+ "os": [
550+ "darwin"
551+ ],
552+ "engines": {
553+ "node": ">=18"
554+ }
555+ },
556+ "node_modules/@esbuild/darwin-x64": {
557+ "version": "0.28.2",
558+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
559+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
560+ "cpu": [
561+ "x64"
562+ ],
563+ "dev": true,
564+ "license": "MIT",
565+ "optional": true,
566+ "os": [
567+ "darwin"
568+ ],
569+ "engines": {
570+ "node": ">=18"
571+ }
572+ },
573+ "node_modules/@esbuild/freebsd-arm64": {
574+ "version": "0.28.2",
575+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
576+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
577+ "cpu": [
578+ "arm64"
579+ ],
580+ "dev": true,
581+ "license": "MIT",
582+ "optional": true,
583+ "os": [
584+ "freebsd"
585+ ],
586+ "engines": {
587+ "node": ">=18"
588+ }
589+ },
590+ "node_modules/@esbuild/freebsd-x64": {
591+ "version": "0.28.2",
592+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
593+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
594+ "cpu": [
595+ "x64"
596+ ],
597+ "dev": true,
598+ "license": "MIT",
599+ "optional": true,
600+ "os": [
601+ "freebsd"
602+ ],
603+ "engines": {
604+ "node": ">=18"
605+ }
606+ },
607+ "node_modules/@esbuild/linux-arm": {
608+ "version": "0.28.2",
609+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
610+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
611+ "cpu": [
612+ "arm"
613+ ],
614+ "dev": true,
615+ "license": "MIT",
616+ "optional": true,
617+ "os": [
618+ "linux"
619+ ],
620+ "engines": {
621+ "node": ">=18"
622+ }
623+ },
624+ "node_modules/@esbuild/linux-arm64": {
625+ "version": "0.28.2",
626+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
627+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
628+ "cpu": [
629+ "arm64"
630+ ],
631+ "dev": true,
632+ "license": "MIT",
633+ "optional": true,
634+ "os": [
635+ "linux"
636+ ],
637+ "engines": {
638+ "node": ">=18"
639+ }
640+ },
641+ "node_modules/@esbuild/linux-ia32": {
642+ "version": "0.28.2",
643+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
644+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
645+ "cpu": [
646+ "ia32"
647+ ],
648+ "dev": true,
649+ "license": "MIT",
650+ "optional": true,
651+ "os": [
652+ "linux"
653+ ],
654+ "engines": {
655+ "node": ">=18"
656+ }
657+ },
658+ "node_modules/@esbuild/linux-loong64": {
659+ "version": "0.28.2",
660+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
661+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
662+ "cpu": [
663+ "loong64"
664+ ],
665+ "dev": true,
666+ "license": "MIT",
667+ "optional": true,
668+ "os": [
669+ "linux"
670+ ],
671+ "engines": {
672+ "node": ">=18"
673+ }
674+ },
675+ "node_modules/@esbuild/linux-mips64el": {
676+ "version": "0.28.2",
677+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
678+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
679+ "cpu": [
680+ "mips64el"
681+ ],
682+ "dev": true,
683+ "license": "MIT",
684+ "optional": true,
685+ "os": [
686+ "linux"
687+ ],
688+ "engines": {
689+ "node": ">=18"
690+ }
691+ },
692+ "node_modules/@esbuild/linux-ppc64": {
693+ "version": "0.28.2",
694+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
695+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
696+ "cpu": [
697+ "ppc64"
698+ ],
699+ "dev": true,
700+ "license": "MIT",
701+ "optional": true,
702+ "os": [
703+ "linux"
704+ ],
705+ "engines": {
706+ "node": ">=18"
707+ }
708+ },
709+ "node_modules/@esbuild/linux-riscv64": {
710+ "version": "0.28.2",
711+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
712+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
713+ "cpu": [
714+ "riscv64"
715+ ],
716+ "dev": true,
717+ "license": "MIT",
718+ "optional": true,
719+ "os": [
720+ "linux"
721+ ],
722+ "engines": {
723+ "node": ">=18"
724+ }
725+ },
726+ "node_modules/@esbuild/linux-s390x": {
727+ "version": "0.28.2",
728+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
729+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
730+ "cpu": [
731+ "s390x"
732+ ],
733+ "dev": true,
734+ "license": "MIT",
735+ "optional": true,
736+ "os": [
737+ "linux"
738+ ],
739+ "engines": {
740+ "node": ">=18"
741+ }
742+ },
743+ "node_modules/@esbuild/linux-x64": {
744+ "version": "0.28.2",
745+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
746+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
747+ "cpu": [
748+ "x64"
749+ ],
750+ "dev": true,
751+ "license": "MIT",
752+ "optional": true,
753+ "os": [
754+ "linux"
755+ ],
756+ "engines": {
757+ "node": ">=18"
758+ }
759+ },
760+ "node_modules/@esbuild/netbsd-arm64": {
761+ "version": "0.28.2",
762+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
763+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
764+ "cpu": [
765+ "arm64"
766+ ],
767+ "dev": true,
768+ "license": "MIT",
769+ "optional": true,
770+ "os": [
771+ "netbsd"
772+ ],
773+ "engines": {
774+ "node": ">=18"
775+ }
776+ },
777+ "node_modules/@esbuild/netbsd-x64": {
778+ "version": "0.28.2",
779+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
780+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
781+ "cpu": [
782+ "x64"
783+ ],
784+ "dev": true,
785+ "license": "MIT",
786+ "optional": true,
787+ "os": [
788+ "netbsd"
789+ ],
790+ "engines": {
791+ "node": ">=18"
792+ }
793+ },
794+ "node_modules/@esbuild/openbsd-arm64": {
795+ "version": "0.28.2",
796+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
797+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
798+ "cpu": [
799+ "arm64"
800+ ],
801+ "dev": true,
802+ "license": "MIT",
803+ "optional": true,
804+ "os": [
805+ "openbsd"
806+ ],
807+ "engines": {
808+ "node": ">=18"
809+ }
810+ },
811+ "node_modules/@esbuild/openbsd-x64": {
812+ "version": "0.28.2",
813+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
814+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
815+ "cpu": [
816+ "x64"
817+ ],
818+ "dev": true,
819+ "license": "MIT",
820+ "optional": true,
821+ "os": [
822+ "openbsd"
823+ ],
824+ "engines": {
825+ "node": ">=18"
826+ }
827+ },
828+ "node_modules/@esbuild/openharmony-arm64": {
829+ "version": "0.28.2",
830+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
831+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
832+ "cpu": [
833+ "arm64"
834+ ],
835+ "dev": true,
836+ "license": "MIT",
837+ "optional": true,
838+ "os": [
839+ "openharmony"
840+ ],
841+ "engines": {
842+ "node": ">=18"
843+ }
844+ },
845+ "node_modules/@esbuild/sunos-x64": {
846+ "version": "0.28.2",
847+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
848+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
849+ "cpu": [
850+ "x64"
851+ ],
852+ "dev": true,
853+ "license": "MIT",
854+ "optional": true,
855+ "os": [
856+ "sunos"
857+ ],
858+ "engines": {
859+ "node": ">=18"
860+ }
861+ },
862+ "node_modules/@esbuild/win32-arm64": {
863+ "version": "0.28.2",
864+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
865+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
866+ "cpu": [
867+ "arm64"
868+ ],
869+ "dev": true,
870+ "license": "MIT",
871+ "optional": true,
872+ "os": [
873+ "win32"
874+ ],
875+ "engines": {
876+ "node": ">=18"
877+ }
878+ },
879+ "node_modules/@esbuild/win32-ia32": {
880+ "version": "0.28.2",
881+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
882+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
883+ "cpu": [
884+ "ia32"
885+ ],
886+ "dev": true,
887+ "license": "MIT",
888+ "optional": true,
889+ "os": [
890+ "win32"
891+ ],
892+ "engines": {
893+ "node": ">=18"
894+ }
895+ },
896+ "node_modules/@esbuild/win32-x64": {
897+ "version": "0.28.2",
898+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
899+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
900+ "cpu": [
901+ "x64"
902+ ],
903+ "dev": true,
904+ "license": "MIT",
905+ "optional": true,
906+ "os": [
907+ "win32"
908+ ],
909+ "engines": {
910+ "node": ">=18"
911+ }
912+ },
913+ "node_modules/@jridgewell/gen-mapping": {
914+ "version": "0.3.13",
915+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
916+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
917+ "dev": true,
918+ "license": "MIT",
919+ "dependencies": {
920+ "@jridgewell/sourcemap-codec": "^1.5.0",
921+ "@jridgewell/trace-mapping": "^0.3.24"
922+ }
923+ },
924+ "node_modules/@jridgewell/remapping": {
925+ "version": "2.3.5",
926+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
927+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
928+ "dev": true,
929+ "license": "MIT",
930+ "dependencies": {
931+ "@jridgewell/gen-mapping": "^0.3.5",
932+ "@jridgewell/trace-mapping": "^0.3.24"
933+ }
934+ },
935+ "node_modules/@jridgewell/resolve-uri": {
936+ "version": "3.1.2",
937+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
938+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
939+ "dev": true,
940+ "license": "MIT",
941+ "engines": {
942+ "node": ">=6.0.0"
943+ }
944+ },
945+ "node_modules/@jridgewell/sourcemap-codec": {
946+ "version": "1.6.0",
947+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
948+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
949+ "dev": true,
950+ "license": "MIT"
951+ },
952+ "node_modules/@jridgewell/trace-mapping": {
953+ "version": "0.3.31",
954+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
955+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
956+ "dev": true,
957+ "license": "MIT",
958+ "dependencies": {
959+ "@jridgewell/resolve-uri": "^3.1.0",
960+ "@jridgewell/sourcemap-codec": "^1.4.14"
961+ }
962+ },
963+ "node_modules/@monaco-editor/loader": {
964+ "version": "1.7.0",
965+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
966+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
967+ "license": "MIT",
968+ "dependencies": {
969+ "state-local": "^1.0.6"
970+ }
971+ },
972+ "node_modules/@monaco-editor/react": {
973+ "version": "4.7.0",
974+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
975+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
976+ "license": "MIT",
977+ "dependencies": {
978+ "@monaco-editor/loader": "^1.5.0"
979+ },
980+ "peerDependencies": {
981+ "monaco-editor": ">= 0.25.0 < 1",
982+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
983+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
984+ }
985+ },
986+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
987+ "version": "1.5.1",
988+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
989+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
990+ "cpu": [
991+ "x64"
992+ ],
993+ "dev": true,
994+ "libc": [
995+ "glibc"
996+ ],
997+ "license": "MIT",
998+ "optional": true,
999+ "os": [
1000+ "linux"
1001+ ],
1002+ "engines": {
1003+ "node": "^22.20 || ^24.12 || >=25"
1004+ }
1005+ },
1006+ "node_modules/@rolldown/pluginutils": {
1007+ "version": "1.0.0-rc.3",
1008+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
1009+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
1010+ "dev": true,
1011+ "license": "MIT"
1012+ },
1013+ "node_modules/@rollup/rollup-android-arm-eabi": {
1014+ "version": "4.63.3",
1015+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",
1016+ "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==",
1017+ "cpu": [
1018+ "arm"
1019+ ],
1020+ "dev": true,
1021+ "license": "MIT",
1022+ "optional": true,
1023+ "os": [
1024+ "android"
1025+ ]
1026+ },
1027+ "node_modules/@rollup/rollup-android-arm64": {
1028+ "version": "4.63.3",
1029+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz",
1030+ "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==",
1031+ "cpu": [
1032+ "arm64"
1033+ ],
1034+ "dev": true,
1035+ "license": "MIT",
1036+ "optional": true,
1037+ "os": [
1038+ "android"
1039+ ]
1040+ },
1041+ "node_modules/@rollup/rollup-darwin-arm64": {
1042+ "version": "4.63.3",
1043+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz",
1044+ "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==",
1045+ "cpu": [
1046+ "arm64"
1047+ ],
1048+ "dev": true,
1049+ "license": "MIT",
1050+ "optional": true,
1051+ "os": [
1052+ "darwin"
1053+ ]
1054+ },
1055+ "node_modules/@rollup/rollup-darwin-x64": {
1056+ "version": "4.63.3",
1057+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz",
1058+ "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==",
1059+ "cpu": [
1060+ "x64"
1061+ ],
1062+ "dev": true,
1063+ "license": "MIT",
1064+ "optional": true,
1065+ "os": [
1066+ "darwin"
1067+ ]
1068+ },
1069+ "node_modules/@rollup/rollup-freebsd-arm64": {
1070+ "version": "4.63.3",
1071+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz",
1072+ "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==",
1073+ "cpu": [
1074+ "arm64"
1075+ ],
1076+ "dev": true,
1077+ "license": "MIT",
1078+ "optional": true,
1079+ "os": [
1080+ "freebsd"
1081+ ]
1082+ },
1083+ "node_modules/@rollup/rollup-freebsd-x64": {
1084+ "version": "4.63.3",
1085+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz",
1086+ "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==",
1087+ "cpu": [
1088+ "x64"
1089+ ],
1090+ "dev": true,
1091+ "license": "MIT",
1092+ "optional": true,
1093+ "os": [
1094+ "freebsd"
1095+ ]
1096+ },
1097+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1098+ "version": "4.63.3",
1099+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz",
1100+ "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==",
1101+ "cpu": [
1102+ "arm"
1103+ ],
1104+ "dev": true,
1105+ "libc": [
1106+ "glibc"
1107+ ],
1108+ "license": "MIT",
1109+ "optional": true,
1110+ "os": [
1111+ "linux"
1112+ ]
1113+ },
1114+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1115+ "version": "4.63.3",
1116+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz",
1117+ "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==",
1118+ "cpu": [
1119+ "arm"
1120+ ],
1121+ "dev": true,
1122+ "libc": [
1123+ "musl"
1124+ ],
1125+ "license": "MIT",
1126+ "optional": true,
1127+ "os": [
1128+ "linux"
1129+ ]
1130+ },
1131+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
1132+ "version": "4.63.3",
1133+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz",
1134+ "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==",
1135+ "cpu": [
1136+ "arm64"
1137+ ],
1138+ "dev": true,
1139+ "libc": [
1140+ "glibc"
1141+ ],
1142+ "license": "MIT",
1143+ "optional": true,
1144+ "os": [
1145+ "linux"
1146+ ]
1147+ },
1148+ "node_modules/@rollup/rollup-linux-arm64-musl": {
1149+ "version": "4.63.3",
1150+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz",
1151+ "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==",
1152+ "cpu": [
1153+ "arm64"
1154+ ],
1155+ "dev": true,
1156+ "libc": [
1157+ "musl"
1158+ ],
1159+ "license": "MIT",
1160+ "optional": true,
1161+ "os": [
1162+ "linux"
1163+ ]
1164+ },
1165+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
1166+ "version": "4.63.3",
1167+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz",
1168+ "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==",
1169+ "cpu": [
1170+ "loong64"
1171+ ],
1172+ "dev": true,
1173+ "libc": [
1174+ "glibc"
1175+ ],
1176+ "license": "MIT",
1177+ "optional": true,
1178+ "os": [
1179+ "linux"
1180+ ]
1181+ },
1182+ "node_modules/@rollup/rollup-linux-loong64-musl": {
1183+ "version": "4.63.3",
1184+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz",
1185+ "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==",
1186+ "cpu": [
1187+ "loong64"
1188+ ],
1189+ "dev": true,
1190+ "libc": [
1191+ "musl"
1192+ ],
1193+ "license": "MIT",
1194+ "optional": true,
1195+ "os": [
1196+ "linux"
1197+ ]
1198+ },
1199+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1200+ "version": "4.63.3",
1201+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz",
1202+ "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==",
1203+ "cpu": [
1204+ "ppc64"
1205+ ],
1206+ "dev": true,
1207+ "libc": [
1208+ "glibc"
1209+ ],
1210+ "license": "MIT",
1211+ "optional": true,
1212+ "os": [
1213+ "linux"
1214+ ]
1215+ },
1216+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1217+ "version": "4.63.3",
1218+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz",
1219+ "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==",
1220+ "cpu": [
1221+ "ppc64"
1222+ ],
1223+ "dev": true,
1224+ "libc": [
1225+ "musl"
1226+ ],
1227+ "license": "MIT",
1228+ "optional": true,
1229+ "os": [
1230+ "linux"
1231+ ]
1232+ },
1233+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1234+ "version": "4.63.3",
1235+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz",
1236+ "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==",
1237+ "cpu": [
1238+ "riscv64"
1239+ ],
1240+ "dev": true,
1241+ "libc": [
1242+ "glibc"
1243+ ],
1244+ "license": "MIT",
1245+ "optional": true,
1246+ "os": [
1247+ "linux"
1248+ ]
1249+ },
1250+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1251+ "version": "4.63.3",
1252+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz",
1253+ "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==",
1254+ "cpu": [
1255+ "riscv64"
1256+ ],
1257+ "dev": true,
1258+ "libc": [
1259+ "musl"
1260+ ],
1261+ "license": "MIT",
1262+ "optional": true,
1263+ "os": [
1264+ "linux"
1265+ ]
1266+ },
1267+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1268+ "version": "4.63.3",
1269+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz",
1270+ "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==",
1271+ "cpu": [
1272+ "s390x"
1273+ ],
1274+ "dev": true,
1275+ "libc": [
1276+ "glibc"
1277+ ],
1278+ "license": "MIT",
1279+ "optional": true,
1280+ "os": [
1281+ "linux"
1282+ ]
1283+ },
1284+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1285+ "version": "4.63.3",
1286+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz",
1287+ "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==",
1288+ "cpu": [
1289+ "x64"
1290+ ],
1291+ "dev": true,
1292+ "libc": [
1293+ "glibc"
1294+ ],
1295+ "license": "MIT",
1296+ "optional": true,
1297+ "os": [
1298+ "linux"
1299+ ]
1300+ },
1301+ "node_modules/@rollup/rollup-linux-x64-musl": {
1302+ "version": "4.63.3",
1303+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz",
1304+ "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==",
1305+ "cpu": [
1306+ "x64"
1307+ ],
1308+ "dev": true,
1309+ "libc": [
1310+ "musl"
1311+ ],
1312+ "license": "MIT",
1313+ "optional": true,
1314+ "os": [
1315+ "linux"
1316+ ]
1317+ },
1318+ "node_modules/@rollup/rollup-openbsd-x64": {
1319+ "version": "4.63.3",
1320+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz",
1321+ "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==",
1322+ "cpu": [
1323+ "x64"
1324+ ],
1325+ "dev": true,
1326+ "license": "MIT",
1327+ "optional": true,
1328+ "os": [
1329+ "openbsd"
1330+ ]
1331+ },
1332+ "node_modules/@rollup/rollup-openharmony-arm64": {
1333+ "version": "4.63.3",
1334+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz",
1335+ "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==",
1336+ "cpu": [
1337+ "arm64"
1338+ ],
1339+ "dev": true,
1340+ "license": "MIT",
1341+ "optional": true,
1342+ "os": [
1343+ "openharmony"
1344+ ]
1345+ },
1346+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1347+ "version": "4.63.3",
1348+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz",
1349+ "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==",
1350+ "cpu": [
1351+ "arm64"
1352+ ],
1353+ "dev": true,
1354+ "license": "MIT",
1355+ "optional": true,
1356+ "os": [
1357+ "win32"
1358+ ]
1359+ },
1360+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1361+ "version": "4.63.3",
1362+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz",
1363+ "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==",
1364+ "cpu": [
1365+ "ia32"
1366+ ],
1367+ "dev": true,
1368+ "license": "MIT",
1369+ "optional": true,
1370+ "os": [
1371+ "win32"
1372+ ]
1373+ },
1374+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1375+ "version": "4.63.3",
1376+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz",
1377+ "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==",
1378+ "cpu": [
1379+ "x64"
1380+ ],
1381+ "dev": true,
1382+ "license": "MIT",
1383+ "optional": true,
1384+ "os": [
1385+ "win32"
1386+ ]
1387+ },
1388+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1389+ "version": "4.63.3",
1390+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz",
1391+ "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==",
1392+ "cpu": [
1393+ "x64"
1394+ ],
1395+ "dev": true,
1396+ "license": "MIT",
1397+ "optional": true,
1398+ "os": [
1399+ "win32"
1400+ ]
1401+ },
1402+ "node_modules/@testing-library/dom": {
1403+ "version": "10.4.2",
1404+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
1405+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
1406+ "dev": true,
1407+ "license": "MIT",
1408+ "peer": true,
1409+ "dependencies": {
1410+ "@babel/code-frame": "^7.10.4",
1411+ "@babel/runtime": "^7.12.5",
1412+ "@types/aria-query": "^5.0.1",
1413+ "aria-query": "5.3.0",
1414+ "dom-accessibility-api": "^0.5.9",
1415+ "lz-string": "^1.5.0",
1416+ "picocolors": "1.1.1",
1417+ "pretty-format": "^27.0.2"
1418+ },
1419+ "engines": {
1420+ "node": ">=18"
1421+ }
1422+ },
1423+ "node_modules/@testing-library/react": {
1424+ "version": "16.3.3",
1425+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
1426+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
1427+ "dev": true,
1428+ "license": "MIT",
1429+ "dependencies": {
1430+ "@babel/runtime": "^7.12.5"
1431+ },
1432+ "engines": {
1433+ "node": ">=18"
1434+ },
1435+ "peerDependencies": {
1436+ "@testing-library/dom": "^10.0.0",
1437+ "@types/react": "^18.0.0 || ^19.0.0",
1438+ "@types/react-dom": "^18.0.0 || ^19.0.0",
1439+ "react": "^18.0.0 || ^19.0.0",
1440+ "react-dom": "^18.0.0 || ^19.0.0"
1441+ },
1442+ "peerDependenciesMeta": {
1443+ "@types/react": {
1444+ "optional": true
1445+ },
1446+ "@types/react-dom": {
1447+ "optional": true
1448+ }
1449+ }
1450+ },
1451+ "node_modules/@types/aria-query": {
1452+ "version": "5.0.4",
1453+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
1454+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
1455+ "dev": true,
1456+ "license": "MIT",
1457+ "peer": true
1458+ },
1459+ "node_modules/@types/babel__core": {
1460+ "version": "7.20.5",
1461+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1462+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1463+ "dev": true,
1464+ "license": "MIT",
1465+ "dependencies": {
1466+ "@babel/parser": "^7.20.7",
1467+ "@babel/types": "^7.20.7",
1468+ "@types/babel__generator": "*",
1469+ "@types/babel__template": "*",
1470+ "@types/babel__traverse": "*"
1471+ }
1472+ },
1473+ "node_modules/@types/babel__generator": {
1474+ "version": "7.27.0",
1475+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1476+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1477+ "dev": true,
1478+ "license": "MIT",
1479+ "dependencies": {
1480+ "@babel/types": "^7.0.0"
1481+ }
1482+ },
1483+ "node_modules/@types/babel__template": {
1484+ "version": "7.4.4",
1485+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1486+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1487+ "dev": true,
1488+ "license": "MIT",
1489+ "dependencies": {
1490+ "@babel/parser": "^7.1.0",
1491+ "@babel/types": "^7.0.0"
1492+ }
1493+ },
1494+ "node_modules/@types/babel__traverse": {
1495+ "version": "7.28.0",
1496+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1497+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1498+ "dev": true,
1499+ "license": "MIT",
1500+ "dependencies": {
1501+ "@babel/types": "^7.28.2"
1502+ }
1503+ },
1504+ "node_modules/@types/chai": {
1505+ "version": "5.2.3",
1506+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
1507+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
1508+ "dev": true,
1509+ "license": "MIT",
1510+ "dependencies": {
1511+ "@types/deep-eql": "*",
1512+ "assertion-error": "^2.0.1"
1513+ }
1514+ },
1515+ "node_modules/@types/debug": {
1516+ "version": "4.1.13",
1517+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
1518+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
1519+ "license": "MIT",
1520+ "dependencies": {
1521+ "@types/ms": "*"
1522+ }
1523+ },
1524+ "node_modules/@types/deep-eql": {
1525+ "version": "4.0.2",
1526+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
1527+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
1528+ "dev": true,
1529+ "license": "MIT"
1530+ },
1531+ "node_modules/@types/estree": {
1532+ "version": "1.0.9",
1533+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1534+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1535+ "license": "MIT"
1536+ },
1537+ "node_modules/@types/estree-jsx": {
1538+ "version": "1.0.5",
1539+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
1540+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
1541+ "license": "MIT",
1542+ "dependencies": {
1543+ "@types/estree": "*"
1544+ }
1545+ },
1546+ "node_modules/@types/hast": {
1547+ "version": "3.0.5",
1548+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
1549+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
1550+ "license": "MIT",
1551+ "dependencies": {
1552+ "@types/unist": "*"
1553+ }
1554+ },
1555+ "node_modules/@types/mdast": {
1556+ "version": "4.0.4",
1557+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1558+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1559+ "license": "MIT",
1560+ "dependencies": {
1561+ "@types/unist": "*"
1562+ }
1563+ },
1564+ "node_modules/@types/ms": {
1565+ "version": "2.1.0",
1566+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1567+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1568+ "license": "MIT"
1569+ },
1570+ "node_modules/@types/react": {
1571+ "version": "19.3.0",
1572+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
1573+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
1574+ "license": "MIT",
1575+ "dependencies": {
1576+ "csstype": "^3.2.2"
1577+ }
1578+ },
1579+ "node_modules/@types/react-dom": {
1580+ "version": "19.3.0",
1581+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
1582+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
1583+ "dev": true,
1584+ "license": "MIT",
1585+ "peerDependencies": {
1586+ "@types/react": "^19.3.0"
1587+ }
1588+ },
1589+ "node_modules/@types/trusted-types": {
1590+ "version": "2.0.7",
1591+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1592+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1593+ "license": "MIT",
1594+ "optional": true
1595+ },
1596+ "node_modules/@types/unist": {
1597+ "version": "3.0.3",
1598+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1599+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1600+ "license": "MIT"
1601+ },
1602+ "node_modules/@ungap/structured-clone": {
1603+ "version": "1.4.0",
1604+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
1605+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
1606+ "license": "ISC"
1607+ },
1608+ "node_modules/@vitejs/plugin-react": {
1609+ "version": "5.2.0",
1610+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1611+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1612+ "dev": true,
1613+ "license": "MIT",
1614+ "dependencies": {
1615+ "@babel/core": "^7.29.0",
1616+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1617+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1618+ "@rolldown/pluginutils": "1.0.0-rc.3",
1619+ "@types/babel__core": "^7.20.5",
1620+ "react-refresh": "^0.18.0"
1621+ },
1622+ "engines": {
1623+ "node": "^20.19.0 || >=22.12.0"
1624+ },
1625+ "peerDependencies": {
1626+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1627+ }
1628+ },
1629+ "node_modules/@vitest/expect": {
1630+ "version": "3.2.7",
1631+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
1632+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
1633+ "dev": true,
1634+ "license": "MIT",
1635+ "dependencies": {
1636+ "@types/chai": "^5.2.2",
1637+ "@vitest/spy": "3.2.7",
1638+ "@vitest/utils": "3.2.7",
1639+ "chai": "^5.2.0",
1640+ "tinyrainbow": "^2.0.0"
1641+ },
1642+ "funding": {
1643+ "url": "https://opencollective.com/vitest"
1644+ }
1645+ },
1646+ "node_modules/@vitest/mocker": {
1647+ "version": "3.2.7",
1648+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
1649+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
1650+ "dev": true,
1651+ "license": "MIT",
1652+ "dependencies": {
1653+ "@vitest/spy": "3.2.7",
1654+ "estree-walker": "^3.0.3",
1655+ "magic-string": "^0.30.17"
1656+ },
1657+ "funding": {
1658+ "url": "https://opencollective.com/vitest"
1659+ },
1660+ "peerDependencies": {
1661+ "msw": "^2.4.9",
1662+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
1663+ },
1664+ "peerDependenciesMeta": {
1665+ "msw": {
1666+ "optional": true
1667+ },
1668+ "vite": {
1669+ "optional": true
1670+ }
1671+ }
1672+ },
1673+ "node_modules/@vitest/pretty-format": {
1674+ "version": "3.2.7",
1675+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
1676+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
1677+ "dev": true,
1678+ "license": "MIT",
1679+ "dependencies": {
1680+ "tinyrainbow": "^2.0.0"
1681+ },
1682+ "funding": {
1683+ "url": "https://opencollective.com/vitest"
1684+ }
1685+ },
1686+ "node_modules/@vitest/runner": {
1687+ "version": "3.2.7",
1688+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
1689+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
1690+ "dev": true,
1691+ "license": "MIT",
1692+ "dependencies": {
1693+ "@vitest/utils": "3.2.7",
1694+ "pathe": "^2.0.3",
1695+ "strip-literal": "^3.0.0"
1696+ },
1697+ "funding": {
1698+ "url": "https://opencollective.com/vitest"
1699+ }
1700+ },
1701+ "node_modules/@vitest/snapshot": {
1702+ "version": "3.2.7",
1703+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
1704+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
1705+ "dev": true,
1706+ "license": "MIT",
1707+ "dependencies": {
1708+ "@vitest/pretty-format": "3.2.7",
1709+ "magic-string": "^0.30.17",
1710+ "pathe": "^2.0.3"
1711+ },
1712+ "funding": {
1713+ "url": "https://opencollective.com/vitest"
1714+ }
1715+ },
1716+ "node_modules/@vitest/spy": {
1717+ "version": "3.2.7",
1718+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
1719+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
1720+ "dev": true,
1721+ "license": "MIT",
1722+ "dependencies": {
1723+ "tinyspy": "^4.0.3"
1724+ },
1725+ "funding": {
1726+ "url": "https://opencollective.com/vitest"
1727+ }
1728+ },
1729+ "node_modules/@vitest/utils": {
1730+ "version": "3.2.7",
1731+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
1732+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
1733+ "dev": true,
1734+ "license": "MIT",
1735+ "dependencies": {
1736+ "@vitest/pretty-format": "3.2.7",
1737+ "loupe": "^3.1.4",
1738+ "tinyrainbow": "^2.0.0"
1739+ },
1740+ "funding": {
1741+ "url": "https://opencollective.com/vitest"
1742+ }
1743+ },
1744+ "node_modules/@xterm/addon-fit": {
1745+ "version": "0.11.0",
1746+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
1747+ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
1748+ "license": "MIT"
1749+ },
1750+ "node_modules/@xterm/xterm": {
1751+ "version": "6.0.0",
1752+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
1753+ "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
1754+ "license": "MIT",
1755+ "workspaces": [
1756+ "addons/*"
1757+ ]
1758+ },
1759+ "node_modules/agent-base": {
1760+ "version": "7.1.4",
1761+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1762+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1763+ "dev": true,
1764+ "license": "MIT",
1765+ "engines": {
1766+ "node": ">= 14"
1767+ }
1768+ },
1769+ "node_modules/ansi-regex": {
1770+ "version": "5.0.1",
1771+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1772+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1773+ "dev": true,
1774+ "license": "MIT",
1775+ "peer": true,
1776+ "engines": {
1777+ "node": ">=8"
1778+ }
1779+ },
1780+ "node_modules/ansi-styles": {
1781+ "version": "5.2.0",
1782+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
1783+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
1784+ "dev": true,
1785+ "license": "MIT",
1786+ "peer": true,
1787+ "engines": {
1788+ "node": ">=10"
1789+ },
1790+ "funding": {
1791+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1792+ }
1793+ },
1794+ "node_modules/aria-query": {
1795+ "version": "5.3.0",
1796+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
1797+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
1798+ "dev": true,
1799+ "license": "Apache-2.0",
1800+ "peer": true,
1801+ "dependencies": {
1802+ "dequal": "^2.0.3"
1803+ }
1804+ },
1805+ "node_modules/assertion-error": {
1806+ "version": "2.0.1",
1807+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1808+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1809+ "dev": true,
1810+ "license": "MIT",
1811+ "engines": {
1812+ "node": ">=12"
1813+ }
1814+ },
1815+ "node_modules/bail": {
1816+ "version": "2.0.2",
1817+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
1818+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
1819+ "license": "MIT",
1820+ "funding": {
1821+ "type": "github",
1822+ "url": "https://github.com/sponsors/wooorm"
1823+ }
1824+ },
1825+ "node_modules/baseline-browser-mapping": {
1826+ "version": "2.11.25",
1827+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz",
1828+ "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==",
1829+ "dev": true,
1830+ "license": "Apache-2.0",
1831+ "bin": {
1832+ "baseline-browser-mapping": "dist/cli.cjs"
1833+ },
1834+ "engines": {
1835+ "node": ">=6.0.0"
1836+ }
1837+ },
1838+ "node_modules/browserslist": {
1839+ "version": "4.29.0",
1840+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",
1841+ "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",
1842+ "dev": true,
1843+ "funding": [
1844+ {
1845+ "type": "opencollective",
1846+ "url": "https://opencollective.com/browserslist"
1847+ },
1848+ {
1849+ "type": "tidelift",
1850+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1851+ },
1852+ {
1853+ "type": "github",
1854+ "url": "https://github.com/sponsors/ai"
1855+ }
1856+ ],
1857+ "license": "MIT",
1858+ "dependencies": {
1859+ "baseline-browser-mapping": "^2.11.23",
1860+ "caniuse-lite": "^1.0.30001810",
1861+ "electron-to-chromium": "^1.5.427",
1862+ "node-releases": "^2.0.55",
1863+ "update-browserslist-db": "^1.3.3"
1864+ },
1865+ "bin": {
1866+ "browserslist": "cli.js"
1867+ },
1868+ "engines": {
1869+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1870+ }
1871+ },
1872+ "node_modules/cac": {
1873+ "version": "6.7.14",
1874+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1875+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1876+ "dev": true,
1877+ "license": "MIT",
1878+ "engines": {
1879+ "node": ">=8"
1880+ }
1881+ },
1882+ "node_modules/caniuse-lite": {
1883+ "version": "1.0.30001810",
1884+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
1885+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
1886+ "dev": true,
1887+ "funding": [
1888+ {
1889+ "type": "opencollective",
1890+ "url": "https://opencollective.com/browserslist"
1891+ },
1892+ {
1893+ "type": "tidelift",
1894+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1895+ },
1896+ {
1897+ "type": "github",
1898+ "url": "https://github.com/sponsors/ai"
1899+ }
1900+ ],
1901+ "license": "CC-BY-4.0"
1902+ },
1903+ "node_modules/ccount": {
1904+ "version": "2.0.1",
1905+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1906+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1907+ "license": "MIT",
1908+ "funding": {
1909+ "type": "github",
1910+ "url": "https://github.com/sponsors/wooorm"
1911+ }
1912+ },
1913+ "node_modules/chai": {
1914+ "version": "5.3.3",
1915+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
1916+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
1917+ "dev": true,
1918+ "license": "MIT",
1919+ "dependencies": {
1920+ "assertion-error": "^2.0.1",
1921+ "check-error": "^2.1.1",
1922+ "deep-eql": "^5.0.1",
1923+ "loupe": "^3.1.0",
1924+ "pathval": "^2.0.0"
1925+ },
1926+ "engines": {
1927+ "node": ">=18"
1928+ }
1929+ },
1930+ "node_modules/character-entities": {
1931+ "version": "2.0.2",
1932+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
1933+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
1934+ "license": "MIT",
1935+ "funding": {
1936+ "type": "github",
1937+ "url": "https://github.com/sponsors/wooorm"
1938+ }
1939+ },
1940+ "node_modules/character-entities-html4": {
1941+ "version": "2.1.0",
1942+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
1943+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
1944+ "license": "MIT",
1945+ "funding": {
1946+ "type": "github",
1947+ "url": "https://github.com/sponsors/wooorm"
1948+ }
1949+ },
1950+ "node_modules/character-entities-legacy": {
1951+ "version": "3.0.0",
1952+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
1953+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
1954+ "license": "MIT",
1955+ "funding": {
1956+ "type": "github",
1957+ "url": "https://github.com/sponsors/wooorm"
1958+ }
1959+ },
1960+ "node_modules/character-reference-invalid": {
1961+ "version": "2.0.1",
1962+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
1963+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
1964+ "license": "MIT",
1965+ "funding": {
1966+ "type": "github",
1967+ "url": "https://github.com/sponsors/wooorm"
1968+ }
1969+ },
1970+ "node_modules/check-error": {
1971+ "version": "2.1.3",
1972+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
1973+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
1974+ "dev": true,
1975+ "license": "MIT",
1976+ "engines": {
1977+ "node": ">= 16"
1978+ }
1979+ },
1980+ "node_modules/comma-separated-tokens": {
1981+ "version": "2.0.3",
1982+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
1983+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
1984+ "license": "MIT",
1985+ "funding": {
1986+ "type": "github",
1987+ "url": "https://github.com/sponsors/wooorm"
1988+ }
1989+ },
1990+ "node_modules/convert-source-map": {
1991+ "version": "2.0.0",
1992+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1993+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1994+ "dev": true,
1995+ "license": "MIT"
1996+ },
1997+ "node_modules/cssstyle": {
1998+ "version": "4.6.0",
1999+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
2000+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
2001+ "dev": true,
2002+ "license": "MIT",
2003+ "dependencies": {
2004+ "@asamuzakjp/css-color": "^3.2.0",
2005+ "rrweb-cssom": "^0.8.0"
2006+ },
2007+ "engines": {
2008+ "node": ">=18"
2009+ }
2010+ },
2011+ "node_modules/csstype": {
2012+ "version": "3.2.3",
2013+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
2014+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
2015+ "license": "MIT"
2016+ },
2017+ "node_modules/data-urls": {
2018+ "version": "5.0.0",
2019+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
2020+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
2021+ "dev": true,
2022+ "license": "MIT",
2023+ "dependencies": {
2024+ "whatwg-mimetype": "^4.0.0",
2025+ "whatwg-url": "^14.0.0"
2026+ },
2027+ "engines": {
2028+ "node": ">=18"
2029+ }
2030+ },
2031+ "node_modules/debug": {
2032+ "version": "4.4.3",
2033+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2034+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
2035+ "license": "MIT",
2036+ "dependencies": {
2037+ "ms": "^2.1.3"
2038+ },
2039+ "engines": {
2040+ "node": ">=6.0"
2041+ },
2042+ "peerDependenciesMeta": {
2043+ "supports-color": {
2044+ "optional": true
2045+ }
2046+ }
2047+ },
2048+ "node_modules/decimal.js": {
2049+ "version": "10.6.0",
2050+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
2051+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
2052+ "dev": true,
2053+ "license": "MIT"
2054+ },
2055+ "node_modules/decode-named-character-reference": {
2056+ "version": "1.3.0",
2057+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
2058+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
2059+ "license": "MIT",
2060+ "dependencies": {
2061+ "character-entities": "^2.0.0"
2062+ },
2063+ "funding": {
2064+ "type": "github",
2065+ "url": "https://github.com/sponsors/wooorm"
2066+ }
2067+ },
2068+ "node_modules/deep-eql": {
2069+ "version": "5.0.2",
2070+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
2071+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
2072+ "dev": true,
2073+ "license": "MIT",
2074+ "engines": {
2075+ "node": ">=6"
2076+ }
2077+ },
2078+ "node_modules/dequal": {
2079+ "version": "2.0.3",
2080+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
2081+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2082+ "license": "MIT",
2083+ "engines": {
2084+ "node": ">=6"
2085+ }
2086+ },
2087+ "node_modules/devlop": {
2088+ "version": "1.1.0",
2089+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
2090+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
2091+ "license": "MIT",
2092+ "dependencies": {
2093+ "dequal": "^2.0.0"
2094+ },
2095+ "funding": {
2096+ "type": "github",
2097+ "url": "https://github.com/sponsors/wooorm"
2098+ }
2099+ },
2100+ "node_modules/dom-accessibility-api": {
2101+ "version": "0.5.16",
2102+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
2103+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
2104+ "dev": true,
2105+ "license": "MIT",
2106+ "peer": true
2107+ },
2108+ "node_modules/dompurify": {
2109+ "version": "3.4.8",
2110+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
2111+ "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
2112+ "license": "(MPL-2.0 OR Apache-2.0)",
2113+ "optionalDependencies": {
2114+ "@types/trusted-types": "^2.0.7"
2115+ }
2116+ },
2117+ "node_modules/electron-to-chromium": {
2118+ "version": "1.5.430",
2119+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz",
2120+ "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==",
2121+ "dev": true,
2122+ "license": "ISC"
2123+ },
2124+ "node_modules/entities": {
2125+ "version": "6.0.1",
2126+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
2127+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
2128+ "dev": true,
2129+ "license": "BSD-2-Clause",
2130+ "engines": {
2131+ "node": ">=0.12"
2132+ },
2133+ "funding": {
2134+ "url": "https://github.com/fb55/entities?sponsor=1"
2135+ }
2136+ },
2137+ "node_modules/es-module-lexer": {
2138+ "version": "1.7.0",
2139+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
2140+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
2141+ "dev": true,
2142+ "license": "MIT"
2143+ },
2144+ "node_modules/esbuild": {
2145+ "version": "0.28.2",
2146+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
2147+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
2148+ "dev": true,
2149+ "hasInstallScript": true,
2150+ "license": "MIT",
2151+ "bin": {
2152+ "esbuild": "bin/esbuild"
2153+ },
2154+ "engines": {
2155+ "node": ">=18"
2156+ },
2157+ "optionalDependencies": {
2158+ "@esbuild/aix-ppc64": "0.28.2",
2159+ "@esbuild/android-arm": "0.28.2",
2160+ "@esbuild/android-arm64": "0.28.2",
2161+ "@esbuild/android-x64": "0.28.2",
2162+ "@esbuild/darwin-arm64": "0.28.2",
2163+ "@esbuild/darwin-x64": "0.28.2",
2164+ "@esbuild/freebsd-arm64": "0.28.2",
2165+ "@esbuild/freebsd-x64": "0.28.2",
2166+ "@esbuild/linux-arm": "0.28.2",
2167+ "@esbuild/linux-arm64": "0.28.2",
2168+ "@esbuild/linux-ia32": "0.28.2",
2169+ "@esbuild/linux-loong64": "0.28.2",
2170+ "@esbuild/linux-mips64el": "0.28.2",
2171+ "@esbuild/linux-ppc64": "0.28.2",
2172+ "@esbuild/linux-riscv64": "0.28.2",
2173+ "@esbuild/linux-s390x": "0.28.2",
2174+ "@esbuild/linux-x64": "0.28.2",
2175+ "@esbuild/netbsd-arm64": "0.28.2",
2176+ "@esbuild/netbsd-x64": "0.28.2",
2177+ "@esbuild/openbsd-arm64": "0.28.2",
2178+ "@esbuild/openbsd-x64": "0.28.2",
2179+ "@esbuild/openharmony-arm64": "0.28.2",
2180+ "@esbuild/sunos-x64": "0.28.2",
2181+ "@esbuild/win32-arm64": "0.28.2",
2182+ "@esbuild/win32-ia32": "0.28.2",
2183+ "@esbuild/win32-x64": "0.28.2"
2184+ }
2185+ },
2186+ "node_modules/escalade": {
2187+ "version": "3.2.0",
2188+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
2189+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
2190+ "dev": true,
2191+ "license": "MIT",
2192+ "engines": {
2193+ "node": ">=6"
2194+ }
2195+ },
2196+ "node_modules/escape-string-regexp": {
2197+ "version": "5.0.0",
2198+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
2199+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
2200+ "license": "MIT",
2201+ "engines": {
2202+ "node": ">=12"
2203+ },
2204+ "funding": {
2205+ "url": "https://github.com/sponsors/sindresorhus"
2206+ }
2207+ },
2208+ "node_modules/estree-util-is-identifier-name": {
2209+ "version": "3.0.0",
2210+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
2211+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
2212+ "license": "MIT",
2213+ "funding": {
2214+ "type": "opencollective",
2215+ "url": "https://opencollective.com/unified"
2216+ }
2217+ },
2218+ "node_modules/estree-walker": {
2219+ "version": "3.0.3",
2220+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
2221+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
2222+ "dev": true,
2223+ "license": "MIT",
2224+ "dependencies": {
2225+ "@types/estree": "^1.0.0"
2226+ }
2227+ },
2228+ "node_modules/expect-type": {
2229+ "version": "1.4.0",
2230+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
2231+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
2232+ "dev": true,
2233+ "license": "Apache-2.0",
2234+ "engines": {
2235+ "node": ">=12.0.0"
2236+ }
2237+ },
2238+ "node_modules/extend": {
2239+ "version": "3.0.2",
2240+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
2241+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
2242+ "license": "MIT"
2243+ },
2244+ "node_modules/fdir": {
2245+ "version": "6.5.0",
2246+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2247+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2248+ "dev": true,
2249+ "license": "MIT",
2250+ "engines": {
2251+ "node": ">=12.0.0"
2252+ },
2253+ "peerDependencies": {
2254+ "picomatch": "^3 || ^4"
2255+ },
2256+ "peerDependenciesMeta": {
2257+ "picomatch": {
2258+ "optional": true
2259+ }
2260+ }
2261+ },
2262+ "node_modules/fsevents": {
2263+ "version": "2.3.3",
2264+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
2265+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2266+ "dev": true,
2267+ "hasInstallScript": true,
2268+ "license": "MIT",
2269+ "optional": true,
2270+ "os": [
2271+ "darwin"
2272+ ],
2273+ "engines": {
2274+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2275+ }
2276+ },
2277+ "node_modules/gensync": {
2278+ "version": "1.0.0-beta.2",
2279+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
2280+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
2281+ "dev": true,
2282+ "license": "MIT",
2283+ "engines": {
2284+ "node": ">=6.9.0"
2285+ }
2286+ },
2287+ "node_modules/hast-util-to-jsx-runtime": {
2288+ "version": "2.3.6",
2289+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
2290+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
2291+ "license": "MIT",
2292+ "dependencies": {
2293+ "@types/estree": "^1.0.0",
2294+ "@types/hast": "^3.0.0",
2295+ "@types/unist": "^3.0.0",
2296+ "comma-separated-tokens": "^2.0.0",
2297+ "devlop": "^1.0.0",
2298+ "estree-util-is-identifier-name": "^3.0.0",
2299+ "hast-util-whitespace": "^3.0.0",
2300+ "mdast-util-mdx-expression": "^2.0.0",
2301+ "mdast-util-mdx-jsx": "^3.0.0",
2302+ "mdast-util-mdxjs-esm": "^2.0.0",
2303+ "property-information": "^7.0.0",
2304+ "space-separated-tokens": "^2.0.0",
2305+ "style-to-js": "^1.0.0",
2306+ "unist-util-position": "^5.0.0",
2307+ "vfile-message": "^4.0.0"
2308+ },
2309+ "funding": {
2310+ "type": "opencollective",
2311+ "url": "https://opencollective.com/unified"
2312+ }
2313+ },
2314+ "node_modules/hast-util-whitespace": {
2315+ "version": "3.0.0",
2316+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
2317+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
2318+ "license": "MIT",
2319+ "dependencies": {
2320+ "@types/hast": "^3.0.0"
2321+ },
2322+ "funding": {
2323+ "type": "opencollective",
2324+ "url": "https://opencollective.com/unified"
2325+ }
2326+ },
2327+ "node_modules/html-encoding-sniffer": {
2328+ "version": "4.0.0",
2329+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
2330+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
2331+ "dev": true,
2332+ "license": "MIT",
2333+ "dependencies": {
2334+ "whatwg-encoding": "^3.1.1"
2335+ },
2336+ "engines": {
2337+ "node": ">=18"
2338+ }
2339+ },
2340+ "node_modules/html-url-attributes": {
2341+ "version": "3.0.1",
2342+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
2343+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
2344+ "license": "MIT",
2345+ "funding": {
2346+ "type": "opencollective",
2347+ "url": "https://opencollective.com/unified"
2348+ }
2349+ },
2350+ "node_modules/http-proxy-agent": {
2351+ "version": "7.0.2",
2352+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
2353+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
2354+ "dev": true,
2355+ "license": "MIT",
2356+ "dependencies": {
2357+ "agent-base": "^7.1.0",
2358+ "debug": "^4.3.4"
2359+ },
2360+ "engines": {
2361+ "node": ">= 14"
2362+ }
2363+ },
2364+ "node_modules/https-proxy-agent": {
2365+ "version": "7.0.6",
2366+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
2367+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
2368+ "dev": true,
2369+ "license": "MIT",
2370+ "dependencies": {
2371+ "agent-base": "^7.1.2",
2372+ "debug": "4"
2373+ },
2374+ "engines": {
2375+ "node": ">= 14"
2376+ }
2377+ },
2378+ "node_modules/iconv-lite": {
2379+ "version": "0.6.3",
2380+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
2381+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
2382+ "dev": true,
2383+ "license": "MIT",
2384+ "dependencies": {
2385+ "safer-buffer": ">= 2.1.2 < 3.0.0"
2386+ },
2387+ "engines": {
2388+ "node": ">=0.10.0"
2389+ }
2390+ },
2391+ "node_modules/inline-style-parser": {
2392+ "version": "0.2.7",
2393+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
2394+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
2395+ "license": "MIT"
2396+ },
2397+ "node_modules/is-alphabetical": {
2398+ "version": "2.0.1",
2399+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
2400+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
2401+ "license": "MIT",
2402+ "funding": {
2403+ "type": "github",
2404+ "url": "https://github.com/sponsors/wooorm"
2405+ }
2406+ },
2407+ "node_modules/is-alphanumerical": {
2408+ "version": "2.0.1",
2409+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
2410+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
2411+ "license": "MIT",
2412+ "dependencies": {
2413+ "is-alphabetical": "^2.0.0",
2414+ "is-decimal": "^2.0.0"
2415+ },
2416+ "funding": {
2417+ "type": "github",
2418+ "url": "https://github.com/sponsors/wooorm"
2419+ }
2420+ },
2421+ "node_modules/is-decimal": {
2422+ "version": "2.0.1",
2423+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
2424+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
2425+ "license": "MIT",
2426+ "funding": {
2427+ "type": "github",
2428+ "url": "https://github.com/sponsors/wooorm"
2429+ }
2430+ },
2431+ "node_modules/is-hexadecimal": {
2432+ "version": "2.0.1",
2433+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
2434+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
2435+ "license": "MIT",
2436+ "funding": {
2437+ "type": "github",
2438+ "url": "https://github.com/sponsors/wooorm"
2439+ }
2440+ },
2441+ "node_modules/is-plain-obj": {
2442+ "version": "4.1.0",
2443+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
2444+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
2445+ "license": "MIT",
2446+ "engines": {
2447+ "node": ">=12"
2448+ },
2449+ "funding": {
2450+ "url": "https://github.com/sponsors/sindresorhus"
2451+ }
2452+ },
2453+ "node_modules/is-potential-custom-element-name": {
2454+ "version": "1.0.1",
2455+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2456+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
2457+ "dev": true,
2458+ "license": "MIT"
2459+ },
2460+ "node_modules/js-tokens": {
2461+ "version": "4.0.0",
2462+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
2463+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
2464+ "dev": true,
2465+ "license": "MIT"
2466+ },
2467+ "node_modules/jsdom": {
2468+ "version": "26.1.0",
2469+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
2470+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
2471+ "dev": true,
2472+ "license": "MIT",
2473+ "dependencies": {
2474+ "cssstyle": "^4.2.1",
2475+ "data-urls": "^5.0.0",
2476+ "decimal.js": "^10.5.0",
2477+ "html-encoding-sniffer": "^4.0.0",
2478+ "http-proxy-agent": "^7.0.2",
2479+ "https-proxy-agent": "^7.0.6",
2480+ "is-potential-custom-element-name": "^1.0.1",
2481+ "nwsapi": "^2.2.16",
2482+ "parse5": "^7.2.1",
2483+ "rrweb-cssom": "^0.8.0",
2484+ "saxes": "^6.0.0",
2485+ "symbol-tree": "^3.2.4",
2486+ "tough-cookie": "^5.1.1",
2487+ "w3c-xmlserializer": "^5.0.0",
2488+ "webidl-conversions": "^7.0.0",
2489+ "whatwg-encoding": "^3.1.1",
2490+ "whatwg-mimetype": "^4.0.0",
2491+ "whatwg-url": "^14.1.1",
2492+ "ws": "^8.18.0",
2493+ "xml-name-validator": "^5.0.0"
2494+ },
2495+ "engines": {
2496+ "node": ">=18"
2497+ },
2498+ "peerDependencies": {
2499+ "canvas": "^3.0.0"
2500+ },
2501+ "peerDependenciesMeta": {
2502+ "canvas": {
2503+ "optional": true
2504+ }
2505+ }
2506+ },
2507+ "node_modules/jsesc": {
2508+ "version": "3.1.0",
2509+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
2510+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
2511+ "dev": true,
2512+ "license": "MIT",
2513+ "bin": {
2514+ "jsesc": "bin/jsesc"
2515+ },
2516+ "engines": {
2517+ "node": ">=6"
2518+ }
2519+ },
2520+ "node_modules/json5": {
2521+ "version": "2.2.3",
2522+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
2523+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
2524+ "dev": true,
2525+ "license": "MIT",
2526+ "bin": {
2527+ "json5": "lib/cli.js"
2528+ },
2529+ "engines": {
2530+ "node": ">=6"
2531+ }
2532+ },
2533+ "node_modules/longest-streak": {
2534+ "version": "3.1.0",
2535+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
2536+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
2537+ "license": "MIT",
2538+ "funding": {
2539+ "type": "github",
2540+ "url": "https://github.com/sponsors/wooorm"
2541+ }
2542+ },
2543+ "node_modules/loupe": {
2544+ "version": "3.2.1",
2545+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
2546+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
2547+ "dev": true,
2548+ "license": "MIT"
2549+ },
2550+ "node_modules/lru-cache": {
2551+ "version": "5.1.1",
2552+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2553+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2554+ "dev": true,
2555+ "license": "ISC",
2556+ "dependencies": {
2557+ "yallist": "^3.0.2"
2558+ }
2559+ },
2560+ "node_modules/lz-string": {
2561+ "version": "1.5.0",
2562+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
2563+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
2564+ "dev": true,
2565+ "license": "MIT",
2566+ "peer": true,
2567+ "bin": {
2568+ "lz-string": "bin/bin.js"
2569+ }
2570+ },
2571+ "node_modules/magic-string": {
2572+ "version": "0.30.21",
2573+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2574+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2575+ "dev": true,
2576+ "license": "MIT",
2577+ "dependencies": {
2578+ "@jridgewell/sourcemap-codec": "^1.5.5"
2579+ }
2580+ },
2581+ "node_modules/markdown-table": {
2582+ "version": "3.0.4",
2583+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
2584+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
2585+ "license": "MIT",
2586+ "funding": {
2587+ "type": "github",
2588+ "url": "https://github.com/sponsors/wooorm"
2589+ }
2590+ },
2591+ "node_modules/marked": {
2592+ "version": "14.0.0",
2593+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
2594+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
2595+ "license": "MIT",
2596+ "bin": {
2597+ "marked": "bin/marked.js"
2598+ },
2599+ "engines": {
2600+ "node": ">= 18"
2601+ }
2602+ },
2603+ "node_modules/mdast-util-find-and-replace": {
2604+ "version": "3.0.2",
2605+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
2606+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
2607+ "license": "MIT",
2608+ "dependencies": {
2609+ "@types/mdast": "^4.0.0",
2610+ "escape-string-regexp": "^5.0.0",
2611+ "unist-util-is": "^6.0.0",
2612+ "unist-util-visit-parents": "^6.0.0"
2613+ },
2614+ "funding": {
2615+ "type": "opencollective",
2616+ "url": "https://opencollective.com/unified"
2617+ }
2618+ },
2619+ "node_modules/mdast-util-from-markdown": {
2620+ "version": "2.0.3",
2621+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
2622+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
2623+ "license": "MIT",
2624+ "dependencies": {
2625+ "@types/mdast": "^4.0.0",
2626+ "@types/unist": "^3.0.0",
2627+ "decode-named-character-reference": "^1.0.0",
2628+ "devlop": "^1.0.0",
2629+ "mdast-util-to-string": "^4.0.0",
2630+ "micromark": "^4.0.0",
2631+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
2632+ "micromark-util-decode-string": "^2.0.0",
2633+ "micromark-util-normalize-identifier": "^2.0.0",
2634+ "micromark-util-symbol": "^2.0.0",
2635+ "micromark-util-types": "^2.0.0",
2636+ "unist-util-stringify-position": "^4.0.0"
2637+ },
2638+ "funding": {
2639+ "type": "opencollective",
2640+ "url": "https://opencollective.com/unified"
2641+ }
2642+ },
2643+ "node_modules/mdast-util-gfm": {
2644+ "version": "3.1.0",
2645+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
2646+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
2647+ "license": "MIT",
2648+ "dependencies": {
2649+ "mdast-util-from-markdown": "^2.0.0",
2650+ "mdast-util-gfm-autolink-literal": "^2.0.0",
2651+ "mdast-util-gfm-footnote": "^2.0.0",
2652+ "mdast-util-gfm-strikethrough": "^2.0.0",
2653+ "mdast-util-gfm-table": "^2.0.0",
2654+ "mdast-util-gfm-task-list-item": "^2.0.0",
2655+ "mdast-util-to-markdown": "^2.0.0"
2656+ },
2657+ "funding": {
2658+ "type": "opencollective",
2659+ "url": "https://opencollective.com/unified"
2660+ }
2661+ },
2662+ "node_modules/mdast-util-gfm-autolink-literal": {
2663+ "version": "2.0.1",
2664+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
2665+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
2666+ "license": "MIT",
2667+ "dependencies": {
2668+ "@types/mdast": "^4.0.0",
2669+ "ccount": "^2.0.0",
2670+ "devlop": "^1.0.0",
2671+ "mdast-util-find-and-replace": "^3.0.0",
2672+ "micromark-util-character": "^2.0.0"
2673+ },
2674+ "funding": {
2675+ "type": "opencollective",
2676+ "url": "https://opencollective.com/unified"
2677+ }
2678+ },
2679+ "node_modules/mdast-util-gfm-footnote": {
2680+ "version": "2.1.0",
2681+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
2682+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
2683+ "license": "MIT",
2684+ "dependencies": {
2685+ "@types/mdast": "^4.0.0",
2686+ "devlop": "^1.1.0",
2687+ "mdast-util-from-markdown": "^2.0.0",
2688+ "mdast-util-to-markdown": "^2.0.0",
2689+ "micromark-util-normalize-identifier": "^2.0.0"
2690+ },
2691+ "funding": {
2692+ "type": "opencollective",
2693+ "url": "https://opencollective.com/unified"
2694+ }
2695+ },
2696+ "node_modules/mdast-util-gfm-strikethrough": {
2697+ "version": "2.0.0",
2698+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
2699+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
2700+ "license": "MIT",
2701+ "dependencies": {
2702+ "@types/mdast": "^4.0.0",
2703+ "mdast-util-from-markdown": "^2.0.0",
2704+ "mdast-util-to-markdown": "^2.0.0"
2705+ },
2706+ "funding": {
2707+ "type": "opencollective",
2708+ "url": "https://opencollective.com/unified"
2709+ }
2710+ },
2711+ "node_modules/mdast-util-gfm-table": {
2712+ "version": "2.0.0",
2713+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
2714+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
2715+ "license": "MIT",
2716+ "dependencies": {
2717+ "@types/mdast": "^4.0.0",
2718+ "devlop": "^1.0.0",
2719+ "markdown-table": "^3.0.0",
2720+ "mdast-util-from-markdown": "^2.0.0",
2721+ "mdast-util-to-markdown": "^2.0.0"
2722+ },
2723+ "funding": {
2724+ "type": "opencollective",
2725+ "url": "https://opencollective.com/unified"
2726+ }
2727+ },
2728+ "node_modules/mdast-util-gfm-task-list-item": {
2729+ "version": "2.0.0",
2730+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
2731+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
2732+ "license": "MIT",
2733+ "dependencies": {
2734+ "@types/mdast": "^4.0.0",
2735+ "devlop": "^1.0.0",
2736+ "mdast-util-from-markdown": "^2.0.0",
2737+ "mdast-util-to-markdown": "^2.0.0"
2738+ },
2739+ "funding": {
2740+ "type": "opencollective",
2741+ "url": "https://opencollective.com/unified"
2742+ }
2743+ },
2744+ "node_modules/mdast-util-mdx-expression": {
2745+ "version": "2.0.1",
2746+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
2747+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
2748+ "license": "MIT",
2749+ "dependencies": {
2750+ "@types/estree-jsx": "^1.0.0",
2751+ "@types/hast": "^3.0.0",
2752+ "@types/mdast": "^4.0.0",
2753+ "devlop": "^1.0.0",
2754+ "mdast-util-from-markdown": "^2.0.0",
2755+ "mdast-util-to-markdown": "^2.0.0"
2756+ },
2757+ "funding": {
2758+ "type": "opencollective",
2759+ "url": "https://opencollective.com/unified"
2760+ }
2761+ },
2762+ "node_modules/mdast-util-mdx-jsx": {
2763+ "version": "3.2.0",
2764+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
2765+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
2766+ "license": "MIT",
2767+ "dependencies": {
2768+ "@types/estree-jsx": "^1.0.0",
2769+ "@types/hast": "^3.0.0",
2770+ "@types/mdast": "^4.0.0",
2771+ "@types/unist": "^3.0.0",
2772+ "ccount": "^2.0.0",
2773+ "devlop": "^1.1.0",
2774+ "mdast-util-from-markdown": "^2.0.0",
2775+ "mdast-util-to-markdown": "^2.0.0",
2776+ "parse-entities": "^4.0.0",
2777+ "stringify-entities": "^4.0.0",
2778+ "unist-util-stringify-position": "^4.0.0",
2779+ "vfile-message": "^4.0.0"
2780+ },
2781+ "funding": {
2782+ "type": "opencollective",
2783+ "url": "https://opencollective.com/unified"
2784+ }
2785+ },
2786+ "node_modules/mdast-util-mdxjs-esm": {
2787+ "version": "2.0.1",
2788+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
2789+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
2790+ "license": "MIT",
2791+ "dependencies": {
2792+ "@types/estree-jsx": "^1.0.0",
2793+ "@types/hast": "^3.0.0",
2794+ "@types/mdast": "^4.0.0",
2795+ "devlop": "^1.0.0",
2796+ "mdast-util-from-markdown": "^2.0.0",
2797+ "mdast-util-to-markdown": "^2.0.0"
2798+ },
2799+ "funding": {
2800+ "type": "opencollective",
2801+ "url": "https://opencollective.com/unified"
2802+ }
2803+ },
2804+ "node_modules/mdast-util-phrasing": {
2805+ "version": "4.1.0",
2806+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
2807+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
2808+ "license": "MIT",
2809+ "dependencies": {
2810+ "@types/mdast": "^4.0.0",
2811+ "unist-util-is": "^6.0.0"
2812+ },
2813+ "funding": {
2814+ "type": "opencollective",
2815+ "url": "https://opencollective.com/unified"
2816+ }
2817+ },
2818+ "node_modules/mdast-util-to-hast": {
2819+ "version": "13.2.1",
2820+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
2821+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
2822+ "license": "MIT",
2823+ "dependencies": {
2824+ "@types/hast": "^3.0.0",
2825+ "@types/mdast": "^4.0.0",
2826+ "@ungap/structured-clone": "^1.0.0",
2827+ "devlop": "^1.0.0",
2828+ "micromark-util-sanitize-uri": "^2.0.0",
2829+ "trim-lines": "^3.0.0",
2830+ "unist-util-position": "^5.0.0",
2831+ "unist-util-visit": "^5.0.0",
2832+ "vfile": "^6.0.0"
2833+ },
2834+ "funding": {
2835+ "type": "opencollective",
2836+ "url": "https://opencollective.com/unified"
2837+ }
2838+ },
2839+ "node_modules/mdast-util-to-markdown": {
2840+ "version": "2.1.2",
2841+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
2842+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
2843+ "license": "MIT",
2844+ "dependencies": {
2845+ "@types/mdast": "^4.0.0",
2846+ "@types/unist": "^3.0.0",
2847+ "longest-streak": "^3.0.0",
2848+ "mdast-util-phrasing": "^4.0.0",
2849+ "mdast-util-to-string": "^4.0.0",
2850+ "micromark-util-classify-character": "^2.0.0",
2851+ "micromark-util-decode-string": "^2.0.0",
2852+ "unist-util-visit": "^5.0.0",
2853+ "zwitch": "^2.0.0"
2854+ },
2855+ "funding": {
2856+ "type": "opencollective",
2857+ "url": "https://opencollective.com/unified"
2858+ }
2859+ },
2860+ "node_modules/mdast-util-to-string": {
2861+ "version": "4.0.0",
2862+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
2863+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
2864+ "license": "MIT",
2865+ "dependencies": {
2866+ "@types/mdast": "^4.0.0"
2867+ },
2868+ "funding": {
2869+ "type": "opencollective",
2870+ "url": "https://opencollective.com/unified"
2871+ }
2872+ },
2873+ "node_modules/micromark": {
2874+ "version": "4.0.2",
2875+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
2876+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
2877+ "funding": [
2878+ {
2879+ "type": "GitHub Sponsors",
2880+ "url": "https://github.com/sponsors/unifiedjs"
2881+ },
2882+ {
2883+ "type": "OpenCollective",
2884+ "url": "https://opencollective.com/unified"
2885+ }
2886+ ],
2887+ "license": "MIT",
2888+ "dependencies": {
2889+ "@types/debug": "^4.0.0",
2890+ "debug": "^4.0.0",
2891+ "decode-named-character-reference": "^1.0.0",
2892+ "devlop": "^1.0.0",
2893+ "micromark-core-commonmark": "^2.0.0",
2894+ "micromark-factory-space": "^2.0.0",
2895+ "micromark-util-character": "^2.0.0",
2896+ "micromark-util-chunked": "^2.0.0",
2897+ "micromark-util-combine-extensions": "^2.0.0",
2898+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
2899+ "micromark-util-encode": "^2.0.0",
2900+ "micromark-util-normalize-identifier": "^2.0.0",
2901+ "micromark-util-resolve-all": "^2.0.0",
2902+ "micromark-util-sanitize-uri": "^2.0.0",
2903+ "micromark-util-subtokenize": "^2.0.0",
2904+ "micromark-util-symbol": "^2.0.0",
2905+ "micromark-util-types": "^2.0.0"
2906+ }
2907+ },
2908+ "node_modules/micromark-core-commonmark": {
2909+ "version": "2.0.3",
2910+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
2911+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
2912+ "funding": [
2913+ {
2914+ "type": "GitHub Sponsors",
2915+ "url": "https://github.com/sponsors/unifiedjs"
2916+ },
2917+ {
2918+ "type": "OpenCollective",
2919+ "url": "https://opencollective.com/unified"
2920+ }
2921+ ],
2922+ "license": "MIT",
2923+ "dependencies": {
2924+ "decode-named-character-reference": "^1.0.0",
2925+ "devlop": "^1.0.0",
2926+ "micromark-factory-destination": "^2.0.0",
2927+ "micromark-factory-label": "^2.0.0",
2928+ "micromark-factory-space": "^2.0.0",
2929+ "micromark-factory-title": "^2.0.0",
2930+ "micromark-factory-whitespace": "^2.0.0",
2931+ "micromark-util-character": "^2.0.0",
2932+ "micromark-util-chunked": "^2.0.0",
2933+ "micromark-util-classify-character": "^2.0.0",
2934+ "micromark-util-html-tag-name": "^2.0.0",
2935+ "micromark-util-normalize-identifier": "^2.0.0",
2936+ "micromark-util-resolve-all": "^2.0.0",
2937+ "micromark-util-subtokenize": "^2.0.0",
2938+ "micromark-util-symbol": "^2.0.0",
2939+ "micromark-util-types": "^2.0.0"
2940+ }
2941+ },
2942+ "node_modules/micromark-extension-gfm": {
2943+ "version": "3.0.0",
2944+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
2945+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
2946+ "license": "MIT",
2947+ "dependencies": {
2948+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
2949+ "micromark-extension-gfm-footnote": "^2.0.0",
2950+ "micromark-extension-gfm-strikethrough": "^2.0.0",
2951+ "micromark-extension-gfm-table": "^2.0.0",
2952+ "micromark-extension-gfm-tagfilter": "^2.0.0",
2953+ "micromark-extension-gfm-task-list-item": "^2.0.0",
2954+ "micromark-util-combine-extensions": "^2.0.0",
2955+ "micromark-util-types": "^2.0.0"
2956+ },
2957+ "funding": {
2958+ "type": "opencollective",
2959+ "url": "https://opencollective.com/unified"
2960+ }
2961+ },
2962+ "node_modules/micromark-extension-gfm-autolink-literal": {
2963+ "version": "2.1.0",
2964+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
2965+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
2966+ "license": "MIT",
2967+ "dependencies": {
2968+ "micromark-util-character": "^2.0.0",
2969+ "micromark-util-sanitize-uri": "^2.0.0",
2970+ "micromark-util-symbol": "^2.0.0",
2971+ "micromark-util-types": "^2.0.0"
2972+ },
2973+ "funding": {
2974+ "type": "opencollective",
2975+ "url": "https://opencollective.com/unified"
2976+ }
2977+ },
2978+ "node_modules/micromark-extension-gfm-footnote": {
2979+ "version": "2.1.0",
2980+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
2981+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
2982+ "license": "MIT",
2983+ "dependencies": {
2984+ "devlop": "^1.0.0",
2985+ "micromark-core-commonmark": "^2.0.0",
2986+ "micromark-factory-space": "^2.0.0",
2987+ "micromark-util-character": "^2.0.0",
2988+ "micromark-util-normalize-identifier": "^2.0.0",
2989+ "micromark-util-sanitize-uri": "^2.0.0",
2990+ "micromark-util-symbol": "^2.0.0",
2991+ "micromark-util-types": "^2.0.0"
2992+ },
2993+ "funding": {
2994+ "type": "opencollective",
2995+ "url": "https://opencollective.com/unified"
2996+ }
2997+ },
2998+ "node_modules/micromark-extension-gfm-strikethrough": {
2999+ "version": "2.1.0",
3000+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
3001+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
3002+ "license": "MIT",
3003+ "dependencies": {
3004+ "devlop": "^1.0.0",
3005+ "micromark-util-chunked": "^2.0.0",
3006+ "micromark-util-classify-character": "^2.0.0",
3007+ "micromark-util-resolve-all": "^2.0.0",
3008+ "micromark-util-symbol": "^2.0.0",
3009+ "micromark-util-types": "^2.0.0"
3010+ },
3011+ "funding": {
3012+ "type": "opencollective",
3013+ "url": "https://opencollective.com/unified"
3014+ }
3015+ },
3016+ "node_modules/micromark-extension-gfm-table": {
3017+ "version": "2.1.2",
3018+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz",
3019+ "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",
3020+ "license": "MIT",
3021+ "dependencies": {
3022+ "devlop": "^1.0.0",
3023+ "micromark-factory-space": "^2.0.0",
3024+ "micromark-util-character": "^2.0.0",
3025+ "micromark-util-symbol": "^2.0.0",
3026+ "micromark-util-types": "^2.0.0"
3027+ },
3028+ "funding": {
3029+ "type": "opencollective",
3030+ "url": "https://opencollective.com/unified"
3031+ }
3032+ },
3033+ "node_modules/micromark-extension-gfm-tagfilter": {
3034+ "version": "2.0.0",
3035+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
3036+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
3037+ "license": "MIT",
3038+ "dependencies": {
3039+ "micromark-util-types": "^2.0.0"
3040+ },
3041+ "funding": {
3042+ "type": "opencollective",
3043+ "url": "https://opencollective.com/unified"
3044+ }
3045+ },
3046+ "node_modules/micromark-extension-gfm-task-list-item": {
3047+ "version": "2.1.0",
3048+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
3049+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
3050+ "license": "MIT",
3051+ "dependencies": {
3052+ "devlop": "^1.0.0",
3053+ "micromark-factory-space": "^2.0.0",
3054+ "micromark-util-character": "^2.0.0",
3055+ "micromark-util-symbol": "^2.0.0",
3056+ "micromark-util-types": "^2.0.0"
3057+ },
3058+ "funding": {
3059+ "type": "opencollective",
3060+ "url": "https://opencollective.com/unified"
3061+ }
3062+ },
3063+ "node_modules/micromark-factory-destination": {
3064+ "version": "2.0.1",
3065+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
3066+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
3067+ "funding": [
3068+ {
3069+ "type": "GitHub Sponsors",
3070+ "url": "https://github.com/sponsors/unifiedjs"
3071+ },
3072+ {
3073+ "type": "OpenCollective",
3074+ "url": "https://opencollective.com/unified"
3075+ }
3076+ ],
3077+ "license": "MIT",
3078+ "dependencies": {
3079+ "micromark-util-character": "^2.0.0",
3080+ "micromark-util-symbol": "^2.0.0",
3081+ "micromark-util-types": "^2.0.0"
3082+ }
3083+ },
3084+ "node_modules/micromark-factory-label": {
3085+ "version": "2.0.1",
3086+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
3087+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
3088+ "funding": [
3089+ {
3090+ "type": "GitHub Sponsors",
3091+ "url": "https://github.com/sponsors/unifiedjs"
3092+ },
3093+ {
3094+ "type": "OpenCollective",
3095+ "url": "https://opencollective.com/unified"
3096+ }
3097+ ],
3098+ "license": "MIT",
3099+ "dependencies": {
3100+ "devlop": "^1.0.0",
3101+ "micromark-util-character": "^2.0.0",
3102+ "micromark-util-symbol": "^2.0.0",
3103+ "micromark-util-types": "^2.0.0"
3104+ }
3105+ },
3106+ "node_modules/micromark-factory-space": {
3107+ "version": "2.0.1",
3108+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
3109+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
3110+ "funding": [
3111+ {
3112+ "type": "GitHub Sponsors",
3113+ "url": "https://github.com/sponsors/unifiedjs"
3114+ },
3115+ {
3116+ "type": "OpenCollective",
3117+ "url": "https://opencollective.com/unified"
3118+ }
3119+ ],
3120+ "license": "MIT",
3121+ "dependencies": {
3122+ "micromark-util-character": "^2.0.0",
3123+ "micromark-util-types": "^2.0.0"
3124+ }
3125+ },
3126+ "node_modules/micromark-factory-title": {
3127+ "version": "2.0.1",
3128+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
3129+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
3130+ "funding": [
3131+ {
3132+ "type": "GitHub Sponsors",
3133+ "url": "https://github.com/sponsors/unifiedjs"
3134+ },
3135+ {
3136+ "type": "OpenCollective",
3137+ "url": "https://opencollective.com/unified"
3138+ }
3139+ ],
3140+ "license": "MIT",
3141+ "dependencies": {
3142+ "micromark-factory-space": "^2.0.0",
3143+ "micromark-util-character": "^2.0.0",
3144+ "micromark-util-symbol": "^2.0.0",
3145+ "micromark-util-types": "^2.0.0"
3146+ }
3147+ },
3148+ "node_modules/micromark-factory-whitespace": {
3149+ "version": "2.0.1",
3150+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
3151+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
3152+ "funding": [
3153+ {
3154+ "type": "GitHub Sponsors",
3155+ "url": "https://github.com/sponsors/unifiedjs"
3156+ },
3157+ {
3158+ "type": "OpenCollective",
3159+ "url": "https://opencollective.com/unified"
3160+ }
3161+ ],
3162+ "license": "MIT",
3163+ "dependencies": {
3164+ "micromark-factory-space": "^2.0.0",
3165+ "micromark-util-character": "^2.0.0",
3166+ "micromark-util-symbol": "^2.0.0",
3167+ "micromark-util-types": "^2.0.0"
3168+ }
3169+ },
3170+ "node_modules/micromark-util-character": {
3171+ "version": "2.1.1",
3172+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
3173+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
3174+ "funding": [
3175+ {
3176+ "type": "GitHub Sponsors",
3177+ "url": "https://github.com/sponsors/unifiedjs"
3178+ },
3179+ {
3180+ "type": "OpenCollective",
3181+ "url": "https://opencollective.com/unified"
3182+ }
3183+ ],
3184+ "license": "MIT",
3185+ "dependencies": {
3186+ "micromark-util-symbol": "^2.0.0",
3187+ "micromark-util-types": "^2.0.0"
3188+ }
3189+ },
3190+ "node_modules/micromark-util-chunked": {
3191+ "version": "2.0.1",
3192+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
3193+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
3194+ "funding": [
3195+ {
3196+ "type": "GitHub Sponsors",
3197+ "url": "https://github.com/sponsors/unifiedjs"
3198+ },
3199+ {
3200+ "type": "OpenCollective",
3201+ "url": "https://opencollective.com/unified"
3202+ }
3203+ ],
3204+ "license": "MIT",
3205+ "dependencies": {
3206+ "micromark-util-symbol": "^2.0.0"
3207+ }
3208+ },
3209+ "node_modules/micromark-util-classify-character": {
3210+ "version": "2.0.1",
3211+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
3212+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
3213+ "funding": [
3214+ {
3215+ "type": "GitHub Sponsors",
3216+ "url": "https://github.com/sponsors/unifiedjs"
3217+ },
3218+ {
3219+ "type": "OpenCollective",
3220+ "url": "https://opencollective.com/unified"
3221+ }
3222+ ],
3223+ "license": "MIT",
3224+ "dependencies": {
3225+ "micromark-util-character": "^2.0.0",
3226+ "micromark-util-symbol": "^2.0.0",
3227+ "micromark-util-types": "^2.0.0"
3228+ }
3229+ },
3230+ "node_modules/micromark-util-combine-extensions": {
3231+ "version": "2.0.1",
3232+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
3233+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
3234+ "funding": [
3235+ {
3236+ "type": "GitHub Sponsors",
3237+ "url": "https://github.com/sponsors/unifiedjs"
3238+ },
3239+ {
3240+ "type": "OpenCollective",
3241+ "url": "https://opencollective.com/unified"
3242+ }
3243+ ],
3244+ "license": "MIT",
3245+ "dependencies": {
3246+ "micromark-util-chunked": "^2.0.0",
3247+ "micromark-util-types": "^2.0.0"
3248+ }
3249+ },
3250+ "node_modules/micromark-util-decode-numeric-character-reference": {
3251+ "version": "2.0.2",
3252+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
3253+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
3254+ "funding": [
3255+ {
3256+ "type": "GitHub Sponsors",
3257+ "url": "https://github.com/sponsors/unifiedjs"
3258+ },
3259+ {
3260+ "type": "OpenCollective",
3261+ "url": "https://opencollective.com/unified"
3262+ }
3263+ ],
3264+ "license": "MIT",
3265+ "dependencies": {
3266+ "micromark-util-symbol": "^2.0.0"
3267+ }
3268+ },
3269+ "node_modules/micromark-util-decode-string": {
3270+ "version": "2.0.1",
3271+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
3272+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
3273+ "funding": [
3274+ {
3275+ "type": "GitHub Sponsors",
3276+ "url": "https://github.com/sponsors/unifiedjs"
3277+ },
3278+ {
3279+ "type": "OpenCollective",
3280+ "url": "https://opencollective.com/unified"
3281+ }
3282+ ],
3283+ "license": "MIT",
3284+ "dependencies": {
3285+ "decode-named-character-reference": "^1.0.0",
3286+ "micromark-util-character": "^2.0.0",
3287+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
3288+ "micromark-util-symbol": "^2.0.0"
3289+ }
3290+ },
3291+ "node_modules/micromark-util-encode": {
3292+ "version": "2.0.1",
3293+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
3294+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
3295+ "funding": [
3296+ {
3297+ "type": "GitHub Sponsors",
3298+ "url": "https://github.com/sponsors/unifiedjs"
3299+ },
3300+ {
3301+ "type": "OpenCollective",
3302+ "url": "https://opencollective.com/unified"
3303+ }
3304+ ],
3305+ "license": "MIT"
3306+ },
3307+ "node_modules/micromark-util-html-tag-name": {
3308+ "version": "2.0.1",
3309+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
3310+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
3311+ "funding": [
3312+ {
3313+ "type": "GitHub Sponsors",
3314+ "url": "https://github.com/sponsors/unifiedjs"
3315+ },
3316+ {
3317+ "type": "OpenCollective",
3318+ "url": "https://opencollective.com/unified"
3319+ }
3320+ ],
3321+ "license": "MIT"
3322+ },
3323+ "node_modules/micromark-util-normalize-identifier": {
3324+ "version": "2.0.1",
3325+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
3326+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
3327+ "funding": [
3328+ {
3329+ "type": "GitHub Sponsors",
3330+ "url": "https://github.com/sponsors/unifiedjs"
3331+ },
3332+ {
3333+ "type": "OpenCollective",
3334+ "url": "https://opencollective.com/unified"
3335+ }
3336+ ],
3337+ "license": "MIT",
3338+ "dependencies": {
3339+ "micromark-util-symbol": "^2.0.0"
3340+ }
3341+ },
3342+ "node_modules/micromark-util-resolve-all": {
3343+ "version": "2.0.1",
3344+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
3345+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
3346+ "funding": [
3347+ {
3348+ "type": "GitHub Sponsors",
3349+ "url": "https://github.com/sponsors/unifiedjs"
3350+ },
3351+ {
3352+ "type": "OpenCollective",
3353+ "url": "https://opencollective.com/unified"
3354+ }
3355+ ],
3356+ "license": "MIT",
3357+ "dependencies": {
3358+ "micromark-util-types": "^2.0.0"
3359+ }
3360+ },
3361+ "node_modules/micromark-util-sanitize-uri": {
3362+ "version": "2.0.1",
3363+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
3364+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
3365+ "funding": [
3366+ {
3367+ "type": "GitHub Sponsors",
3368+ "url": "https://github.com/sponsors/unifiedjs"
3369+ },
3370+ {
3371+ "type": "OpenCollective",
3372+ "url": "https://opencollective.com/unified"
3373+ }
3374+ ],
3375+ "license": "MIT",
3376+ "dependencies": {
3377+ "micromark-util-character": "^2.0.0",
3378+ "micromark-util-encode": "^2.0.0",
3379+ "micromark-util-symbol": "^2.0.0"
3380+ }
3381+ },
3382+ "node_modules/micromark-util-subtokenize": {
3383+ "version": "2.1.0",
3384+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
3385+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
3386+ "funding": [
3387+ {
3388+ "type": "GitHub Sponsors",
3389+ "url": "https://github.com/sponsors/unifiedjs"
3390+ },
3391+ {
3392+ "type": "OpenCollective",
3393+ "url": "https://opencollective.com/unified"
3394+ }
3395+ ],
3396+ "license": "MIT",
3397+ "dependencies": {
3398+ "devlop": "^1.0.0",
3399+ "micromark-util-chunked": "^2.0.0",
3400+ "micromark-util-symbol": "^2.0.0",
3401+ "micromark-util-types": "^2.0.0"
3402+ }
3403+ },
3404+ "node_modules/micromark-util-symbol": {
3405+ "version": "2.0.1",
3406+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
3407+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
3408+ "funding": [
3409+ {
3410+ "type": "GitHub Sponsors",
3411+ "url": "https://github.com/sponsors/unifiedjs"
3412+ },
3413+ {
3414+ "type": "OpenCollective",
3415+ "url": "https://opencollective.com/unified"
3416+ }
3417+ ],
3418+ "license": "MIT"
3419+ },
3420+ "node_modules/micromark-util-types": {
3421+ "version": "2.0.2",
3422+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
3423+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
3424+ "funding": [
3425+ {
3426+ "type": "GitHub Sponsors",
3427+ "url": "https://github.com/sponsors/unifiedjs"
3428+ },
3429+ {
3430+ "type": "OpenCollective",
3431+ "url": "https://opencollective.com/unified"
3432+ }
3433+ ],
3434+ "license": "MIT"
3435+ },
3436+ "node_modules/monaco-editor": {
3437+ "version": "0.56.0",
3438+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz",
3439+ "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==",
3440+ "license": "MIT",
3441+ "dependencies": {
3442+ "dompurify": "3.4.8",
3443+ "marked": "14.0.0"
3444+ }
3445+ },
3446+ "node_modules/ms": {
3447+ "version": "2.1.3",
3448+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
3449+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
3450+ "license": "MIT"
3451+ },
3452+ "node_modules/nanoid": {
3453+ "version": "3.3.19",
3454+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
3455+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
3456+ "dev": true,
3457+ "funding": [
3458+ {
3459+ "type": "github",
3460+ "url": "https://github.com/sponsors/ai"
3461+ }
3462+ ],
3463+ "license": "MIT",
3464+ "bin": {
3465+ "nanoid": "bin/nanoid.cjs"
3466+ },
3467+ "engines": {
3468+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
3469+ }
3470+ },
3471+ "node_modules/node-releases": {
3472+ "version": "2.0.56",
3473+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz",
3474+ "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",
3475+ "dev": true,
3476+ "license": "MIT",
3477+ "engines": {
3478+ "node": ">=18"
3479+ }
3480+ },
3481+ "node_modules/nwsapi": {
3482+ "version": "2.2.27",
3483+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
3484+ "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
3485+ "dev": true,
3486+ "license": "MIT"
3487+ },
3488+ "node_modules/parse-entities": {
3489+ "version": "4.0.2",
3490+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
3491+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
3492+ "license": "MIT",
3493+ "dependencies": {
3494+ "@types/unist": "^2.0.0",
3495+ "character-entities-legacy": "^3.0.0",
3496+ "character-reference-invalid": "^2.0.0",
3497+ "decode-named-character-reference": "^1.0.0",
3498+ "is-alphanumerical": "^2.0.0",
3499+ "is-decimal": "^2.0.0",
3500+ "is-hexadecimal": "^2.0.0"
3501+ },
3502+ "funding": {
3503+ "type": "github",
3504+ "url": "https://github.com/sponsors/wooorm"
3505+ }
3506+ },
3507+ "node_modules/parse-entities/node_modules/@types/unist": {
3508+ "version": "2.0.11",
3509+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
3510+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
3511+ "license": "MIT"
3512+ },
3513+ "node_modules/parse5": {
3514+ "version": "7.3.0",
3515+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
3516+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
3517+ "dev": true,
3518+ "license": "MIT",
3519+ "dependencies": {
3520+ "entities": "^6.0.0"
3521+ },
3522+ "funding": {
3523+ "url": "https://github.com/inikulin/parse5?sponsor=1"
3524+ }
3525+ },
3526+ "node_modules/pathe": {
3527+ "version": "2.0.3",
3528+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
3529+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
3530+ "dev": true,
3531+ "license": "MIT"
3532+ },
3533+ "node_modules/pathval": {
3534+ "version": "2.0.1",
3535+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
3536+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
3537+ "dev": true,
3538+ "license": "MIT",
3539+ "engines": {
3540+ "node": ">= 14.16"
3541+ }
3542+ },
3543+ "node_modules/picocolors": {
3544+ "version": "1.1.1",
3545+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
3546+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
3547+ "dev": true,
3548+ "license": "ISC"
3549+ },
3550+ "node_modules/picomatch": {
3551+ "version": "4.0.7",
3552+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
3553+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
3554+ "dev": true,
3555+ "license": "MIT",
3556+ "engines": {
3557+ "node": ">=12"
3558+ },
3559+ "funding": {
3560+ "url": "https://github.com/sponsors/jonschlinkert"
3561+ }
3562+ },
3563+ "node_modules/postcss": {
3564+ "version": "8.5.28",
3565+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
3566+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
3567+ "dev": true,
3568+ "funding": [
3569+ {
3570+ "type": "opencollective",
3571+ "url": "https://opencollective.com/postcss/"
3572+ },
3573+ {
3574+ "type": "tidelift",
3575+ "url": "https://tidelift.com/funding/github/npm/postcss"
3576+ },
3577+ {
3578+ "type": "github",
3579+ "url": "https://github.com/sponsors/ai"
3580+ }
3581+ ],
3582+ "license": "MIT",
3583+ "dependencies": {
3584+ "nanoid": "^3.3.18",
3585+ "picocolors": "^1.1.1",
3586+ "source-map-js": "^1.2.1"
3587+ },
3588+ "engines": {
3589+ "node": "^10 || ^12 || >=14"
3590+ }
3591+ },
3592+ "node_modules/pretty-format": {
3593+ "version": "27.5.1",
3594+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
3595+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
3596+ "dev": true,
3597+ "license": "MIT",
3598+ "peer": true,
3599+ "dependencies": {
3600+ "ansi-regex": "^5.0.1",
3601+ "ansi-styles": "^5.0.0",
3602+ "react-is": "^17.0.1"
3603+ },
3604+ "engines": {
3605+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
3606+ }
3607+ },
3608+ "node_modules/property-information": {
3609+ "version": "7.2.0",
3610+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
3611+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
3612+ "license": "MIT",
3613+ "funding": {
3614+ "type": "github",
3615+ "url": "https://github.com/sponsors/wooorm"
3616+ }
3617+ },
3618+ "node_modules/punycode": {
3619+ "version": "2.3.1",
3620+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
3621+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
3622+ "dev": true,
3623+ "license": "MIT",
3624+ "engines": {
3625+ "node": ">=6"
3626+ }
3627+ },
3628+ "node_modules/react": {
3629+ "version": "19.3.0",
3630+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
3631+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
3632+ "license": "MIT",
3633+ "engines": {
3634+ "node": ">=0.10.0"
3635+ }
3636+ },
3637+ "node_modules/react-dom": {
3638+ "version": "19.3.0",
3639+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
3640+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
3641+ "license": "MIT",
3642+ "dependencies": {
3643+ "scheduler": "^0.28.0"
3644+ },
3645+ "peerDependencies": {
3646+ "react": "^19.3.0"
3647+ }
3648+ },
3649+ "node_modules/react-is": {
3650+ "version": "17.0.2",
3651+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
3652+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
3653+ "dev": true,
3654+ "license": "MIT",
3655+ "peer": true
3656+ },
3657+ "node_modules/react-markdown": {
3658+ "version": "10.1.0",
3659+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
3660+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
3661+ "license": "MIT",
3662+ "dependencies": {
3663+ "@types/hast": "^3.0.0",
3664+ "@types/mdast": "^4.0.0",
3665+ "devlop": "^1.0.0",
3666+ "hast-util-to-jsx-runtime": "^2.0.0",
3667+ "html-url-attributes": "^3.0.0",
3668+ "mdast-util-to-hast": "^13.0.0",
3669+ "remark-parse": "^11.0.0",
3670+ "remark-rehype": "^11.0.0",
3671+ "unified": "^11.0.0",
3672+ "unist-util-visit": "^5.0.0",
3673+ "vfile": "^6.0.0"
3674+ },
3675+ "funding": {
3676+ "type": "opencollective",
3677+ "url": "https://opencollective.com/unified"
3678+ },
3679+ "peerDependencies": {
3680+ "@types/react": ">=18",
3681+ "react": ">=18"
3682+ }
3683+ },
3684+ "node_modules/react-refresh": {
3685+ "version": "0.18.0",
3686+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
3687+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
3688+ "dev": true,
3689+ "license": "MIT",
3690+ "engines": {
3691+ "node": ">=0.10.0"
3692+ }
3693+ },
3694+ "node_modules/remark-gfm": {
3695+ "version": "4.0.1",
3696+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
3697+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
3698+ "license": "MIT",
3699+ "dependencies": {
3700+ "@types/mdast": "^4.0.0",
3701+ "mdast-util-gfm": "^3.0.0",
3702+ "micromark-extension-gfm": "^3.0.0",
3703+ "remark-parse": "^11.0.0",
3704+ "remark-stringify": "^11.0.0",
3705+ "unified": "^11.0.0"
3706+ },
3707+ "funding": {
3708+ "type": "opencollective",
3709+ "url": "https://opencollective.com/unified"
3710+ }
3711+ },
3712+ "node_modules/remark-parse": {
3713+ "version": "11.0.0",
3714+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
3715+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
3716+ "license": "MIT",
3717+ "dependencies": {
3718+ "@types/mdast": "^4.0.0",
3719+ "mdast-util-from-markdown": "^2.0.0",
3720+ "micromark-util-types": "^2.0.0",
3721+ "unified": "^11.0.0"
3722+ },
3723+ "funding": {
3724+ "type": "opencollective",
3725+ "url": "https://opencollective.com/unified"
3726+ }
3727+ },
3728+ "node_modules/remark-rehype": {
3729+ "version": "11.1.2",
3730+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
3731+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
3732+ "license": "MIT",
3733+ "dependencies": {
3734+ "@types/hast": "^3.0.0",
3735+ "@types/mdast": "^4.0.0",
3736+ "mdast-util-to-hast": "^13.0.0",
3737+ "unified": "^11.0.0",
3738+ "vfile": "^6.0.0"
3739+ },
3740+ "funding": {
3741+ "type": "opencollective",
3742+ "url": "https://opencollective.com/unified"
3743+ }
3744+ },
3745+ "node_modules/remark-stringify": {
3746+ "version": "11.0.0",
3747+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
3748+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
3749+ "license": "MIT",
3750+ "dependencies": {
3751+ "@types/mdast": "^4.0.0",
3752+ "mdast-util-to-markdown": "^2.0.0",
3753+ "unified": "^11.0.0"
3754+ },
3755+ "funding": {
3756+ "type": "opencollective",
3757+ "url": "https://opencollective.com/unified"
3758+ }
3759+ },
3760+ "node_modules/rollup": {
3761+ "version": "4.63.3",
3762+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz",
3763+ "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==",
3764+ "dev": true,
3765+ "license": "MIT",
3766+ "dependencies": {
3767+ "@types/estree": "1.0.9"
3768+ },
3769+ "bin": {
3770+ "rollup": "dist/bin/rollup"
3771+ },
3772+ "engines": {
3773+ "node": ">=18.0.0",
3774+ "npm": ">=8.0.0"
3775+ },
3776+ "optionalDependencies": {
3777+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
3778+ "@rollup/rollup-android-arm-eabi": "4.63.3",
3779+ "@rollup/rollup-android-arm64": "4.63.3",
3780+ "@rollup/rollup-darwin-arm64": "4.63.3",
3781+ "@rollup/rollup-darwin-x64": "4.63.3",
3782+ "@rollup/rollup-freebsd-arm64": "4.63.3",
3783+ "@rollup/rollup-freebsd-x64": "4.63.3",
3784+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.3",
3785+ "@rollup/rollup-linux-arm-musleabihf": "4.63.3",
3786+ "@rollup/rollup-linux-arm64-gnu": "4.63.3",
3787+ "@rollup/rollup-linux-arm64-musl": "4.63.3",
3788+ "@rollup/rollup-linux-loong64-gnu": "4.63.3",
3789+ "@rollup/rollup-linux-loong64-musl": "4.63.3",
3790+ "@rollup/rollup-linux-ppc64-gnu": "4.63.3",
3791+ "@rollup/rollup-linux-ppc64-musl": "4.63.3",
3792+ "@rollup/rollup-linux-riscv64-gnu": "4.63.3",
3793+ "@rollup/rollup-linux-riscv64-musl": "4.63.3",
3794+ "@rollup/rollup-linux-s390x-gnu": "4.63.3",
3795+ "@rollup/rollup-linux-x64-gnu": "4.63.3",
3796+ "@rollup/rollup-linux-x64-musl": "4.63.3",
3797+ "@rollup/rollup-openbsd-x64": "4.63.3",
3798+ "@rollup/rollup-openharmony-arm64": "4.63.3",
3799+ "@rollup/rollup-win32-arm64-msvc": "4.63.3",
3800+ "@rollup/rollup-win32-ia32-msvc": "4.63.3",
3801+ "@rollup/rollup-win32-x64-gnu": "4.63.3",
3802+ "@rollup/rollup-win32-x64-msvc": "4.63.3",
3803+ "fsevents": "~2.3.2"
3804+ }
3805+ },
3806+ "node_modules/rrweb-cssom": {
3807+ "version": "0.8.0",
3808+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
3809+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
3810+ "dev": true,
3811+ "license": "MIT"
3812+ },
3813+ "node_modules/safer-buffer": {
3814+ "version": "2.1.2",
3815+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
3816+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
3817+ "dev": true,
3818+ "license": "MIT"
3819+ },
3820+ "node_modules/saxes": {
3821+ "version": "6.0.0",
3822+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
3823+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
3824+ "dev": true,
3825+ "license": "ISC",
3826+ "dependencies": {
3827+ "xmlchars": "^2.2.0"
3828+ },
3829+ "engines": {
3830+ "node": ">=v12.22.7"
3831+ }
3832+ },
3833+ "node_modules/scheduler": {
3834+ "version": "0.28.0",
3835+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
3836+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
3837+ "license": "MIT"
3838+ },
3839+ "node_modules/semver": {
3840+ "version": "6.3.1",
3841+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
3842+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
3843+ "dev": true,
3844+ "license": "ISC",
3845+ "bin": {
3846+ "semver": "bin/semver.js"
3847+ }
3848+ },
3849+ "node_modules/siginfo": {
3850+ "version": "2.0.0",
3851+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
3852+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
3853+ "dev": true,
3854+ "license": "ISC"
3855+ },
3856+ "node_modules/source-map-js": {
3857+ "version": "1.2.1",
3858+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
3859+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
3860+ "dev": true,
3861+ "license": "BSD-3-Clause",
3862+ "engines": {
3863+ "node": ">=0.10.0"
3864+ }
3865+ },
3866+ "node_modules/space-separated-tokens": {
3867+ "version": "2.0.2",
3868+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
3869+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
3870+ "license": "MIT",
3871+ "funding": {
3872+ "type": "github",
3873+ "url": "https://github.com/sponsors/wooorm"
3874+ }
3875+ },
3876+ "node_modules/stackback": {
3877+ "version": "0.0.2",
3878+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
3879+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
3880+ "dev": true,
3881+ "license": "MIT"
3882+ },
3883+ "node_modules/state-local": {
3884+ "version": "1.0.7",
3885+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
3886+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
3887+ "license": "MIT"
3888+ },
3889+ "node_modules/std-env": {
3890+ "version": "3.10.0",
3891+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
3892+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
3893+ "dev": true,
3894+ "license": "MIT"
3895+ },
3896+ "node_modules/stringify-entities": {
3897+ "version": "4.0.4",
3898+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
3899+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
3900+ "license": "MIT",
3901+ "dependencies": {
3902+ "character-entities-html4": "^2.0.0",
3903+ "character-entities-legacy": "^3.0.0"
3904+ },
3905+ "funding": {
3906+ "type": "github",
3907+ "url": "https://github.com/sponsors/wooorm"
3908+ }
3909+ },
3910+ "node_modules/strip-literal": {
3911+ "version": "3.1.0",
3912+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
3913+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
3914+ "dev": true,
3915+ "license": "MIT",
3916+ "dependencies": {
3917+ "js-tokens": "^9.0.1"
3918+ },
3919+ "funding": {
3920+ "url": "https://github.com/sponsors/antfu"
3921+ }
3922+ },
3923+ "node_modules/strip-literal/node_modules/js-tokens": {
3924+ "version": "9.0.1",
3925+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
3926+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
3927+ "dev": true,
3928+ "license": "MIT"
3929+ },
3930+ "node_modules/style-to-js": {
3931+ "version": "1.1.21",
3932+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
3933+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
3934+ "license": "MIT",
3935+ "dependencies": {
3936+ "style-to-object": "1.0.14"
3937+ }
3938+ },
3939+ "node_modules/style-to-object": {
3940+ "version": "1.0.14",
3941+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
3942+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
3943+ "license": "MIT",
3944+ "dependencies": {
3945+ "inline-style-parser": "0.2.7"
3946+ }
3947+ },
3948+ "node_modules/symbol-tree": {
3949+ "version": "3.2.4",
3950+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
3951+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
3952+ "dev": true,
3953+ "license": "MIT"
3954+ },
3955+ "node_modules/tinybench": {
3956+ "version": "2.9.0",
3957+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
3958+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
3959+ "dev": true,
3960+ "license": "MIT"
3961+ },
3962+ "node_modules/tinyexec": {
3963+ "version": "0.3.2",
3964+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
3965+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
3966+ "dev": true,
3967+ "license": "MIT"
3968+ },
3969+ "node_modules/tinyglobby": {
3970+ "version": "0.2.17",
3971+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
3972+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
3973+ "dev": true,
3974+ "license": "MIT",
3975+ "dependencies": {
3976+ "fdir": "^6.5.0",
3977+ "picomatch": "^4.0.4"
3978+ },
3979+ "engines": {
3980+ "node": ">=12.0.0"
3981+ },
3982+ "funding": {
3983+ "url": "https://github.com/sponsors/SuperchupuDev"
3984+ }
3985+ },
3986+ "node_modules/tinypool": {
3987+ "version": "1.1.1",
3988+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
3989+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
3990+ "dev": true,
3991+ "license": "MIT",
3992+ "engines": {
3993+ "node": "^18.0.0 || >=20.0.0"
3994+ }
3995+ },
3996+ "node_modules/tinyrainbow": {
3997+ "version": "2.0.0",
3998+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
3999+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
4000+ "dev": true,
4001+ "license": "MIT",
4002+ "engines": {
4003+ "node": ">=14.0.0"
4004+ }
4005+ },
4006+ "node_modules/tinyspy": {
4007+ "version": "4.0.6",
4008+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
4009+ "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
4010+ "dev": true,
4011+ "license": "MIT",
4012+ "engines": {
4013+ "node": ">=14.0.0"
4014+ }
4015+ },
4016+ "node_modules/tldts": {
4017+ "version": "6.1.86",
4018+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
4019+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
4020+ "dev": true,
4021+ "license": "MIT",
4022+ "dependencies": {
4023+ "tldts-core": "^6.1.86"
4024+ },
4025+ "bin": {
4026+ "tldts": "bin/cli.js"
4027+ }
4028+ },
4029+ "node_modules/tldts-core": {
4030+ "version": "6.1.86",
4031+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
4032+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
4033+ "dev": true,
4034+ "license": "MIT"
4035+ },
4036+ "node_modules/tough-cookie": {
4037+ "version": "5.1.2",
4038+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
4039+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
4040+ "dev": true,
4041+ "license": "BSD-3-Clause",
4042+ "dependencies": {
4043+ "tldts": "^6.1.32"
4044+ },
4045+ "engines": {
4046+ "node": ">=16"
4047+ }
4048+ },
4049+ "node_modules/tr46": {
4050+ "version": "5.1.1",
4051+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
4052+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
4053+ "dev": true,
4054+ "license": "MIT",
4055+ "dependencies": {
4056+ "punycode": "^2.3.1"
4057+ },
4058+ "engines": {
4059+ "node": ">=18"
4060+ }
4061+ },
4062+ "node_modules/trim-lines": {
4063+ "version": "3.0.1",
4064+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
4065+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
4066+ "license": "MIT",
4067+ "funding": {
4068+ "type": "github",
4069+ "url": "https://github.com/sponsors/wooorm"
4070+ }
4071+ },
4072+ "node_modules/trough": {
4073+ "version": "2.2.0",
4074+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
4075+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
4076+ "license": "MIT",
4077+ "funding": {
4078+ "type": "github",
4079+ "url": "https://github.com/sponsors/wooorm"
4080+ }
4081+ },
4082+ "node_modules/typescript": {
4083+ "version": "5.9.3",
4084+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
4085+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
4086+ "dev": true,
4087+ "license": "Apache-2.0",
4088+ "bin": {
4089+ "tsc": "bin/tsc",
4090+ "tsserver": "bin/tsserver"
4091+ },
4092+ "engines": {
4093+ "node": ">=14.17"
4094+ }
4095+ },
4096+ "node_modules/unified": {
4097+ "version": "11.0.5",
4098+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
4099+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
4100+ "license": "MIT",
4101+ "dependencies": {
4102+ "@types/unist": "^3.0.0",
4103+ "bail": "^2.0.0",
4104+ "devlop": "^1.0.0",
4105+ "extend": "^3.0.0",
4106+ "is-plain-obj": "^4.0.0",
4107+ "trough": "^2.0.0",
4108+ "vfile": "^6.0.0"
4109+ },
4110+ "funding": {
4111+ "type": "opencollective",
4112+ "url": "https://opencollective.com/unified"
4113+ }
4114+ },
4115+ "node_modules/unist-util-is": {
4116+ "version": "6.0.1",
4117+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
4118+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
4119+ "license": "MIT",
4120+ "dependencies": {
4121+ "@types/unist": "^3.0.0"
4122+ },
4123+ "funding": {
4124+ "type": "opencollective",
4125+ "url": "https://opencollective.com/unified"
4126+ }
4127+ },
4128+ "node_modules/unist-util-position": {
4129+ "version": "5.0.0",
4130+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
4131+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
4132+ "license": "MIT",
4133+ "dependencies": {
4134+ "@types/unist": "^3.0.0"
4135+ },
4136+ "funding": {
4137+ "type": "opencollective",
4138+ "url": "https://opencollective.com/unified"
4139+ }
4140+ },
4141+ "node_modules/unist-util-stringify-position": {
4142+ "version": "4.0.0",
4143+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
4144+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
4145+ "license": "MIT",
4146+ "dependencies": {
4147+ "@types/unist": "^3.0.0"
4148+ },
4149+ "funding": {
4150+ "type": "opencollective",
4151+ "url": "https://opencollective.com/unified"
4152+ }
4153+ },
4154+ "node_modules/unist-util-visit": {
4155+ "version": "5.1.0",
4156+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
4157+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
4158+ "license": "MIT",
4159+ "dependencies": {
4160+ "@types/unist": "^3.0.0",
4161+ "unist-util-is": "^6.0.0",
4162+ "unist-util-visit-parents": "^6.0.0"
4163+ },
4164+ "funding": {
4165+ "type": "opencollective",
4166+ "url": "https://opencollective.com/unified"
4167+ }
4168+ },
4169+ "node_modules/unist-util-visit-parents": {
4170+ "version": "6.0.2",
4171+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
4172+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
4173+ "license": "MIT",
4174+ "dependencies": {
4175+ "@types/unist": "^3.0.0",
4176+ "unist-util-is": "^6.0.0"
4177+ },
4178+ "funding": {
4179+ "type": "opencollective",
4180+ "url": "https://opencollective.com/unified"
4181+ }
4182+ },
4183+ "node_modules/update-browserslist-db": {
4184+ "version": "1.3.3",
4185+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
4186+ "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
4187+ "dev": true,
4188+ "funding": [
4189+ {
4190+ "type": "opencollective",
4191+ "url": "https://opencollective.com/browserslist"
4192+ },
4193+ {
4194+ "type": "tidelift",
4195+ "url": "https://tidelift.com/funding/github/npm/browserslist"
4196+ },
4197+ {
4198+ "type": "github",
4199+ "url": "https://github.com/sponsors/ai"
4200+ }
4201+ ],
4202+ "license": "MIT",
4203+ "dependencies": {
4204+ "escalade": "^3.2.0",
4205+ "picocolors": "^1.1.1"
4206+ },
4207+ "bin": {
4208+ "update-browserslist-db": "cli.js"
4209+ },
4210+ "peerDependencies": {
4211+ "browserslist": ">= 4.21.0"
4212+ }
4213+ },
4214+ "node_modules/vfile": {
4215+ "version": "6.0.3",
4216+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
4217+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
4218+ "license": "MIT",
4219+ "dependencies": {
4220+ "@types/unist": "^3.0.0",
4221+ "vfile-message": "^4.0.0"
4222+ },
4223+ "funding": {
4224+ "type": "opencollective",
4225+ "url": "https://opencollective.com/unified"
4226+ }
4227+ },
4228+ "node_modules/vfile-message": {
4229+ "version": "4.0.3",
4230+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
4231+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
4232+ "license": "MIT",
4233+ "dependencies": {
4234+ "@types/unist": "^3.0.0",
4235+ "unist-util-stringify-position": "^4.0.0"
4236+ },
4237+ "funding": {
4238+ "type": "opencollective",
4239+ "url": "https://opencollective.com/unified"
4240+ }
4241+ },
4242+ "node_modules/vite": {
4243+ "version": "7.3.6",
4244+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
4245+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
4246+ "dev": true,
4247+ "license": "MIT",
4248+ "dependencies": {
4249+ "esbuild": "^0.27.0 || ^0.28.0",
4250+ "fdir": "^6.5.0",
4251+ "picomatch": "^4.0.3",
4252+ "postcss": "^8.5.6",
4253+ "rollup": "^4.43.0",
4254+ "tinyglobby": "^0.2.15"
4255+ },
4256+ "bin": {
4257+ "vite": "bin/vite.js"
4258+ },
4259+ "engines": {
4260+ "node": "^20.19.0 || >=22.12.0"
4261+ },
4262+ "funding": {
4263+ "url": "https://github.com/vitejs/vite?sponsor=1"
4264+ },
4265+ "optionalDependencies": {
4266+ "fsevents": "~2.3.3"
4267+ },
4268+ "peerDependencies": {
4269+ "@types/node": "^20.19.0 || >=22.12.0",
4270+ "jiti": ">=1.21.0",
4271+ "less": "^4.0.0",
4272+ "lightningcss": "^1.21.0",
4273+ "sass": "^1.70.0",
4274+ "sass-embedded": "^1.70.0",
4275+ "stylus": ">=0.54.8",
4276+ "sugarss": "^5.0.0",
4277+ "terser": "^5.16.0",
4278+ "tsx": "^4.8.1",
4279+ "yaml": "^2.4.2"
4280+ },
4281+ "peerDependenciesMeta": {
4282+ "@types/node": {
4283+ "optional": true
4284+ },
4285+ "jiti": {
4286+ "optional": true
4287+ },
4288+ "less": {
4289+ "optional": true
4290+ },
4291+ "lightningcss": {
4292+ "optional": true
4293+ },
4294+ "sass": {
4295+ "optional": true
4296+ },
4297+ "sass-embedded": {
4298+ "optional": true
4299+ },
4300+ "stylus": {
4301+ "optional": true
4302+ },
4303+ "sugarss": {
4304+ "optional": true
4305+ },
4306+ "terser": {
4307+ "optional": true
4308+ },
4309+ "tsx": {
4310+ "optional": true
4311+ },
4312+ "yaml": {
4313+ "optional": true
4314+ }
4315+ }
4316+ },
4317+ "node_modules/vite-node": {
4318+ "version": "3.2.4",
4319+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
4320+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
4321+ "dev": true,
4322+ "license": "MIT",
4323+ "dependencies": {
4324+ "cac": "^6.7.14",
4325+ "debug": "^4.4.1",
4326+ "es-module-lexer": "^1.7.0",
4327+ "pathe": "^2.0.3",
4328+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
4329+ },
4330+ "bin": {
4331+ "vite-node": "vite-node.mjs"
4332+ },
4333+ "engines": {
4334+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4335+ },
4336+ "funding": {
4337+ "url": "https://opencollective.com/vitest"
4338+ }
4339+ },
4340+ "node_modules/vitest": {
4341+ "version": "3.2.7",
4342+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
4343+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
4344+ "dev": true,
4345+ "license": "MIT",
4346+ "dependencies": {
4347+ "@types/chai": "^5.2.2",
4348+ "@vitest/expect": "3.2.7",
4349+ "@vitest/mocker": "3.2.7",
4350+ "@vitest/pretty-format": "^3.2.7",
4351+ "@vitest/runner": "3.2.7",
4352+ "@vitest/snapshot": "3.2.7",
4353+ "@vitest/spy": "3.2.7",
4354+ "@vitest/utils": "3.2.7",
4355+ "chai": "^5.2.0",
4356+ "debug": "^4.4.1",
4357+ "expect-type": "^1.2.1",
4358+ "magic-string": "^0.30.17",
4359+ "pathe": "^2.0.3",
4360+ "picomatch": "^4.0.2",
4361+ "std-env": "^3.9.0",
4362+ "tinybench": "^2.9.0",
4363+ "tinyexec": "^0.3.2",
4364+ "tinyglobby": "^0.2.14",
4365+ "tinypool": "^1.1.1",
4366+ "tinyrainbow": "^2.0.0",
4367+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
4368+ "vite-node": "3.2.4",
4369+ "why-is-node-running": "^2.3.0"
4370+ },
4371+ "bin": {
4372+ "vitest": "vitest.mjs"
4373+ },
4374+ "engines": {
4375+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4376+ },
4377+ "funding": {
4378+ "url": "https://opencollective.com/vitest"
4379+ },
4380+ "peerDependencies": {
4381+ "@edge-runtime/vm": "*",
4382+ "@types/debug": "^4.1.12",
4383+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
4384+ "@vitest/browser": "3.2.7",
4385+ "@vitest/ui": "3.2.7",
4386+ "happy-dom": "*",
4387+ "jsdom": "*"
4388+ },
4389+ "peerDependenciesMeta": {
4390+ "@edge-runtime/vm": {
4391+ "optional": true
4392+ },
4393+ "@types/debug": {
4394+ "optional": true
4395+ },
4396+ "@types/node": {
4397+ "optional": true
4398+ },
4399+ "@vitest/browser": {
4400+ "optional": true
4401+ },
4402+ "@vitest/ui": {
4403+ "optional": true
4404+ },
4405+ "happy-dom": {
4406+ "optional": true
4407+ },
4408+ "jsdom": {
4409+ "optional": true
4410+ }
4411+ }
4412+ },
4413+ "node_modules/vscode-material-icons": {
4414+ "version": "0.1.1",
4415+ "resolved": "https://registry.npmjs.org/vscode-material-icons/-/vscode-material-icons-0.1.1.tgz",
4416+ "integrity": "sha512-GsoEEF8Tbb0yUFQ6N6FPvh11kFkL9F95x0FkKlbbfRQN9eFms67h+L3t6b9cUv58dSn2gu8kEhNfoESVCrz4ag==",
4417+ "license": "MIT"
4418+ },
4419+ "node_modules/w3c-xmlserializer": {
4420+ "version": "5.0.0",
4421+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
4422+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
4423+ "dev": true,
4424+ "license": "MIT",
4425+ "dependencies": {
4426+ "xml-name-validator": "^5.0.0"
4427+ },
4428+ "engines": {
4429+ "node": ">=18"
4430+ }
4431+ },
4432+ "node_modules/webidl-conversions": {
4433+ "version": "7.0.0",
4434+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
4435+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
4436+ "dev": true,
4437+ "license": "BSD-2-Clause",
4438+ "engines": {
4439+ "node": ">=12"
4440+ }
4441+ },
4442+ "node_modules/whatwg-encoding": {
4443+ "version": "3.1.1",
4444+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
4445+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
4446+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
4447+ "dev": true,
4448+ "license": "MIT",
4449+ "dependencies": {
4450+ "iconv-lite": "0.6.3"
4451+ },
4452+ "engines": {
4453+ "node": ">=18"
4454+ }
4455+ },
4456+ "node_modules/whatwg-mimetype": {
4457+ "version": "4.0.0",
4458+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
4459+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
4460+ "dev": true,
4461+ "license": "MIT",
4462+ "engines": {
4463+ "node": ">=18"
4464+ }
4465+ },
4466+ "node_modules/whatwg-url": {
4467+ "version": "14.2.0",
4468+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
4469+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
4470+ "dev": true,
4471+ "license": "MIT",
4472+ "dependencies": {
4473+ "tr46": "^5.1.0",
4474+ "webidl-conversions": "^7.0.0"
4475+ },
4476+ "engines": {
4477+ "node": ">=18"
4478+ }
4479+ },
4480+ "node_modules/why-is-node-running": {
4481+ "version": "2.3.0",
4482+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
4483+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
4484+ "dev": true,
4485+ "license": "MIT",
4486+ "dependencies": {
4487+ "siginfo": "^2.0.0",
4488+ "stackback": "0.0.2"
4489+ },
4490+ "bin": {
4491+ "why-is-node-running": "cli.js"
4492+ },
4493+ "engines": {
4494+ "node": ">=8"
4495+ }
4496+ },
4497+ "node_modules/ws": {
4498+ "version": "8.21.3",
4499+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
4500+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
4501+ "dev": true,
4502+ "license": "MIT",
4503+ "engines": {
4504+ "node": ">=10.0.0"
4505+ },
4506+ "peerDependencies": {
4507+ "bufferutil": "^4.0.1",
4508+ "utf-8-validate": ">=5.0.2"
4509+ },
4510+ "peerDependenciesMeta": {
4511+ "bufferutil": {
4512+ "optional": true
4513+ },
4514+ "utf-8-validate": {
4515+ "optional": true
4516+ }
4517+ }
4518+ },
4519+ "node_modules/xml-name-validator": {
4520+ "version": "5.0.0",
4521+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
4522+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
4523+ "dev": true,
4524+ "license": "Apache-2.0",
4525+ "engines": {
4526+ "node": ">=18"
4527+ }
4528+ },
4529+ "node_modules/xmlchars": {
4530+ "version": "2.2.0",
4531+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
4532+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
4533+ "dev": true,
4534+ "license": "MIT"
4535+ },
4536+ "node_modules/yallist": {
4537+ "version": "3.1.1",
4538+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
4539+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
4540+ "dev": true,
4541+ "license": "ISC"
4542+ },
4543+ "node_modules/zustand": {
4544+ "version": "5.0.15",
4545+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
4546+ "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
4547+ "license": "MIT",
4548+ "engines": {
4549+ "node": ">=12.20.0"
4550+ },
4551+ "peerDependencies": {
4552+ "@types/react": ">=18.0.0",
4553+ "immer": ">=9.0.6",
4554+ "react": ">=18.0.0",
4555+ "use-sync-external-store": ">=1.2.0"
4556+ },
4557+ "peerDependenciesMeta": {
4558+ "@types/react": {
4559+ "optional": true
4560+ },
4561+ "immer": {
4562+ "optional": true
4563+ },
4564+ "react": {
4565+ "optional": true
4566+ },
4567+ "use-sync-external-store": {
4568+ "optional": true
4569+ }
4570+ }
4571+ },
4572+ "node_modules/zwitch": {
4573+ "version": "2.0.4",
4574+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
4575+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
4576+ "license": "MIT",
4577+ "funding": {
4578+ "type": "github",
4579+ "url": "https://github.com/sponsors/wooorm"
4580+ }
4581+ }
4582+ }
44804583 }
@@ -1,4480 +1,4583 @@
1 {1 {
2- "name": "ori-ui",2+ "name": "ori-ui",
3- "version": "0.1.0",3+ "version": "0.1.0",
4- "lockfileVersion": 3,4+ "lockfileVersion": 3,
5- "requires": true,5+ "requires": true,
6- "packages": {6+ "packages": {
7- "": {7+ "": {
8- "name": "ori-ui",8+ "name": "ori-ui",
9- "version": "0.1.0",9+ "version": "0.1.0",
10- "dependencies": {10+ "dependencies": {
11- "react": "^19.0.0",11+ "@asciidoctor/core": "^4.0.11",
12- "react-dom": "^19.0.0",12+ "@monaco-editor/react": "^4.7.0",
13- "react-markdown": "^10.1.0",13+ "@xterm/addon-fit": "^0.11.0",
14- "remark-gfm": "^4.0.1",14+ "@xterm/xterm": "^6.0.0",
15- "zustand": "^5.0.15"15+ "monaco-editor": "^0.56.0",
16- },16+ "react": "^19.0.0",
17- "devDependencies": {17+ "react-dom": "^19.0.0",
18- "@testing-library/react": "^16.0.0",18+ "react-markdown": "^10.1.0",
19- "@types/react": "^19.0.0",19+ "remark-gfm": "^4.0.1",
20- "@types/react-dom": "^19.0.0",20+ "vscode-material-icons": "^0.1.1",
21- "@vitejs/plugin-react": "^5.0.0",21+ "zustand": "^5.0.15"
22- "jsdom": "^26.0.0",22+ },
23- "typescript": "^5.6.0",23+ "devDependencies": {
24- "vite": "^7.0.0",24+ "@testing-library/react": "^16.0.0",
25- "vitest": "^3.0.0"25+ "@types/react": "^19.0.0",
26- }26+ "@types/react-dom": "^19.0.0",
27- },27+ "@vitejs/plugin-react": "^5.0.0",
28- "node_modules/@asamuzakjp/css-color": {28+ "jsdom": "^26.0.0",
29- "version": "3.2.0",29+ "typescript": "^5.6.0",
30- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",30+ "vite": "^7.0.0",
31- "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",31+ "vitest": "^3.0.0"
32- "dev": true,32+ }
33- "license": "MIT",33+ },
34- "dependencies": {34+ "node_modules/@asamuzakjp/css-color": {
35- "@csstools/css-calc": "^2.1.3",35+ "version": "3.2.0",
36- "@csstools/css-color-parser": "^3.0.9",36+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
37- "@csstools/css-parser-algorithms": "^3.0.4",37+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
38- "@csstools/css-tokenizer": "^3.0.3",38+ "dev": true,
39- "lru-cache": "^10.4.3"39+ "license": "MIT",
40- }40+ "dependencies": {
41- },41+ "@csstools/css-calc": "^2.1.3",
42- "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {42+ "@csstools/css-color-parser": "^3.0.9",
43- "version": "10.4.3",43+ "@csstools/css-parser-algorithms": "^3.0.4",
44- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",44+ "@csstools/css-tokenizer": "^3.0.3",
45- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",45+ "lru-cache": "^10.4.3"
46- "dev": true,46+ }
47- "license": "ISC"47+ },
48- },48+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
49- "node_modules/@babel/code-frame": {49+ "version": "10.4.3",
50- "version": "7.29.7",50+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
51- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",51+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
52- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",52+ "dev": true,
53- "dev": true,53+ "license": "ISC"
54- "license": "MIT",54+ },
55- "dependencies": {55+ "node_modules/@asciidoctor/core": {
56- "@babel/helper-validator-identifier": "^7.29.7",56+ "version": "4.0.11",
57- "js-tokens": "^4.0.0",57+ "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-4.0.11.tgz",
58- "picocolors": "^1.1.1"58+ "integrity": "sha512-5uWPTLTiPJVQp9q4MllSROM2avdECzoT7oeVw/onT9H2PIdCCtQwKrSTvztwxMbSu/8cqHqZox/2qTBrQN454g==",
59- },59+ "license": "MIT",
60- "engines": {60+ "engines": {
61- "node": ">=6.9.0"61+ "node": ">=20"
62- }62+ }
63- },63+ },
64- "node_modules/@babel/compat-data": {64+ "node_modules/@babel/code-frame": {
65- "version": "7.29.7",65+ "version": "7.29.7",
66- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",66+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
67- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",67+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
68- "dev": true,68+ "dev": true,
69- "license": "MIT",69+ "license": "MIT",
70- "engines": {70+ "dependencies": {
71- "node": ">=6.9.0"71+ "@babel/helper-validator-identifier": "^7.29.7",
72- }72+ "js-tokens": "^4.0.0",
73- },73+ "picocolors": "^1.1.1"
74- "node_modules/@babel/core": {74+ },
75- "version": "7.29.7",75+ "engines": {
76- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",76+ "node": ">=6.9.0"
77- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",77+ }
78- "dev": true,78+ },
79- "license": "MIT",79+ "node_modules/@babel/compat-data": {
80- "dependencies": {80+ "version": "7.29.7",
81- "@babel/code-frame": "^7.29.7",81+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
82- "@babel/generator": "^7.29.7",82+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
83- "@babel/helper-compilation-targets": "^7.29.7",83+ "dev": true,
84- "@babel/helper-module-transforms": "^7.29.7",84+ "license": "MIT",
85- "@babel/helpers": "^7.29.7",85+ "engines": {
86- "@babel/parser": "^7.29.7",86+ "node": ">=6.9.0"
87- "@babel/template": "^7.29.7",87+ }
88- "@babel/traverse": "^7.29.7",88+ },
89- "@babel/types": "^7.29.7",89+ "node_modules/@babel/core": {
90- "@jridgewell/remapping": "^2.3.5",90+ "version": "7.29.7",
91- "convert-source-map": "^2.0.0",91+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
92- "debug": "^4.1.0",92+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
93- "gensync": "^1.0.0-beta.2",93+ "dev": true,
94- "json5": "^2.2.3",94+ "license": "MIT",
95- "semver": "^6.3.1"95+ "dependencies": {
96- },96+ "@babel/code-frame": "^7.29.7",
97- "engines": {97+ "@babel/generator": "^7.29.7",
98- "node": ">=6.9.0"98+ "@babel/helper-compilation-targets": "^7.29.7",
99- },99+ "@babel/helper-module-transforms": "^7.29.7",
100- "funding": {100+ "@babel/helpers": "^7.29.7",
101- "type": "opencollective",101+ "@babel/parser": "^7.29.7",
102- "url": "https://opencollective.com/babel"102+ "@babel/template": "^7.29.7",
103- }103+ "@babel/traverse": "^7.29.7",
104- },104+ "@babel/types": "^7.29.7",
105- "node_modules/@babel/generator": {105+ "@jridgewell/remapping": "^2.3.5",
106- "version": "7.29.8",106+ "convert-source-map": "^2.0.0",
107- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",107+ "debug": "^4.1.0",
108- "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",108+ "gensync": "^1.0.0-beta.2",
109- "dev": true,109+ "json5": "^2.2.3",
110- "license": "MIT",110+ "semver": "^6.3.1"
111- "dependencies": {111+ },
112- "@babel/parser": "^7.29.8",112+ "engines": {
113- "@babel/types": "^7.29.8",113+ "node": ">=6.9.0"
114- "@jridgewell/gen-mapping": "^0.3.12",114+ },
115- "@jridgewell/trace-mapping": "^0.3.28",115+ "funding": {
116- "jsesc": "^3.0.2"116+ "type": "opencollective",
117- },117+ "url": "https://opencollective.com/babel"
118- "engines": {118+ }
119- "node": ">=6.9.0"119+ },
120- }120+ "node_modules/@babel/generator": {
121- },121+ "version": "7.29.8",
122- "node_modules/@babel/helper-compilation-targets": {122+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
123- "version": "7.29.7",123+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
124- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",124+ "dev": true,
125- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",125+ "license": "MIT",
126- "dev": true,126+ "dependencies": {
127- "license": "MIT",127+ "@babel/parser": "^7.29.8",
128- "dependencies": {128+ "@babel/types": "^7.29.8",
129- "@babel/compat-data": "^7.29.7",129+ "@jridgewell/gen-mapping": "^0.3.12",
130- "@babel/helper-validator-option": "^7.29.7",130+ "@jridgewell/trace-mapping": "^0.3.28",
131- "browserslist": "^4.24.0",131+ "jsesc": "^3.0.2"
132- "lru-cache": "^5.1.1",132+ },
133- "semver": "^6.3.1"133+ "engines": {
134- },134+ "node": ">=6.9.0"
135- "engines": {135+ }
136- "node": ">=6.9.0"136+ },
137- }137+ "node_modules/@babel/helper-compilation-targets": {
138- },138+ "version": "7.29.7",
139- "node_modules/@babel/helper-globals": {139+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
140- "version": "7.29.7",140+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
141- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",141+ "dev": true,
142- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",142+ "license": "MIT",
143- "dev": true,143+ "dependencies": {
144- "license": "MIT",144+ "@babel/compat-data": "^7.29.7",
145- "engines": {145+ "@babel/helper-validator-option": "^7.29.7",
146- "node": ">=6.9.0"146+ "browserslist": "^4.24.0",
147- }147+ "lru-cache": "^5.1.1",
148- },148+ "semver": "^6.3.1"
149- "node_modules/@babel/helper-module-imports": {149+ },
150- "version": "7.29.7",150+ "engines": {
151- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",151+ "node": ">=6.9.0"
152- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",152+ }
153- "dev": true,153+ },
154- "license": "MIT",154+ "node_modules/@babel/helper-globals": {
155- "dependencies": {155+ "version": "7.29.7",
156- "@babel/traverse": "^7.29.7",156+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
157- "@babel/types": "^7.29.7"157+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
158- },158+ "dev": true,
159- "engines": {159+ "license": "MIT",
160- "node": ">=6.9.0"160+ "engines": {
161- }161+ "node": ">=6.9.0"
162- },162+ }
163- "node_modules/@babel/helper-module-transforms": {163+ },
164- "version": "7.29.7",164+ "node_modules/@babel/helper-module-imports": {
165- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",165+ "version": "7.29.7",
166- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",166+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
167- "dev": true,167+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
168- "license": "MIT",168+ "dev": true,
169- "dependencies": {169+ "license": "MIT",
170- "@babel/helper-module-imports": "^7.29.7",170+ "dependencies": {
171- "@babel/helper-validator-identifier": "^7.29.7",171+ "@babel/traverse": "^7.29.7",
172- "@babel/traverse": "^7.29.7"172+ "@babel/types": "^7.29.7"
173- },173+ },
174- "engines": {174+ "engines": {
175- "node": ">=6.9.0"175+ "node": ">=6.9.0"
176- },176+ }
177- "peerDependencies": {177+ },
178- "@babel/core": "^7.0.0"178+ "node_modules/@babel/helper-module-transforms": {
179- }179+ "version": "7.29.7",
180- },180+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
181- "node_modules/@babel/helper-plugin-utils": {181+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
182- "version": "7.29.7",182+ "dev": true,
183- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",183+ "license": "MIT",
184- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",184+ "dependencies": {
185- "dev": true,185+ "@babel/helper-module-imports": "^7.29.7",
186- "license": "MIT",186+ "@babel/helper-validator-identifier": "^7.29.7",
187- "engines": {187+ "@babel/traverse": "^7.29.7"
188- "node": ">=6.9.0"188+ },
189- }189+ "engines": {
190- },190+ "node": ">=6.9.0"
191- "node_modules/@babel/helper-string-parser": {191+ },
192- "version": "7.29.7",192+ "peerDependencies": {
193- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",193+ "@babel/core": "^7.0.0"
194- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",194+ }
195- "dev": true,195+ },
196- "license": "MIT",196+ "node_modules/@babel/helper-plugin-utils": {
197- "engines": {197+ "version": "7.29.7",
198- "node": ">=6.9.0"198+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
199- }199+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
200- },200+ "dev": true,
201- "node_modules/@babel/helper-validator-identifier": {201+ "license": "MIT",
202- "version": "7.29.7",202+ "engines": {
203- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",203+ "node": ">=6.9.0"
204- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",204+ }
205- "dev": true,205+ },
206- "license": "MIT",206+ "node_modules/@babel/helper-string-parser": {
207- "engines": {207+ "version": "7.29.7",
208- "node": ">=6.9.0"208+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
209- }209+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
210- },210+ "dev": true,
211- "node_modules/@babel/helper-validator-option": {211+ "license": "MIT",
212- "version": "7.29.7",212+ "engines": {
213- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",213+ "node": ">=6.9.0"
214- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",214+ }
215- "dev": true,215+ },
216- "license": "MIT",216+ "node_modules/@babel/helper-validator-identifier": {
217- "engines": {217+ "version": "7.29.7",
218- "node": ">=6.9.0"218+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
219- }219+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
220- },220+ "dev": true,
221- "node_modules/@babel/helpers": {221+ "license": "MIT",
222- "version": "7.29.7",222+ "engines": {
223- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",223+ "node": ">=6.9.0"
224- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",224+ }
225- "dev": true,225+ },
226- "license": "MIT",226+ "node_modules/@babel/helper-validator-option": {
227- "dependencies": {227+ "version": "7.29.7",
228- "@babel/template": "^7.29.7",228+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
229- "@babel/types": "^7.29.7"229+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
230- },230+ "dev": true,
231- "engines": {231+ "license": "MIT",
232- "node": ">=6.9.0"232+ "engines": {
233- }233+ "node": ">=6.9.0"
234- },234+ }
235- "node_modules/@babel/parser": {235+ },
236- "version": "7.29.8",236+ "node_modules/@babel/helpers": {
237- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",237+ "version": "7.29.7",
238- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",238+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
239- "dev": true,239+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
240- "license": "MIT",240+ "dev": true,
241- "dependencies": {241+ "license": "MIT",
242- "@babel/types": "^7.29.8"242+ "dependencies": {
243- },243+ "@babel/template": "^7.29.7",
244- "bin": {244+ "@babel/types": "^7.29.7"
245- "parser": "bin/babel-parser.js"245+ },
246- },246+ "engines": {
247- "engines": {247+ "node": ">=6.9.0"
248- "node": ">=6.0.0"248+ }
249- }249+ },
250- },250+ "node_modules/@babel/parser": {
251- "node_modules/@babel/plugin-transform-react-jsx-self": {251+ "version": "7.29.8",
252- "version": "7.29.7",252+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
253- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",253+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
254- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",254+ "dev": true,
255- "dev": true,255+ "license": "MIT",
256- "license": "MIT",256+ "dependencies": {
257- "dependencies": {257+ "@babel/types": "^7.29.8"
258- "@babel/helper-plugin-utils": "^7.29.7"258+ },
259- },259+ "bin": {
260- "engines": {260+ "parser": "bin/babel-parser.js"
261- "node": ">=6.9.0"261+ },
262- },262+ "engines": {
263- "peerDependencies": {263+ "node": ">=6.0.0"
264- "@babel/core": "^7.0.0-0"264+ }
265- }265+ },
266- },266+ "node_modules/@babel/plugin-transform-react-jsx-self": {
267- "node_modules/@babel/plugin-transform-react-jsx-source": {267+ "version": "7.29.7",
268- "version": "7.29.7",268+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
269- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",269+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
270- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",270+ "dev": true,
271- "dev": true,271+ "license": "MIT",
272- "license": "MIT",272+ "dependencies": {
273- "dependencies": {273+ "@babel/helper-plugin-utils": "^7.29.7"
274- "@babel/helper-plugin-utils": "^7.29.7"274+ },
275- },275+ "engines": {
276- "engines": {276+ "node": ">=6.9.0"
277- "node": ">=6.9.0"277+ },
278- },278+ "peerDependencies": {
279- "peerDependencies": {279+ "@babel/core": "^7.0.0-0"
280- "@babel/core": "^7.0.0-0"280+ }
281- }281+ },
282- },282+ "node_modules/@babel/plugin-transform-react-jsx-source": {
283- "node_modules/@babel/runtime": {283+ "version": "7.29.7",
284- "version": "7.29.7",284+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
285- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",285+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
286- "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",286+ "dev": true,
287- "dev": true,287+ "license": "MIT",
288- "license": "MIT",288+ "dependencies": {
289- "engines": {289+ "@babel/helper-plugin-utils": "^7.29.7"
290- "node": ">=6.9.0"290+ },
291- }291+ "engines": {
292- },292+ "node": ">=6.9.0"
293- "node_modules/@babel/template": {293+ },
294- "version": "7.29.7",294+ "peerDependencies": {
295- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",295+ "@babel/core": "^7.0.0-0"
296- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",296+ }
297- "dev": true,297+ },
298- "license": "MIT",298+ "node_modules/@babel/runtime": {
299- "dependencies": {299+ "version": "7.29.7",
300- "@babel/code-frame": "^7.29.7",300+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
301- "@babel/parser": "^7.29.7",301+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
302- "@babel/types": "^7.29.7"302+ "dev": true,
303- },303+ "license": "MIT",
304- "engines": {304+ "engines": {
305- "node": ">=6.9.0"305+ "node": ">=6.9.0"
306- }306+ }
307- },307+ },
308- "node_modules/@babel/traverse": {308+ "node_modules/@babel/template": {
309- "version": "7.29.8",309+ "version": "7.29.7",
310- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",310+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
311- "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",311+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
312- "dev": true,312+ "dev": true,
313- "license": "MIT",313+ "license": "MIT",
314- "dependencies": {314+ "dependencies": {
315- "@babel/code-frame": "^7.29.7",315+ "@babel/code-frame": "^7.29.7",
316- "@babel/generator": "^7.29.8",316+ "@babel/parser": "^7.29.7",
317- "@babel/helper-globals": "^7.29.7",317+ "@babel/types": "^7.29.7"
318- "@babel/parser": "^7.29.8",318+ },
319- "@babel/template": "^7.29.7",319+ "engines": {
320- "@babel/types": "^7.29.8",320+ "node": ">=6.9.0"
321- "debug": "^4.3.1"321+ }
322- },322+ },
323- "engines": {323+ "node_modules/@babel/traverse": {
324- "node": ">=6.9.0"324+ "version": "7.29.8",
325- }325+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
326- },326+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
327- "node_modules/@babel/types": {327+ "dev": true,
328- "version": "7.29.8",328+ "license": "MIT",
329- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",329+ "dependencies": {
330- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",330+ "@babel/code-frame": "^7.29.7",
331- "dev": true,331+ "@babel/generator": "^7.29.8",
332- "license": "MIT",332+ "@babel/helper-globals": "^7.29.7",
333- "dependencies": {333+ "@babel/parser": "^7.29.8",
334- "@babel/helper-string-parser": "^7.29.7",334+ "@babel/template": "^7.29.7",
335- "@babel/helper-validator-identifier": "^7.29.7"335+ "@babel/types": "^7.29.8",
336- },336+ "debug": "^4.3.1"
337- "engines": {337+ },
338- "node": ">=6.9.0"338+ "engines": {
339- }339+ "node": ">=6.9.0"
340- },340+ }
341- "node_modules/@csstools/color-helpers": {341+ },
342- "version": "5.1.0",342+ "node_modules/@babel/types": {
343- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",343+ "version": "7.29.8",
344- "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",344+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
345- "dev": true,345+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
346- "funding": [346+ "dev": true,
347- {347+ "license": "MIT",
348- "type": "github",348+ "dependencies": {
349- "url": "https://github.com/sponsors/csstools"349+ "@babel/helper-string-parser": "^7.29.7",
350- },350+ "@babel/helper-validator-identifier": "^7.29.7"
351- {351+ },
352- "type": "opencollective",352+ "engines": {
353- "url": "https://opencollective.com/csstools"353+ "node": ">=6.9.0"
354- }354+ }
355- ],355+ },
356- "license": "MIT-0",356+ "node_modules/@csstools/color-helpers": {
357- "engines": {357+ "version": "5.1.0",
358- "node": ">=18"358+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
359- }359+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
360- },360+ "dev": true,
361- "node_modules/@csstools/css-calc": {361+ "funding": [
362- "version": "2.1.4",362+ {
363- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",363+ "type": "github",
364- "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",364+ "url": "https://github.com/sponsors/csstools"
365- "dev": true,365+ },
366- "funding": [366+ {
367- {367+ "type": "opencollective",
368- "type": "github",368+ "url": "https://opencollective.com/csstools"
369- "url": "https://github.com/sponsors/csstools"369+ }
370- },370+ ],
371- {371+ "license": "MIT-0",
372- "type": "opencollective",372+ "engines": {
373- "url": "https://opencollective.com/csstools"373+ "node": ">=18"
374- }374+ }
375- ],375+ },
376- "license": "MIT",376+ "node_modules/@csstools/css-calc": {
377- "engines": {377+ "version": "2.1.4",
378- "node": ">=18"378+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
379- },379+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
380- "peerDependencies": {380+ "dev": true,
381- "@csstools/css-parser-algorithms": "^3.0.5",381+ "funding": [
382- "@csstools/css-tokenizer": "^3.0.4"382+ {
383- }383+ "type": "github",
384- },384+ "url": "https://github.com/sponsors/csstools"
385- "node_modules/@csstools/css-color-parser": {385+ },
386- "version": "3.1.0",386+ {
387- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",387+ "type": "opencollective",
388- "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",388+ "url": "https://opencollective.com/csstools"
389- "dev": true,389+ }
390- "funding": [390+ ],
391- {391+ "license": "MIT",
392- "type": "github",392+ "engines": {
393- "url": "https://github.com/sponsors/csstools"393+ "node": ">=18"
394- },394+ },
395- {395+ "peerDependencies": {
396- "type": "opencollective",396+ "@csstools/css-parser-algorithms": "^3.0.5",
397- "url": "https://opencollective.com/csstools"397+ "@csstools/css-tokenizer": "^3.0.4"
398- }398+ }
399- ],399+ },
400- "license": "MIT",400+ "node_modules/@csstools/css-color-parser": {
401- "dependencies": {401+ "version": "3.1.0",
402- "@csstools/color-helpers": "^5.1.0",402+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
403- "@csstools/css-calc": "^2.1.4"403+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
404- },404+ "dev": true,
405- "engines": {405+ "funding": [
406- "node": ">=18"406+ {
407- },407+ "type": "github",
408- "peerDependencies": {408+ "url": "https://github.com/sponsors/csstools"
409- "@csstools/css-parser-algorithms": "^3.0.5",409+ },
410- "@csstools/css-tokenizer": "^3.0.4"410+ {
411- }411+ "type": "opencollective",
412- },412+ "url": "https://opencollective.com/csstools"
413- "node_modules/@csstools/css-parser-algorithms": {413+ }
414- "version": "3.0.5",414+ ],
415- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",415+ "license": "MIT",
416- "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",416+ "dependencies": {
417- "dev": true,417+ "@csstools/color-helpers": "^5.1.0",
418- "funding": [418+ "@csstools/css-calc": "^2.1.4"
419- {419+ },
420- "type": "github",420+ "engines": {
421- "url": "https://github.com/sponsors/csstools"421+ "node": ">=18"
422- },422+ },
423- {423+ "peerDependencies": {
424- "type": "opencollective",424+ "@csstools/css-parser-algorithms": "^3.0.5",
425- "url": "https://opencollective.com/csstools"425+ "@csstools/css-tokenizer": "^3.0.4"
426- }426+ }
427- ],427+ },
428- "license": "MIT",428+ "node_modules/@csstools/css-parser-algorithms": {
429- "engines": {429+ "version": "3.0.5",
430- "node": ">=18"430+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
431- },431+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
432- "peerDependencies": {432+ "dev": true,
433- "@csstools/css-tokenizer": "^3.0.4"433+ "funding": [
434- }434+ {
435- },435+ "type": "github",
436- "node_modules/@csstools/css-tokenizer": {436+ "url": "https://github.com/sponsors/csstools"
437- "version": "3.0.4",437+ },
438- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",438+ {
439- "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",439+ "type": "opencollective",
440- "dev": true,440+ "url": "https://opencollective.com/csstools"
441- "funding": [441+ }
442- {442+ ],
443- "type": "github",443+ "license": "MIT",
444- "url": "https://github.com/sponsors/csstools"444+ "engines": {
445- },445+ "node": ">=18"
446- {446+ },
447- "type": "opencollective",447+ "peerDependencies": {
448- "url": "https://opencollective.com/csstools"448+ "@csstools/css-tokenizer": "^3.0.4"
449- }449+ }
450- ],450+ },
451- "license": "MIT",451+ "node_modules/@csstools/css-tokenizer": {
452- "engines": {452+ "version": "3.0.4",
453- "node": ">=18"453+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
454- }454+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
455- },455+ "dev": true,
456- "node_modules/@esbuild/aix-ppc64": {456+ "funding": [
457- "version": "0.28.2",457+ {
458- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",458+ "type": "github",
459- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",459+ "url": "https://github.com/sponsors/csstools"
460- "cpu": [460+ },
461- "ppc64"461+ {
462- ],462+ "type": "opencollective",
463- "dev": true,463+ "url": "https://opencollective.com/csstools"
464- "license": "MIT",464+ }
465- "optional": true,465+ ],
466- "os": [466+ "license": "MIT",
467- "aix"467+ "engines": {
468- ],468+ "node": ">=18"
469- "engines": {469+ }
470- "node": ">=18"470+ },
471- }471+ "node_modules/@esbuild/aix-ppc64": {
472- },472+ "version": "0.28.2",
473- "node_modules/@esbuild/android-arm": {473+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
474- "version": "0.28.2",474+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
475- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",475+ "cpu": [
476- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",476+ "ppc64"
477- "cpu": [477+ ],
478- "arm"478+ "dev": true,
479- ],479+ "license": "MIT",
480- "dev": true,480+ "optional": true,
481- "license": "MIT",481+ "os": [
482- "optional": true,482+ "aix"
483- "os": [483+ ],
484- "android"484+ "engines": {
485- ],485+ "node": ">=18"
486- "engines": {486+ }
487- "node": ">=18"487+ },
488- }488+ "node_modules/@esbuild/android-arm": {
489- },489+ "version": "0.28.2",
490- "node_modules/@esbuild/android-arm64": {490+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
491- "version": "0.28.2",491+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
492- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",492+ "cpu": [
493- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",493+ "arm"
494- "cpu": [494+ ],
495- "arm64"495+ "dev": true,
496- ],496+ "license": "MIT",
497- "dev": true,497+ "optional": true,
498- "license": "MIT",498+ "os": [
499- "optional": true,499+ "android"
500- "os": [500+ ],
501- "android"501+ "engines": {
502- ],502+ "node": ">=18"
503- "engines": {503+ }
504- "node": ">=18"504+ },
505- }505+ "node_modules/@esbuild/android-arm64": {
506- },506+ "version": "0.28.2",
507- "node_modules/@esbuild/android-x64": {507+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
508- "version": "0.28.2",508+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
509- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",509+ "cpu": [
510- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",510+ "arm64"
511- "cpu": [511+ ],
512- "x64"512+ "dev": true,
513- ],513+ "license": "MIT",
514- "dev": true,514+ "optional": true,
515- "license": "MIT",515+ "os": [
516- "optional": true,516+ "android"
517- "os": [517+ ],
518- "android"518+ "engines": {
519- ],519+ "node": ">=18"
520- "engines": {520+ }
521- "node": ">=18"521+ },
522- }522+ "node_modules/@esbuild/android-x64": {
523- },523+ "version": "0.28.2",
524- "node_modules/@esbuild/darwin-arm64": {524+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
525- "version": "0.28.2",525+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
526- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",526+ "cpu": [
527- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",527+ "x64"
528- "cpu": [528+ ],
529- "arm64"529+ "dev": true,
530- ],530+ "license": "MIT",
531- "dev": true,531+ "optional": true,
532- "license": "MIT",532+ "os": [
533- "optional": true,533+ "android"
534- "os": [534+ ],
535- "darwin"535+ "engines": {
536- ],536+ "node": ">=18"
537- "engines": {537+ }
538- "node": ">=18"538+ },
539- }539+ "node_modules/@esbuild/darwin-arm64": {
540- },540+ "version": "0.28.2",
541- "node_modules/@esbuild/darwin-x64": {541+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
542- "version": "0.28.2",542+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
543- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",543+ "cpu": [
544- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",544+ "arm64"
545- "cpu": [545+ ],
546- "x64"546+ "dev": true,
547- ],547+ "license": "MIT",
548- "dev": true,548+ "optional": true,
549- "license": "MIT",549+ "os": [
550- "optional": true,550+ "darwin"
551- "os": [551+ ],
552- "darwin"552+ "engines": {
553- ],553+ "node": ">=18"
554- "engines": {554+ }
555- "node": ">=18"555+ },
556- }556+ "node_modules/@esbuild/darwin-x64": {
557- },557+ "version": "0.28.2",
558- "node_modules/@esbuild/freebsd-arm64": {558+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
559- "version": "0.28.2",559+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
560- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",560+ "cpu": [
561- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",561+ "x64"
562- "cpu": [562+ ],
563- "arm64"563+ "dev": true,
564- ],564+ "license": "MIT",
565- "dev": true,565+ "optional": true,
566- "license": "MIT",566+ "os": [
567- "optional": true,567+ "darwin"
568- "os": [568+ ],
569- "freebsd"569+ "engines": {
570- ],570+ "node": ">=18"
571- "engines": {571+ }
572- "node": ">=18"572+ },
573- }573+ "node_modules/@esbuild/freebsd-arm64": {
574- },574+ "version": "0.28.2",
575- "node_modules/@esbuild/freebsd-x64": {575+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
576- "version": "0.28.2",576+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
577- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",577+ "cpu": [
578- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",578+ "arm64"
579- "cpu": [579+ ],
580- "x64"580+ "dev": true,
581- ],581+ "license": "MIT",
582- "dev": true,582+ "optional": true,
583- "license": "MIT",583+ "os": [
584- "optional": true,584+ "freebsd"
585- "os": [585+ ],
586- "freebsd"586+ "engines": {
587- ],587+ "node": ">=18"
588- "engines": {588+ }
589- "node": ">=18"589+ },
590- }590+ "node_modules/@esbuild/freebsd-x64": {
591- },591+ "version": "0.28.2",
592- "node_modules/@esbuild/linux-arm": {592+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
593- "version": "0.28.2",593+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
594- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",594+ "cpu": [
595- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",595+ "x64"
596- "cpu": [596+ ],
597- "arm"597+ "dev": true,
598- ],598+ "license": "MIT",
599- "dev": true,599+ "optional": true,
600- "license": "MIT",600+ "os": [
601- "optional": true,601+ "freebsd"
602- "os": [602+ ],
603- "linux"603+ "engines": {
604- ],604+ "node": ">=18"
605- "engines": {605+ }
606- "node": ">=18"606+ },
607- }607+ "node_modules/@esbuild/linux-arm": {
608- },608+ "version": "0.28.2",
609- "node_modules/@esbuild/linux-arm64": {609+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
610- "version": "0.28.2",610+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
611- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",611+ "cpu": [
612- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",612+ "arm"
613- "cpu": [613+ ],
614- "arm64"614+ "dev": true,
615- ],615+ "license": "MIT",
616- "dev": true,616+ "optional": true,
617- "license": "MIT",617+ "os": [
618- "optional": true,618+ "linux"
619- "os": [619+ ],
620- "linux"620+ "engines": {
621- ],621+ "node": ">=18"
622- "engines": {622+ }
623- "node": ">=18"623+ },
624- }624+ "node_modules/@esbuild/linux-arm64": {
625- },625+ "version": "0.28.2",
626- "node_modules/@esbuild/linux-ia32": {626+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
627- "version": "0.28.2",627+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
628- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",628+ "cpu": [
629- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",629+ "arm64"
630- "cpu": [630+ ],
631- "ia32"631+ "dev": true,
632- ],632+ "license": "MIT",
633- "dev": true,633+ "optional": true,
634- "license": "MIT",634+ "os": [
635- "optional": true,635+ "linux"
636- "os": [636+ ],
637- "linux"637+ "engines": {
638- ],638+ "node": ">=18"
639- "engines": {639+ }
640- "node": ">=18"640+ },
641- }641+ "node_modules/@esbuild/linux-ia32": {
642- },642+ "version": "0.28.2",
643- "node_modules/@esbuild/linux-loong64": {643+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
644- "version": "0.28.2",644+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
645- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",645+ "cpu": [
646- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",646+ "ia32"
647- "cpu": [647+ ],
648- "loong64"648+ "dev": true,
649- ],649+ "license": "MIT",
650- "dev": true,650+ "optional": true,
651- "license": "MIT",651+ "os": [
652- "optional": true,652+ "linux"
653- "os": [653+ ],
654- "linux"654+ "engines": {
655- ],655+ "node": ">=18"
656- "engines": {656+ }
657- "node": ">=18"657+ },
658- }658+ "node_modules/@esbuild/linux-loong64": {
659- },659+ "version": "0.28.2",
660- "node_modules/@esbuild/linux-mips64el": {660+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
661- "version": "0.28.2",661+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
662- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",662+ "cpu": [
663- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",663+ "loong64"
664- "cpu": [664+ ],
665- "mips64el"665+ "dev": true,
666- ],666+ "license": "MIT",
667- "dev": true,667+ "optional": true,
668- "license": "MIT",668+ "os": [
669- "optional": true,669+ "linux"
670- "os": [670+ ],
671- "linux"671+ "engines": {
672- ],672+ "node": ">=18"
673- "engines": {673+ }
674- "node": ">=18"674+ },
675- }675+ "node_modules/@esbuild/linux-mips64el": {
676- },676+ "version": "0.28.2",
677- "node_modules/@esbuild/linux-ppc64": {677+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
678- "version": "0.28.2",678+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
679- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",679+ "cpu": [
680- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",680+ "mips64el"
681- "cpu": [681+ ],
682- "ppc64"682+ "dev": true,
683- ],683+ "license": "MIT",
684- "dev": true,684+ "optional": true,
685- "license": "MIT",685+ "os": [
686- "optional": true,686+ "linux"
687- "os": [687+ ],
688- "linux"688+ "engines": {
689- ],689+ "node": ">=18"
690- "engines": {690+ }
691- "node": ">=18"691+ },
692- }692+ "node_modules/@esbuild/linux-ppc64": {
693- },693+ "version": "0.28.2",
694- "node_modules/@esbuild/linux-riscv64": {694+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
695- "version": "0.28.2",695+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
696- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",696+ "cpu": [
697- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",697+ "ppc64"
698- "cpu": [698+ ],
699- "riscv64"699+ "dev": true,
700- ],700+ "license": "MIT",
701- "dev": true,701+ "optional": true,
702- "license": "MIT",702+ "os": [
703- "optional": true,703+ "linux"
704- "os": [704+ ],
705- "linux"705+ "engines": {
706- ],706+ "node": ">=18"
707- "engines": {707+ }
708- "node": ">=18"708+ },
709- }709+ "node_modules/@esbuild/linux-riscv64": {
710- },710+ "version": "0.28.2",
711- "node_modules/@esbuild/linux-s390x": {711+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
712- "version": "0.28.2",712+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
713- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",713+ "cpu": [
714- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",714+ "riscv64"
715- "cpu": [715+ ],
716- "s390x"716+ "dev": true,
717- ],717+ "license": "MIT",
718- "dev": true,718+ "optional": true,
719- "license": "MIT",719+ "os": [
720- "optional": true,720+ "linux"
721- "os": [721+ ],
722- "linux"722+ "engines": {
723- ],723+ "node": ">=18"
724- "engines": {724+ }
725- "node": ">=18"725+ },
726- }726+ "node_modules/@esbuild/linux-s390x": {
727- },727+ "version": "0.28.2",
728- "node_modules/@esbuild/linux-x64": {728+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
729- "version": "0.28.2",729+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
730- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",730+ "cpu": [
731- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",731+ "s390x"
732- "cpu": [732+ ],
733- "x64"733+ "dev": true,
734- ],734+ "license": "MIT",
735- "dev": true,735+ "optional": true,
736- "license": "MIT",736+ "os": [
737- "optional": true,737+ "linux"
738- "os": [738+ ],
739- "linux"739+ "engines": {
740- ],740+ "node": ">=18"
741- "engines": {741+ }
742- "node": ">=18"742+ },
743- }743+ "node_modules/@esbuild/linux-x64": {
744- },744+ "version": "0.28.2",
745- "node_modules/@esbuild/netbsd-arm64": {745+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
746- "version": "0.28.2",746+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
747- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",747+ "cpu": [
748- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",748+ "x64"
749- "cpu": [749+ ],
750- "arm64"750+ "dev": true,
751- ],751+ "license": "MIT",
752- "dev": true,752+ "optional": true,
753- "license": "MIT",753+ "os": [
754- "optional": true,754+ "linux"
755- "os": [755+ ],
756- "netbsd"756+ "engines": {
757- ],757+ "node": ">=18"
758- "engines": {758+ }
759- "node": ">=18"759+ },
760- }760+ "node_modules/@esbuild/netbsd-arm64": {
761- },761+ "version": "0.28.2",
762- "node_modules/@esbuild/netbsd-x64": {762+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
763- "version": "0.28.2",763+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
764- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",764+ "cpu": [
765- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",765+ "arm64"
766- "cpu": [766+ ],
767- "x64"767+ "dev": true,
768- ],768+ "license": "MIT",
769- "dev": true,769+ "optional": true,
770- "license": "MIT",770+ "os": [
771- "optional": true,771+ "netbsd"
772- "os": [772+ ],
773- "netbsd"773+ "engines": {
774- ],774+ "node": ">=18"
775- "engines": {775+ }
776- "node": ">=18"776+ },
777- }777+ "node_modules/@esbuild/netbsd-x64": {
778- },778+ "version": "0.28.2",
779- "node_modules/@esbuild/openbsd-arm64": {779+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
780- "version": "0.28.2",780+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
781- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",781+ "cpu": [
782- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",782+ "x64"
783- "cpu": [783+ ],
784- "arm64"784+ "dev": true,
785- ],785+ "license": "MIT",
786- "dev": true,786+ "optional": true,
787- "license": "MIT",787+ "os": [
788- "optional": true,788+ "netbsd"
789- "os": [789+ ],
790- "openbsd"790+ "engines": {
791- ],791+ "node": ">=18"
792- "engines": {792+ }
793- "node": ">=18"793+ },
794- }794+ "node_modules/@esbuild/openbsd-arm64": {
795- },795+ "version": "0.28.2",
796- "node_modules/@esbuild/openbsd-x64": {796+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
797- "version": "0.28.2",797+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
798- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",798+ "cpu": [
799- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",799+ "arm64"
800- "cpu": [800+ ],
801- "x64"801+ "dev": true,
802- ],802+ "license": "MIT",
803- "dev": true,803+ "optional": true,
804- "license": "MIT",804+ "os": [
805- "optional": true,805+ "openbsd"
806- "os": [806+ ],
807- "openbsd"807+ "engines": {
808- ],808+ "node": ">=18"
809- "engines": {809+ }
810- "node": ">=18"810+ },
811- }811+ "node_modules/@esbuild/openbsd-x64": {
812- },812+ "version": "0.28.2",
813- "node_modules/@esbuild/openharmony-arm64": {813+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
814- "version": "0.28.2",814+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
815- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",815+ "cpu": [
816- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",816+ "x64"
817- "cpu": [817+ ],
818- "arm64"818+ "dev": true,
819- ],819+ "license": "MIT",
820- "dev": true,820+ "optional": true,
821- "license": "MIT",821+ "os": [
822- "optional": true,822+ "openbsd"
823- "os": [823+ ],
824- "openharmony"824+ "engines": {
825- ],825+ "node": ">=18"
826- "engines": {826+ }
827- "node": ">=18"827+ },
828- }828+ "node_modules/@esbuild/openharmony-arm64": {
829- },829+ "version": "0.28.2",
830- "node_modules/@esbuild/sunos-x64": {830+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
831- "version": "0.28.2",831+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
832- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",832+ "cpu": [
833- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",833+ "arm64"
834- "cpu": [834+ ],
835- "x64"835+ "dev": true,
836- ],836+ "license": "MIT",
837- "dev": true,837+ "optional": true,
838- "license": "MIT",838+ "os": [
839- "optional": true,839+ "openharmony"
840- "os": [840+ ],
841- "sunos"841+ "engines": {
842- ],842+ "node": ">=18"
843- "engines": {843+ }
844- "node": ">=18"844+ },
845- }845+ "node_modules/@esbuild/sunos-x64": {
846- },846+ "version": "0.28.2",
847- "node_modules/@esbuild/win32-arm64": {847+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
848- "version": "0.28.2",848+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
849- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",849+ "cpu": [
850- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",850+ "x64"
851- "cpu": [851+ ],
852- "arm64"852+ "dev": true,
853- ],853+ "license": "MIT",
854- "dev": true,854+ "optional": true,
855- "license": "MIT",855+ "os": [
856- "optional": true,856+ "sunos"
857- "os": [857+ ],
858- "win32"858+ "engines": {
859- ],859+ "node": ">=18"
860- "engines": {860+ }
861- "node": ">=18"861+ },
862- }862+ "node_modules/@esbuild/win32-arm64": {
863- },863+ "version": "0.28.2",
864- "node_modules/@esbuild/win32-ia32": {864+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
865- "version": "0.28.2",865+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
866- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",866+ "cpu": [
867- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",867+ "arm64"
868- "cpu": [868+ ],
869- "ia32"869+ "dev": true,
870- ],870+ "license": "MIT",
871- "dev": true,871+ "optional": true,
872- "license": "MIT",872+ "os": [
873- "optional": true,873+ "win32"
874- "os": [874+ ],
875- "win32"875+ "engines": {
876- ],876+ "node": ">=18"
877- "engines": {877+ }
878- "node": ">=18"878+ },
879- }879+ "node_modules/@esbuild/win32-ia32": {
880- },880+ "version": "0.28.2",
881- "node_modules/@esbuild/win32-x64": {881+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
882- "version": "0.28.2",882+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
883- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",883+ "cpu": [
884- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",884+ "ia32"
885- "cpu": [885+ ],
886- "x64"886+ "dev": true,
887- ],887+ "license": "MIT",
888- "dev": true,888+ "optional": true,
889- "license": "MIT",889+ "os": [
890- "optional": true,890+ "win32"
891- "os": [891+ ],
892- "win32"892+ "engines": {
893- ],893+ "node": ">=18"
894- "engines": {894+ }
895- "node": ">=18"895+ },
896- }896+ "node_modules/@esbuild/win32-x64": {
897- },897+ "version": "0.28.2",
898- "node_modules/@jridgewell/gen-mapping": {898+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
899- "version": "0.3.13",899+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
900- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",900+ "cpu": [
901- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",901+ "x64"
902- "dev": true,902+ ],
903- "license": "MIT",903+ "dev": true,
904- "dependencies": {904+ "license": "MIT",
905- "@jridgewell/sourcemap-codec": "^1.5.0",905+ "optional": true,
906- "@jridgewell/trace-mapping": "^0.3.24"906+ "os": [
907- }907+ "win32"
908- },908+ ],
909- "node_modules/@jridgewell/remapping": {909+ "engines": {
910- "version": "2.3.5",910+ "node": ">=18"
911- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",911+ }
912- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",912+ },
913- "dev": true,913+ "node_modules/@jridgewell/gen-mapping": {
914- "license": "MIT",914+ "version": "0.3.13",
915- "dependencies": {915+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
916- "@jridgewell/gen-mapping": "^0.3.5",916+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
917- "@jridgewell/trace-mapping": "^0.3.24"917+ "dev": true,
918- }918+ "license": "MIT",
919- },919+ "dependencies": {
920- "node_modules/@jridgewell/resolve-uri": {920+ "@jridgewell/sourcemap-codec": "^1.5.0",
921- "version": "3.1.2",921+ "@jridgewell/trace-mapping": "^0.3.24"
922- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",922+ }
923- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",923+ },
924- "dev": true,924+ "node_modules/@jridgewell/remapping": {
925- "license": "MIT",925+ "version": "2.3.5",
926- "engines": {926+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
927- "node": ">=6.0.0"927+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
928- }928+ "dev": true,
929- },929+ "license": "MIT",
930- "node_modules/@jridgewell/sourcemap-codec": {930+ "dependencies": {
931- "version": "1.6.0",931+ "@jridgewell/gen-mapping": "^0.3.5",
932- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",932+ "@jridgewell/trace-mapping": "^0.3.24"
933- "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",933+ }
934- "dev": true,934+ },
935- "license": "MIT"935+ "node_modules/@jridgewell/resolve-uri": {
936- },936+ "version": "3.1.2",
937- "node_modules/@jridgewell/trace-mapping": {937+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
938- "version": "0.3.31",938+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
939- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",939+ "dev": true,
940- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",940+ "license": "MIT",
941- "dev": true,941+ "engines": {
942- "license": "MIT",942+ "node": ">=6.0.0"
943- "dependencies": {943+ }
944- "@jridgewell/resolve-uri": "^3.1.0",944+ },
945- "@jridgewell/sourcemap-codec": "^1.4.14"945+ "node_modules/@jridgewell/sourcemap-codec": {
946- }946+ "version": "1.6.0",
947- },947+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
948- "node_modules/@napi-rs/lzma-linux-x64-gnu": {948+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
949- "version": "1.5.1",949+ "dev": true,
950- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",950+ "license": "MIT"
951- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",951+ },
952- "cpu": [952+ "node_modules/@jridgewell/trace-mapping": {
953- "x64"953+ "version": "0.3.31",
954- ],954+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
955- "dev": true,955+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
956- "libc": [956+ "dev": true,
957- "glibc"957+ "license": "MIT",
958- ],958+ "dependencies": {
959- "license": "MIT",959+ "@jridgewell/resolve-uri": "^3.1.0",
960- "optional": true,960+ "@jridgewell/sourcemap-codec": "^1.4.14"
961- "os": [961+ }
962- "linux"962+ },
963- ],963+ "node_modules/@monaco-editor/loader": {
964- "engines": {964+ "version": "1.7.0",
965- "node": "^22.20 || ^24.12 || >=25"965+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
966- }966+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
967- },967+ "license": "MIT",
968- "node_modules/@rolldown/pluginutils": {968+ "dependencies": {
969- "version": "1.0.0-rc.3",969+ "state-local": "^1.0.6"
970- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",970+ }
971- "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",971+ },
972- "dev": true,972+ "node_modules/@monaco-editor/react": {
973- "license": "MIT"973+ "version": "4.7.0",
974- },974+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
975- "node_modules/@rollup/rollup-android-arm-eabi": {975+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
976- "version": "4.63.3",976+ "license": "MIT",
977- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",977+ "dependencies": {
978- "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==",978+ "@monaco-editor/loader": "^1.5.0"
979- "cpu": [979+ },
980- "arm"980+ "peerDependencies": {
981- ],981+ "monaco-editor": ">= 0.25.0 < 1",
982- "dev": true,982+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
983- "license": "MIT",983+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
984- "optional": true,984+ }
985- "os": [985+ },
986- "android"986+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
987- ]987+ "version": "1.5.1",
988- },988+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
989- "node_modules/@rollup/rollup-android-arm64": {989+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
990- "version": "4.63.3",990+ "cpu": [
991- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz",991+ "x64"
992- "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==",992+ ],
993- "cpu": [993+ "dev": true,
994- "arm64"994+ "libc": [
995- ],995+ "glibc"
996- "dev": true,996+ ],
997- "license": "MIT",997+ "license": "MIT",
998- "optional": true,998+ "optional": true,
999- "os": [999+ "os": [
1000- "android"1000+ "linux"
1001- ]1001+ ],
1002- },1002+ "engines": {
1003- "node_modules/@rollup/rollup-darwin-arm64": {1003+ "node": "^22.20 || ^24.12 || >=25"
1004- "version": "4.63.3",1004+ }
1005- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz",1005+ },
1006- "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==",1006+ "node_modules/@rolldown/pluginutils": {
1007- "cpu": [1007+ "version": "1.0.0-rc.3",
1008- "arm64"1008+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
1009- ],1009+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
1010- "dev": true,1010+ "dev": true,
1011- "license": "MIT",1011+ "license": "MIT"
1012- "optional": true,1012+ },
1013- "os": [1013+ "node_modules/@rollup/rollup-android-arm-eabi": {
1014- "darwin"1014+ "version": "4.63.3",
1015- ]1015+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",
1016- },1016+ "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==",
1017- "node_modules/@rollup/rollup-darwin-x64": {1017+ "cpu": [
1018- "version": "4.63.3",1018+ "arm"
1019- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz",1019+ ],
1020- "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==",1020+ "dev": true,
1021- "cpu": [1021+ "license": "MIT",
1022- "x64"1022+ "optional": true,
1023- ],1023+ "os": [
1024- "dev": true,1024+ "android"
1025- "license": "MIT",1025+ ]
1026- "optional": true,1026+ },
1027- "os": [1027+ "node_modules/@rollup/rollup-android-arm64": {
1028- "darwin"1028+ "version": "4.63.3",
1029- ]1029+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz",
1030- },1030+ "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==",
1031- "node_modules/@rollup/rollup-freebsd-arm64": {1031+ "cpu": [
1032- "version": "4.63.3",1032+ "arm64"
1033- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz",1033+ ],
1034- "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==",1034+ "dev": true,
1035- "cpu": [1035+ "license": "MIT",
1036- "arm64"1036+ "optional": true,
1037- ],1037+ "os": [
1038- "dev": true,1038+ "android"
1039- "license": "MIT",1039+ ]
1040- "optional": true,1040+ },
1041- "os": [1041+ "node_modules/@rollup/rollup-darwin-arm64": {
1042- "freebsd"1042+ "version": "4.63.3",
1043- ]1043+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz",
1044- },1044+ "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==",
1045- "node_modules/@rollup/rollup-freebsd-x64": {1045+ "cpu": [
1046- "version": "4.63.3",1046+ "arm64"
1047- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz",1047+ ],
1048- "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==",1048+ "dev": true,
1049- "cpu": [1049+ "license": "MIT",
1050- "x64"1050+ "optional": true,
1051- ],1051+ "os": [
1052- "dev": true,1052+ "darwin"
1053- "license": "MIT",1053+ ]
1054- "optional": true,1054+ },
1055- "os": [1055+ "node_modules/@rollup/rollup-darwin-x64": {
1056- "freebsd"1056+ "version": "4.63.3",
1057- ]1057+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz",
1058- },1058+ "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==",
1059- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {1059+ "cpu": [
1060- "version": "4.63.3",1060+ "x64"
1061- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz",1061+ ],
1062- "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==",1062+ "dev": true,
1063- "cpu": [1063+ "license": "MIT",
1064- "arm"1064+ "optional": true,
1065- ],1065+ "os": [
1066- "dev": true,1066+ "darwin"
1067- "libc": [1067+ ]
1068- "glibc"1068+ },
1069- ],1069+ "node_modules/@rollup/rollup-freebsd-arm64": {
1070- "license": "MIT",1070+ "version": "4.63.3",
1071- "optional": true,1071+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz",
1072- "os": [1072+ "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==",
1073- "linux"1073+ "cpu": [
1074- ]1074+ "arm64"
1075- },1075+ ],
1076- "node_modules/@rollup/rollup-linux-arm-musleabihf": {1076+ "dev": true,
1077- "version": "4.63.3",1077+ "license": "MIT",
1078- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz",1078+ "optional": true,
1079- "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==",1079+ "os": [
1080- "cpu": [1080+ "freebsd"
1081- "arm"1081+ ]
1082- ],1082+ },
1083- "dev": true,1083+ "node_modules/@rollup/rollup-freebsd-x64": {
1084- "libc": [1084+ "version": "4.63.3",
1085- "musl"1085+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz",
1086- ],1086+ "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==",
1087- "license": "MIT",1087+ "cpu": [
1088- "optional": true,1088+ "x64"
1089- "os": [1089+ ],
1090- "linux"1090+ "dev": true,
1091- ]1091+ "license": "MIT",
1092- },1092+ "optional": true,
1093- "node_modules/@rollup/rollup-linux-arm64-gnu": {1093+ "os": [
1094- "version": "4.63.3",1094+ "freebsd"
1095- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz",1095+ ]
1096- "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==",1096+ },
1097- "cpu": [1097+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1098- "arm64"1098+ "version": "4.63.3",
1099- ],1099+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz",
1100- "dev": true,1100+ "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==",
1101- "libc": [1101+ "cpu": [
1102- "glibc"1102+ "arm"
1103- ],1103+ ],
1104- "license": "MIT",1104+ "dev": true,
1105- "optional": true,1105+ "libc": [
1106- "os": [1106+ "glibc"
1107- "linux"1107+ ],
1108- ]1108+ "license": "MIT",
1109- },1109+ "optional": true,
1110- "node_modules/@rollup/rollup-linux-arm64-musl": {1110+ "os": [
1111- "version": "4.63.3",1111+ "linux"
1112- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz",1112+ ]
1113- "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==",1113+ },
1114- "cpu": [1114+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1115- "arm64"1115+ "version": "4.63.3",
1116- ],1116+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz",
1117- "dev": true,1117+ "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==",
1118- "libc": [1118+ "cpu": [
1119- "musl"1119+ "arm"
1120- ],1120+ ],
1121- "license": "MIT",1121+ "dev": true,
1122- "optional": true,1122+ "libc": [
1123- "os": [1123+ "musl"
1124- "linux"1124+ ],
1125- ]1125+ "license": "MIT",
1126- },1126+ "optional": true,
1127- "node_modules/@rollup/rollup-linux-loong64-gnu": {1127+ "os": [
1128- "version": "4.63.3",1128+ "linux"
1129- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz",1129+ ]
1130- "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==",1130+ },
1131- "cpu": [1131+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
1132- "loong64"1132+ "version": "4.63.3",
1133- ],1133+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz",
1134- "dev": true,1134+ "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==",
1135- "libc": [1135+ "cpu": [
1136- "glibc"1136+ "arm64"
1137- ],1137+ ],
1138- "license": "MIT",1138+ "dev": true,
1139- "optional": true,1139+ "libc": [
1140- "os": [1140+ "glibc"
1141- "linux"1141+ ],
1142- ]1142+ "license": "MIT",
1143- },1143+ "optional": true,
1144- "node_modules/@rollup/rollup-linux-loong64-musl": {1144+ "os": [
1145- "version": "4.63.3",1145+ "linux"
1146- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz",1146+ ]
1147- "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==",1147+ },
1148- "cpu": [1148+ "node_modules/@rollup/rollup-linux-arm64-musl": {
1149- "loong64"1149+ "version": "4.63.3",
1150- ],1150+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz",
1151- "dev": true,1151+ "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==",
1152- "libc": [1152+ "cpu": [
1153- "musl"1153+ "arm64"
1154- ],1154+ ],
1155- "license": "MIT",1155+ "dev": true,
1156- "optional": true,1156+ "libc": [
1157- "os": [1157+ "musl"
1158- "linux"1158+ ],
1159- ]1159+ "license": "MIT",
1160- },1160+ "optional": true,
1161- "node_modules/@rollup/rollup-linux-ppc64-gnu": {1161+ "os": [
1162- "version": "4.63.3",1162+ "linux"
1163- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz",1163+ ]
1164- "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==",1164+ },
1165- "cpu": [1165+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
1166- "ppc64"1166+ "version": "4.63.3",
1167- ],1167+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz",
1168- "dev": true,1168+ "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==",
1169- "libc": [1169+ "cpu": [
1170- "glibc"1170+ "loong64"
1171- ],1171+ ],
1172- "license": "MIT",1172+ "dev": true,
1173- "optional": true,1173+ "libc": [
1174- "os": [1174+ "glibc"
1175- "linux"1175+ ],
1176- ]1176+ "license": "MIT",
1177- },1177+ "optional": true,
1178- "node_modules/@rollup/rollup-linux-ppc64-musl": {1178+ "os": [
1179- "version": "4.63.3",1179+ "linux"
1180- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz",1180+ ]
1181- "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==",1181+ },
1182- "cpu": [1182+ "node_modules/@rollup/rollup-linux-loong64-musl": {
1183- "ppc64"1183+ "version": "4.63.3",
1184- ],1184+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz",
1185- "dev": true,1185+ "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==",
1186- "libc": [1186+ "cpu": [
1187- "musl"1187+ "loong64"
1188- ],1188+ ],
1189- "license": "MIT",1189+ "dev": true,
1190- "optional": true,1190+ "libc": [
1191- "os": [1191+ "musl"
1192- "linux"1192+ ],
1193- ]1193+ "license": "MIT",
1194- },1194+ "optional": true,
1195- "node_modules/@rollup/rollup-linux-riscv64-gnu": {1195+ "os": [
1196- "version": "4.63.3",1196+ "linux"
1197- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz",1197+ ]
1198- "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==",1198+ },
1199- "cpu": [1199+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1200- "riscv64"1200+ "version": "4.63.3",
1201- ],1201+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz",
1202- "dev": true,1202+ "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==",
1203- "libc": [1203+ "cpu": [
1204- "glibc"1204+ "ppc64"
1205- ],1205+ ],
1206- "license": "MIT",1206+ "dev": true,
1207- "optional": true,1207+ "libc": [
1208- "os": [1208+ "glibc"
1209- "linux"1209+ ],
1210- ]1210+ "license": "MIT",
1211- },1211+ "optional": true,
1212- "node_modules/@rollup/rollup-linux-riscv64-musl": {1212+ "os": [
1213- "version": "4.63.3",1213+ "linux"
1214- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz",1214+ ]
1215- "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==",1215+ },
1216- "cpu": [1216+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1217- "riscv64"1217+ "version": "4.63.3",
1218- ],1218+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz",
1219- "dev": true,1219+ "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==",
1220- "libc": [1220+ "cpu": [
1221- "musl"1221+ "ppc64"
1222- ],1222+ ],
1223- "license": "MIT",1223+ "dev": true,
1224- "optional": true,1224+ "libc": [
1225- "os": [1225+ "musl"
1226- "linux"1226+ ],
1227- ]1227+ "license": "MIT",
1228- },1228+ "optional": true,
1229- "node_modules/@rollup/rollup-linux-s390x-gnu": {1229+ "os": [
1230- "version": "4.63.3",1230+ "linux"
1231- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz",1231+ ]
1232- "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==",1232+ },
1233- "cpu": [1233+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1234- "s390x"1234+ "version": "4.63.3",
1235- ],1235+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz",
1236- "dev": true,1236+ "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==",
1237- "libc": [1237+ "cpu": [
1238- "glibc"1238+ "riscv64"
1239- ],1239+ ],
1240- "license": "MIT",1240+ "dev": true,
1241- "optional": true,1241+ "libc": [
1242- "os": [1242+ "glibc"
1243- "linux"1243+ ],
1244- ]1244+ "license": "MIT",
1245- },1245+ "optional": true,
1246- "node_modules/@rollup/rollup-linux-x64-gnu": {1246+ "os": [
1247- "version": "4.63.3",1247+ "linux"
1248- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz",1248+ ]
1249- "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==",1249+ },
1250- "cpu": [1250+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1251- "x64"1251+ "version": "4.63.3",
1252- ],1252+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz",
1253- "dev": true,1253+ "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==",
1254- "libc": [1254+ "cpu": [
1255- "glibc"1255+ "riscv64"
1256- ],1256+ ],
1257- "license": "MIT",1257+ "dev": true,
1258- "optional": true,1258+ "libc": [
1259- "os": [1259+ "musl"
1260- "linux"1260+ ],
1261- ]1261+ "license": "MIT",
1262- },1262+ "optional": true,
1263- "node_modules/@rollup/rollup-linux-x64-musl": {1263+ "os": [
1264- "version": "4.63.3",1264+ "linux"
1265- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz",1265+ ]
1266- "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==",1266+ },
1267- "cpu": [1267+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1268- "x64"1268+ "version": "4.63.3",
1269- ],1269+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz",
1270- "dev": true,1270+ "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==",
1271- "libc": [1271+ "cpu": [
1272- "musl"1272+ "s390x"
1273- ],1273+ ],
1274- "license": "MIT",1274+ "dev": true,
1275- "optional": true,1275+ "libc": [
1276- "os": [1276+ "glibc"
1277- "linux"1277+ ],
1278- ]1278+ "license": "MIT",
1279- },1279+ "optional": true,
1280- "node_modules/@rollup/rollup-openbsd-x64": {1280+ "os": [
1281- "version": "4.63.3",1281+ "linux"
1282- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz",1282+ ]
1283- "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==",1283+ },
1284- "cpu": [1284+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1285- "x64"1285+ "version": "4.63.3",
1286- ],1286+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz",
1287- "dev": true,1287+ "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==",
1288- "license": "MIT",1288+ "cpu": [
1289- "optional": true,1289+ "x64"
1290- "os": [1290+ ],
1291- "openbsd"1291+ "dev": true,
1292- ]1292+ "libc": [
1293- },1293+ "glibc"
1294- "node_modules/@rollup/rollup-openharmony-arm64": {1294+ ],
1295- "version": "4.63.3",1295+ "license": "MIT",
1296- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz",1296+ "optional": true,
1297- "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==",1297+ "os": [
1298- "cpu": [1298+ "linux"
1299- "arm64"1299+ ]
1300- ],1300+ },
1301- "dev": true,1301+ "node_modules/@rollup/rollup-linux-x64-musl": {
1302- "license": "MIT",1302+ "version": "4.63.3",
1303- "optional": true,1303+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz",
1304- "os": [1304+ "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==",
1305- "openharmony"1305+ "cpu": [
1306- ]1306+ "x64"
1307- },1307+ ],
1308- "node_modules/@rollup/rollup-win32-arm64-msvc": {1308+ "dev": true,
1309- "version": "4.63.3",1309+ "libc": [
1310- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz",1310+ "musl"
1311- "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==",1311+ ],
1312- "cpu": [1312+ "license": "MIT",
1313- "arm64"1313+ "optional": true,
1314- ],1314+ "os": [
1315- "dev": true,1315+ "linux"
1316- "license": "MIT",1316+ ]
1317- "optional": true,1317+ },
1318- "os": [1318+ "node_modules/@rollup/rollup-openbsd-x64": {
1319- "win32"1319+ "version": "4.63.3",
1320- ]1320+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz",
1321- },1321+ "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==",
1322- "node_modules/@rollup/rollup-win32-ia32-msvc": {1322+ "cpu": [
1323- "version": "4.63.3",1323+ "x64"
1324- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz",1324+ ],
1325- "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==",1325+ "dev": true,
1326- "cpu": [1326+ "license": "MIT",
1327- "ia32"1327+ "optional": true,
1328- ],1328+ "os": [
1329- "dev": true,1329+ "openbsd"
1330- "license": "MIT",1330+ ]
1331- "optional": true,1331+ },
1332- "os": [1332+ "node_modules/@rollup/rollup-openharmony-arm64": {
1333- "win32"1333+ "version": "4.63.3",
1334- ]1334+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz",
1335- },1335+ "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==",
1336- "node_modules/@rollup/rollup-win32-x64-gnu": {1336+ "cpu": [
1337- "version": "4.63.3",1337+ "arm64"
1338- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz",1338+ ],
1339- "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==",1339+ "dev": true,
1340- "cpu": [1340+ "license": "MIT",
1341- "x64"1341+ "optional": true,
1342- ],1342+ "os": [
1343- "dev": true,1343+ "openharmony"
1344- "license": "MIT",1344+ ]
1345- "optional": true,1345+ },
1346- "os": [1346+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1347- "win32"1347+ "version": "4.63.3",
1348- ]1348+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz",
1349- },1349+ "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==",
1350- "node_modules/@rollup/rollup-win32-x64-msvc": {1350+ "cpu": [
1351- "version": "4.63.3",1351+ "arm64"
1352- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz",1352+ ],
1353- "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==",1353+ "dev": true,
1354- "cpu": [1354+ "license": "MIT",
1355- "x64"1355+ "optional": true,
1356- ],1356+ "os": [
1357- "dev": true,1357+ "win32"
1358- "license": "MIT",1358+ ]
1359- "optional": true,1359+ },
1360- "os": [1360+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1361- "win32"1361+ "version": "4.63.3",
1362- ]1362+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz",
1363- },1363+ "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==",
1364- "node_modules/@testing-library/dom": {1364+ "cpu": [
1365- "version": "10.4.2",1365+ "ia32"
1366- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",1366+ ],
1367- "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",1367+ "dev": true,
1368- "dev": true,1368+ "license": "MIT",
1369- "license": "MIT",1369+ "optional": true,
1370- "peer": true,1370+ "os": [
1371- "dependencies": {1371+ "win32"
1372- "@babel/code-frame": "^7.10.4",1372+ ]
1373- "@babel/runtime": "^7.12.5",1373+ },
1374- "@types/aria-query": "^5.0.1",1374+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1375- "aria-query": "5.3.0",1375+ "version": "4.63.3",
1376- "dom-accessibility-api": "^0.5.9",1376+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz",
1377- "lz-string": "^1.5.0",1377+ "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==",
1378- "picocolors": "1.1.1",1378+ "cpu": [
1379- "pretty-format": "^27.0.2"1379+ "x64"
1380- },1380+ ],
1381- "engines": {1381+ "dev": true,
1382- "node": ">=18"1382+ "license": "MIT",
1383- }1383+ "optional": true,
1384- },1384+ "os": [
1385- "node_modules/@testing-library/react": {1385+ "win32"
1386- "version": "16.3.3",1386+ ]
1387- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",1387+ },
1388- "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",1388+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1389- "dev": true,1389+ "version": "4.63.3",
1390- "license": "MIT",1390+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz",
1391- "dependencies": {1391+ "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==",
1392- "@babel/runtime": "^7.12.5"1392+ "cpu": [
1393- },1393+ "x64"
1394- "engines": {1394+ ],
1395- "node": ">=18"1395+ "dev": true,
1396- },1396+ "license": "MIT",
1397- "peerDependencies": {1397+ "optional": true,
1398- "@testing-library/dom": "^10.0.0",1398+ "os": [
1399- "@types/react": "^18.0.0 || ^19.0.0",1399+ "win32"
1400- "@types/react-dom": "^18.0.0 || ^19.0.0",1400+ ]
1401- "react": "^18.0.0 || ^19.0.0",1401+ },
1402- "react-dom": "^18.0.0 || ^19.0.0"1402+ "node_modules/@testing-library/dom": {
1403- },1403+ "version": "10.4.2",
1404- "peerDependenciesMeta": {1404+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
1405- "@types/react": {1405+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
1406- "optional": true1406+ "dev": true,
1407- },1407+ "license": "MIT",
1408- "@types/react-dom": {1408+ "peer": true,
1409- "optional": true1409+ "dependencies": {
1410- }1410+ "@babel/code-frame": "^7.10.4",
1411- }1411+ "@babel/runtime": "^7.12.5",
1412- },1412+ "@types/aria-query": "^5.0.1",
1413- "node_modules/@types/aria-query": {1413+ "aria-query": "5.3.0",
1414- "version": "5.0.4",1414+ "dom-accessibility-api": "^0.5.9",
1415- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",1415+ "lz-string": "^1.5.0",
1416- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",1416+ "picocolors": "1.1.1",
1417- "dev": true,1417+ "pretty-format": "^27.0.2"
1418- "license": "MIT",1418+ },
1419- "peer": true1419+ "engines": {
1420- },1420+ "node": ">=18"
1421- "node_modules/@types/babel__core": {1421+ }
1422- "version": "7.20.5",1422+ },
1423- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",1423+ "node_modules/@testing-library/react": {
1424- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",1424+ "version": "16.3.3",
1425- "dev": true,1425+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
1426- "license": "MIT",1426+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
1427- "dependencies": {1427+ "dev": true,
1428- "@babel/parser": "^7.20.7",1428+ "license": "MIT",
1429- "@babel/types": "^7.20.7",1429+ "dependencies": {
1430- "@types/babel__generator": "*",1430+ "@babel/runtime": "^7.12.5"
1431- "@types/babel__template": "*",1431+ },
1432- "@types/babel__traverse": "*"1432+ "engines": {
1433- }1433+ "node": ">=18"
1434- },1434+ },
1435- "node_modules/@types/babel__generator": {1435+ "peerDependencies": {
1436- "version": "7.27.0",1436+ "@testing-library/dom": "^10.0.0",
1437- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",1437+ "@types/react": "^18.0.0 || ^19.0.0",
1438- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",1438+ "@types/react-dom": "^18.0.0 || ^19.0.0",
1439- "dev": true,1439+ "react": "^18.0.0 || ^19.0.0",
1440- "license": "MIT",1440+ "react-dom": "^18.0.0 || ^19.0.0"
1441- "dependencies": {1441+ },
1442- "@babel/types": "^7.0.0"1442+ "peerDependenciesMeta": {
1443- }1443+ "@types/react": {
1444- },1444+ "optional": true
1445- "node_modules/@types/babel__template": {1445+ },
1446- "version": "7.4.4",1446+ "@types/react-dom": {
1447- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",1447+ "optional": true
1448- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",1448+ }
1449- "dev": true,1449+ }
1450- "license": "MIT",1450+ },
1451- "dependencies": {1451+ "node_modules/@types/aria-query": {
1452- "@babel/parser": "^7.1.0",1452+ "version": "5.0.4",
1453- "@babel/types": "^7.0.0"1453+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
1454- }1454+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
1455- },1455+ "dev": true,
1456- "node_modules/@types/babel__traverse": {1456+ "license": "MIT",
1457- "version": "7.28.0",1457+ "peer": true
1458- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",1458+ },
1459- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",1459+ "node_modules/@types/babel__core": {
1460- "dev": true,1460+ "version": "7.20.5",
1461- "license": "MIT",1461+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1462- "dependencies": {1462+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1463- "@babel/types": "^7.28.2"1463+ "dev": true,
1464- }1464+ "license": "MIT",
1465- },1465+ "dependencies": {
1466- "node_modules/@types/chai": {1466+ "@babel/parser": "^7.20.7",
1467- "version": "5.2.3",1467+ "@babel/types": "^7.20.7",
1468- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",1468+ "@types/babel__generator": "*",
1469- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",1469+ "@types/babel__template": "*",
1470- "dev": true,1470+ "@types/babel__traverse": "*"
1471- "license": "MIT",1471+ }
1472- "dependencies": {1472+ },
1473- "@types/deep-eql": "*",1473+ "node_modules/@types/babel__generator": {
1474- "assertion-error": "^2.0.1"1474+ "version": "7.27.0",
1475- }1475+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1476- },1476+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1477- "node_modules/@types/debug": {1477+ "dev": true,
1478- "version": "4.1.13",1478+ "license": "MIT",
1479- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",1479+ "dependencies": {
1480- "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",1480+ "@babel/types": "^7.0.0"
1481- "license": "MIT",1481+ }
1482- "dependencies": {1482+ },
1483- "@types/ms": "*"1483+ "node_modules/@types/babel__template": {
1484- }1484+ "version": "7.4.4",
1485- },1485+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1486- "node_modules/@types/deep-eql": {1486+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1487- "version": "4.0.2",1487+ "dev": true,
1488- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",1488+ "license": "MIT",
1489- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",1489+ "dependencies": {
1490- "dev": true,1490+ "@babel/parser": "^7.1.0",
1491- "license": "MIT"1491+ "@babel/types": "^7.0.0"
1492- },1492+ }
1493- "node_modules/@types/estree": {1493+ },
1494- "version": "1.0.9",1494+ "node_modules/@types/babel__traverse": {
1495- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",1495+ "version": "7.28.0",
1496- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",1496+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1497- "license": "MIT"1497+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1498- },1498+ "dev": true,
1499- "node_modules/@types/estree-jsx": {1499+ "license": "MIT",
1500- "version": "1.0.5",1500+ "dependencies": {
1501- "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",1501+ "@babel/types": "^7.28.2"
1502- "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",1502+ }
1503- "license": "MIT",1503+ },
1504- "dependencies": {1504+ "node_modules/@types/chai": {
1505- "@types/estree": "*"1505+ "version": "5.2.3",
1506- }1506+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
1507- },1507+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
1508- "node_modules/@types/hast": {1508+ "dev": true,
1509- "version": "3.0.5",1509+ "license": "MIT",
1510- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",1510+ "dependencies": {
1511- "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",1511+ "@types/deep-eql": "*",
1512- "license": "MIT",1512+ "assertion-error": "^2.0.1"
1513- "dependencies": {1513+ }
1514- "@types/unist": "*"1514+ },
1515- }1515+ "node_modules/@types/debug": {
1516- },1516+ "version": "4.1.13",
1517- "node_modules/@types/mdast": {1517+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
1518- "version": "4.0.4",1518+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
1519- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",1519+ "license": "MIT",
1520- "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",1520+ "dependencies": {
1521- "license": "MIT",1521+ "@types/ms": "*"
1522- "dependencies": {1522+ }
1523- "@types/unist": "*"1523+ },
1524- }1524+ "node_modules/@types/deep-eql": {
1525- },1525+ "version": "4.0.2",
1526- "node_modules/@types/ms": {1526+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
1527- "version": "2.1.0",1527+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
1528- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",1528+ "dev": true,
1529- "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",1529+ "license": "MIT"
1530- "license": "MIT"1530+ },
1531- },1531+ "node_modules/@types/estree": {
1532- "node_modules/@types/react": {1532+ "version": "1.0.9",
1533- "version": "19.3.0",1533+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1534- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",1534+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1535- "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",1535+ "license": "MIT"
1536- "license": "MIT",1536+ },
1537- "dependencies": {1537+ "node_modules/@types/estree-jsx": {
1538- "csstype": "^3.2.2"1538+ "version": "1.0.5",
1539- }1539+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
1540- },1540+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
1541- "node_modules/@types/react-dom": {1541+ "license": "MIT",
1542- "version": "19.3.0",1542+ "dependencies": {
1543- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",1543+ "@types/estree": "*"
1544- "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",1544+ }
1545- "dev": true,1545+ },
1546- "license": "MIT",1546+ "node_modules/@types/hast": {
1547- "peerDependencies": {1547+ "version": "3.0.5",
1548- "@types/react": "^19.3.0"1548+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
1549- }1549+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
1550- },1550+ "license": "MIT",
1551- "node_modules/@types/unist": {1551+ "dependencies": {
1552- "version": "3.0.3",1552+ "@types/unist": "*"
1553- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",1553+ }
1554- "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",1554+ },
1555- "license": "MIT"1555+ "node_modules/@types/mdast": {
1556- },1556+ "version": "4.0.4",
1557- "node_modules/@ungap/structured-clone": {1557+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1558- "version": "1.4.0",1558+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1559- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",1559+ "license": "MIT",
1560- "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",1560+ "dependencies": {
1561- "license": "ISC"1561+ "@types/unist": "*"
1562- },1562+ }
1563- "node_modules/@vitejs/plugin-react": {1563+ },
1564- "version": "5.2.0",1564+ "node_modules/@types/ms": {
1565- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",1565+ "version": "2.1.0",
1566- "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",1566+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1567- "dev": true,1567+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1568- "license": "MIT",1568+ "license": "MIT"
1569- "dependencies": {1569+ },
1570- "@babel/core": "^7.29.0",1570+ "node_modules/@types/react": {
1571- "@babel/plugin-transform-react-jsx-self": "^7.27.1",1571+ "version": "19.3.0",
1572- "@babel/plugin-transform-react-jsx-source": "^7.27.1",1572+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
1573- "@rolldown/pluginutils": "1.0.0-rc.3",1573+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
1574- "@types/babel__core": "^7.20.5",1574+ "license": "MIT",
1575- "react-refresh": "^0.18.0"1575+ "dependencies": {
1576- },1576+ "csstype": "^3.2.2"
1577- "engines": {1577+ }
1578- "node": "^20.19.0 || >=22.12.0"1578+ },
1579- },1579+ "node_modules/@types/react-dom": {
1580- "peerDependencies": {1580+ "version": "19.3.0",
1581- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"1581+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
1582- }1582+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
1583- },1583+ "dev": true,
1584- "node_modules/@vitest/expect": {1584+ "license": "MIT",
1585- "version": "3.2.7",1585+ "peerDependencies": {
1586- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",1586+ "@types/react": "^19.3.0"
1587- "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",1587+ }
1588- "dev": true,1588+ },
1589- "license": "MIT",1589+ "node_modules/@types/trusted-types": {
1590- "dependencies": {1590+ "version": "2.0.7",
1591- "@types/chai": "^5.2.2",1591+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1592- "@vitest/spy": "3.2.7",1592+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1593- "@vitest/utils": "3.2.7",1593+ "license": "MIT",
1594- "chai": "^5.2.0",1594+ "optional": true
1595- "tinyrainbow": "^2.0.0"1595+ },
1596- },1596+ "node_modules/@types/unist": {
1597- "funding": {1597+ "version": "3.0.3",
1598- "url": "https://opencollective.com/vitest"1598+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1599- }1599+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1600- },1600+ "license": "MIT"
1601- "node_modules/@vitest/mocker": {1601+ },
1602- "version": "3.2.7",1602+ "node_modules/@ungap/structured-clone": {
1603- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",1603+ "version": "1.4.0",
1604- "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",1604+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
1605- "dev": true,1605+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
1606- "license": "MIT",1606+ "license": "ISC"
1607- "dependencies": {1607+ },
1608- "@vitest/spy": "3.2.7",1608+ "node_modules/@vitejs/plugin-react": {
1609- "estree-walker": "^3.0.3",1609+ "version": "5.2.0",
1610- "magic-string": "^0.30.17"1610+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1611- },1611+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1612- "funding": {1612+ "dev": true,
1613- "url": "https://opencollective.com/vitest"1613+ "license": "MIT",
1614- },1614+ "dependencies": {
1615- "peerDependencies": {1615+ "@babel/core": "^7.29.0",
1616- "msw": "^2.4.9",1616+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1617- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"1617+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1618- },1618+ "@rolldown/pluginutils": "1.0.0-rc.3",
1619- "peerDependenciesMeta": {1619+ "@types/babel__core": "^7.20.5",
1620- "msw": {1620+ "react-refresh": "^0.18.0"
1621- "optional": true1621+ },
1622- },1622+ "engines": {
1623- "vite": {1623+ "node": "^20.19.0 || >=22.12.0"
1624- "optional": true1624+ },
1625- }1625+ "peerDependencies": {
1626- }1626+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1627- },1627+ }
1628- "node_modules/@vitest/pretty-format": {1628+ },
1629- "version": "3.2.7",1629+ "node_modules/@vitest/expect": {
1630- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",1630+ "version": "3.2.7",
1631- "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",1631+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
1632- "dev": true,1632+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
1633- "license": "MIT",1633+ "dev": true,
1634- "dependencies": {1634+ "license": "MIT",
1635- "tinyrainbow": "^2.0.0"1635+ "dependencies": {
1636- },1636+ "@types/chai": "^5.2.2",
1637- "funding": {1637+ "@vitest/spy": "3.2.7",
1638- "url": "https://opencollective.com/vitest"1638+ "@vitest/utils": "3.2.7",
1639- }1639+ "chai": "^5.2.0",
1640- },1640+ "tinyrainbow": "^2.0.0"
1641- "node_modules/@vitest/runner": {1641+ },
1642- "version": "3.2.7",1642+ "funding": {
1643- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",1643+ "url": "https://opencollective.com/vitest"
1644- "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",1644+ }
1645- "dev": true,1645+ },
1646- "license": "MIT",1646+ "node_modules/@vitest/mocker": {
1647- "dependencies": {1647+ "version": "3.2.7",
1648- "@vitest/utils": "3.2.7",1648+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
1649- "pathe": "^2.0.3",1649+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
1650- "strip-literal": "^3.0.0"1650+ "dev": true,
1651- },1651+ "license": "MIT",
1652- "funding": {1652+ "dependencies": {
1653- "url": "https://opencollective.com/vitest"1653+ "@vitest/spy": "3.2.7",
1654- }1654+ "estree-walker": "^3.0.3",
1655- },1655+ "magic-string": "^0.30.17"
1656- "node_modules/@vitest/snapshot": {1656+ },
1657- "version": "3.2.7",1657+ "funding": {
1658- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",1658+ "url": "https://opencollective.com/vitest"
1659- "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",1659+ },
1660- "dev": true,1660+ "peerDependencies": {
1661- "license": "MIT",1661+ "msw": "^2.4.9",
1662- "dependencies": {1662+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
1663- "@vitest/pretty-format": "3.2.7",1663+ },
1664- "magic-string": "^0.30.17",1664+ "peerDependenciesMeta": {
1665- "pathe": "^2.0.3"1665+ "msw": {
1666- },1666+ "optional": true
1667- "funding": {1667+ },
1668- "url": "https://opencollective.com/vitest"1668+ "vite": {
1669- }1669+ "optional": true
1670- },1670+ }
1671- "node_modules/@vitest/spy": {1671+ }
1672- "version": "3.2.7",1672+ },
1673- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",1673+ "node_modules/@vitest/pretty-format": {
1674- "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",1674+ "version": "3.2.7",
1675- "dev": true,1675+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
1676- "license": "MIT",1676+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
1677- "dependencies": {1677+ "dev": true,
1678- "tinyspy": "^4.0.3"1678+ "license": "MIT",
1679- },1679+ "dependencies": {
1680- "funding": {1680+ "tinyrainbow": "^2.0.0"
1681- "url": "https://opencollective.com/vitest"1681+ },
1682- }1682+ "funding": {
1683- },1683+ "url": "https://opencollective.com/vitest"
1684- "node_modules/@vitest/utils": {1684+ }
1685- "version": "3.2.7",1685+ },
1686- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",1686+ "node_modules/@vitest/runner": {
1687- "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",1687+ "version": "3.2.7",
1688- "dev": true,1688+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
1689- "license": "MIT",1689+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
1690- "dependencies": {1690+ "dev": true,
1691- "@vitest/pretty-format": "3.2.7",1691+ "license": "MIT",
1692- "loupe": "^3.1.4",1692+ "dependencies": {
1693- "tinyrainbow": "^2.0.0"1693+ "@vitest/utils": "3.2.7",
1694- },1694+ "pathe": "^2.0.3",
1695- "funding": {1695+ "strip-literal": "^3.0.0"
1696- "url": "https://opencollective.com/vitest"1696+ },
1697- }1697+ "funding": {
1698- },1698+ "url": "https://opencollective.com/vitest"
1699- "node_modules/agent-base": {1699+ }
1700- "version": "7.1.4",1700+ },
1701- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",1701+ "node_modules/@vitest/snapshot": {
1702- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",1702+ "version": "3.2.7",
1703- "dev": true,1703+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
1704- "license": "MIT",1704+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
1705- "engines": {1705+ "dev": true,
1706- "node": ">= 14"1706+ "license": "MIT",
1707- }1707+ "dependencies": {
1708- },1708+ "@vitest/pretty-format": "3.2.7",
1709- "node_modules/ansi-regex": {1709+ "magic-string": "^0.30.17",
1710- "version": "5.0.1",1710+ "pathe": "^2.0.3"
1711- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",1711+ },
1712- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",1712+ "funding": {
1713- "dev": true,1713+ "url": "https://opencollective.com/vitest"
1714- "license": "MIT",1714+ }
1715- "peer": true,1715+ },
1716- "engines": {1716+ "node_modules/@vitest/spy": {
1717- "node": ">=8"1717+ "version": "3.2.7",
1718- }1718+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
1719- },1719+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
1720- "node_modules/ansi-styles": {1720+ "dev": true,
1721- "version": "5.2.0",1721+ "license": "MIT",
1722- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",1722+ "dependencies": {
1723- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",1723+ "tinyspy": "^4.0.3"
1724- "dev": true,1724+ },
1725- "license": "MIT",1725+ "funding": {
1726- "peer": true,1726+ "url": "https://opencollective.com/vitest"
1727- "engines": {1727+ }
1728- "node": ">=10"1728+ },
1729- },1729+ "node_modules/@vitest/utils": {
1730- "funding": {1730+ "version": "3.2.7",
1731- "url": "https://github.com/chalk/ansi-styles?sponsor=1"1731+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
1732- }1732+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
1733- },1733+ "dev": true,
1734- "node_modules/aria-query": {1734+ "license": "MIT",
1735- "version": "5.3.0",1735+ "dependencies": {
1736- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",1736+ "@vitest/pretty-format": "3.2.7",
1737- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",1737+ "loupe": "^3.1.4",
1738- "dev": true,1738+ "tinyrainbow": "^2.0.0"
1739- "license": "Apache-2.0",1739+ },
1740- "peer": true,1740+ "funding": {
1741- "dependencies": {1741+ "url": "https://opencollective.com/vitest"
1742- "dequal": "^2.0.3"1742+ }
1743- }1743+ },
1744- },1744+ "node_modules/@xterm/addon-fit": {
1745- "node_modules/assertion-error": {1745+ "version": "0.11.0",
1746- "version": "2.0.1",1746+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
1747- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",1747+ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
1748- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",1748+ "license": "MIT"
1749- "dev": true,1749+ },
1750- "license": "MIT",1750+ "node_modules/@xterm/xterm": {
1751- "engines": {1751+ "version": "6.0.0",
1752- "node": ">=12"1752+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
1753- }1753+ "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
1754- },1754+ "license": "MIT",
1755- "node_modules/bail": {1755+ "workspaces": [
1756- "version": "2.0.2",1756+ "addons/*"
1757- "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",1757+ ]
1758- "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",1758+ },
1759- "license": "MIT",1759+ "node_modules/agent-base": {
1760- "funding": {1760+ "version": "7.1.4",
1761- "type": "github",1761+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1762- "url": "https://github.com/sponsors/wooorm"1762+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1763- }1763+ "dev": true,
1764- },1764+ "license": "MIT",
1765- "node_modules/baseline-browser-mapping": {1765+ "engines": {
1766- "version": "2.11.25",1766+ "node": ">= 14"
1767- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz",1767+ }
1768- "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==",1768+ },
1769- "dev": true,1769+ "node_modules/ansi-regex": {
1770- "license": "Apache-2.0",1770+ "version": "5.0.1",
1771- "bin": {1771+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1772- "baseline-browser-mapping": "dist/cli.cjs"1772+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1773- },1773+ "dev": true,
1774- "engines": {1774+ "license": "MIT",
1775- "node": ">=6.0.0"1775+ "peer": true,
1776- }1776+ "engines": {
1777- },1777+ "node": ">=8"
1778- "node_modules/browserslist": {1778+ }
1779- "version": "4.29.0",1779+ },
1780- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",1780+ "node_modules/ansi-styles": {
1781- "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",1781+ "version": "5.2.0",
1782- "dev": true,1782+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
1783- "funding": [1783+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
1784- {1784+ "dev": true,
1785- "type": "opencollective",1785+ "license": "MIT",
1786- "url": "https://opencollective.com/browserslist"1786+ "peer": true,
1787- },1787+ "engines": {
1788- {1788+ "node": ">=10"
1789- "type": "tidelift",1789+ },
1790- "url": "https://tidelift.com/funding/github/npm/browserslist"1790+ "funding": {
1791- },1791+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1792- {1792+ }
1793- "type": "github",1793+ },
1794- "url": "https://github.com/sponsors/ai"1794+ "node_modules/aria-query": {
1795- }1795+ "version": "5.3.0",
1796- ],1796+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
1797- "license": "MIT",1797+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
1798- "dependencies": {1798+ "dev": true,
1799- "baseline-browser-mapping": "^2.11.23",1799+ "license": "Apache-2.0",
1800- "caniuse-lite": "^1.0.30001810",1800+ "peer": true,
1801- "electron-to-chromium": "^1.5.427",1801+ "dependencies": {
1802- "node-releases": "^2.0.55",1802+ "dequal": "^2.0.3"
1803- "update-browserslist-db": "^1.3.3"1803+ }
1804- },1804+ },
1805- "bin": {1805+ "node_modules/assertion-error": {
1806- "browserslist": "cli.js"1806+ "version": "2.0.1",
1807- },1807+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1808- "engines": {1808+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1809- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"1809+ "dev": true,
1810- }1810+ "license": "MIT",
1811- },1811+ "engines": {
1812- "node_modules/cac": {1812+ "node": ">=12"
1813- "version": "6.7.14",1813+ }
1814- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",1814+ },
1815- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",1815+ "node_modules/bail": {
1816- "dev": true,1816+ "version": "2.0.2",
1817- "license": "MIT",1817+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
1818- "engines": {1818+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
1819- "node": ">=8"1819+ "license": "MIT",
1820- }1820+ "funding": {
1821- },1821+ "type": "github",
1822- "node_modules/caniuse-lite": {1822+ "url": "https://github.com/sponsors/wooorm"
1823- "version": "1.0.30001810",1823+ }
1824- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",1824+ },
1825- "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",1825+ "node_modules/baseline-browser-mapping": {
1826- "dev": true,1826+ "version": "2.11.25",
1827- "funding": [1827+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz",
1828- {1828+ "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==",
1829- "type": "opencollective",1829+ "dev": true,
1830- "url": "https://opencollective.com/browserslist"1830+ "license": "Apache-2.0",
1831- },1831+ "bin": {
1832- {1832+ "baseline-browser-mapping": "dist/cli.cjs"
1833- "type": "tidelift",1833+ },
1834- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"1834+ "engines": {
1835- },1835+ "node": ">=6.0.0"
1836- {1836+ }
1837- "type": "github",1837+ },
1838- "url": "https://github.com/sponsors/ai"1838+ "node_modules/browserslist": {
1839- }1839+ "version": "4.29.0",
1840- ],1840+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",
1841- "license": "CC-BY-4.0"1841+ "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",
1842- },1842+ "dev": true,
1843- "node_modules/ccount": {1843+ "funding": [
1844- "version": "2.0.1",1844+ {
1845- "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",1845+ "type": "opencollective",
1846- "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",1846+ "url": "https://opencollective.com/browserslist"
1847- "license": "MIT",1847+ },
1848- "funding": {1848+ {
1849- "type": "github",1849+ "type": "tidelift",
1850- "url": "https://github.com/sponsors/wooorm"1850+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1851- }1851+ },
1852- },1852+ {
1853- "node_modules/chai": {1853+ "type": "github",
1854- "version": "5.3.3",1854+ "url": "https://github.com/sponsors/ai"
1855- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",1855+ }
1856- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",1856+ ],
1857- "dev": true,1857+ "license": "MIT",
1858- "license": "MIT",1858+ "dependencies": {
1859- "dependencies": {1859+ "baseline-browser-mapping": "^2.11.23",
1860- "assertion-error": "^2.0.1",1860+ "caniuse-lite": "^1.0.30001810",
1861- "check-error": "^2.1.1",1861+ "electron-to-chromium": "^1.5.427",
1862- "deep-eql": "^5.0.1",1862+ "node-releases": "^2.0.55",
1863- "loupe": "^3.1.0",1863+ "update-browserslist-db": "^1.3.3"
1864- "pathval": "^2.0.0"1864+ },
1865- },1865+ "bin": {
1866- "engines": {1866+ "browserslist": "cli.js"
1867- "node": ">=18"1867+ },
1868- }1868+ "engines": {
1869- },1869+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1870- "node_modules/character-entities": {1870+ }
1871- "version": "2.0.2",1871+ },
1872- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",1872+ "node_modules/cac": {
1873- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",1873+ "version": "6.7.14",
1874- "license": "MIT",1874+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1875- "funding": {1875+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1876- "type": "github",1876+ "dev": true,
1877- "url": "https://github.com/sponsors/wooorm"1877+ "license": "MIT",
1878- }1878+ "engines": {
1879- },1879+ "node": ">=8"
1880- "node_modules/character-entities-html4": {1880+ }
1881- "version": "2.1.0",1881+ },
1882- "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",1882+ "node_modules/caniuse-lite": {
1883- "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",1883+ "version": "1.0.30001810",
1884- "license": "MIT",1884+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
1885- "funding": {1885+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
1886- "type": "github",1886+ "dev": true,
1887- "url": "https://github.com/sponsors/wooorm"1887+ "funding": [
1888- }1888+ {
1889- },1889+ "type": "opencollective",
1890- "node_modules/character-entities-legacy": {1890+ "url": "https://opencollective.com/browserslist"
1891- "version": "3.0.0",1891+ },
1892- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",1892+ {
1893- "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",1893+ "type": "tidelift",
1894- "license": "MIT",1894+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1895- "funding": {1895+ },
1896- "type": "github",1896+ {
1897- "url": "https://github.com/sponsors/wooorm"1897+ "type": "github",
1898- }1898+ "url": "https://github.com/sponsors/ai"
1899- },1899+ }
1900- "node_modules/character-reference-invalid": {1900+ ],
1901- "version": "2.0.1",1901+ "license": "CC-BY-4.0"
1902- "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",1902+ },
1903- "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",1903+ "node_modules/ccount": {
1904- "license": "MIT",1904+ "version": "2.0.1",
1905- "funding": {1905+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1906- "type": "github",1906+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1907- "url": "https://github.com/sponsors/wooorm"1907+ "license": "MIT",
1908- }1908+ "funding": {
1909- },1909+ "type": "github",
1910- "node_modules/check-error": {1910+ "url": "https://github.com/sponsors/wooorm"
1911- "version": "2.1.3",1911+ }
1912- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",1912+ },
1913- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",1913+ "node_modules/chai": {
1914- "dev": true,1914+ "version": "5.3.3",
1915- "license": "MIT",1915+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
1916- "engines": {1916+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
1917- "node": ">= 16"1917+ "dev": true,
1918- }1918+ "license": "MIT",
1919- },1919+ "dependencies": {
1920- "node_modules/comma-separated-tokens": {1920+ "assertion-error": "^2.0.1",
1921- "version": "2.0.3",1921+ "check-error": "^2.1.1",
1922- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",1922+ "deep-eql": "^5.0.1",
1923- "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",1923+ "loupe": "^3.1.0",
1924- "license": "MIT",1924+ "pathval": "^2.0.0"
1925- "funding": {1925+ },
1926- "type": "github",1926+ "engines": {
1927- "url": "https://github.com/sponsors/wooorm"1927+ "node": ">=18"
1928- }1928+ }
1929- },1929+ },
1930- "node_modules/convert-source-map": {1930+ "node_modules/character-entities": {
1931- "version": "2.0.0",1931+ "version": "2.0.2",
1932- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",1932+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
1933- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",1933+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
1934- "dev": true,1934+ "license": "MIT",
1935- "license": "MIT"1935+ "funding": {
1936- },1936+ "type": "github",
1937- "node_modules/cssstyle": {1937+ "url": "https://github.com/sponsors/wooorm"
1938- "version": "4.6.0",1938+ }
1939- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",1939+ },
1940- "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",1940+ "node_modules/character-entities-html4": {
1941- "dev": true,1941+ "version": "2.1.0",
1942- "license": "MIT",1942+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
1943- "dependencies": {1943+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
1944- "@asamuzakjp/css-color": "^3.2.0",1944+ "license": "MIT",
1945- "rrweb-cssom": "^0.8.0"1945+ "funding": {
1946- },1946+ "type": "github",
1947- "engines": {1947+ "url": "https://github.com/sponsors/wooorm"
1948- "node": ">=18"1948+ }
1949- }1949+ },
1950- },1950+ "node_modules/character-entities-legacy": {
1951- "node_modules/csstype": {1951+ "version": "3.0.0",
1952- "version": "3.2.3",1952+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
1953- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",1953+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
1954- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",1954+ "license": "MIT",
1955- "license": "MIT"1955+ "funding": {
1956- },1956+ "type": "github",
1957- "node_modules/data-urls": {1957+ "url": "https://github.com/sponsors/wooorm"
1958- "version": "5.0.0",1958+ }
1959- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",1959+ },
1960- "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",1960+ "node_modules/character-reference-invalid": {
1961- "dev": true,1961+ "version": "2.0.1",
1962- "license": "MIT",1962+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
1963- "dependencies": {1963+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
1964- "whatwg-mimetype": "^4.0.0",1964+ "license": "MIT",
1965- "whatwg-url": "^14.0.0"1965+ "funding": {
1966- },1966+ "type": "github",
1967- "engines": {1967+ "url": "https://github.com/sponsors/wooorm"
1968- "node": ">=18"1968+ }
1969- }1969+ },
1970- },1970+ "node_modules/check-error": {
1971- "node_modules/debug": {1971+ "version": "2.1.3",
1972- "version": "4.4.3",1972+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
1973- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",1973+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
1974- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",1974+ "dev": true,
1975- "license": "MIT",1975+ "license": "MIT",
1976- "dependencies": {1976+ "engines": {
1977- "ms": "^2.1.3"1977+ "node": ">= 16"
1978- },1978+ }
1979- "engines": {1979+ },
1980- "node": ">=6.0"1980+ "node_modules/comma-separated-tokens": {
1981- },1981+ "version": "2.0.3",
1982- "peerDependenciesMeta": {1982+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
1983- "supports-color": {1983+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
1984- "optional": true1984+ "license": "MIT",
1985- }1985+ "funding": {
1986- }1986+ "type": "github",
1987- },1987+ "url": "https://github.com/sponsors/wooorm"
1988- "node_modules/decimal.js": {1988+ }
1989- "version": "10.6.0",1989+ },
1990- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",1990+ "node_modules/convert-source-map": {
1991- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",1991+ "version": "2.0.0",
1992- "dev": true,1992+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1993- "license": "MIT"1993+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1994- },1994+ "dev": true,
1995- "node_modules/decode-named-character-reference": {1995+ "license": "MIT"
1996- "version": "1.3.0",1996+ },
1997- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",1997+ "node_modules/cssstyle": {
1998- "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",1998+ "version": "4.6.0",
1999- "license": "MIT",1999+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
2000- "dependencies": {2000+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
2001- "character-entities": "^2.0.0"2001+ "dev": true,
2002- },2002+ "license": "MIT",
2003- "funding": {2003+ "dependencies": {
2004- "type": "github",2004+ "@asamuzakjp/css-color": "^3.2.0",
2005- "url": "https://github.com/sponsors/wooorm"2005+ "rrweb-cssom": "^0.8.0"
2006- }2006+ },
2007- },2007+ "engines": {
2008- "node_modules/deep-eql": {2008+ "node": ">=18"
2009- "version": "5.0.2",2009+ }
2010- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",2010+ },
2011- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",2011+ "node_modules/csstype": {
2012- "dev": true,2012+ "version": "3.2.3",
2013- "license": "MIT",2013+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
2014- "engines": {2014+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
2015- "node": ">=6"2015+ "license": "MIT"
2016- }2016+ },
2017- },2017+ "node_modules/data-urls": {
2018- "node_modules/dequal": {2018+ "version": "5.0.0",
2019- "version": "2.0.3",2019+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
2020- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",2020+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
2021- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",2021+ "dev": true,
2022- "license": "MIT",2022+ "license": "MIT",
2023- "engines": {2023+ "dependencies": {
2024- "node": ">=6"2024+ "whatwg-mimetype": "^4.0.0",
2025- }2025+ "whatwg-url": "^14.0.0"
2026- },2026+ },
2027- "node_modules/devlop": {2027+ "engines": {
2028- "version": "1.1.0",2028+ "node": ">=18"
2029- "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",2029+ }
2030- "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",2030+ },
2031- "license": "MIT",2031+ "node_modules/debug": {
2032- "dependencies": {2032+ "version": "4.4.3",
2033- "dequal": "^2.0.0"2033+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2034- },2034+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
2035- "funding": {2035+ "license": "MIT",
2036- "type": "github",2036+ "dependencies": {
2037- "url": "https://github.com/sponsors/wooorm"2037+ "ms": "^2.1.3"
2038- }2038+ },
2039- },2039+ "engines": {
2040- "node_modules/dom-accessibility-api": {2040+ "node": ">=6.0"
2041- "version": "0.5.16",2041+ },
2042- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",2042+ "peerDependenciesMeta": {
2043- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",2043+ "supports-color": {
2044- "dev": true,2044+ "optional": true
2045- "license": "MIT",2045+ }
2046- "peer": true2046+ }
2047- },2047+ },
2048- "node_modules/electron-to-chromium": {2048+ "node_modules/decimal.js": {
2049- "version": "1.5.430",2049+ "version": "10.6.0",
2050- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz",2050+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
2051- "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==",2051+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
2052- "dev": true,2052+ "dev": true,
2053- "license": "ISC"2053+ "license": "MIT"
2054- },2054+ },
2055- "node_modules/entities": {2055+ "node_modules/decode-named-character-reference": {
2056- "version": "6.0.1",2056+ "version": "1.3.0",
2057- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",2057+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
2058- "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",2058+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
2059- "dev": true,2059+ "license": "MIT",
2060- "license": "BSD-2-Clause",2060+ "dependencies": {
2061- "engines": {2061+ "character-entities": "^2.0.0"
2062- "node": ">=0.12"2062+ },
2063- },2063+ "funding": {
2064- "funding": {2064+ "type": "github",
2065- "url": "https://github.com/fb55/entities?sponsor=1"2065+ "url": "https://github.com/sponsors/wooorm"
2066- }2066+ }
2067- },2067+ },
2068- "node_modules/es-module-lexer": {2068+ "node_modules/deep-eql": {
2069- "version": "1.7.0",2069+ "version": "5.0.2",
2070- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",2070+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
2071- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",2071+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
2072- "dev": true,2072+ "dev": true,
2073- "license": "MIT"2073+ "license": "MIT",
2074- },2074+ "engines": {
2075- "node_modules/esbuild": {2075+ "node": ">=6"
2076- "version": "0.28.2",2076+ }
2077- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",2077+ },
2078- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",2078+ "node_modules/dequal": {
2079- "dev": true,2079+ "version": "2.0.3",
2080- "hasInstallScript": true,2080+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
2081- "license": "MIT",2081+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2082- "bin": {2082+ "license": "MIT",
2083- "esbuild": "bin/esbuild"2083+ "engines": {
2084- },2084+ "node": ">=6"
2085- "engines": {2085+ }
2086- "node": ">=18"2086+ },
2087- },2087+ "node_modules/devlop": {
2088- "optionalDependencies": {2088+ "version": "1.1.0",
2089- "@esbuild/aix-ppc64": "0.28.2",2089+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
2090- "@esbuild/android-arm": "0.28.2",2090+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
2091- "@esbuild/android-arm64": "0.28.2",2091+ "license": "MIT",
2092- "@esbuild/android-x64": "0.28.2",2092+ "dependencies": {
2093- "@esbuild/darwin-arm64": "0.28.2",2093+ "dequal": "^2.0.0"
2094- "@esbuild/darwin-x64": "0.28.2",2094+ },
2095- "@esbuild/freebsd-arm64": "0.28.2",2095+ "funding": {
2096- "@esbuild/freebsd-x64": "0.28.2",2096+ "type": "github",
2097- "@esbuild/linux-arm": "0.28.2",2097+ "url": "https://github.com/sponsors/wooorm"
2098- "@esbuild/linux-arm64": "0.28.2",2098+ }
2099- "@esbuild/linux-ia32": "0.28.2",2099+ },
2100- "@esbuild/linux-loong64": "0.28.2",2100+ "node_modules/dom-accessibility-api": {
2101- "@esbuild/linux-mips64el": "0.28.2",2101+ "version": "0.5.16",
2102- "@esbuild/linux-ppc64": "0.28.2",2102+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
2103- "@esbuild/linux-riscv64": "0.28.2",2103+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
2104- "@esbuild/linux-s390x": "0.28.2",2104+ "dev": true,
2105- "@esbuild/linux-x64": "0.28.2",2105+ "license": "MIT",
2106- "@esbuild/netbsd-arm64": "0.28.2",2106+ "peer": true
2107- "@esbuild/netbsd-x64": "0.28.2",2107+ },
2108- "@esbuild/openbsd-arm64": "0.28.2",2108+ "node_modules/dompurify": {
2109- "@esbuild/openbsd-x64": "0.28.2",2109+ "version": "3.4.8",
2110- "@esbuild/openharmony-arm64": "0.28.2",2110+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
2111- "@esbuild/sunos-x64": "0.28.2",2111+ "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
2112- "@esbuild/win32-arm64": "0.28.2",2112+ "license": "(MPL-2.0 OR Apache-2.0)",
2113- "@esbuild/win32-ia32": "0.28.2",2113+ "optionalDependencies": {
2114- "@esbuild/win32-x64": "0.28.2"2114+ "@types/trusted-types": "^2.0.7"
2115- }2115+ }
2116- },2116+ },
2117- "node_modules/escalade": {2117+ "node_modules/electron-to-chromium": {
2118- "version": "3.2.0",2118+ "version": "1.5.430",
2119- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",2119+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz",
2120- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",2120+ "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==",
2121- "dev": true,2121+ "dev": true,
2122- "license": "MIT",2122+ "license": "ISC"
2123- "engines": {2123+ },
2124- "node": ">=6"2124+ "node_modules/entities": {
2125- }2125+ "version": "6.0.1",
2126- },2126+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
2127- "node_modules/escape-string-regexp": {2127+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
2128- "version": "5.0.0",2128+ "dev": true,
2129- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",2129+ "license": "BSD-2-Clause",
2130- "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",2130+ "engines": {
2131- "license": "MIT",2131+ "node": ">=0.12"
2132- "engines": {2132+ },
2133- "node": ">=12"2133+ "funding": {
2134- },2134+ "url": "https://github.com/fb55/entities?sponsor=1"
2135- "funding": {2135+ }
2136- "url": "https://github.com/sponsors/sindresorhus"2136+ },
2137- }2137+ "node_modules/es-module-lexer": {
2138- },2138+ "version": "1.7.0",
2139- "node_modules/estree-util-is-identifier-name": {2139+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
2140- "version": "3.0.0",2140+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
2141- "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",2141+ "dev": true,
2142- "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",2142+ "license": "MIT"
2143- "license": "MIT",2143+ },
2144- "funding": {2144+ "node_modules/esbuild": {
2145- "type": "opencollective",2145+ "version": "0.28.2",
2146- "url": "https://opencollective.com/unified"2146+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
2147- }2147+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
2148- },2148+ "dev": true,
2149- "node_modules/estree-walker": {2149+ "hasInstallScript": true,
2150- "version": "3.0.3",2150+ "license": "MIT",
2151- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",2151+ "bin": {
2152- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",2152+ "esbuild": "bin/esbuild"
2153- "dev": true,2153+ },
2154- "license": "MIT",2154+ "engines": {
2155- "dependencies": {2155+ "node": ">=18"
2156- "@types/estree": "^1.0.0"2156+ },
2157- }2157+ "optionalDependencies": {
2158- },2158+ "@esbuild/aix-ppc64": "0.28.2",
2159- "node_modules/expect-type": {2159+ "@esbuild/android-arm": "0.28.2",
2160- "version": "1.4.0",2160+ "@esbuild/android-arm64": "0.28.2",
2161- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",2161+ "@esbuild/android-x64": "0.28.2",
2162- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",2162+ "@esbuild/darwin-arm64": "0.28.2",
2163- "dev": true,2163+ "@esbuild/darwin-x64": "0.28.2",
2164- "license": "Apache-2.0",2164+ "@esbuild/freebsd-arm64": "0.28.2",
2165- "engines": {2165+ "@esbuild/freebsd-x64": "0.28.2",
2166- "node": ">=12.0.0"2166+ "@esbuild/linux-arm": "0.28.2",
2167- }2167+ "@esbuild/linux-arm64": "0.28.2",
2168- },2168+ "@esbuild/linux-ia32": "0.28.2",
2169- "node_modules/extend": {2169+ "@esbuild/linux-loong64": "0.28.2",
2170- "version": "3.0.2",2170+ "@esbuild/linux-mips64el": "0.28.2",
2171- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",2171+ "@esbuild/linux-ppc64": "0.28.2",
2172- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",2172+ "@esbuild/linux-riscv64": "0.28.2",
2173- "license": "MIT"2173+ "@esbuild/linux-s390x": "0.28.2",
2174- },2174+ "@esbuild/linux-x64": "0.28.2",
2175- "node_modules/fdir": {2175+ "@esbuild/netbsd-arm64": "0.28.2",
2176- "version": "6.5.0",2176+ "@esbuild/netbsd-x64": "0.28.2",
2177- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",2177+ "@esbuild/openbsd-arm64": "0.28.2",
2178- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",2178+ "@esbuild/openbsd-x64": "0.28.2",
2179- "dev": true,2179+ "@esbuild/openharmony-arm64": "0.28.2",
2180- "license": "MIT",2180+ "@esbuild/sunos-x64": "0.28.2",
2181- "engines": {2181+ "@esbuild/win32-arm64": "0.28.2",
2182- "node": ">=12.0.0"2182+ "@esbuild/win32-ia32": "0.28.2",
2183- },2183+ "@esbuild/win32-x64": "0.28.2"
2184- "peerDependencies": {2184+ }
2185- "picomatch": "^3 || ^4"2185+ },
2186- },2186+ "node_modules/escalade": {
2187- "peerDependenciesMeta": {2187+ "version": "3.2.0",
2188- "picomatch": {2188+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
2189- "optional": true2189+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
2190- }2190+ "dev": true,
2191- }2191+ "license": "MIT",
2192- },2192+ "engines": {
2193- "node_modules/fsevents": {2193+ "node": ">=6"
2194- "version": "2.3.3",2194+ }
2195- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",2195+ },
2196- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",2196+ "node_modules/escape-string-regexp": {
2197- "dev": true,2197+ "version": "5.0.0",
2198- "hasInstallScript": true,2198+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
2199- "license": "MIT",2199+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
2200- "optional": true,2200+ "license": "MIT",
2201- "os": [2201+ "engines": {
2202- "darwin"2202+ "node": ">=12"
2203- ],2203+ },
2204- "engines": {2204+ "funding": {
2205- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"2205+ "url": "https://github.com/sponsors/sindresorhus"
2206- }2206+ }
2207- },2207+ },
2208- "node_modules/gensync": {2208+ "node_modules/estree-util-is-identifier-name": {
2209- "version": "1.0.0-beta.2",2209+ "version": "3.0.0",
2210- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",2210+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
2211- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",2211+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
2212- "dev": true,2212+ "license": "MIT",
2213- "license": "MIT",2213+ "funding": {
2214- "engines": {2214+ "type": "opencollective",
2215- "node": ">=6.9.0"2215+ "url": "https://opencollective.com/unified"
2216- }2216+ }
2217- },2217+ },
2218- "node_modules/hast-util-to-jsx-runtime": {2218+ "node_modules/estree-walker": {
2219- "version": "2.3.6",2219+ "version": "3.0.3",
2220- "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",2220+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
2221- "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",2221+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
2222- "license": "MIT",2222+ "dev": true,
2223- "dependencies": {2223+ "license": "MIT",
2224- "@types/estree": "^1.0.0",2224+ "dependencies": {
2225- "@types/hast": "^3.0.0",2225+ "@types/estree": "^1.0.0"
2226- "@types/unist": "^3.0.0",2226+ }
2227- "comma-separated-tokens": "^2.0.0",2227+ },
2228- "devlop": "^1.0.0",2228+ "node_modules/expect-type": {
2229- "estree-util-is-identifier-name": "^3.0.0",2229+ "version": "1.4.0",
2230- "hast-util-whitespace": "^3.0.0",2230+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
2231- "mdast-util-mdx-expression": "^2.0.0",2231+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
2232- "mdast-util-mdx-jsx": "^3.0.0",2232+ "dev": true,
2233- "mdast-util-mdxjs-esm": "^2.0.0",2233+ "license": "Apache-2.0",
2234- "property-information": "^7.0.0",2234+ "engines": {
2235- "space-separated-tokens": "^2.0.0",2235+ "node": ">=12.0.0"
2236- "style-to-js": "^1.0.0",2236+ }
2237- "unist-util-position": "^5.0.0",2237+ },
2238- "vfile-message": "^4.0.0"2238+ "node_modules/extend": {
2239- },2239+ "version": "3.0.2",
2240- "funding": {2240+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
2241- "type": "opencollective",2241+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
2242- "url": "https://opencollective.com/unified"2242+ "license": "MIT"
2243- }2243+ },
2244- },2244+ "node_modules/fdir": {
2245- "node_modules/hast-util-whitespace": {2245+ "version": "6.5.0",
2246- "version": "3.0.0",2246+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2247- "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",2247+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2248- "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",2248+ "dev": true,
2249- "license": "MIT",2249+ "license": "MIT",
2250- "dependencies": {2250+ "engines": {
2251- "@types/hast": "^3.0.0"2251+ "node": ">=12.0.0"
2252- },2252+ },
2253- "funding": {2253+ "peerDependencies": {
2254- "type": "opencollective",2254+ "picomatch": "^3 || ^4"
2255- "url": "https://opencollective.com/unified"2255+ },
2256- }2256+ "peerDependenciesMeta": {
2257- },2257+ "picomatch": {
2258- "node_modules/html-encoding-sniffer": {2258+ "optional": true
2259- "version": "4.0.0",2259+ }
2260- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",2260+ }
2261- "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",2261+ },
2262- "dev": true,2262+ "node_modules/fsevents": {
2263- "license": "MIT",2263+ "version": "2.3.3",
2264- "dependencies": {2264+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
2265- "whatwg-encoding": "^3.1.1"2265+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2266- },2266+ "dev": true,
2267- "engines": {2267+ "hasInstallScript": true,
2268- "node": ">=18"2268+ "license": "MIT",
2269- }2269+ "optional": true,
2270- },2270+ "os": [
2271- "node_modules/html-url-attributes": {2271+ "darwin"
2272- "version": "3.0.1",2272+ ],
2273- "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",2273+ "engines": {
2274- "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",2274+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2275- "license": "MIT",2275+ }
2276- "funding": {2276+ },
2277- "type": "opencollective",2277+ "node_modules/gensync": {
2278- "url": "https://opencollective.com/unified"2278+ "version": "1.0.0-beta.2",
2279- }2279+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
2280- },2280+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
2281- "node_modules/http-proxy-agent": {2281+ "dev": true,
2282- "version": "7.0.2",2282+ "license": "MIT",
2283- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",2283+ "engines": {
2284- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",2284+ "node": ">=6.9.0"
2285- "dev": true,2285+ }
2286- "license": "MIT",2286+ },
2287- "dependencies": {2287+ "node_modules/hast-util-to-jsx-runtime": {
2288- "agent-base": "^7.1.0",2288+ "version": "2.3.6",
2289- "debug": "^4.3.4"2289+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
2290- },2290+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
2291- "engines": {2291+ "license": "MIT",
2292- "node": ">= 14"2292+ "dependencies": {
2293- }2293+ "@types/estree": "^1.0.0",
2294- },2294+ "@types/hast": "^3.0.0",
2295- "node_modules/https-proxy-agent": {2295+ "@types/unist": "^3.0.0",
2296- "version": "7.0.6",2296+ "comma-separated-tokens": "^2.0.0",
2297- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",2297+ "devlop": "^1.0.0",
2298- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",2298+ "estree-util-is-identifier-name": "^3.0.0",
2299- "dev": true,2299+ "hast-util-whitespace": "^3.0.0",
2300- "license": "MIT",2300+ "mdast-util-mdx-expression": "^2.0.0",
2301- "dependencies": {2301+ "mdast-util-mdx-jsx": "^3.0.0",
2302- "agent-base": "^7.1.2",2302+ "mdast-util-mdxjs-esm": "^2.0.0",
2303- "debug": "4"2303+ "property-information": "^7.0.0",
2304- },2304+ "space-separated-tokens": "^2.0.0",
2305- "engines": {2305+ "style-to-js": "^1.0.0",
2306- "node": ">= 14"2306+ "unist-util-position": "^5.0.0",
2307- }2307+ "vfile-message": "^4.0.0"
2308- },2308+ },
2309- "node_modules/iconv-lite": {2309+ "funding": {
2310- "version": "0.6.3",2310+ "type": "opencollective",
2311- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",2311+ "url": "https://opencollective.com/unified"
2312- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",2312+ }
2313- "dev": true,2313+ },
2314- "license": "MIT",2314+ "node_modules/hast-util-whitespace": {
2315- "dependencies": {2315+ "version": "3.0.0",
2316- "safer-buffer": ">= 2.1.2 < 3.0.0"2316+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
2317- },2317+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
2318- "engines": {2318+ "license": "MIT",
2319- "node": ">=0.10.0"2319+ "dependencies": {
2320- }2320+ "@types/hast": "^3.0.0"
2321- },2321+ },
2322- "node_modules/inline-style-parser": {2322+ "funding": {
2323- "version": "0.2.7",2323+ "type": "opencollective",
2324- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",2324+ "url": "https://opencollective.com/unified"
2325- "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",2325+ }
2326- "license": "MIT"2326+ },
2327- },2327+ "node_modules/html-encoding-sniffer": {
2328- "node_modules/is-alphabetical": {2328+ "version": "4.0.0",
2329- "version": "2.0.1",2329+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
2330- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",2330+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
2331- "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",2331+ "dev": true,
2332- "license": "MIT",2332+ "license": "MIT",
2333- "funding": {2333+ "dependencies": {
2334- "type": "github",2334+ "whatwg-encoding": "^3.1.1"
2335- "url": "https://github.com/sponsors/wooorm"2335+ },
2336- }2336+ "engines": {
2337- },2337+ "node": ">=18"
2338- "node_modules/is-alphanumerical": {2338+ }
2339- "version": "2.0.1",2339+ },
2340- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",2340+ "node_modules/html-url-attributes": {
2341- "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",2341+ "version": "3.0.1",
2342- "license": "MIT",2342+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
2343- "dependencies": {2343+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
2344- "is-alphabetical": "^2.0.0",2344+ "license": "MIT",
2345- "is-decimal": "^2.0.0"2345+ "funding": {
2346- },2346+ "type": "opencollective",
2347- "funding": {2347+ "url": "https://opencollective.com/unified"
2348- "type": "github",2348+ }
2349- "url": "https://github.com/sponsors/wooorm"2349+ },
2350- }2350+ "node_modules/http-proxy-agent": {
2351- },2351+ "version": "7.0.2",
2352- "node_modules/is-decimal": {2352+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
2353- "version": "2.0.1",2353+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
2354- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",2354+ "dev": true,
2355- "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",2355+ "license": "MIT",
2356- "license": "MIT",2356+ "dependencies": {
2357- "funding": {2357+ "agent-base": "^7.1.0",
2358- "type": "github",2358+ "debug": "^4.3.4"
2359- "url": "https://github.com/sponsors/wooorm"2359+ },
2360- }2360+ "engines": {
2361- },2361+ "node": ">= 14"
2362- "node_modules/is-hexadecimal": {2362+ }
2363- "version": "2.0.1",2363+ },
2364- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",2364+ "node_modules/https-proxy-agent": {
2365- "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",2365+ "version": "7.0.6",
2366- "license": "MIT",2366+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
2367- "funding": {2367+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
2368- "type": "github",2368+ "dev": true,
2369- "url": "https://github.com/sponsors/wooorm"2369+ "license": "MIT",
2370- }2370+ "dependencies": {
2371- },2371+ "agent-base": "^7.1.2",
2372- "node_modules/is-plain-obj": {2372+ "debug": "4"
2373- "version": "4.1.0",2373+ },
2374- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",2374+ "engines": {
2375- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",2375+ "node": ">= 14"
2376- "license": "MIT",2376+ }
2377- "engines": {2377+ },
2378- "node": ">=12"2378+ "node_modules/iconv-lite": {
2379- },2379+ "version": "0.6.3",
2380- "funding": {2380+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
2381- "url": "https://github.com/sponsors/sindresorhus"2381+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
2382- }2382+ "dev": true,
2383- },2383+ "license": "MIT",
2384- "node_modules/is-potential-custom-element-name": {2384+ "dependencies": {
2385- "version": "1.0.1",2385+ "safer-buffer": ">= 2.1.2 < 3.0.0"
2386- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",2386+ },
2387- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",2387+ "engines": {
2388- "dev": true,2388+ "node": ">=0.10.0"
2389- "license": "MIT"2389+ }
2390- },2390+ },
2391- "node_modules/js-tokens": {2391+ "node_modules/inline-style-parser": {
2392- "version": "4.0.0",2392+ "version": "0.2.7",
2393- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",2393+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
2394- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",2394+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
2395- "dev": true,2395+ "license": "MIT"
2396- "license": "MIT"2396+ },
2397- },2397+ "node_modules/is-alphabetical": {
2398- "node_modules/jsdom": {2398+ "version": "2.0.1",
2399- "version": "26.1.0",2399+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
2400- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",2400+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
2401- "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",2401+ "license": "MIT",
2402- "dev": true,2402+ "funding": {
2403- "license": "MIT",2403+ "type": "github",
2404- "dependencies": {2404+ "url": "https://github.com/sponsors/wooorm"
2405- "cssstyle": "^4.2.1",2405+ }
2406- "data-urls": "^5.0.0",2406+ },
2407- "decimal.js": "^10.5.0",2407+ "node_modules/is-alphanumerical": {
2408- "html-encoding-sniffer": "^4.0.0",2408+ "version": "2.0.1",
2409- "http-proxy-agent": "^7.0.2",2409+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
2410- "https-proxy-agent": "^7.0.6",2410+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
2411- "is-potential-custom-element-name": "^1.0.1",2411+ "license": "MIT",
2412- "nwsapi": "^2.2.16",2412+ "dependencies": {
2413- "parse5": "^7.2.1",2413+ "is-alphabetical": "^2.0.0",
2414- "rrweb-cssom": "^0.8.0",2414+ "is-decimal": "^2.0.0"
2415- "saxes": "^6.0.0",2415+ },
2416- "symbol-tree": "^3.2.4",2416+ "funding": {
2417- "tough-cookie": "^5.1.1",2417+ "type": "github",
2418- "w3c-xmlserializer": "^5.0.0",2418+ "url": "https://github.com/sponsors/wooorm"
2419- "webidl-conversions": "^7.0.0",2419+ }
2420- "whatwg-encoding": "^3.1.1",2420+ },
2421- "whatwg-mimetype": "^4.0.0",2421+ "node_modules/is-decimal": {
2422- "whatwg-url": "^14.1.1",2422+ "version": "2.0.1",
2423- "ws": "^8.18.0",2423+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
2424- "xml-name-validator": "^5.0.0"2424+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
2425- },2425+ "license": "MIT",
2426- "engines": {2426+ "funding": {
2427- "node": ">=18"2427+ "type": "github",
2428- },2428+ "url": "https://github.com/sponsors/wooorm"
2429- "peerDependencies": {2429+ }
2430- "canvas": "^3.0.0"2430+ },
2431- },2431+ "node_modules/is-hexadecimal": {
2432- "peerDependenciesMeta": {2432+ "version": "2.0.1",
2433- "canvas": {2433+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
2434- "optional": true2434+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
2435- }2435+ "license": "MIT",
2436- }2436+ "funding": {
2437- },2437+ "type": "github",
2438- "node_modules/jsesc": {2438+ "url": "https://github.com/sponsors/wooorm"
2439- "version": "3.1.0",2439+ }
2440- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",2440+ },
2441- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",2441+ "node_modules/is-plain-obj": {
2442- "dev": true,2442+ "version": "4.1.0",
2443- "license": "MIT",2443+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
2444- "bin": {2444+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
2445- "jsesc": "bin/jsesc"2445+ "license": "MIT",
2446- },2446+ "engines": {
2447- "engines": {2447+ "node": ">=12"
2448- "node": ">=6"2448+ },
2449- }2449+ "funding": {
2450- },2450+ "url": "https://github.com/sponsors/sindresorhus"
2451- "node_modules/json5": {2451+ }
2452- "version": "2.2.3",2452+ },
2453- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",2453+ "node_modules/is-potential-custom-element-name": {
2454- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",2454+ "version": "1.0.1",
2455- "dev": true,2455+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2456- "license": "MIT",2456+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
2457- "bin": {2457+ "dev": true,
2458- "json5": "lib/cli.js"2458+ "license": "MIT"
2459- },2459+ },
2460- "engines": {2460+ "node_modules/js-tokens": {
2461- "node": ">=6"2461+ "version": "4.0.0",
2462- }2462+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
2463- },2463+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
2464- "node_modules/longest-streak": {2464+ "dev": true,
2465- "version": "3.1.0",2465+ "license": "MIT"
2466- "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",2466+ },
2467- "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",2467+ "node_modules/jsdom": {
2468- "license": "MIT",2468+ "version": "26.1.0",
2469- "funding": {2469+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
2470- "type": "github",2470+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
2471- "url": "https://github.com/sponsors/wooorm"2471+ "dev": true,
2472- }2472+ "license": "MIT",
2473- },2473+ "dependencies": {
2474- "node_modules/loupe": {2474+ "cssstyle": "^4.2.1",
2475- "version": "3.2.1",2475+ "data-urls": "^5.0.0",
2476- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",2476+ "decimal.js": "^10.5.0",
2477- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",2477+ "html-encoding-sniffer": "^4.0.0",
2478- "dev": true,2478+ "http-proxy-agent": "^7.0.2",
2479- "license": "MIT"2479+ "https-proxy-agent": "^7.0.6",
2480- },2480+ "is-potential-custom-element-name": "^1.0.1",
2481- "node_modules/lru-cache": {2481+ "nwsapi": "^2.2.16",
2482- "version": "5.1.1",2482+ "parse5": "^7.2.1",
2483- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",2483+ "rrweb-cssom": "^0.8.0",
2484- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",2484+ "saxes": "^6.0.0",
2485- "dev": true,2485+ "symbol-tree": "^3.2.4",
2486- "license": "ISC",2486+ "tough-cookie": "^5.1.1",
2487- "dependencies": {2487+ "w3c-xmlserializer": "^5.0.0",
2488- "yallist": "^3.0.2"2488+ "webidl-conversions": "^7.0.0",
2489- }2489+ "whatwg-encoding": "^3.1.1",
2490- },2490+ "whatwg-mimetype": "^4.0.0",
2491- "node_modules/lz-string": {2491+ "whatwg-url": "^14.1.1",
2492- "version": "1.5.0",2492+ "ws": "^8.18.0",
2493- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",2493+ "xml-name-validator": "^5.0.0"
2494- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",2494+ },
2495- "dev": true,2495+ "engines": {
2496- "license": "MIT",2496+ "node": ">=18"
2497- "peer": true,2497+ },
2498- "bin": {2498+ "peerDependencies": {
2499- "lz-string": "bin/bin.js"2499+ "canvas": "^3.0.0"
2500- }2500+ },
2501- },2501+ "peerDependenciesMeta": {
2502- "node_modules/magic-string": {2502+ "canvas": {
2503- "version": "0.30.21",2503+ "optional": true
2504- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",2504+ }
2505- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",2505+ }
2506- "dev": true,2506+ },
2507- "license": "MIT",2507+ "node_modules/jsesc": {
2508- "dependencies": {2508+ "version": "3.1.0",
2509- "@jridgewell/sourcemap-codec": "^1.5.5"2509+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
2510- }2510+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
2511- },2511+ "dev": true,
2512- "node_modules/markdown-table": {2512+ "license": "MIT",
2513- "version": "3.0.4",2513+ "bin": {
2514- "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",2514+ "jsesc": "bin/jsesc"
2515- "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",2515+ },
2516- "license": "MIT",2516+ "engines": {
2517- "funding": {2517+ "node": ">=6"
2518- "type": "github",2518+ }
2519- "url": "https://github.com/sponsors/wooorm"2519+ },
2520- }2520+ "node_modules/json5": {
2521- },2521+ "version": "2.2.3",
2522- "node_modules/mdast-util-find-and-replace": {2522+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
2523- "version": "3.0.2",2523+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
2524- "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",2524+ "dev": true,
2525- "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",2525+ "license": "MIT",
2526- "license": "MIT",2526+ "bin": {
2527- "dependencies": {2527+ "json5": "lib/cli.js"
2528- "@types/mdast": "^4.0.0",2528+ },
2529- "escape-string-regexp": "^5.0.0",2529+ "engines": {
2530- "unist-util-is": "^6.0.0",2530+ "node": ">=6"
2531- "unist-util-visit-parents": "^6.0.0"2531+ }
2532- },2532+ },
2533- "funding": {2533+ "node_modules/longest-streak": {
2534- "type": "opencollective",2534+ "version": "3.1.0",
2535- "url": "https://opencollective.com/unified"2535+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
2536- }2536+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
2537- },2537+ "license": "MIT",
2538- "node_modules/mdast-util-from-markdown": {2538+ "funding": {
2539- "version": "2.0.3",2539+ "type": "github",
2540- "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",2540+ "url": "https://github.com/sponsors/wooorm"
2541- "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",2541+ }
2542- "license": "MIT",2542+ },
2543- "dependencies": {2543+ "node_modules/loupe": {
2544- "@types/mdast": "^4.0.0",2544+ "version": "3.2.1",
2545- "@types/unist": "^3.0.0",2545+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
2546- "decode-named-character-reference": "^1.0.0",2546+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
2547- "devlop": "^1.0.0",2547+ "dev": true,
2548- "mdast-util-to-string": "^4.0.0",2548+ "license": "MIT"
2549- "micromark": "^4.0.0",2549+ },
2550- "micromark-util-decode-numeric-character-reference": "^2.0.0",2550+ "node_modules/lru-cache": {
2551- "micromark-util-decode-string": "^2.0.0",2551+ "version": "5.1.1",
2552- "micromark-util-normalize-identifier": "^2.0.0",2552+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2553- "micromark-util-symbol": "^2.0.0",2553+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2554- "micromark-util-types": "^2.0.0",2554+ "dev": true,
2555- "unist-util-stringify-position": "^4.0.0"2555+ "license": "ISC",
2556- },2556+ "dependencies": {
2557- "funding": {2557+ "yallist": "^3.0.2"
2558- "type": "opencollective",2558+ }
2559- "url": "https://opencollective.com/unified"2559+ },
2560- }2560+ "node_modules/lz-string": {
2561- },2561+ "version": "1.5.0",
2562- "node_modules/mdast-util-gfm": {2562+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
2563- "version": "3.1.0",2563+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
2564- "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",2564+ "dev": true,
2565- "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",2565+ "license": "MIT",
2566- "license": "MIT",2566+ "peer": true,
2567- "dependencies": {2567+ "bin": {
2568- "mdast-util-from-markdown": "^2.0.0",2568+ "lz-string": "bin/bin.js"
2569- "mdast-util-gfm-autolink-literal": "^2.0.0",2569+ }
2570- "mdast-util-gfm-footnote": "^2.0.0",2570+ },
2571- "mdast-util-gfm-strikethrough": "^2.0.0",2571+ "node_modules/magic-string": {
2572- "mdast-util-gfm-table": "^2.0.0",2572+ "version": "0.30.21",
2573- "mdast-util-gfm-task-list-item": "^2.0.0",2573+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2574- "mdast-util-to-markdown": "^2.0.0"2574+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2575- },2575+ "dev": true,
2576- "funding": {2576+ "license": "MIT",
2577- "type": "opencollective",2577+ "dependencies": {
2578- "url": "https://opencollective.com/unified"2578+ "@jridgewell/sourcemap-codec": "^1.5.5"
2579- }2579+ }
2580- },2580+ },
2581- "node_modules/mdast-util-gfm-autolink-literal": {2581+ "node_modules/markdown-table": {
2582- "version": "2.0.1",2582+ "version": "3.0.4",
2583- "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",2583+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
2584- "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",2584+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
2585- "license": "MIT",2585+ "license": "MIT",
2586- "dependencies": {2586+ "funding": {
2587- "@types/mdast": "^4.0.0",2587+ "type": "github",
2588- "ccount": "^2.0.0",2588+ "url": "https://github.com/sponsors/wooorm"
2589- "devlop": "^1.0.0",2589+ }
2590- "mdast-util-find-and-replace": "^3.0.0",2590+ },
2591- "micromark-util-character": "^2.0.0"2591+ "node_modules/marked": {
2592- },2592+ "version": "14.0.0",
2593- "funding": {2593+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
2594- "type": "opencollective",2594+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
2595- "url": "https://opencollective.com/unified"2595+ "license": "MIT",
2596- }2596+ "bin": {
2597- },2597+ "marked": "bin/marked.js"
2598- "node_modules/mdast-util-gfm-footnote": {2598+ },
2599- "version": "2.1.0",2599+ "engines": {
2600- "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",2600+ "node": ">= 18"
2601- "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",2601+ }
2602- "license": "MIT",2602+ },
2603- "dependencies": {2603+ "node_modules/mdast-util-find-and-replace": {
2604- "@types/mdast": "^4.0.0",2604+ "version": "3.0.2",
2605- "devlop": "^1.1.0",2605+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
2606- "mdast-util-from-markdown": "^2.0.0",2606+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
2607- "mdast-util-to-markdown": "^2.0.0",2607+ "license": "MIT",
2608- "micromark-util-normalize-identifier": "^2.0.0"2608+ "dependencies": {
2609- },2609+ "@types/mdast": "^4.0.0",
2610- "funding": {2610+ "escape-string-regexp": "^5.0.0",
2611- "type": "opencollective",2611+ "unist-util-is": "^6.0.0",
2612- "url": "https://opencollective.com/unified"2612+ "unist-util-visit-parents": "^6.0.0"
2613- }2613+ },
2614- },2614+ "funding": {
2615- "node_modules/mdast-util-gfm-strikethrough": {2615+ "type": "opencollective",
2616- "version": "2.0.0",2616+ "url": "https://opencollective.com/unified"
2617- "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",2617+ }
2618- "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",2618+ },
2619- "license": "MIT",2619+ "node_modules/mdast-util-from-markdown": {
2620- "dependencies": {2620+ "version": "2.0.3",
2621- "@types/mdast": "^4.0.0",2621+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
2622- "mdast-util-from-markdown": "^2.0.0",2622+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
2623- "mdast-util-to-markdown": "^2.0.0"2623+ "license": "MIT",
2624- },2624+ "dependencies": {
2625- "funding": {2625+ "@types/mdast": "^4.0.0",
2626- "type": "opencollective",2626+ "@types/unist": "^3.0.0",
2627- "url": "https://opencollective.com/unified"2627+ "decode-named-character-reference": "^1.0.0",
2628- }2628+ "devlop": "^1.0.0",
2629- },2629+ "mdast-util-to-string": "^4.0.0",
2630- "node_modules/mdast-util-gfm-table": {2630+ "micromark": "^4.0.0",
2631- "version": "2.0.0",2631+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
2632- "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",2632+ "micromark-util-decode-string": "^2.0.0",
2633- "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",2633+ "micromark-util-normalize-identifier": "^2.0.0",
2634- "license": "MIT",2634+ "micromark-util-symbol": "^2.0.0",
2635- "dependencies": {2635+ "micromark-util-types": "^2.0.0",
2636- "@types/mdast": "^4.0.0",2636+ "unist-util-stringify-position": "^4.0.0"
2637- "devlop": "^1.0.0",2637+ },
2638- "markdown-table": "^3.0.0",2638+ "funding": {
2639- "mdast-util-from-markdown": "^2.0.0",2639+ "type": "opencollective",
2640- "mdast-util-to-markdown": "^2.0.0"2640+ "url": "https://opencollective.com/unified"
2641- },2641+ }
2642- "funding": {2642+ },
2643- "type": "opencollective",2643+ "node_modules/mdast-util-gfm": {
2644- "url": "https://opencollective.com/unified"2644+ "version": "3.1.0",
2645- }2645+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
2646- },2646+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
2647- "node_modules/mdast-util-gfm-task-list-item": {2647+ "license": "MIT",
2648- "version": "2.0.0",2648+ "dependencies": {
2649- "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",2649+ "mdast-util-from-markdown": "^2.0.0",
2650- "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",2650+ "mdast-util-gfm-autolink-literal": "^2.0.0",
2651- "license": "MIT",2651+ "mdast-util-gfm-footnote": "^2.0.0",
2652- "dependencies": {2652+ "mdast-util-gfm-strikethrough": "^2.0.0",
2653- "@types/mdast": "^4.0.0",2653+ "mdast-util-gfm-table": "^2.0.0",
2654- "devlop": "^1.0.0",2654+ "mdast-util-gfm-task-list-item": "^2.0.0",
2655- "mdast-util-from-markdown": "^2.0.0",2655+ "mdast-util-to-markdown": "^2.0.0"
2656- "mdast-util-to-markdown": "^2.0.0"2656+ },
2657- },2657+ "funding": {
2658- "funding": {2658+ "type": "opencollective",
2659- "type": "opencollective",2659+ "url": "https://opencollective.com/unified"
2660- "url": "https://opencollective.com/unified"2660+ }
2661- }2661+ },
2662- },2662+ "node_modules/mdast-util-gfm-autolink-literal": {
2663- "node_modules/mdast-util-mdx-expression": {2663+ "version": "2.0.1",
2664- "version": "2.0.1",2664+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
2665- "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",2665+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
2666- "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",2666+ "license": "MIT",
2667- "license": "MIT",2667+ "dependencies": {
2668- "dependencies": {2668+ "@types/mdast": "^4.0.0",
2669- "@types/estree-jsx": "^1.0.0",2669+ "ccount": "^2.0.0",
2670- "@types/hast": "^3.0.0",2670+ "devlop": "^1.0.0",
2671- "@types/mdast": "^4.0.0",2671+ "mdast-util-find-and-replace": "^3.0.0",
2672- "devlop": "^1.0.0",2672+ "micromark-util-character": "^2.0.0"
2673- "mdast-util-from-markdown": "^2.0.0",2673+ },
2674- "mdast-util-to-markdown": "^2.0.0"2674+ "funding": {
2675- },2675+ "type": "opencollective",
2676- "funding": {2676+ "url": "https://opencollective.com/unified"
2677- "type": "opencollective",2677+ }
2678- "url": "https://opencollective.com/unified"2678+ },
2679- }2679+ "node_modules/mdast-util-gfm-footnote": {
2680- },2680+ "version": "2.1.0",
2681- "node_modules/mdast-util-mdx-jsx": {2681+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
2682- "version": "3.2.0",2682+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
2683- "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",2683+ "license": "MIT",
2684- "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",2684+ "dependencies": {
2685- "license": "MIT",2685+ "@types/mdast": "^4.0.0",
2686- "dependencies": {2686+ "devlop": "^1.1.0",
2687- "@types/estree-jsx": "^1.0.0",2687+ "mdast-util-from-markdown": "^2.0.0",
2688- "@types/hast": "^3.0.0",2688+ "mdast-util-to-markdown": "^2.0.0",
2689- "@types/mdast": "^4.0.0",2689+ "micromark-util-normalize-identifier": "^2.0.0"
2690- "@types/unist": "^3.0.0",2690+ },
2691- "ccount": "^2.0.0",2691+ "funding": {
2692- "devlop": "^1.1.0",2692+ "type": "opencollective",
2693- "mdast-util-from-markdown": "^2.0.0",2693+ "url": "https://opencollective.com/unified"
2694- "mdast-util-to-markdown": "^2.0.0",2694+ }
2695- "parse-entities": "^4.0.0",2695+ },
2696- "stringify-entities": "^4.0.0",2696+ "node_modules/mdast-util-gfm-strikethrough": {
2697- "unist-util-stringify-position": "^4.0.0",2697+ "version": "2.0.0",
2698- "vfile-message": "^4.0.0"2698+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
2699- },2699+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
2700- "funding": {2700+ "license": "MIT",
2701- "type": "opencollective",2701+ "dependencies": {
2702- "url": "https://opencollective.com/unified"2702+ "@types/mdast": "^4.0.0",
2703- }2703+ "mdast-util-from-markdown": "^2.0.0",
2704- },2704+ "mdast-util-to-markdown": "^2.0.0"
2705- "node_modules/mdast-util-mdxjs-esm": {2705+ },
2706- "version": "2.0.1",2706+ "funding": {
2707- "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",2707+ "type": "opencollective",
2708- "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",2708+ "url": "https://opencollective.com/unified"
2709- "license": "MIT",2709+ }
2710- "dependencies": {2710+ },
2711- "@types/estree-jsx": "^1.0.0",2711+ "node_modules/mdast-util-gfm-table": {
2712- "@types/hast": "^3.0.0",2712+ "version": "2.0.0",
2713- "@types/mdast": "^4.0.0",2713+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
2714- "devlop": "^1.0.0",2714+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
2715- "mdast-util-from-markdown": "^2.0.0",2715+ "license": "MIT",
2716- "mdast-util-to-markdown": "^2.0.0"2716+ "dependencies": {
2717- },2717+ "@types/mdast": "^4.0.0",
2718- "funding": {2718+ "devlop": "^1.0.0",
2719- "type": "opencollective",2719+ "markdown-table": "^3.0.0",
2720- "url": "https://opencollective.com/unified"2720+ "mdast-util-from-markdown": "^2.0.0",
2721- }2721+ "mdast-util-to-markdown": "^2.0.0"
2722- },2722+ },
2723- "node_modules/mdast-util-phrasing": {2723+ "funding": {
2724- "version": "4.1.0",2724+ "type": "opencollective",
2725- "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",2725+ "url": "https://opencollective.com/unified"
2726- "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",2726+ }
2727- "license": "MIT",2727+ },
2728- "dependencies": {2728+ "node_modules/mdast-util-gfm-task-list-item": {
2729- "@types/mdast": "^4.0.0",2729+ "version": "2.0.0",
2730- "unist-util-is": "^6.0.0"2730+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
2731- },2731+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
2732- "funding": {2732+ "license": "MIT",
2733- "type": "opencollective",2733+ "dependencies": {
2734- "url": "https://opencollective.com/unified"2734+ "@types/mdast": "^4.0.0",
2735- }2735+ "devlop": "^1.0.0",
2736- },2736+ "mdast-util-from-markdown": "^2.0.0",
2737- "node_modules/mdast-util-to-hast": {2737+ "mdast-util-to-markdown": "^2.0.0"
2738- "version": "13.2.1",2738+ },
2739- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",2739+ "funding": {
2740- "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",2740+ "type": "opencollective",
2741- "license": "MIT",2741+ "url": "https://opencollective.com/unified"
2742- "dependencies": {2742+ }
2743- "@types/hast": "^3.0.0",2743+ },
2744- "@types/mdast": "^4.0.0",2744+ "node_modules/mdast-util-mdx-expression": {
2745- "@ungap/structured-clone": "^1.0.0",2745+ "version": "2.0.1",
2746- "devlop": "^1.0.0",2746+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
2747- "micromark-util-sanitize-uri": "^2.0.0",2747+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
2748- "trim-lines": "^3.0.0",2748+ "license": "MIT",
2749- "unist-util-position": "^5.0.0",2749+ "dependencies": {
2750- "unist-util-visit": "^5.0.0",2750+ "@types/estree-jsx": "^1.0.0",
2751- "vfile": "^6.0.0"2751+ "@types/hast": "^3.0.0",
2752- },2752+ "@types/mdast": "^4.0.0",
2753- "funding": {2753+ "devlop": "^1.0.0",
2754- "type": "opencollective",2754+ "mdast-util-from-markdown": "^2.0.0",
2755- "url": "https://opencollective.com/unified"2755+ "mdast-util-to-markdown": "^2.0.0"
2756- }2756+ },
2757- },2757+ "funding": {
2758- "node_modules/mdast-util-to-markdown": {2758+ "type": "opencollective",
2759- "version": "2.1.2",2759+ "url": "https://opencollective.com/unified"
2760- "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",2760+ }
2761- "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",2761+ },
2762- "license": "MIT",2762+ "node_modules/mdast-util-mdx-jsx": {
2763- "dependencies": {2763+ "version": "3.2.0",
2764- "@types/mdast": "^4.0.0",2764+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
2765- "@types/unist": "^3.0.0",2765+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
2766- "longest-streak": "^3.0.0",2766+ "license": "MIT",
2767- "mdast-util-phrasing": "^4.0.0",2767+ "dependencies": {
2768- "mdast-util-to-string": "^4.0.0",2768+ "@types/estree-jsx": "^1.0.0",
2769- "micromark-util-classify-character": "^2.0.0",2769+ "@types/hast": "^3.0.0",
2770- "micromark-util-decode-string": "^2.0.0",2770+ "@types/mdast": "^4.0.0",
2771- "unist-util-visit": "^5.0.0",2771+ "@types/unist": "^3.0.0",
2772- "zwitch": "^2.0.0"2772+ "ccount": "^2.0.0",
2773- },2773+ "devlop": "^1.1.0",
2774- "funding": {2774+ "mdast-util-from-markdown": "^2.0.0",
2775- "type": "opencollective",2775+ "mdast-util-to-markdown": "^2.0.0",
2776- "url": "https://opencollective.com/unified"2776+ "parse-entities": "^4.0.0",
2777- }2777+ "stringify-entities": "^4.0.0",
2778- },2778+ "unist-util-stringify-position": "^4.0.0",
2779- "node_modules/mdast-util-to-string": {2779+ "vfile-message": "^4.0.0"
2780- "version": "4.0.0",2780+ },
2781- "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",2781+ "funding": {
2782- "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",2782+ "type": "opencollective",
2783- "license": "MIT",2783+ "url": "https://opencollective.com/unified"
2784- "dependencies": {2784+ }
2785- "@types/mdast": "^4.0.0"2785+ },
2786- },2786+ "node_modules/mdast-util-mdxjs-esm": {
2787- "funding": {2787+ "version": "2.0.1",
2788- "type": "opencollective",2788+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
2789- "url": "https://opencollective.com/unified"2789+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
2790- }2790+ "license": "MIT",
2791- },2791+ "dependencies": {
2792- "node_modules/micromark": {2792+ "@types/estree-jsx": "^1.0.0",
2793- "version": "4.0.2",2793+ "@types/hast": "^3.0.0",
2794- "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",2794+ "@types/mdast": "^4.0.0",
2795- "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",2795+ "devlop": "^1.0.0",
2796- "funding": [2796+ "mdast-util-from-markdown": "^2.0.0",
2797- {2797+ "mdast-util-to-markdown": "^2.0.0"
2798- "type": "GitHub Sponsors",2798+ },
2799- "url": "https://github.com/sponsors/unifiedjs"2799+ "funding": {
2800- },2800+ "type": "opencollective",
2801- {2801+ "url": "https://opencollective.com/unified"
2802- "type": "OpenCollective",2802+ }
2803- "url": "https://opencollective.com/unified"2803+ },
2804- }2804+ "node_modules/mdast-util-phrasing": {
2805- ],2805+ "version": "4.1.0",
2806- "license": "MIT",2806+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
2807- "dependencies": {2807+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
2808- "@types/debug": "^4.0.0",2808+ "license": "MIT",
2809- "debug": "^4.0.0",2809+ "dependencies": {
2810- "decode-named-character-reference": "^1.0.0",2810+ "@types/mdast": "^4.0.0",
2811- "devlop": "^1.0.0",2811+ "unist-util-is": "^6.0.0"
2812- "micromark-core-commonmark": "^2.0.0",2812+ },
2813- "micromark-factory-space": "^2.0.0",2813+ "funding": {
2814- "micromark-util-character": "^2.0.0",2814+ "type": "opencollective",
2815- "micromark-util-chunked": "^2.0.0",2815+ "url": "https://opencollective.com/unified"
2816- "micromark-util-combine-extensions": "^2.0.0",2816+ }
2817- "micromark-util-decode-numeric-character-reference": "^2.0.0",2817+ },
2818- "micromark-util-encode": "^2.0.0",2818+ "node_modules/mdast-util-to-hast": {
2819- "micromark-util-normalize-identifier": "^2.0.0",2819+ "version": "13.2.1",
2820- "micromark-util-resolve-all": "^2.0.0",2820+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
2821- "micromark-util-sanitize-uri": "^2.0.0",2821+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
2822- "micromark-util-subtokenize": "^2.0.0",2822+ "license": "MIT",
2823- "micromark-util-symbol": "^2.0.0",2823+ "dependencies": {
2824- "micromark-util-types": "^2.0.0"2824+ "@types/hast": "^3.0.0",
2825- }2825+ "@types/mdast": "^4.0.0",
2826- },2826+ "@ungap/structured-clone": "^1.0.0",
2827- "node_modules/micromark-core-commonmark": {2827+ "devlop": "^1.0.0",
2828- "version": "2.0.3",2828+ "micromark-util-sanitize-uri": "^2.0.0",
2829- "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",2829+ "trim-lines": "^3.0.0",
2830- "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",2830+ "unist-util-position": "^5.0.0",
2831- "funding": [2831+ "unist-util-visit": "^5.0.0",
2832- {2832+ "vfile": "^6.0.0"
2833- "type": "GitHub Sponsors",2833+ },
2834- "url": "https://github.com/sponsors/unifiedjs"2834+ "funding": {
2835- },2835+ "type": "opencollective",
2836- {2836+ "url": "https://opencollective.com/unified"
2837- "type": "OpenCollective",2837+ }
2838- "url": "https://opencollective.com/unified"2838+ },
2839- }2839+ "node_modules/mdast-util-to-markdown": {
2840- ],2840+ "version": "2.1.2",
2841- "license": "MIT",2841+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
2842- "dependencies": {2842+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
2843- "decode-named-character-reference": "^1.0.0",2843+ "license": "MIT",
2844- "devlop": "^1.0.0",2844+ "dependencies": {
2845- "micromark-factory-destination": "^2.0.0",2845+ "@types/mdast": "^4.0.0",
2846- "micromark-factory-label": "^2.0.0",2846+ "@types/unist": "^3.0.0",
2847- "micromark-factory-space": "^2.0.0",2847+ "longest-streak": "^3.0.0",
2848- "micromark-factory-title": "^2.0.0",2848+ "mdast-util-phrasing": "^4.0.0",
2849- "micromark-factory-whitespace": "^2.0.0",2849+ "mdast-util-to-string": "^4.0.0",
2850- "micromark-util-character": "^2.0.0",2850+ "micromark-util-classify-character": "^2.0.0",
2851- "micromark-util-chunked": "^2.0.0",2851+ "micromark-util-decode-string": "^2.0.0",
2852- "micromark-util-classify-character": "^2.0.0",2852+ "unist-util-visit": "^5.0.0",
2853- "micromark-util-html-tag-name": "^2.0.0",2853+ "zwitch": "^2.0.0"
2854- "micromark-util-normalize-identifier": "^2.0.0",2854+ },
2855- "micromark-util-resolve-all": "^2.0.0",2855+ "funding": {
2856- "micromark-util-subtokenize": "^2.0.0",2856+ "type": "opencollective",
2857- "micromark-util-symbol": "^2.0.0",2857+ "url": "https://opencollective.com/unified"
2858- "micromark-util-types": "^2.0.0"2858+ }
2859- }2859+ },
2860- },2860+ "node_modules/mdast-util-to-string": {
2861- "node_modules/micromark-extension-gfm": {2861+ "version": "4.0.0",
2862- "version": "3.0.0",2862+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
2863- "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",2863+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
2864- "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",2864+ "license": "MIT",
2865- "license": "MIT",2865+ "dependencies": {
2866- "dependencies": {2866+ "@types/mdast": "^4.0.0"
2867- "micromark-extension-gfm-autolink-literal": "^2.0.0",2867+ },
2868- "micromark-extension-gfm-footnote": "^2.0.0",2868+ "funding": {
2869- "micromark-extension-gfm-strikethrough": "^2.0.0",2869+ "type": "opencollective",
2870- "micromark-extension-gfm-table": "^2.0.0",2870+ "url": "https://opencollective.com/unified"
2871- "micromark-extension-gfm-tagfilter": "^2.0.0",2871+ }
2872- "micromark-extension-gfm-task-list-item": "^2.0.0",2872+ },
2873- "micromark-util-combine-extensions": "^2.0.0",2873+ "node_modules/micromark": {
2874- "micromark-util-types": "^2.0.0"2874+ "version": "4.0.2",
2875- },2875+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
2876- "funding": {2876+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
2877- "type": "opencollective",2877+ "funding": [
2878- "url": "https://opencollective.com/unified"2878+ {
2879- }2879+ "type": "GitHub Sponsors",
2880- },2880+ "url": "https://github.com/sponsors/unifiedjs"
2881- "node_modules/micromark-extension-gfm-autolink-literal": {2881+ },
2882- "version": "2.1.0",2882+ {
2883- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",2883+ "type": "OpenCollective",
2884- "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",2884+ "url": "https://opencollective.com/unified"
2885- "license": "MIT",2885+ }
2886- "dependencies": {2886+ ],
2887- "micromark-util-character": "^2.0.0",2887+ "license": "MIT",
2888- "micromark-util-sanitize-uri": "^2.0.0",2888+ "dependencies": {
2889- "micromark-util-symbol": "^2.0.0",2889+ "@types/debug": "^4.0.0",
2890- "micromark-util-types": "^2.0.0"2890+ "debug": "^4.0.0",
2891- },2891+ "decode-named-character-reference": "^1.0.0",
2892- "funding": {2892+ "devlop": "^1.0.0",
2893- "type": "opencollective",2893+ "micromark-core-commonmark": "^2.0.0",
2894- "url": "https://opencollective.com/unified"2894+ "micromark-factory-space": "^2.0.0",
2895- }2895+ "micromark-util-character": "^2.0.0",
2896- },2896+ "micromark-util-chunked": "^2.0.0",
2897- "node_modules/micromark-extension-gfm-footnote": {2897+ "micromark-util-combine-extensions": "^2.0.0",
2898- "version": "2.1.0",2898+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
2899- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",2899+ "micromark-util-encode": "^2.0.0",
2900- "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",2900+ "micromark-util-normalize-identifier": "^2.0.0",
2901- "license": "MIT",2901+ "micromark-util-resolve-all": "^2.0.0",
2902- "dependencies": {2902+ "micromark-util-sanitize-uri": "^2.0.0",
2903- "devlop": "^1.0.0",2903+ "micromark-util-subtokenize": "^2.0.0",
2904- "micromark-core-commonmark": "^2.0.0",2904+ "micromark-util-symbol": "^2.0.0",
2905- "micromark-factory-space": "^2.0.0",2905+ "micromark-util-types": "^2.0.0"
2906- "micromark-util-character": "^2.0.0",2906+ }
2907- "micromark-util-normalize-identifier": "^2.0.0",2907+ },
2908- "micromark-util-sanitize-uri": "^2.0.0",2908+ "node_modules/micromark-core-commonmark": {
2909- "micromark-util-symbol": "^2.0.0",2909+ "version": "2.0.3",
2910- "micromark-util-types": "^2.0.0"2910+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
2911- },2911+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
2912- "funding": {2912+ "funding": [
2913- "type": "opencollective",2913+ {
2914- "url": "https://opencollective.com/unified"2914+ "type": "GitHub Sponsors",
2915- }2915+ "url": "https://github.com/sponsors/unifiedjs"
2916- },2916+ },
2917- "node_modules/micromark-extension-gfm-strikethrough": {2917+ {
2918- "version": "2.1.0",2918+ "type": "OpenCollective",
2919- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",2919+ "url": "https://opencollective.com/unified"
2920- "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",2920+ }
2921- "license": "MIT",2921+ ],
2922- "dependencies": {2922+ "license": "MIT",
2923- "devlop": "^1.0.0",2923+ "dependencies": {
2924- "micromark-util-chunked": "^2.0.0",2924+ "decode-named-character-reference": "^1.0.0",
2925- "micromark-util-classify-character": "^2.0.0",2925+ "devlop": "^1.0.0",
2926- "micromark-util-resolve-all": "^2.0.0",2926+ "micromark-factory-destination": "^2.0.0",
2927- "micromark-util-symbol": "^2.0.0",2927+ "micromark-factory-label": "^2.0.0",
2928- "micromark-util-types": "^2.0.0"2928+ "micromark-factory-space": "^2.0.0",
2929- },2929+ "micromark-factory-title": "^2.0.0",
2930- "funding": {2930+ "micromark-factory-whitespace": "^2.0.0",
2931- "type": "opencollective",2931+ "micromark-util-character": "^2.0.0",
2932- "url": "https://opencollective.com/unified"2932+ "micromark-util-chunked": "^2.0.0",
2933- }2933+ "micromark-util-classify-character": "^2.0.0",
2934- },2934+ "micromark-util-html-tag-name": "^2.0.0",
2935- "node_modules/micromark-extension-gfm-table": {2935+ "micromark-util-normalize-identifier": "^2.0.0",
2936- "version": "2.1.2",2936+ "micromark-util-resolve-all": "^2.0.0",
2937- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz",2937+ "micromark-util-subtokenize": "^2.0.0",
2938- "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",2938+ "micromark-util-symbol": "^2.0.0",
2939- "license": "MIT",2939+ "micromark-util-types": "^2.0.0"
2940- "dependencies": {2940+ }
2941- "devlop": "^1.0.0",2941+ },
2942- "micromark-factory-space": "^2.0.0",2942+ "node_modules/micromark-extension-gfm": {
2943- "micromark-util-character": "^2.0.0",2943+ "version": "3.0.0",
2944- "micromark-util-symbol": "^2.0.0",2944+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
2945- "micromark-util-types": "^2.0.0"2945+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
2946- },2946+ "license": "MIT",
2947- "funding": {2947+ "dependencies": {
2948- "type": "opencollective",2948+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
2949- "url": "https://opencollective.com/unified"2949+ "micromark-extension-gfm-footnote": "^2.0.0",
2950- }2950+ "micromark-extension-gfm-strikethrough": "^2.0.0",
2951- },2951+ "micromark-extension-gfm-table": "^2.0.0",
2952- "node_modules/micromark-extension-gfm-tagfilter": {2952+ "micromark-extension-gfm-tagfilter": "^2.0.0",
2953- "version": "2.0.0",2953+ "micromark-extension-gfm-task-list-item": "^2.0.0",
2954- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",2954+ "micromark-util-combine-extensions": "^2.0.0",
2955- "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",2955+ "micromark-util-types": "^2.0.0"
2956- "license": "MIT",2956+ },
2957- "dependencies": {2957+ "funding": {
2958- "micromark-util-types": "^2.0.0"2958+ "type": "opencollective",
2959- },2959+ "url": "https://opencollective.com/unified"
2960- "funding": {2960+ }
2961- "type": "opencollective",2961+ },
2962- "url": "https://opencollective.com/unified"2962+ "node_modules/micromark-extension-gfm-autolink-literal": {
2963- }2963+ "version": "2.1.0",
2964- },2964+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
2965- "node_modules/micromark-extension-gfm-task-list-item": {2965+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
2966- "version": "2.1.0",2966+ "license": "MIT",
2967- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",2967+ "dependencies": {
2968- "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",2968+ "micromark-util-character": "^2.0.0",
2969- "license": "MIT",2969+ "micromark-util-sanitize-uri": "^2.0.0",
2970- "dependencies": {2970+ "micromark-util-symbol": "^2.0.0",
2971- "devlop": "^1.0.0",2971+ "micromark-util-types": "^2.0.0"
2972- "micromark-factory-space": "^2.0.0",2972+ },
2973- "micromark-util-character": "^2.0.0",2973+ "funding": {
2974- "micromark-util-symbol": "^2.0.0",2974+ "type": "opencollective",
2975- "micromark-util-types": "^2.0.0"2975+ "url": "https://opencollective.com/unified"
2976- },2976+ }
2977- "funding": {2977+ },
2978- "type": "opencollective",2978+ "node_modules/micromark-extension-gfm-footnote": {
2979- "url": "https://opencollective.com/unified"2979+ "version": "2.1.0",
2980- }2980+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
2981- },2981+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
2982- "node_modules/micromark-factory-destination": {2982+ "license": "MIT",
2983- "version": "2.0.1",2983+ "dependencies": {
2984- "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",2984+ "devlop": "^1.0.0",
2985- "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",2985+ "micromark-core-commonmark": "^2.0.0",
2986- "funding": [2986+ "micromark-factory-space": "^2.0.0",
2987- {2987+ "micromark-util-character": "^2.0.0",
2988- "type": "GitHub Sponsors",2988+ "micromark-util-normalize-identifier": "^2.0.0",
2989- "url": "https://github.com/sponsors/unifiedjs"2989+ "micromark-util-sanitize-uri": "^2.0.0",
2990- },2990+ "micromark-util-symbol": "^2.0.0",
2991- {2991+ "micromark-util-types": "^2.0.0"
2992- "type": "OpenCollective",2992+ },
2993- "url": "https://opencollective.com/unified"2993+ "funding": {
2994- }2994+ "type": "opencollective",
2995- ],2995+ "url": "https://opencollective.com/unified"
2996- "license": "MIT",2996+ }
2997- "dependencies": {2997+ },
2998- "micromark-util-character": "^2.0.0",2998+ "node_modules/micromark-extension-gfm-strikethrough": {
2999- "micromark-util-symbol": "^2.0.0",2999+ "version": "2.1.0",
3000- "micromark-util-types": "^2.0.0"3000+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
3001- }3001+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
3002- },3002+ "license": "MIT",
3003- "node_modules/micromark-factory-label": {3003+ "dependencies": {
3004- "version": "2.0.1",3004+ "devlop": "^1.0.0",
3005- "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",3005+ "micromark-util-chunked": "^2.0.0",
3006- "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",3006+ "micromark-util-classify-character": "^2.0.0",
3007- "funding": [3007+ "micromark-util-resolve-all": "^2.0.0",
3008- {3008+ "micromark-util-symbol": "^2.0.0",
3009- "type": "GitHub Sponsors",3009+ "micromark-util-types": "^2.0.0"
3010- "url": "https://github.com/sponsors/unifiedjs"3010+ },
3011- },3011+ "funding": {
3012- {3012+ "type": "opencollective",
3013- "type": "OpenCollective",3013+ "url": "https://opencollective.com/unified"
3014- "url": "https://opencollective.com/unified"3014+ }
3015- }3015+ },
3016- ],3016+ "node_modules/micromark-extension-gfm-table": {
3017- "license": "MIT",3017+ "version": "2.1.2",
3018- "dependencies": {3018+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz",
3019- "devlop": "^1.0.0",3019+ "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",
3020- "micromark-util-character": "^2.0.0",3020+ "license": "MIT",
3021- "micromark-util-symbol": "^2.0.0",3021+ "dependencies": {
3022- "micromark-util-types": "^2.0.0"3022+ "devlop": "^1.0.0",
3023- }3023+ "micromark-factory-space": "^2.0.0",
3024- },3024+ "micromark-util-character": "^2.0.0",
3025- "node_modules/micromark-factory-space": {3025+ "micromark-util-symbol": "^2.0.0",
3026- "version": "2.0.1",3026+ "micromark-util-types": "^2.0.0"
3027- "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",3027+ },
3028- "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",3028+ "funding": {
3029- "funding": [3029+ "type": "opencollective",
3030- {3030+ "url": "https://opencollective.com/unified"
3031- "type": "GitHub Sponsors",3031+ }
3032- "url": "https://github.com/sponsors/unifiedjs"3032+ },
3033- },3033+ "node_modules/micromark-extension-gfm-tagfilter": {
3034- {3034+ "version": "2.0.0",
3035- "type": "OpenCollective",3035+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
3036- "url": "https://opencollective.com/unified"3036+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
3037- }3037+ "license": "MIT",
3038- ],3038+ "dependencies": {
3039- "license": "MIT",3039+ "micromark-util-types": "^2.0.0"
3040- "dependencies": {3040+ },
3041- "micromark-util-character": "^2.0.0",3041+ "funding": {
3042- "micromark-util-types": "^2.0.0"3042+ "type": "opencollective",
3043- }3043+ "url": "https://opencollective.com/unified"
3044- },3044+ }
3045- "node_modules/micromark-factory-title": {3045+ },
3046- "version": "2.0.1",3046+ "node_modules/micromark-extension-gfm-task-list-item": {
3047- "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",3047+ "version": "2.1.0",
3048- "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",3048+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
3049- "funding": [3049+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
3050- {3050+ "license": "MIT",
3051- "type": "GitHub Sponsors",3051+ "dependencies": {
3052- "url": "https://github.com/sponsors/unifiedjs"3052+ "devlop": "^1.0.0",
3053- },3053+ "micromark-factory-space": "^2.0.0",
3054- {3054+ "micromark-util-character": "^2.0.0",
3055- "type": "OpenCollective",3055+ "micromark-util-symbol": "^2.0.0",
3056- "url": "https://opencollective.com/unified"3056+ "micromark-util-types": "^2.0.0"
3057- }3057+ },
3058- ],3058+ "funding": {
3059- "license": "MIT",3059+ "type": "opencollective",
3060- "dependencies": {3060+ "url": "https://opencollective.com/unified"
3061- "micromark-factory-space": "^2.0.0",3061+ }
3062- "micromark-util-character": "^2.0.0",3062+ },
3063- "micromark-util-symbol": "^2.0.0",3063+ "node_modules/micromark-factory-destination": {
3064- "micromark-util-types": "^2.0.0"3064+ "version": "2.0.1",
3065- }3065+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
3066- },3066+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
3067- "node_modules/micromark-factory-whitespace": {3067+ "funding": [
3068- "version": "2.0.1",3068+ {
3069- "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",3069+ "type": "GitHub Sponsors",
3070- "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",3070+ "url": "https://github.com/sponsors/unifiedjs"
3071- "funding": [3071+ },
3072- {3072+ {
3073- "type": "GitHub Sponsors",3073+ "type": "OpenCollective",
3074- "url": "https://github.com/sponsors/unifiedjs"3074+ "url": "https://opencollective.com/unified"
3075- },3075+ }
3076- {3076+ ],
3077- "type": "OpenCollective",3077+ "license": "MIT",
3078- "url": "https://opencollective.com/unified"3078+ "dependencies": {
3079- }3079+ "micromark-util-character": "^2.0.0",
3080- ],3080+ "micromark-util-symbol": "^2.0.0",
3081- "license": "MIT",3081+ "micromark-util-types": "^2.0.0"
3082- "dependencies": {3082+ }
3083- "micromark-factory-space": "^2.0.0",3083+ },
3084- "micromark-util-character": "^2.0.0",3084+ "node_modules/micromark-factory-label": {
3085- "micromark-util-symbol": "^2.0.0",3085+ "version": "2.0.1",
3086- "micromark-util-types": "^2.0.0"3086+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
3087- }3087+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
3088- },3088+ "funding": [
3089- "node_modules/micromark-util-character": {3089+ {
3090- "version": "2.1.1",3090+ "type": "GitHub Sponsors",
3091- "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",3091+ "url": "https://github.com/sponsors/unifiedjs"
3092- "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",3092+ },
3093- "funding": [3093+ {
3094- {3094+ "type": "OpenCollective",
3095- "type": "GitHub Sponsors",3095+ "url": "https://opencollective.com/unified"
3096- "url": "https://github.com/sponsors/unifiedjs"3096+ }
3097- },3097+ ],
3098- {3098+ "license": "MIT",
3099- "type": "OpenCollective",3099+ "dependencies": {
3100- "url": "https://opencollective.com/unified"3100+ "devlop": "^1.0.0",
3101- }3101+ "micromark-util-character": "^2.0.0",
3102- ],3102+ "micromark-util-symbol": "^2.0.0",
3103- "license": "MIT",3103+ "micromark-util-types": "^2.0.0"
3104- "dependencies": {3104+ }
3105- "micromark-util-symbol": "^2.0.0",3105+ },
3106- "micromark-util-types": "^2.0.0"3106+ "node_modules/micromark-factory-space": {
3107- }3107+ "version": "2.0.1",
3108- },3108+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
3109- "node_modules/micromark-util-chunked": {3109+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
3110- "version": "2.0.1",3110+ "funding": [
3111- "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",3111+ {
3112- "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",3112+ "type": "GitHub Sponsors",
3113- "funding": [3113+ "url": "https://github.com/sponsors/unifiedjs"
3114- {3114+ },
3115- "type": "GitHub Sponsors",3115+ {
3116- "url": "https://github.com/sponsors/unifiedjs"3116+ "type": "OpenCollective",
3117- },3117+ "url": "https://opencollective.com/unified"
3118- {3118+ }
3119- "type": "OpenCollective",3119+ ],
3120- "url": "https://opencollective.com/unified"3120+ "license": "MIT",
3121- }3121+ "dependencies": {
3122- ],3122+ "micromark-util-character": "^2.0.0",
3123- "license": "MIT",3123+ "micromark-util-types": "^2.0.0"
3124- "dependencies": {3124+ }
3125- "micromark-util-symbol": "^2.0.0"3125+ },
3126- }3126+ "node_modules/micromark-factory-title": {
3127- },3127+ "version": "2.0.1",
3128- "node_modules/micromark-util-classify-character": {3128+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
3129- "version": "2.0.1",3129+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
3130- "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",3130+ "funding": [
3131- "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",3131+ {
3132- "funding": [3132+ "type": "GitHub Sponsors",
3133- {3133+ "url": "https://github.com/sponsors/unifiedjs"
3134- "type": "GitHub Sponsors",3134+ },
3135- "url": "https://github.com/sponsors/unifiedjs"3135+ {
3136- },3136+ "type": "OpenCollective",
3137- {3137+ "url": "https://opencollective.com/unified"
3138- "type": "OpenCollective",3138+ }
3139- "url": "https://opencollective.com/unified"3139+ ],
3140- }3140+ "license": "MIT",
3141- ],3141+ "dependencies": {
3142- "license": "MIT",3142+ "micromark-factory-space": "^2.0.0",
3143- "dependencies": {3143+ "micromark-util-character": "^2.0.0",
3144- "micromark-util-character": "^2.0.0",3144+ "micromark-util-symbol": "^2.0.0",
3145- "micromark-util-symbol": "^2.0.0",3145+ "micromark-util-types": "^2.0.0"
3146- "micromark-util-types": "^2.0.0"3146+ }
3147- }3147+ },
3148- },3148+ "node_modules/micromark-factory-whitespace": {
3149- "node_modules/micromark-util-combine-extensions": {3149+ "version": "2.0.1",
3150- "version": "2.0.1",3150+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
3151- "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",3151+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
3152- "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",3152+ "funding": [
3153- "funding": [3153+ {
3154- {3154+ "type": "GitHub Sponsors",
3155- "type": "GitHub Sponsors",3155+ "url": "https://github.com/sponsors/unifiedjs"
3156- "url": "https://github.com/sponsors/unifiedjs"3156+ },
3157- },3157+ {
3158- {3158+ "type": "OpenCollective",
3159- "type": "OpenCollective",3159+ "url": "https://opencollective.com/unified"
3160- "url": "https://opencollective.com/unified"3160+ }
3161- }3161+ ],
3162- ],3162+ "license": "MIT",
3163- "license": "MIT",3163+ "dependencies": {
3164- "dependencies": {3164+ "micromark-factory-space": "^2.0.0",
3165- "micromark-util-chunked": "^2.0.0",3165+ "micromark-util-character": "^2.0.0",
3166- "micromark-util-types": "^2.0.0"3166+ "micromark-util-symbol": "^2.0.0",
3167- }3167+ "micromark-util-types": "^2.0.0"
3168- },3168+ }
3169- "node_modules/micromark-util-decode-numeric-character-reference": {3169+ },
3170- "version": "2.0.2",3170+ "node_modules/micromark-util-character": {
3171- "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",3171+ "version": "2.1.1",
3172- "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",3172+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
3173- "funding": [3173+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
3174- {3174+ "funding": [
3175- "type": "GitHub Sponsors",3175+ {
3176- "url": "https://github.com/sponsors/unifiedjs"3176+ "type": "GitHub Sponsors",
3177- },3177+ "url": "https://github.com/sponsors/unifiedjs"
3178- {3178+ },
3179- "type": "OpenCollective",3179+ {
3180- "url": "https://opencollective.com/unified"3180+ "type": "OpenCollective",
3181- }3181+ "url": "https://opencollective.com/unified"
3182- ],3182+ }
3183- "license": "MIT",3183+ ],
3184- "dependencies": {3184+ "license": "MIT",
3185- "micromark-util-symbol": "^2.0.0"3185+ "dependencies": {
3186- }3186+ "micromark-util-symbol": "^2.0.0",
3187- },3187+ "micromark-util-types": "^2.0.0"
3188- "node_modules/micromark-util-decode-string": {3188+ }
3189- "version": "2.0.1",3189+ },
3190- "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",3190+ "node_modules/micromark-util-chunked": {
3191- "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",3191+ "version": "2.0.1",
3192- "funding": [3192+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
3193- {3193+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
3194- "type": "GitHub Sponsors",3194+ "funding": [
3195- "url": "https://github.com/sponsors/unifiedjs"3195+ {
3196- },3196+ "type": "GitHub Sponsors",
3197- {3197+ "url": "https://github.com/sponsors/unifiedjs"
3198- "type": "OpenCollective",3198+ },
3199- "url": "https://opencollective.com/unified"3199+ {
3200- }3200+ "type": "OpenCollective",
3201- ],3201+ "url": "https://opencollective.com/unified"
3202- "license": "MIT",3202+ }
3203- "dependencies": {3203+ ],
3204- "decode-named-character-reference": "^1.0.0",3204+ "license": "MIT",
3205- "micromark-util-character": "^2.0.0",3205+ "dependencies": {
3206- "micromark-util-decode-numeric-character-reference": "^2.0.0",3206+ "micromark-util-symbol": "^2.0.0"
3207- "micromark-util-symbol": "^2.0.0"3207+ }
3208- }3208+ },
3209- },3209+ "node_modules/micromark-util-classify-character": {
3210- "node_modules/micromark-util-encode": {3210+ "version": "2.0.1",
3211- "version": "2.0.1",3211+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
3212- "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",3212+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
3213- "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",3213+ "funding": [
3214- "funding": [3214+ {
3215- {3215+ "type": "GitHub Sponsors",
3216- "type": "GitHub Sponsors",3216+ "url": "https://github.com/sponsors/unifiedjs"
3217- "url": "https://github.com/sponsors/unifiedjs"3217+ },
3218- },3218+ {
3219- {3219+ "type": "OpenCollective",
3220- "type": "OpenCollective",3220+ "url": "https://opencollective.com/unified"
3221- "url": "https://opencollective.com/unified"3221+ }
3222- }3222+ ],
3223- ],3223+ "license": "MIT",
3224- "license": "MIT"3224+ "dependencies": {
3225- },3225+ "micromark-util-character": "^2.0.0",
3226- "node_modules/micromark-util-html-tag-name": {3226+ "micromark-util-symbol": "^2.0.0",
3227- "version": "2.0.1",3227+ "micromark-util-types": "^2.0.0"
3228- "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",3228+ }
3229- "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",3229+ },
3230- "funding": [3230+ "node_modules/micromark-util-combine-extensions": {
3231- {3231+ "version": "2.0.1",
3232- "type": "GitHub Sponsors",3232+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
3233- "url": "https://github.com/sponsors/unifiedjs"3233+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
3234- },3234+ "funding": [
3235- {3235+ {
3236- "type": "OpenCollective",3236+ "type": "GitHub Sponsors",
3237- "url": "https://opencollective.com/unified"3237+ "url": "https://github.com/sponsors/unifiedjs"
3238- }3238+ },
3239- ],3239+ {
3240- "license": "MIT"3240+ "type": "OpenCollective",
3241- },3241+ "url": "https://opencollective.com/unified"
3242- "node_modules/micromark-util-normalize-identifier": {3242+ }
3243- "version": "2.0.1",3243+ ],
3244- "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",3244+ "license": "MIT",
3245- "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",3245+ "dependencies": {
3246- "funding": [3246+ "micromark-util-chunked": "^2.0.0",
3247- {3247+ "micromark-util-types": "^2.0.0"
3248- "type": "GitHub Sponsors",3248+ }
3249- "url": "https://github.com/sponsors/unifiedjs"3249+ },
3250- },3250+ "node_modules/micromark-util-decode-numeric-character-reference": {
3251- {3251+ "version": "2.0.2",
3252- "type": "OpenCollective",3252+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
3253- "url": "https://opencollective.com/unified"3253+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
3254- }3254+ "funding": [
3255- ],3255+ {
3256- "license": "MIT",3256+ "type": "GitHub Sponsors",
3257- "dependencies": {3257+ "url": "https://github.com/sponsors/unifiedjs"
3258- "micromark-util-symbol": "^2.0.0"3258+ },
3259- }3259+ {
3260- },3260+ "type": "OpenCollective",
3261- "node_modules/micromark-util-resolve-all": {3261+ "url": "https://opencollective.com/unified"
3262- "version": "2.0.1",3262+ }
3263- "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",3263+ ],
3264- "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",3264+ "license": "MIT",
3265- "funding": [3265+ "dependencies": {
3266- {3266+ "micromark-util-symbol": "^2.0.0"
3267- "type": "GitHub Sponsors",3267+ }
3268- "url": "https://github.com/sponsors/unifiedjs"3268+ },
3269- },3269+ "node_modules/micromark-util-decode-string": {
3270- {3270+ "version": "2.0.1",
3271- "type": "OpenCollective",3271+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
3272- "url": "https://opencollective.com/unified"3272+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
3273- }3273+ "funding": [
3274- ],3274+ {
3275- "license": "MIT",3275+ "type": "GitHub Sponsors",
3276- "dependencies": {3276+ "url": "https://github.com/sponsors/unifiedjs"
3277- "micromark-util-types": "^2.0.0"3277+ },
3278- }3278+ {
3279- },3279+ "type": "OpenCollective",
3280- "node_modules/micromark-util-sanitize-uri": {3280+ "url": "https://opencollective.com/unified"
3281- "version": "2.0.1",3281+ }
3282- "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",3282+ ],
3283- "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",3283+ "license": "MIT",
3284- "funding": [3284+ "dependencies": {
3285- {3285+ "decode-named-character-reference": "^1.0.0",
3286- "type": "GitHub Sponsors",3286+ "micromark-util-character": "^2.0.0",
3287- "url": "https://github.com/sponsors/unifiedjs"3287+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
3288- },3288+ "micromark-util-symbol": "^2.0.0"
3289- {3289+ }
3290- "type": "OpenCollective",3290+ },
3291- "url": "https://opencollective.com/unified"3291+ "node_modules/micromark-util-encode": {
3292- }3292+ "version": "2.0.1",
3293- ],3293+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
3294- "license": "MIT",3294+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
3295- "dependencies": {3295+ "funding": [
3296- "micromark-util-character": "^2.0.0",3296+ {
3297- "micromark-util-encode": "^2.0.0",3297+ "type": "GitHub Sponsors",
3298- "micromark-util-symbol": "^2.0.0"3298+ "url": "https://github.com/sponsors/unifiedjs"
3299- }3299+ },
3300- },3300+ {
3301- "node_modules/micromark-util-subtokenize": {3301+ "type": "OpenCollective",
3302- "version": "2.1.0",3302+ "url": "https://opencollective.com/unified"
3303- "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",3303+ }
3304- "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",3304+ ],
3305- "funding": [3305+ "license": "MIT"
3306- {3306+ },
3307- "type": "GitHub Sponsors",3307+ "node_modules/micromark-util-html-tag-name": {
3308- "url": "https://github.com/sponsors/unifiedjs"3308+ "version": "2.0.1",
3309- },3309+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
3310- {3310+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
3311- "type": "OpenCollective",3311+ "funding": [
3312- "url": "https://opencollective.com/unified"3312+ {
3313- }3313+ "type": "GitHub Sponsors",
3314- ],3314+ "url": "https://github.com/sponsors/unifiedjs"
3315- "license": "MIT",3315+ },
3316- "dependencies": {3316+ {
3317- "devlop": "^1.0.0",3317+ "type": "OpenCollective",
3318- "micromark-util-chunked": "^2.0.0",3318+ "url": "https://opencollective.com/unified"
3319- "micromark-util-symbol": "^2.0.0",3319+ }
3320- "micromark-util-types": "^2.0.0"3320+ ],
3321- }3321+ "license": "MIT"
3322- },3322+ },
3323- "node_modules/micromark-util-symbol": {3323+ "node_modules/micromark-util-normalize-identifier": {
3324- "version": "2.0.1",3324+ "version": "2.0.1",
3325- "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",3325+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
3326- "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",3326+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
3327- "funding": [3327+ "funding": [
3328- {3328+ {
3329- "type": "GitHub Sponsors",3329+ "type": "GitHub Sponsors",
3330- "url": "https://github.com/sponsors/unifiedjs"3330+ "url": "https://github.com/sponsors/unifiedjs"
3331- },3331+ },
3332- {3332+ {
3333- "type": "OpenCollective",3333+ "type": "OpenCollective",
3334- "url": "https://opencollective.com/unified"3334+ "url": "https://opencollective.com/unified"
3335- }3335+ }
3336- ],3336+ ],
3337- "license": "MIT"3337+ "license": "MIT",
3338- },3338+ "dependencies": {
3339- "node_modules/micromark-util-types": {3339+ "micromark-util-symbol": "^2.0.0"
3340- "version": "2.0.2",3340+ }
3341- "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",3341+ },
3342- "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",3342+ "node_modules/micromark-util-resolve-all": {
3343- "funding": [3343+ "version": "2.0.1",
3344- {3344+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
3345- "type": "GitHub Sponsors",3345+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
3346- "url": "https://github.com/sponsors/unifiedjs"3346+ "funding": [
3347- },3347+ {
3348- {3348+ "type": "GitHub Sponsors",
3349- "type": "OpenCollective",3349+ "url": "https://github.com/sponsors/unifiedjs"
3350- "url": "https://opencollective.com/unified"3350+ },
3351- }3351+ {
3352- ],3352+ "type": "OpenCollective",
3353- "license": "MIT"3353+ "url": "https://opencollective.com/unified"
3354- },3354+ }
3355- "node_modules/ms": {3355+ ],
3356- "version": "2.1.3",3356+ "license": "MIT",
3357- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",3357+ "dependencies": {
3358- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",3358+ "micromark-util-types": "^2.0.0"
3359- "license": "MIT"3359+ }
3360- },3360+ },
3361- "node_modules/nanoid": {3361+ "node_modules/micromark-util-sanitize-uri": {
3362- "version": "3.3.19",3362+ "version": "2.0.1",
3363- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",3363+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
3364- "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",3364+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
3365- "dev": true,3365+ "funding": [
3366- "funding": [3366+ {
3367- {3367+ "type": "GitHub Sponsors",
3368- "type": "github",3368+ "url": "https://github.com/sponsors/unifiedjs"
3369- "url": "https://github.com/sponsors/ai"3369+ },
3370- }3370+ {
3371- ],3371+ "type": "OpenCollective",
3372- "license": "MIT",3372+ "url": "https://opencollective.com/unified"
3373- "bin": {3373+ }
3374- "nanoid": "bin/nanoid.cjs"3374+ ],
3375- },3375+ "license": "MIT",
3376- "engines": {3376+ "dependencies": {
3377- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"3377+ "micromark-util-character": "^2.0.0",
3378- }3378+ "micromark-util-encode": "^2.0.0",
3379- },3379+ "micromark-util-symbol": "^2.0.0"
3380- "node_modules/node-releases": {3380+ }
3381- "version": "2.0.56",3381+ },
3382- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz",3382+ "node_modules/micromark-util-subtokenize": {
3383- "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",3383+ "version": "2.1.0",
3384- "dev": true,3384+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
3385- "license": "MIT",3385+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
3386- "engines": {3386+ "funding": [
3387- "node": ">=18"3387+ {
3388- }3388+ "type": "GitHub Sponsors",
3389- },3389+ "url": "https://github.com/sponsors/unifiedjs"
3390- "node_modules/nwsapi": {3390+ },
3391- "version": "2.2.27",3391+ {
3392- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",3392+ "type": "OpenCollective",
3393- "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",3393+ "url": "https://opencollective.com/unified"
3394- "dev": true,3394+ }
3395- "license": "MIT"3395+ ],
3396- },3396+ "license": "MIT",
3397- "node_modules/parse-entities": {3397+ "dependencies": {
3398- "version": "4.0.2",3398+ "devlop": "^1.0.0",
3399- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",3399+ "micromark-util-chunked": "^2.0.0",
3400- "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",3400+ "micromark-util-symbol": "^2.0.0",
3401- "license": "MIT",3401+ "micromark-util-types": "^2.0.0"
3402- "dependencies": {3402+ }
3403- "@types/unist": "^2.0.0",3403+ },
3404- "character-entities-legacy": "^3.0.0",3404+ "node_modules/micromark-util-symbol": {
3405- "character-reference-invalid": "^2.0.0",3405+ "version": "2.0.1",
3406- "decode-named-character-reference": "^1.0.0",3406+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
3407- "is-alphanumerical": "^2.0.0",3407+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
3408- "is-decimal": "^2.0.0",3408+ "funding": [
3409- "is-hexadecimal": "^2.0.0"3409+ {
3410- },3410+ "type": "GitHub Sponsors",
3411- "funding": {3411+ "url": "https://github.com/sponsors/unifiedjs"
3412- "type": "github",3412+ },
3413- "url": "https://github.com/sponsors/wooorm"3413+ {
3414- }3414+ "type": "OpenCollective",
3415- },3415+ "url": "https://opencollective.com/unified"
3416- "node_modules/parse-entities/node_modules/@types/unist": {3416+ }
3417- "version": "2.0.11",3417+ ],
3418- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",3418+ "license": "MIT"
3419- "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",3419+ },
3420- "license": "MIT"3420+ "node_modules/micromark-util-types": {
3421- },3421+ "version": "2.0.2",
3422- "node_modules/parse5": {3422+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
3423- "version": "7.3.0",3423+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
3424- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",3424+ "funding": [
3425- "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",3425+ {
3426- "dev": true,3426+ "type": "GitHub Sponsors",
3427- "license": "MIT",3427+ "url": "https://github.com/sponsors/unifiedjs"
3428- "dependencies": {3428+ },
3429- "entities": "^6.0.0"3429+ {
3430- },3430+ "type": "OpenCollective",
3431- "funding": {3431+ "url": "https://opencollective.com/unified"
3432- "url": "https://github.com/inikulin/parse5?sponsor=1"3432+ }
3433- }3433+ ],
3434- },3434+ "license": "MIT"
3435- "node_modules/pathe": {3435+ },
3436- "version": "2.0.3",3436+ "node_modules/monaco-editor": {
3437- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",3437+ "version": "0.56.0",
3438- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",3438+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz",
3439- "dev": true,3439+ "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==",
3440- "license": "MIT"3440+ "license": "MIT",
3441- },3441+ "dependencies": {
3442- "node_modules/pathval": {3442+ "dompurify": "3.4.8",
3443- "version": "2.0.1",3443+ "marked": "14.0.0"
3444- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",3444+ }
3445- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",3445+ },
3446- "dev": true,3446+ "node_modules/ms": {
3447- "license": "MIT",3447+ "version": "2.1.3",
3448- "engines": {3448+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
3449- "node": ">= 14.16"3449+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
3450- }3450+ "license": "MIT"
3451- },3451+ },
3452- "node_modules/picocolors": {3452+ "node_modules/nanoid": {
3453- "version": "1.1.1",3453+ "version": "3.3.19",
3454- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",3454+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
3455- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",3455+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
3456- "dev": true,3456+ "dev": true,
3457- "license": "ISC"3457+ "funding": [
3458- },3458+ {
3459- "node_modules/picomatch": {3459+ "type": "github",
3460- "version": "4.0.7",3460+ "url": "https://github.com/sponsors/ai"
3461- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",3461+ }
3462- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",3462+ ],
3463- "dev": true,3463+ "license": "MIT",
3464- "license": "MIT",3464+ "bin": {
3465- "engines": {3465+ "nanoid": "bin/nanoid.cjs"
3466- "node": ">=12"3466+ },
3467- },3467+ "engines": {
3468- "funding": {3468+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
3469- "url": "https://github.com/sponsors/jonschlinkert"3469+ }
3470- }3470+ },
3471- },3471+ "node_modules/node-releases": {
3472- "node_modules/postcss": {3472+ "version": "2.0.56",
3473- "version": "8.5.28",3473+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz",
3474- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",3474+ "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",
3475- "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",3475+ "dev": true,
3476- "dev": true,3476+ "license": "MIT",
3477- "funding": [3477+ "engines": {
3478- {3478+ "node": ">=18"
3479- "type": "opencollective",3479+ }
3480- "url": "https://opencollective.com/postcss/"3480+ },
3481- },3481+ "node_modules/nwsapi": {
3482- {3482+ "version": "2.2.27",
3483- "type": "tidelift",3483+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
3484- "url": "https://tidelift.com/funding/github/npm/postcss"3484+ "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
3485- },3485+ "dev": true,
3486- {3486+ "license": "MIT"
3487- "type": "github",3487+ },
3488- "url": "https://github.com/sponsors/ai"3488+ "node_modules/parse-entities": {
3489- }3489+ "version": "4.0.2",
3490- ],3490+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
3491- "license": "MIT",3491+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
3492- "dependencies": {3492+ "license": "MIT",
3493- "nanoid": "^3.3.18",3493+ "dependencies": {
3494- "picocolors": "^1.1.1",3494+ "@types/unist": "^2.0.0",
3495- "source-map-js": "^1.2.1"3495+ "character-entities-legacy": "^3.0.0",
3496- },3496+ "character-reference-invalid": "^2.0.0",
3497- "engines": {3497+ "decode-named-character-reference": "^1.0.0",
3498- "node": "^10 || ^12 || >=14"3498+ "is-alphanumerical": "^2.0.0",
3499- }3499+ "is-decimal": "^2.0.0",
3500- },3500+ "is-hexadecimal": "^2.0.0"
3501- "node_modules/pretty-format": {3501+ },
3502- "version": "27.5.1",3502+ "funding": {
3503- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",3503+ "type": "github",
3504- "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",3504+ "url": "https://github.com/sponsors/wooorm"
3505- "dev": true,3505+ }
3506- "license": "MIT",3506+ },
3507- "peer": true,3507+ "node_modules/parse-entities/node_modules/@types/unist": {
3508- "dependencies": {3508+ "version": "2.0.11",
3509- "ansi-regex": "^5.0.1",3509+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
3510- "ansi-styles": "^5.0.0",3510+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
3511- "react-is": "^17.0.1"3511+ "license": "MIT"
3512- },3512+ },
3513- "engines": {3513+ "node_modules/parse5": {
3514- "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"3514+ "version": "7.3.0",
3515- }3515+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
3516- },3516+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
3517- "node_modules/property-information": {3517+ "dev": true,
3518- "version": "7.2.0",3518+ "license": "MIT",
3519- "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",3519+ "dependencies": {
3520- "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",3520+ "entities": "^6.0.0"
3521- "license": "MIT",3521+ },
3522- "funding": {3522+ "funding": {
3523- "type": "github",3523+ "url": "https://github.com/inikulin/parse5?sponsor=1"
3524- "url": "https://github.com/sponsors/wooorm"3524+ }
3525- }3525+ },
3526- },3526+ "node_modules/pathe": {
3527- "node_modules/punycode": {3527+ "version": "2.0.3",
3528- "version": "2.3.1",3528+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
3529- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",3529+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
3530- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",3530+ "dev": true,
3531- "dev": true,3531+ "license": "MIT"
3532- "license": "MIT",3532+ },
3533- "engines": {3533+ "node_modules/pathval": {
3534- "node": ">=6"3534+ "version": "2.0.1",
3535- }3535+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
3536- },3536+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
3537- "node_modules/react": {3537+ "dev": true,
3538- "version": "19.3.0",3538+ "license": "MIT",
3539- "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",3539+ "engines": {
3540- "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",3540+ "node": ">= 14.16"
3541- "license": "MIT",3541+ }
3542- "engines": {3542+ },
3543- "node": ">=0.10.0"3543+ "node_modules/picocolors": {
3544- }3544+ "version": "1.1.1",
3545- },3545+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
3546- "node_modules/react-dom": {3546+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
3547- "version": "19.3.0",3547+ "dev": true,
3548- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",3548+ "license": "ISC"
3549- "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",3549+ },
3550- "license": "MIT",3550+ "node_modules/picomatch": {
3551- "dependencies": {3551+ "version": "4.0.7",
3552- "scheduler": "^0.28.0"3552+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
3553- },3553+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
3554- "peerDependencies": {3554+ "dev": true,
3555- "react": "^19.3.0"3555+ "license": "MIT",
3556- }3556+ "engines": {
3557- },3557+ "node": ">=12"
3558- "node_modules/react-is": {3558+ },
3559- "version": "17.0.2",3559+ "funding": {
3560- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",3560+ "url": "https://github.com/sponsors/jonschlinkert"
3561- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",3561+ }
3562- "dev": true,3562+ },
3563- "license": "MIT",3563+ "node_modules/postcss": {
3564- "peer": true3564+ "version": "8.5.28",
3565- },3565+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
3566- "node_modules/react-markdown": {3566+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
3567- "version": "10.1.0",3567+ "dev": true,
3568- "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",3568+ "funding": [
3569- "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",3569+ {
3570- "license": "MIT",3570+ "type": "opencollective",
3571- "dependencies": {3571+ "url": "https://opencollective.com/postcss/"
3572- "@types/hast": "^3.0.0",3572+ },
3573- "@types/mdast": "^4.0.0",3573+ {
3574- "devlop": "^1.0.0",3574+ "type": "tidelift",
3575- "hast-util-to-jsx-runtime": "^2.0.0",3575+ "url": "https://tidelift.com/funding/github/npm/postcss"
3576- "html-url-attributes": "^3.0.0",3576+ },
3577- "mdast-util-to-hast": "^13.0.0",3577+ {
3578- "remark-parse": "^11.0.0",3578+ "type": "github",
3579- "remark-rehype": "^11.0.0",3579+ "url": "https://github.com/sponsors/ai"
3580- "unified": "^11.0.0",3580+ }
3581- "unist-util-visit": "^5.0.0",3581+ ],
3582- "vfile": "^6.0.0"3582+ "license": "MIT",
3583- },3583+ "dependencies": {
3584- "funding": {3584+ "nanoid": "^3.3.18",
3585- "type": "opencollective",3585+ "picocolors": "^1.1.1",
3586- "url": "https://opencollective.com/unified"3586+ "source-map-js": "^1.2.1"
3587- },3587+ },
3588- "peerDependencies": {3588+ "engines": {
3589- "@types/react": ">=18",3589+ "node": "^10 || ^12 || >=14"
3590- "react": ">=18"3590+ }
3591- }3591+ },
3592- },3592+ "node_modules/pretty-format": {
3593- "node_modules/react-refresh": {3593+ "version": "27.5.1",
3594- "version": "0.18.0",3594+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
3595- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",3595+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
3596- "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",3596+ "dev": true,
3597- "dev": true,3597+ "license": "MIT",
3598- "license": "MIT",3598+ "peer": true,
3599- "engines": {3599+ "dependencies": {
3600- "node": ">=0.10.0"3600+ "ansi-regex": "^5.0.1",
3601- }3601+ "ansi-styles": "^5.0.0",
3602- },3602+ "react-is": "^17.0.1"
3603- "node_modules/remark-gfm": {3603+ },
3604- "version": "4.0.1",3604+ "engines": {
3605- "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",3605+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
3606- "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",3606+ }
3607- "license": "MIT",3607+ },
3608- "dependencies": {3608+ "node_modules/property-information": {
3609- "@types/mdast": "^4.0.0",3609+ "version": "7.2.0",
3610- "mdast-util-gfm": "^3.0.0",3610+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
3611- "micromark-extension-gfm": "^3.0.0",3611+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
3612- "remark-parse": "^11.0.0",3612+ "license": "MIT",
3613- "remark-stringify": "^11.0.0",3613+ "funding": {
3614- "unified": "^11.0.0"3614+ "type": "github",
3615- },3615+ "url": "https://github.com/sponsors/wooorm"
3616- "funding": {3616+ }
3617- "type": "opencollective",3617+ },
3618- "url": "https://opencollective.com/unified"3618+ "node_modules/punycode": {
3619- }3619+ "version": "2.3.1",
3620- },3620+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
3621- "node_modules/remark-parse": {3621+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
3622- "version": "11.0.0",3622+ "dev": true,
3623- "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",3623+ "license": "MIT",
3624- "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",3624+ "engines": {
3625- "license": "MIT",3625+ "node": ">=6"
3626- "dependencies": {3626+ }
3627- "@types/mdast": "^4.0.0",3627+ },
3628- "mdast-util-from-markdown": "^2.0.0",3628+ "node_modules/react": {
3629- "micromark-util-types": "^2.0.0",3629+ "version": "19.3.0",
3630- "unified": "^11.0.0"3630+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
3631- },3631+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
3632- "funding": {3632+ "license": "MIT",
3633- "type": "opencollective",3633+ "engines": {
3634- "url": "https://opencollective.com/unified"3634+ "node": ">=0.10.0"
3635- }3635+ }
3636- },3636+ },
3637- "node_modules/remark-rehype": {3637+ "node_modules/react-dom": {
3638- "version": "11.1.2",3638+ "version": "19.3.0",
3639- "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",3639+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
3640- "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",3640+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
3641- "license": "MIT",3641+ "license": "MIT",
3642- "dependencies": {3642+ "dependencies": {
3643- "@types/hast": "^3.0.0",3643+ "scheduler": "^0.28.0"
3644- "@types/mdast": "^4.0.0",3644+ },
3645- "mdast-util-to-hast": "^13.0.0",3645+ "peerDependencies": {
3646- "unified": "^11.0.0",3646+ "react": "^19.3.0"
3647- "vfile": "^6.0.0"3647+ }
3648- },3648+ },
3649- "funding": {3649+ "node_modules/react-is": {
3650- "type": "opencollective",3650+ "version": "17.0.2",
3651- "url": "https://opencollective.com/unified"3651+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
3652- }3652+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
3653- },3653+ "dev": true,
3654- "node_modules/remark-stringify": {3654+ "license": "MIT",
3655- "version": "11.0.0",3655+ "peer": true
3656- "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",3656+ },
3657- "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",3657+ "node_modules/react-markdown": {
3658- "license": "MIT",3658+ "version": "10.1.0",
3659- "dependencies": {3659+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
3660- "@types/mdast": "^4.0.0",3660+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
3661- "mdast-util-to-markdown": "^2.0.0",3661+ "license": "MIT",
3662- "unified": "^11.0.0"3662+ "dependencies": {
3663- },3663+ "@types/hast": "^3.0.0",
3664- "funding": {3664+ "@types/mdast": "^4.0.0",
3665- "type": "opencollective",3665+ "devlop": "^1.0.0",
3666- "url": "https://opencollective.com/unified"3666+ "hast-util-to-jsx-runtime": "^2.0.0",
3667- }3667+ "html-url-attributes": "^3.0.0",
3668- },3668+ "mdast-util-to-hast": "^13.0.0",
3669- "node_modules/rollup": {3669+ "remark-parse": "^11.0.0",
3670- "version": "4.63.3",3670+ "remark-rehype": "^11.0.0",
3671- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz",3671+ "unified": "^11.0.0",
3672- "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==",3672+ "unist-util-visit": "^5.0.0",
3673- "dev": true,3673+ "vfile": "^6.0.0"
3674- "license": "MIT",3674+ },
3675- "dependencies": {3675+ "funding": {
3676- "@types/estree": "1.0.9"3676+ "type": "opencollective",
3677- },3677+ "url": "https://opencollective.com/unified"
3678- "bin": {3678+ },
3679- "rollup": "dist/bin/rollup"3679+ "peerDependencies": {
3680- },3680+ "@types/react": ">=18",
3681- "engines": {3681+ "react": ">=18"
3682- "node": ">=18.0.0",3682+ }
3683- "npm": ">=8.0.0"3683+ },
3684- },3684+ "node_modules/react-refresh": {
3685- "optionalDependencies": {3685+ "version": "0.18.0",
3686- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",3686+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
3687- "@rollup/rollup-android-arm-eabi": "4.63.3",3687+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
3688- "@rollup/rollup-android-arm64": "4.63.3",3688+ "dev": true,
3689- "@rollup/rollup-darwin-arm64": "4.63.3",3689+ "license": "MIT",
3690- "@rollup/rollup-darwin-x64": "4.63.3",3690+ "engines": {
3691- "@rollup/rollup-freebsd-arm64": "4.63.3",3691+ "node": ">=0.10.0"
3692- "@rollup/rollup-freebsd-x64": "4.63.3",3692+ }
3693- "@rollup/rollup-linux-arm-gnueabihf": "4.63.3",3693+ },
3694- "@rollup/rollup-linux-arm-musleabihf": "4.63.3",3694+ "node_modules/remark-gfm": {
3695- "@rollup/rollup-linux-arm64-gnu": "4.63.3",3695+ "version": "4.0.1",
3696- "@rollup/rollup-linux-arm64-musl": "4.63.3",3696+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
3697- "@rollup/rollup-linux-loong64-gnu": "4.63.3",3697+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
3698- "@rollup/rollup-linux-loong64-musl": "4.63.3",3698+ "license": "MIT",
3699- "@rollup/rollup-linux-ppc64-gnu": "4.63.3",3699+ "dependencies": {
3700- "@rollup/rollup-linux-ppc64-musl": "4.63.3",3700+ "@types/mdast": "^4.0.0",
3701- "@rollup/rollup-linux-riscv64-gnu": "4.63.3",3701+ "mdast-util-gfm": "^3.0.0",
3702- "@rollup/rollup-linux-riscv64-musl": "4.63.3",3702+ "micromark-extension-gfm": "^3.0.0",
3703- "@rollup/rollup-linux-s390x-gnu": "4.63.3",3703+ "remark-parse": "^11.0.0",
3704- "@rollup/rollup-linux-x64-gnu": "4.63.3",3704+ "remark-stringify": "^11.0.0",
3705- "@rollup/rollup-linux-x64-musl": "4.63.3",3705+ "unified": "^11.0.0"
3706- "@rollup/rollup-openbsd-x64": "4.63.3",3706+ },
3707- "@rollup/rollup-openharmony-arm64": "4.63.3",3707+ "funding": {
3708- "@rollup/rollup-win32-arm64-msvc": "4.63.3",3708+ "type": "opencollective",
3709- "@rollup/rollup-win32-ia32-msvc": "4.63.3",3709+ "url": "https://opencollective.com/unified"
3710- "@rollup/rollup-win32-x64-gnu": "4.63.3",3710+ }
3711- "@rollup/rollup-win32-x64-msvc": "4.63.3",3711+ },
3712- "fsevents": "~2.3.2"3712+ "node_modules/remark-parse": {
3713- }3713+ "version": "11.0.0",
3714- },3714+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
3715- "node_modules/rrweb-cssom": {3715+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
3716- "version": "0.8.0",3716+ "license": "MIT",
3717- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",3717+ "dependencies": {
3718- "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",3718+ "@types/mdast": "^4.0.0",
3719- "dev": true,3719+ "mdast-util-from-markdown": "^2.0.0",
3720- "license": "MIT"3720+ "micromark-util-types": "^2.0.0",
3721- },3721+ "unified": "^11.0.0"
3722- "node_modules/safer-buffer": {3722+ },
3723- "version": "2.1.2",3723+ "funding": {
3724- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",3724+ "type": "opencollective",
3725- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",3725+ "url": "https://opencollective.com/unified"
3726- "dev": true,3726+ }
3727- "license": "MIT"3727+ },
3728- },3728+ "node_modules/remark-rehype": {
3729- "node_modules/saxes": {3729+ "version": "11.1.2",
3730- "version": "6.0.0",3730+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
3731- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",3731+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
3732- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",3732+ "license": "MIT",
3733- "dev": true,3733+ "dependencies": {
3734- "license": "ISC",3734+ "@types/hast": "^3.0.0",
3735- "dependencies": {3735+ "@types/mdast": "^4.0.0",
3736- "xmlchars": "^2.2.0"3736+ "mdast-util-to-hast": "^13.0.0",
3737- },3737+ "unified": "^11.0.0",
3738- "engines": {3738+ "vfile": "^6.0.0"
3739- "node": ">=v12.22.7"3739+ },
3740- }3740+ "funding": {
3741- },3741+ "type": "opencollective",
3742- "node_modules/scheduler": {3742+ "url": "https://opencollective.com/unified"
3743- "version": "0.28.0",3743+ }
3744- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",3744+ },
3745- "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",3745+ "node_modules/remark-stringify": {
3746- "license": "MIT"3746+ "version": "11.0.0",
3747- },3747+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
3748- "node_modules/semver": {3748+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
3749- "version": "6.3.1",3749+ "license": "MIT",
3750- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",3750+ "dependencies": {
3751- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",3751+ "@types/mdast": "^4.0.0",
3752- "dev": true,3752+ "mdast-util-to-markdown": "^2.0.0",
3753- "license": "ISC",3753+ "unified": "^11.0.0"
3754- "bin": {3754+ },
3755- "semver": "bin/semver.js"3755+ "funding": {
3756- }3756+ "type": "opencollective",
3757- },3757+ "url": "https://opencollective.com/unified"
3758- "node_modules/siginfo": {3758+ }
3759- "version": "2.0.0",3759+ },
3760- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",3760+ "node_modules/rollup": {
3761- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",3761+ "version": "4.63.3",
3762- "dev": true,3762+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz",
3763- "license": "ISC"3763+ "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==",
3764- },3764+ "dev": true,
3765- "node_modules/source-map-js": {3765+ "license": "MIT",
3766- "version": "1.2.1",3766+ "dependencies": {
3767- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",3767+ "@types/estree": "1.0.9"
3768- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",3768+ },
3769- "dev": true,3769+ "bin": {
3770- "license": "BSD-3-Clause",3770+ "rollup": "dist/bin/rollup"
3771- "engines": {3771+ },
3772- "node": ">=0.10.0"3772+ "engines": {
3773- }3773+ "node": ">=18.0.0",
3774- },3774+ "npm": ">=8.0.0"
3775- "node_modules/space-separated-tokens": {3775+ },
3776- "version": "2.0.2",3776+ "optionalDependencies": {
3777- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",3777+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
3778- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",3778+ "@rollup/rollup-android-arm-eabi": "4.63.3",
3779- "license": "MIT",3779+ "@rollup/rollup-android-arm64": "4.63.3",
3780- "funding": {3780+ "@rollup/rollup-darwin-arm64": "4.63.3",
3781- "type": "github",3781+ "@rollup/rollup-darwin-x64": "4.63.3",
3782- "url": "https://github.com/sponsors/wooorm"3782+ "@rollup/rollup-freebsd-arm64": "4.63.3",
3783- }3783+ "@rollup/rollup-freebsd-x64": "4.63.3",
3784- },3784+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.3",
3785- "node_modules/stackback": {3785+ "@rollup/rollup-linux-arm-musleabihf": "4.63.3",
3786- "version": "0.0.2",3786+ "@rollup/rollup-linux-arm64-gnu": "4.63.3",
3787- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",3787+ "@rollup/rollup-linux-arm64-musl": "4.63.3",
3788- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",3788+ "@rollup/rollup-linux-loong64-gnu": "4.63.3",
3789- "dev": true,3789+ "@rollup/rollup-linux-loong64-musl": "4.63.3",
3790- "license": "MIT"3790+ "@rollup/rollup-linux-ppc64-gnu": "4.63.3",
3791- },3791+ "@rollup/rollup-linux-ppc64-musl": "4.63.3",
3792- "node_modules/std-env": {3792+ "@rollup/rollup-linux-riscv64-gnu": "4.63.3",
3793- "version": "3.10.0",3793+ "@rollup/rollup-linux-riscv64-musl": "4.63.3",
3794- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",3794+ "@rollup/rollup-linux-s390x-gnu": "4.63.3",
3795- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",3795+ "@rollup/rollup-linux-x64-gnu": "4.63.3",
3796- "dev": true,3796+ "@rollup/rollup-linux-x64-musl": "4.63.3",
3797- "license": "MIT"3797+ "@rollup/rollup-openbsd-x64": "4.63.3",
3798- },3798+ "@rollup/rollup-openharmony-arm64": "4.63.3",
3799- "node_modules/stringify-entities": {3799+ "@rollup/rollup-win32-arm64-msvc": "4.63.3",
3800- "version": "4.0.4",3800+ "@rollup/rollup-win32-ia32-msvc": "4.63.3",
3801- "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",3801+ "@rollup/rollup-win32-x64-gnu": "4.63.3",
3802- "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",3802+ "@rollup/rollup-win32-x64-msvc": "4.63.3",
3803- "license": "MIT",3803+ "fsevents": "~2.3.2"
3804- "dependencies": {3804+ }
3805- "character-entities-html4": "^2.0.0",3805+ },
3806- "character-entities-legacy": "^3.0.0"3806+ "node_modules/rrweb-cssom": {
3807- },3807+ "version": "0.8.0",
3808- "funding": {3808+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
3809- "type": "github",3809+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
3810- "url": "https://github.com/sponsors/wooorm"3810+ "dev": true,
3811- }3811+ "license": "MIT"
3812- },3812+ },
3813- "node_modules/strip-literal": {3813+ "node_modules/safer-buffer": {
3814- "version": "3.1.0",3814+ "version": "2.1.2",
3815- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",3815+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
3816- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",3816+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
3817- "dev": true,3817+ "dev": true,
3818- "license": "MIT",3818+ "license": "MIT"
3819- "dependencies": {3819+ },
3820- "js-tokens": "^9.0.1"3820+ "node_modules/saxes": {
3821- },3821+ "version": "6.0.0",
3822- "funding": {3822+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
3823- "url": "https://github.com/sponsors/antfu"3823+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
3824- }3824+ "dev": true,
3825- },3825+ "license": "ISC",
3826- "node_modules/strip-literal/node_modules/js-tokens": {3826+ "dependencies": {
3827- "version": "9.0.1",3827+ "xmlchars": "^2.2.0"
3828- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",3828+ },
3829- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",3829+ "engines": {
3830- "dev": true,3830+ "node": ">=v12.22.7"
3831- "license": "MIT"3831+ }
3832- },3832+ },
3833- "node_modules/style-to-js": {3833+ "node_modules/scheduler": {
3834- "version": "1.1.21",3834+ "version": "0.28.0",
3835- "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",3835+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
3836- "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",3836+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
3837- "license": "MIT",3837+ "license": "MIT"
3838- "dependencies": {3838+ },
3839- "style-to-object": "1.0.14"3839+ "node_modules/semver": {
3840- }3840+ "version": "6.3.1",
3841- },3841+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
3842- "node_modules/style-to-object": {3842+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
3843- "version": "1.0.14",3843+ "dev": true,
3844- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",3844+ "license": "ISC",
3845- "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",3845+ "bin": {
3846- "license": "MIT",3846+ "semver": "bin/semver.js"
3847- "dependencies": {3847+ }
3848- "inline-style-parser": "0.2.7"3848+ },
3849- }3849+ "node_modules/siginfo": {
3850- },3850+ "version": "2.0.0",
3851- "node_modules/symbol-tree": {3851+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
3852- "version": "3.2.4",3852+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
3853- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",3853+ "dev": true,
3854- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",3854+ "license": "ISC"
3855- "dev": true,3855+ },
3856- "license": "MIT"3856+ "node_modules/source-map-js": {
3857- },3857+ "version": "1.2.1",
3858- "node_modules/tinybench": {3858+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
3859- "version": "2.9.0",3859+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
3860- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",3860+ "dev": true,
3861- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",3861+ "license": "BSD-3-Clause",
3862- "dev": true,3862+ "engines": {
3863- "license": "MIT"3863+ "node": ">=0.10.0"
3864- },3864+ }
3865- "node_modules/tinyexec": {3865+ },
3866- "version": "0.3.2",3866+ "node_modules/space-separated-tokens": {
3867- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",3867+ "version": "2.0.2",
3868- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",3868+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
3869- "dev": true,3869+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
3870- "license": "MIT"3870+ "license": "MIT",
3871- },3871+ "funding": {
3872- "node_modules/tinyglobby": {3872+ "type": "github",
3873- "version": "0.2.17",3873+ "url": "https://github.com/sponsors/wooorm"
3874- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",3874+ }
3875- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",3875+ },
3876- "dev": true,3876+ "node_modules/stackback": {
3877- "license": "MIT",3877+ "version": "0.0.2",
3878- "dependencies": {3878+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
3879- "fdir": "^6.5.0",3879+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
3880- "picomatch": "^4.0.4"3880+ "dev": true,
3881- },3881+ "license": "MIT"
3882- "engines": {3882+ },
3883- "node": ">=12.0.0"3883+ "node_modules/state-local": {
3884- },3884+ "version": "1.0.7",
3885- "funding": {3885+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
3886- "url": "https://github.com/sponsors/SuperchupuDev"3886+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
3887- }3887+ "license": "MIT"
3888- },3888+ },
3889- "node_modules/tinypool": {3889+ "node_modules/std-env": {
3890- "version": "1.1.1",3890+ "version": "3.10.0",
3891- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",3891+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
3892- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",3892+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
3893- "dev": true,3893+ "dev": true,
3894- "license": "MIT",3894+ "license": "MIT"
3895- "engines": {3895+ },
3896- "node": "^18.0.0 || >=20.0.0"3896+ "node_modules/stringify-entities": {
3897- }3897+ "version": "4.0.4",
3898- },3898+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
3899- "node_modules/tinyrainbow": {3899+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
3900- "version": "2.0.0",3900+ "license": "MIT",
3901- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",3901+ "dependencies": {
3902- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",3902+ "character-entities-html4": "^2.0.0",
3903- "dev": true,3903+ "character-entities-legacy": "^3.0.0"
3904- "license": "MIT",3904+ },
3905- "engines": {3905+ "funding": {
3906- "node": ">=14.0.0"3906+ "type": "github",
3907- }3907+ "url": "https://github.com/sponsors/wooorm"
3908- },3908+ }
3909- "node_modules/tinyspy": {3909+ },
3910- "version": "4.0.6",3910+ "node_modules/strip-literal": {
3911- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",3911+ "version": "3.1.0",
3912- "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",3912+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
3913- "dev": true,3913+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
3914- "license": "MIT",3914+ "dev": true,
3915- "engines": {3915+ "license": "MIT",
3916- "node": ">=14.0.0"3916+ "dependencies": {
3917- }3917+ "js-tokens": "^9.0.1"
3918- },3918+ },
3919- "node_modules/tldts": {3919+ "funding": {
3920- "version": "6.1.86",3920+ "url": "https://github.com/sponsors/antfu"
3921- "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",3921+ }
3922- "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",3922+ },
3923- "dev": true,3923+ "node_modules/strip-literal/node_modules/js-tokens": {
3924- "license": "MIT",3924+ "version": "9.0.1",
3925- "dependencies": {3925+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
3926- "tldts-core": "^6.1.86"3926+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
3927- },3927+ "dev": true,
3928- "bin": {3928+ "license": "MIT"
3929- "tldts": "bin/cli.js"3929+ },
3930- }3930+ "node_modules/style-to-js": {
3931- },3931+ "version": "1.1.21",
3932- "node_modules/tldts-core": {3932+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
3933- "version": "6.1.86",3933+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
3934- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",3934+ "license": "MIT",
3935- "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",3935+ "dependencies": {
3936- "dev": true,3936+ "style-to-object": "1.0.14"
3937- "license": "MIT"3937+ }
3938- },3938+ },
3939- "node_modules/tough-cookie": {3939+ "node_modules/style-to-object": {
3940- "version": "5.1.2",3940+ "version": "1.0.14",
3941- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",3941+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
3942- "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",3942+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
3943- "dev": true,3943+ "license": "MIT",
3944- "license": "BSD-3-Clause",3944+ "dependencies": {
3945- "dependencies": {3945+ "inline-style-parser": "0.2.7"
3946- "tldts": "^6.1.32"3946+ }
3947- },3947+ },
3948- "engines": {3948+ "node_modules/symbol-tree": {
3949- "node": ">=16"3949+ "version": "3.2.4",
3950- }3950+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
3951- },3951+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
3952- "node_modules/tr46": {3952+ "dev": true,
3953- "version": "5.1.1",3953+ "license": "MIT"
3954- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",3954+ },
3955- "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",3955+ "node_modules/tinybench": {
3956- "dev": true,3956+ "version": "2.9.0",
3957- "license": "MIT",3957+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
3958- "dependencies": {3958+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
3959- "punycode": "^2.3.1"3959+ "dev": true,
3960- },3960+ "license": "MIT"
3961- "engines": {3961+ },
3962- "node": ">=18"3962+ "node_modules/tinyexec": {
3963- }3963+ "version": "0.3.2",
3964- },3964+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
3965- "node_modules/trim-lines": {3965+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
3966- "version": "3.0.1",3966+ "dev": true,
3967- "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",3967+ "license": "MIT"
3968- "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",3968+ },
3969- "license": "MIT",3969+ "node_modules/tinyglobby": {
3970- "funding": {3970+ "version": "0.2.17",
3971- "type": "github",3971+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
3972- "url": "https://github.com/sponsors/wooorm"3972+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
3973- }3973+ "dev": true,
3974- },3974+ "license": "MIT",
3975- "node_modules/trough": {3975+ "dependencies": {
3976- "version": "2.2.0",3976+ "fdir": "^6.5.0",
3977- "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",3977+ "picomatch": "^4.0.4"
3978- "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",3978+ },
3979- "license": "MIT",3979+ "engines": {
3980- "funding": {3980+ "node": ">=12.0.0"
3981- "type": "github",3981+ },
3982- "url": "https://github.com/sponsors/wooorm"3982+ "funding": {
3983- }3983+ "url": "https://github.com/sponsors/SuperchupuDev"
3984- },3984+ }
3985- "node_modules/typescript": {3985+ },
3986- "version": "5.9.3",3986+ "node_modules/tinypool": {
3987- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",3987+ "version": "1.1.1",
3988- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",3988+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
3989- "dev": true,3989+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
3990- "license": "Apache-2.0",3990+ "dev": true,
3991- "bin": {3991+ "license": "MIT",
3992- "tsc": "bin/tsc",3992+ "engines": {
3993- "tsserver": "bin/tsserver"3993+ "node": "^18.0.0 || >=20.0.0"
3994- },3994+ }
3995- "engines": {3995+ },
3996- "node": ">=14.17"3996+ "node_modules/tinyrainbow": {
3997- }3997+ "version": "2.0.0",
3998- },3998+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
3999- "node_modules/unified": {3999+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
4000- "version": "11.0.5",4000+ "dev": true,
4001- "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",4001+ "license": "MIT",
4002- "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",4002+ "engines": {
4003- "license": "MIT",4003+ "node": ">=14.0.0"
4004- "dependencies": {4004+ }
4005- "@types/unist": "^3.0.0",4005+ },
4006- "bail": "^2.0.0",4006+ "node_modules/tinyspy": {
4007- "devlop": "^1.0.0",4007+ "version": "4.0.6",
4008- "extend": "^3.0.0",4008+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
4009- "is-plain-obj": "^4.0.0",4009+ "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
4010- "trough": "^2.0.0",4010+ "dev": true,
4011- "vfile": "^6.0.0"4011+ "license": "MIT",
4012- },4012+ "engines": {
4013- "funding": {4013+ "node": ">=14.0.0"
4014- "type": "opencollective",4014+ }
4015- "url": "https://opencollective.com/unified"4015+ },
4016- }4016+ "node_modules/tldts": {
4017- },4017+ "version": "6.1.86",
4018- "node_modules/unist-util-is": {4018+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
4019- "version": "6.0.1",4019+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
4020- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",4020+ "dev": true,
4021- "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",4021+ "license": "MIT",
4022- "license": "MIT",4022+ "dependencies": {
4023- "dependencies": {4023+ "tldts-core": "^6.1.86"
4024- "@types/unist": "^3.0.0"4024+ },
4025- },4025+ "bin": {
4026- "funding": {4026+ "tldts": "bin/cli.js"
4027- "type": "opencollective",4027+ }
4028- "url": "https://opencollective.com/unified"4028+ },
4029- }4029+ "node_modules/tldts-core": {
4030- },4030+ "version": "6.1.86",
4031- "node_modules/unist-util-position": {4031+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
4032- "version": "5.0.0",4032+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
4033- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",4033+ "dev": true,
4034- "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",4034+ "license": "MIT"
4035- "license": "MIT",4035+ },
4036- "dependencies": {4036+ "node_modules/tough-cookie": {
4037- "@types/unist": "^3.0.0"4037+ "version": "5.1.2",
4038- },4038+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
4039- "funding": {4039+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
4040- "type": "opencollective",4040+ "dev": true,
4041- "url": "https://opencollective.com/unified"4041+ "license": "BSD-3-Clause",
4042- }4042+ "dependencies": {
4043- },4043+ "tldts": "^6.1.32"
4044- "node_modules/unist-util-stringify-position": {4044+ },
4045- "version": "4.0.0",4045+ "engines": {
4046- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",4046+ "node": ">=16"
4047- "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",4047+ }
4048- "license": "MIT",4048+ },
4049- "dependencies": {4049+ "node_modules/tr46": {
4050- "@types/unist": "^3.0.0"4050+ "version": "5.1.1",
4051- },4051+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
4052- "funding": {4052+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
4053- "type": "opencollective",4053+ "dev": true,
4054- "url": "https://opencollective.com/unified"4054+ "license": "MIT",
4055- }4055+ "dependencies": {
4056- },4056+ "punycode": "^2.3.1"
4057- "node_modules/unist-util-visit": {4057+ },
4058- "version": "5.1.0",4058+ "engines": {
4059- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",4059+ "node": ">=18"
4060- "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",4060+ }
4061- "license": "MIT",4061+ },
4062- "dependencies": {4062+ "node_modules/trim-lines": {
4063- "@types/unist": "^3.0.0",4063+ "version": "3.0.1",
4064- "unist-util-is": "^6.0.0",4064+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
4065- "unist-util-visit-parents": "^6.0.0"4065+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
4066- },4066+ "license": "MIT",
4067- "funding": {4067+ "funding": {
4068- "type": "opencollective",4068+ "type": "github",
4069- "url": "https://opencollective.com/unified"4069+ "url": "https://github.com/sponsors/wooorm"
4070- }4070+ }
4071- },4071+ },
4072- "node_modules/unist-util-visit-parents": {4072+ "node_modules/trough": {
4073- "version": "6.0.2",4073+ "version": "2.2.0",
4074- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",4074+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
4075- "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",4075+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
4076- "license": "MIT",4076+ "license": "MIT",
4077- "dependencies": {4077+ "funding": {
4078- "@types/unist": "^3.0.0",4078+ "type": "github",
4079- "unist-util-is": "^6.0.0"4079+ "url": "https://github.com/sponsors/wooorm"
4080- },4080+ }
4081- "funding": {4081+ },
4082- "type": "opencollective",4082+ "node_modules/typescript": {
4083- "url": "https://opencollective.com/unified"4083+ "version": "5.9.3",
4084- }4084+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
4085- },4085+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
4086- "node_modules/update-browserslist-db": {4086+ "dev": true,
4087- "version": "1.3.3",4087+ "license": "Apache-2.0",
4088- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",4088+ "bin": {
4089- "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",4089+ "tsc": "bin/tsc",
4090- "dev": true,4090+ "tsserver": "bin/tsserver"
4091- "funding": [4091+ },
4092- {4092+ "engines": {
4093- "type": "opencollective",4093+ "node": ">=14.17"
4094- "url": "https://opencollective.com/browserslist"4094+ }
4095- },4095+ },
4096- {4096+ "node_modules/unified": {
4097- "type": "tidelift",4097+ "version": "11.0.5",
4098- "url": "https://tidelift.com/funding/github/npm/browserslist"4098+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
4099- },4099+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
4100- {4100+ "license": "MIT",
4101- "type": "github",4101+ "dependencies": {
4102- "url": "https://github.com/sponsors/ai"4102+ "@types/unist": "^3.0.0",
4103- }4103+ "bail": "^2.0.0",
4104- ],4104+ "devlop": "^1.0.0",
4105- "license": "MIT",4105+ "extend": "^3.0.0",
4106- "dependencies": {4106+ "is-plain-obj": "^4.0.0",
4107- "escalade": "^3.2.0",4107+ "trough": "^2.0.0",
4108- "picocolors": "^1.1.1"4108+ "vfile": "^6.0.0"
4109- },4109+ },
4110- "bin": {4110+ "funding": {
4111- "update-browserslist-db": "cli.js"4111+ "type": "opencollective",
4112- },4112+ "url": "https://opencollective.com/unified"
4113- "peerDependencies": {4113+ }
4114- "browserslist": ">= 4.21.0"4114+ },
4115- }4115+ "node_modules/unist-util-is": {
4116- },4116+ "version": "6.0.1",
4117- "node_modules/vfile": {4117+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
4118- "version": "6.0.3",4118+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
4119- "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",4119+ "license": "MIT",
4120- "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",4120+ "dependencies": {
4121- "license": "MIT",4121+ "@types/unist": "^3.0.0"
4122- "dependencies": {4122+ },
4123- "@types/unist": "^3.0.0",4123+ "funding": {
4124- "vfile-message": "^4.0.0"4124+ "type": "opencollective",
4125- },4125+ "url": "https://opencollective.com/unified"
4126- "funding": {4126+ }
4127- "type": "opencollective",4127+ },
4128- "url": "https://opencollective.com/unified"4128+ "node_modules/unist-util-position": {
4129- }4129+ "version": "5.0.0",
4130- },4130+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
4131- "node_modules/vfile-message": {4131+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
4132- "version": "4.0.3",4132+ "license": "MIT",
4133- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",4133+ "dependencies": {
4134- "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",4134+ "@types/unist": "^3.0.0"
4135- "license": "MIT",4135+ },
4136- "dependencies": {4136+ "funding": {
4137- "@types/unist": "^3.0.0",4137+ "type": "opencollective",
4138- "unist-util-stringify-position": "^4.0.0"4138+ "url": "https://opencollective.com/unified"
4139- },4139+ }
4140- "funding": {4140+ },
4141- "type": "opencollective",4141+ "node_modules/unist-util-stringify-position": {
4142- "url": "https://opencollective.com/unified"4142+ "version": "4.0.0",
4143- }4143+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
4144- },4144+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
4145- "node_modules/vite": {4145+ "license": "MIT",
4146- "version": "7.3.6",4146+ "dependencies": {
4147- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",4147+ "@types/unist": "^3.0.0"
4148- "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",4148+ },
4149- "dev": true,4149+ "funding": {
4150- "license": "MIT",4150+ "type": "opencollective",
4151- "dependencies": {4151+ "url": "https://opencollective.com/unified"
4152- "esbuild": "^0.27.0 || ^0.28.0",4152+ }
4153- "fdir": "^6.5.0",4153+ },
4154- "picomatch": "^4.0.3",4154+ "node_modules/unist-util-visit": {
4155- "postcss": "^8.5.6",4155+ "version": "5.1.0",
4156- "rollup": "^4.43.0",4156+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
4157- "tinyglobby": "^0.2.15"4157+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
4158- },4158+ "license": "MIT",
4159- "bin": {4159+ "dependencies": {
4160- "vite": "bin/vite.js"4160+ "@types/unist": "^3.0.0",
4161- },4161+ "unist-util-is": "^6.0.0",
4162- "engines": {4162+ "unist-util-visit-parents": "^6.0.0"
4163- "node": "^20.19.0 || >=22.12.0"4163+ },
4164- },4164+ "funding": {
4165- "funding": {4165+ "type": "opencollective",
4166- "url": "https://github.com/vitejs/vite?sponsor=1"4166+ "url": "https://opencollective.com/unified"
4167- },4167+ }
4168- "optionalDependencies": {4168+ },
4169- "fsevents": "~2.3.3"4169+ "node_modules/unist-util-visit-parents": {
4170- },4170+ "version": "6.0.2",
4171- "peerDependencies": {4171+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
4172- "@types/node": "^20.19.0 || >=22.12.0",4172+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
4173- "jiti": ">=1.21.0",4173+ "license": "MIT",
4174- "less": "^4.0.0",4174+ "dependencies": {
4175- "lightningcss": "^1.21.0",4175+ "@types/unist": "^3.0.0",
4176- "sass": "^1.70.0",4176+ "unist-util-is": "^6.0.0"
4177- "sass-embedded": "^1.70.0",4177+ },
4178- "stylus": ">=0.54.8",4178+ "funding": {
4179- "sugarss": "^5.0.0",4179+ "type": "opencollective",
4180- "terser": "^5.16.0",4180+ "url": "https://opencollective.com/unified"
4181- "tsx": "^4.8.1",4181+ }
4182- "yaml": "^2.4.2"4182+ },
4183- },4183+ "node_modules/update-browserslist-db": {
4184- "peerDependenciesMeta": {4184+ "version": "1.3.3",
4185- "@types/node": {4185+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
4186- "optional": true4186+ "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
4187- },4187+ "dev": true,
4188- "jiti": {4188+ "funding": [
4189- "optional": true4189+ {
4190- },4190+ "type": "opencollective",
4191- "less": {4191+ "url": "https://opencollective.com/browserslist"
4192- "optional": true4192+ },
4193- },4193+ {
4194- "lightningcss": {4194+ "type": "tidelift",
4195- "optional": true4195+ "url": "https://tidelift.com/funding/github/npm/browserslist"
4196- },4196+ },
4197- "sass": {4197+ {
4198- "optional": true4198+ "type": "github",
4199- },4199+ "url": "https://github.com/sponsors/ai"
4200- "sass-embedded": {4200+ }
4201- "optional": true4201+ ],
4202- },4202+ "license": "MIT",
4203- "stylus": {4203+ "dependencies": {
4204- "optional": true4204+ "escalade": "^3.2.0",
4205- },4205+ "picocolors": "^1.1.1"
4206- "sugarss": {4206+ },
4207- "optional": true4207+ "bin": {
4208- },4208+ "update-browserslist-db": "cli.js"
4209- "terser": {4209+ },
4210- "optional": true4210+ "peerDependencies": {
4211- },4211+ "browserslist": ">= 4.21.0"
4212- "tsx": {4212+ }
4213- "optional": true4213+ },
4214- },4214+ "node_modules/vfile": {
4215- "yaml": {4215+ "version": "6.0.3",
4216- "optional": true4216+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
4217- }4217+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
4218- }4218+ "license": "MIT",
4219- },4219+ "dependencies": {
4220- "node_modules/vite-node": {4220+ "@types/unist": "^3.0.0",
4221- "version": "3.2.4",4221+ "vfile-message": "^4.0.0"
4222- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",4222+ },
4223- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",4223+ "funding": {
4224- "dev": true,4224+ "type": "opencollective",
4225- "license": "MIT",4225+ "url": "https://opencollective.com/unified"
4226- "dependencies": {4226+ }
4227- "cac": "^6.7.14",4227+ },
4228- "debug": "^4.4.1",4228+ "node_modules/vfile-message": {
4229- "es-module-lexer": "^1.7.0",4229+ "version": "4.0.3",
4230- "pathe": "^2.0.3",4230+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
4231- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"4231+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
4232- },4232+ "license": "MIT",
4233- "bin": {4233+ "dependencies": {
4234- "vite-node": "vite-node.mjs"4234+ "@types/unist": "^3.0.0",
4235- },4235+ "unist-util-stringify-position": "^4.0.0"
4236- "engines": {4236+ },
4237- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"4237+ "funding": {
4238- },4238+ "type": "opencollective",
4239- "funding": {4239+ "url": "https://opencollective.com/unified"
4240- "url": "https://opencollective.com/vitest"4240+ }
4241- }4241+ },
4242- },4242+ "node_modules/vite": {
4243- "node_modules/vitest": {4243+ "version": "7.3.6",
4244- "version": "3.2.7",4244+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
4245- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",4245+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
4246- "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",4246+ "dev": true,
4247- "dev": true,4247+ "license": "MIT",
4248- "license": "MIT",4248+ "dependencies": {
4249- "dependencies": {4249+ "esbuild": "^0.27.0 || ^0.28.0",
4250- "@types/chai": "^5.2.2",4250+ "fdir": "^6.5.0",
4251- "@vitest/expect": "3.2.7",4251+ "picomatch": "^4.0.3",
4252- "@vitest/mocker": "3.2.7",4252+ "postcss": "^8.5.6",
4253- "@vitest/pretty-format": "^3.2.7",4253+ "rollup": "^4.43.0",
4254- "@vitest/runner": "3.2.7",4254+ "tinyglobby": "^0.2.15"
4255- "@vitest/snapshot": "3.2.7",4255+ },
4256- "@vitest/spy": "3.2.7",4256+ "bin": {
4257- "@vitest/utils": "3.2.7",4257+ "vite": "bin/vite.js"
4258- "chai": "^5.2.0",4258+ },
4259- "debug": "^4.4.1",4259+ "engines": {
4260- "expect-type": "^1.2.1",4260+ "node": "^20.19.0 || >=22.12.0"
4261- "magic-string": "^0.30.17",4261+ },
4262- "pathe": "^2.0.3",4262+ "funding": {
4263- "picomatch": "^4.0.2",4263+ "url": "https://github.com/vitejs/vite?sponsor=1"
4264- "std-env": "^3.9.0",4264+ },
4265- "tinybench": "^2.9.0",4265+ "optionalDependencies": {
4266- "tinyexec": "^0.3.2",4266+ "fsevents": "~2.3.3"
4267- "tinyglobby": "^0.2.14",4267+ },
4268- "tinypool": "^1.1.1",4268+ "peerDependencies": {
4269- "tinyrainbow": "^2.0.0",4269+ "@types/node": "^20.19.0 || >=22.12.0",
4270- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",4270+ "jiti": ">=1.21.0",
4271- "vite-node": "3.2.4",4271+ "less": "^4.0.0",
4272- "why-is-node-running": "^2.3.0"4272+ "lightningcss": "^1.21.0",
4273- },4273+ "sass": "^1.70.0",
4274- "bin": {4274+ "sass-embedded": "^1.70.0",
4275- "vitest": "vitest.mjs"4275+ "stylus": ">=0.54.8",
4276- },4276+ "sugarss": "^5.0.0",
4277- "engines": {4277+ "terser": "^5.16.0",
4278- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"4278+ "tsx": "^4.8.1",
4279- },4279+ "yaml": "^2.4.2"
4280- "funding": {4280+ },
4281- "url": "https://opencollective.com/vitest"4281+ "peerDependenciesMeta": {
4282- },4282+ "@types/node": {
4283- "peerDependencies": {4283+ "optional": true
4284- "@edge-runtime/vm": "*",4284+ },
4285- "@types/debug": "^4.1.12",4285+ "jiti": {
4286- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",4286+ "optional": true
4287- "@vitest/browser": "3.2.7",4287+ },
4288- "@vitest/ui": "3.2.7",4288+ "less": {
4289- "happy-dom": "*",4289+ "optional": true
4290- "jsdom": "*"4290+ },
4291- },4291+ "lightningcss": {
4292- "peerDependenciesMeta": {4292+ "optional": true
4293- "@edge-runtime/vm": {4293+ },
4294- "optional": true4294+ "sass": {
4295- },4295+ "optional": true
4296- "@types/debug": {4296+ },
4297- "optional": true4297+ "sass-embedded": {
4298- },4298+ "optional": true
4299- "@types/node": {4299+ },
4300- "optional": true4300+ "stylus": {
4301- },4301+ "optional": true
4302- "@vitest/browser": {4302+ },
4303- "optional": true4303+ "sugarss": {
4304- },4304+ "optional": true
4305- "@vitest/ui": {4305+ },
4306- "optional": true4306+ "terser": {
4307- },4307+ "optional": true
4308- "happy-dom": {4308+ },
4309- "optional": true4309+ "tsx": {
4310- },4310+ "optional": true
4311- "jsdom": {4311+ },
4312- "optional": true4312+ "yaml": {
4313- }4313+ "optional": true
4314- }4314+ }
4315- },4315+ }
4316- "node_modules/w3c-xmlserializer": {4316+ },
4317- "version": "5.0.0",4317+ "node_modules/vite-node": {
4318- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",4318+ "version": "3.2.4",
4319- "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",4319+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
4320- "dev": true,4320+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
4321- "license": "MIT",4321+ "dev": true,
4322- "dependencies": {4322+ "license": "MIT",
4323- "xml-name-validator": "^5.0.0"4323+ "dependencies": {
4324- },4324+ "cac": "^6.7.14",
4325- "engines": {4325+ "debug": "^4.4.1",
4326- "node": ">=18"4326+ "es-module-lexer": "^1.7.0",
4327- }4327+ "pathe": "^2.0.3",
4328- },4328+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
4329- "node_modules/webidl-conversions": {4329+ },
4330- "version": "7.0.0",4330+ "bin": {
4331- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",4331+ "vite-node": "vite-node.mjs"
4332- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",4332+ },
4333- "dev": true,4333+ "engines": {
4334- "license": "BSD-2-Clause",4334+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4335- "engines": {4335+ },
4336- "node": ">=12"4336+ "funding": {
4337- }4337+ "url": "https://opencollective.com/vitest"
4338- },4338+ }
4339- "node_modules/whatwg-encoding": {4339+ },
4340- "version": "3.1.1",4340+ "node_modules/vitest": {
4341- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",4341+ "version": "3.2.7",
4342- "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",4342+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
4343- "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",4343+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
4344- "dev": true,4344+ "dev": true,
4345- "license": "MIT",4345+ "license": "MIT",
4346- "dependencies": {4346+ "dependencies": {
4347- "iconv-lite": "0.6.3"4347+ "@types/chai": "^5.2.2",
4348- },4348+ "@vitest/expect": "3.2.7",
4349- "engines": {4349+ "@vitest/mocker": "3.2.7",
4350- "node": ">=18"4350+ "@vitest/pretty-format": "^3.2.7",
4351- }4351+ "@vitest/runner": "3.2.7",
4352- },4352+ "@vitest/snapshot": "3.2.7",
4353- "node_modules/whatwg-mimetype": {4353+ "@vitest/spy": "3.2.7",
4354- "version": "4.0.0",4354+ "@vitest/utils": "3.2.7",
4355- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",4355+ "chai": "^5.2.0",
4356- "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",4356+ "debug": "^4.4.1",
4357- "dev": true,4357+ "expect-type": "^1.2.1",
4358- "license": "MIT",4358+ "magic-string": "^0.30.17",
4359- "engines": {4359+ "pathe": "^2.0.3",
4360- "node": ">=18"4360+ "picomatch": "^4.0.2",
4361- }4361+ "std-env": "^3.9.0",
4362- },4362+ "tinybench": "^2.9.0",
4363- "node_modules/whatwg-url": {4363+ "tinyexec": "^0.3.2",
4364- "version": "14.2.0",4364+ "tinyglobby": "^0.2.14",
4365- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",4365+ "tinypool": "^1.1.1",
4366- "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",4366+ "tinyrainbow": "^2.0.0",
4367- "dev": true,4367+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
4368- "license": "MIT",4368+ "vite-node": "3.2.4",
4369- "dependencies": {4369+ "why-is-node-running": "^2.3.0"
4370- "tr46": "^5.1.0",4370+ },
4371- "webidl-conversions": "^7.0.0"4371+ "bin": {
4372- },4372+ "vitest": "vitest.mjs"
4373- "engines": {4373+ },
4374- "node": ">=18"4374+ "engines": {
4375- }4375+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
4376- },4376+ },
4377- "node_modules/why-is-node-running": {4377+ "funding": {
4378- "version": "2.3.0",4378+ "url": "https://opencollective.com/vitest"
4379- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",4379+ },
4380- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",4380+ "peerDependencies": {
4381- "dev": true,4381+ "@edge-runtime/vm": "*",
4382- "license": "MIT",4382+ "@types/debug": "^4.1.12",
4383- "dependencies": {4383+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
4384- "siginfo": "^2.0.0",4384+ "@vitest/browser": "3.2.7",
4385- "stackback": "0.0.2"4385+ "@vitest/ui": "3.2.7",
4386- },4386+ "happy-dom": "*",
4387- "bin": {4387+ "jsdom": "*"
4388- "why-is-node-running": "cli.js"4388+ },
4389- },4389+ "peerDependenciesMeta": {
4390- "engines": {4390+ "@edge-runtime/vm": {
4391- "node": ">=8"4391+ "optional": true
4392- }4392+ },
4393- },4393+ "@types/debug": {
4394- "node_modules/ws": {4394+ "optional": true
4395- "version": "8.21.3",4395+ },
4396- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",4396+ "@types/node": {
4397- "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",4397+ "optional": true
4398- "dev": true,4398+ },
4399- "license": "MIT",4399+ "@vitest/browser": {
4400- "engines": {4400+ "optional": true
4401- "node": ">=10.0.0"4401+ },
4402- },4402+ "@vitest/ui": {
4403- "peerDependencies": {4403+ "optional": true
4404- "bufferutil": "^4.0.1",4404+ },
4405- "utf-8-validate": ">=5.0.2"4405+ "happy-dom": {
4406- },4406+ "optional": true
4407- "peerDependenciesMeta": {4407+ },
4408- "bufferutil": {4408+ "jsdom": {
4409- "optional": true4409+ "optional": true
4410- },4410+ }
4411- "utf-8-validate": {4411+ }
4412- "optional": true4412+ },
4413- }4413+ "node_modules/vscode-material-icons": {
4414- }4414+ "version": "0.1.1",
4415- },4415+ "resolved": "https://registry.npmjs.org/vscode-material-icons/-/vscode-material-icons-0.1.1.tgz",
4416- "node_modules/xml-name-validator": {4416+ "integrity": "sha512-GsoEEF8Tbb0yUFQ6N6FPvh11kFkL9F95x0FkKlbbfRQN9eFms67h+L3t6b9cUv58dSn2gu8kEhNfoESVCrz4ag==",
4417- "version": "5.0.0",4417+ "license": "MIT"
4418- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",4418+ },
4419- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",4419+ "node_modules/w3c-xmlserializer": {
4420- "dev": true,4420+ "version": "5.0.0",
4421- "license": "Apache-2.0",4421+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
4422- "engines": {4422+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
4423- "node": ">=18"4423+ "dev": true,
4424- }4424+ "license": "MIT",
4425- },4425+ "dependencies": {
4426- "node_modules/xmlchars": {4426+ "xml-name-validator": "^5.0.0"
4427- "version": "2.2.0",4427+ },
4428- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",4428+ "engines": {
4429- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",4429+ "node": ">=18"
4430- "dev": true,4430+ }
4431- "license": "MIT"4431+ },
4432- },4432+ "node_modules/webidl-conversions": {
4433- "node_modules/yallist": {4433+ "version": "7.0.0",
4434- "version": "3.1.1",4434+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
4435- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",4435+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
4436- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",4436+ "dev": true,
4437- "dev": true,4437+ "license": "BSD-2-Clause",
4438- "license": "ISC"4438+ "engines": {
4439- },4439+ "node": ">=12"
4440- "node_modules/zustand": {4440+ }
4441- "version": "5.0.15",4441+ },
4442- "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",4442+ "node_modules/whatwg-encoding": {
4443- "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",4443+ "version": "3.1.1",
4444- "license": "MIT",4444+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
4445- "engines": {4445+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
4446- "node": ">=12.20.0"4446+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
4447- },4447+ "dev": true,
4448- "peerDependencies": {4448+ "license": "MIT",
4449- "@types/react": ">=18.0.0",4449+ "dependencies": {
4450- "immer": ">=9.0.6",4450+ "iconv-lite": "0.6.3"
4451- "react": ">=18.0.0",4451+ },
4452- "use-sync-external-store": ">=1.2.0"4452+ "engines": {
4453- },4453+ "node": ">=18"
4454- "peerDependenciesMeta": {4454+ }
4455- "@types/react": {4455+ },
4456- "optional": true4456+ "node_modules/whatwg-mimetype": {
4457- },4457+ "version": "4.0.0",
4458- "immer": {4458+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
4459- "optional": true4459+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
4460- },4460+ "dev": true,
4461- "react": {4461+ "license": "MIT",
4462- "optional": true4462+ "engines": {
4463- },4463+ "node": ">=18"
4464- "use-sync-external-store": {4464+ }
4465- "optional": true4465+ },
4466- }4466+ "node_modules/whatwg-url": {
4467- }4467+ "version": "14.2.0",
4468- },4468+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
4469- "node_modules/zwitch": {4469+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
4470- "version": "2.0.4",4470+ "dev": true,
4471- "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",4471+ "license": "MIT",
4472- "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",4472+ "dependencies": {
4473- "license": "MIT",4473+ "tr46": "^5.1.0",
4474- "funding": {4474+ "webidl-conversions": "^7.0.0"
4475- "type": "github",4475+ },
4476- "url": "https://github.com/sponsors/wooorm"4476+ "engines": {
4477- }4477+ "node": ">=18"
4478- }4478+ }
4479- }4479+ },
4480+ "node_modules/why-is-node-running": {
4481+ "version": "2.3.0",
4482+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
4483+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
4484+ "dev": true,
4485+ "license": "MIT",
4486+ "dependencies": {
4487+ "siginfo": "^2.0.0",
4488+ "stackback": "0.0.2"
4489+ },
4490+ "bin": {
4491+ "why-is-node-running": "cli.js"
4492+ },
4493+ "engines": {
4494+ "node": ">=8"
4495+ }
4496+ },
4497+ "node_modules/ws": {
4498+ "version": "8.21.3",
4499+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
4500+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
4501+ "dev": true,
4502+ "license": "MIT",
4503+ "engines": {
4504+ "node": ">=10.0.0"
4505+ },
4506+ "peerDependencies": {
4507+ "bufferutil": "^4.0.1",
4508+ "utf-8-validate": ">=5.0.2"
4509+ },
4510+ "peerDependenciesMeta": {
4511+ "bufferutil": {
4512+ "optional": true
4513+ },
4514+ "utf-8-validate": {
4515+ "optional": true
4516+ }
4517+ }
4518+ },
4519+ "node_modules/xml-name-validator": {
4520+ "version": "5.0.0",
4521+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
4522+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
4523+ "dev": true,
4524+ "license": "Apache-2.0",
4525+ "engines": {
4526+ "node": ">=18"
4527+ }
4528+ },
4529+ "node_modules/xmlchars": {
4530+ "version": "2.2.0",
4531+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
4532+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
4533+ "dev": true,
4534+ "license": "MIT"
4535+ },
4536+ "node_modules/yallist": {
4537+ "version": "3.1.1",
4538+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
4539+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
4540+ "dev": true,
4541+ "license": "ISC"
4542+ },
4543+ "node_modules/zustand": {
4544+ "version": "5.0.15",
4545+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
4546+ "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
4547+ "license": "MIT",
4548+ "engines": {
4549+ "node": ">=12.20.0"
4550+ },
4551+ "peerDependencies": {
4552+ "@types/react": ">=18.0.0",
4553+ "immer": ">=9.0.6",
4554+ "react": ">=18.0.0",
4555+ "use-sync-external-store": ">=1.2.0"
4556+ },
4557+ "peerDependenciesMeta": {
4558+ "@types/react": {
4559+ "optional": true
4560+ },
4561+ "immer": {
4562+ "optional": true
4563+ },
4564+ "react": {
4565+ "optional": true
4566+ },
4567+ "use-sync-external-store": {
4568+ "optional": true
4569+ }
4570+ }
4571+ },
4572+ "node_modules/zwitch": {
4573+ "version": "2.0.4",
4574+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
4575+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
4576+ "license": "MIT",
4577+ "funding": {
4578+ "type": "github",
4579+ "url": "https://github.com/sponsors/wooorm"
4580+ }
4581+ }
4582+ }
4480 }4583 }
modified ui/package.json +9 -1
@@ -7,13 +7,21 @@
77 "dev": "vite",
88 "build": "tsc -b && vite build",
99 "typecheck": "tsc -b",
10- "test": "vitest run"
10+ "test": "vitest run",
11+ "predev": "node scripts/copy-icons.mjs",
12+ "prebuild": "node scripts/copy-icons.mjs"
1113 },
1214 "dependencies": {
15+ "@asciidoctor/core": "^4.0.11",
16+ "@monaco-editor/react": "^4.7.0",
17+ "@xterm/addon-fit": "^0.11.0",
18+ "@xterm/xterm": "^6.0.0",
19+ "monaco-editor": "^0.56.0",
1320 "react": "^19.0.0",
1421 "react-dom": "^19.0.0",
1522 "react-markdown": "^10.1.0",
1623 "remark-gfm": "^4.0.1",
24+ "vscode-material-icons": "^0.1.1",
1725 "zustand": "^5.0.15"
1826 },
1927 "devDependencies": {
@@ -7,13 +7,21 @@
7 "dev": "vite",7 "dev": "vite",
8 "build": "tsc -b && vite build",8 "build": "tsc -b && vite build",
9 "typecheck": "tsc -b",9 "typecheck": "tsc -b",
10- "test": "vitest run"10+ "test": "vitest run",
11+ "predev": "node scripts/copy-icons.mjs",
12+ "prebuild": "node scripts/copy-icons.mjs"
11 },13 },
12 "dependencies": {14 "dependencies": {
15+ "@asciidoctor/core": "^4.0.11",
16+ "@monaco-editor/react": "^4.7.0",
17+ "@xterm/addon-fit": "^0.11.0",
18+ "@xterm/xterm": "^6.0.0",
19+ "monaco-editor": "^0.56.0",
13 "react": "^19.0.0",20 "react": "^19.0.0",
14 "react-dom": "^19.0.0",21 "react-dom": "^19.0.0",
15 "react-markdown": "^10.1.0",22 "react-markdown": "^10.1.0",
16 "remark-gfm": "^4.0.1",23 "remark-gfm": "^4.0.1",
24+ "vscode-material-icons": "^0.1.1",
17 "zustand": "^5.0.15"25 "zustand": "^5.0.15"
18 },26 },
19 "devDependencies": {27 "devDependencies": {
added ui/scripts/copy-icons.mjs +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copies the Material icon SVGs from vscode-material-icons into public/, so
2+// Vite ships them as static assets. Runs automatically before `dev` and
3+// `build` (npm pre-hooks); the copy is skipped when already present.
4+import { cpSync, existsSync } from "node:fs";
5+import { fileURLToPath } from "node:url";
6+
7+const source = fileURLToPath(
8+ new URL(
9+ "../node_modules/vscode-material-icons/generated/icons",
10+ import.meta.url,
11+ ),
12+);
13+const destination = fileURLToPath(
14+ new URL("../public/material-icons", import.meta.url),
15+);
16+
17+if (!existsSync(destination)) {
18+ cpSync(source, destination, { recursive: true });
19+ console.log("material icons copied to public/material-icons");
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copies the Material icon SVGs from vscode-material-icons into public/, so
2+// Vite ships them as static assets. Runs automatically before `dev` and
3+// `build` (npm pre-hooks); the copy is skipped when already present.
4+import { cpSync, existsSync } from "node:fs";
5+import { fileURLToPath } from "node:url";
6+
7+const source = fileURLToPath(
8+ new URL(
9+ "../node_modules/vscode-material-icons/generated/icons",
10+ import.meta.url,
11+ ),
12+);
13+const destination = fileURLToPath(
14+ new URL("../public/material-icons", import.meta.url),
15+);
16+
17+if (!existsSync(destination)) {
18+ cpSync(source, destination, { recursive: true });
19+ console.log("material icons copied to public/material-icons");
20+}
modified ui/src/App.test.tsx +118 -3
@@ -1,9 +1,31 @@
1-import { beforeEach, describe, expect, it } from "vitest";
2-import { act, fireEvent, render, screen } from "@testing-library/react";
1+import {
2+ act,
3+ fireEvent,
4+ render,
5+ screen,
6+ waitFor,
7+} from "@testing-library/react";
8+import { beforeEach, describe, expect, it, vi } from "vitest";
39 import { App } from "./App";
10+import type { ClientMessage } from "./protocol";
411 import { dispatch, resetStore } from "./store";
12+import { resetTheme } from "./theme";
13+import { resetWorkspace } from "./workspace";
514 import type { OriSocket } from "./ws";
6-import type { ClientMessage } from "./protocol";
15+
16+vi.mock("./api", () => ({
17+ listFiles: vi.fn().mockResolvedValue({ path: "/w", entries: [] }),
18+ readFile: vi.fn().mockResolvedValue({ path: "/w/x", content: "" }),
19+ writeFile: vi.fn().mockResolvedValue(undefined),
20+ searchFiles: vi.fn().mockResolvedValue({ root: "/w", files: [] }),
21+ listSkills: vi.fn().mockResolvedValue({ skills: [] }),
22+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
23+}));
24+
25+// The real terminal drags xterm and a WebSocket along; a stub is enough here.
26+vi.mock("./components/TerminalPane", () => ({
27+ TerminalPane: () => <div data-testid="terminal-pane" />,
28+}));
729
830 function fakeSocket() {
931 const sent: ClientMessage[] = [];
@@ -17,6 +39,44 @@ function fakeSocket() {
1739 describe("App", () => {
1840 beforeEach(() => {
1941 resetStore();
42+ resetWorkspace();
43+ resetTheme();
44+ });
45+
46+ it("switches between the light and dark themes from the header", () => {
47+ const { socket } = fakeSocket();
48+ render(<App socket={socket} />);
49+ expect(document.documentElement.dataset.theme).toBe("light");
50+
51+ fireEvent.click(
52+ screen.getByRole("button", { name: "Switch to dark theme" }),
53+ );
54+ expect(document.documentElement.dataset.theme).toBe("dark");
55+
56+ fireEvent.click(
57+ screen.getByRole("button", { name: "Switch to light theme" }),
58+ );
59+ expect(document.documentElement.dataset.theme).toBe("light");
60+ });
61+
62+ it("toggles the workspace panel and switches its tabs", async () => {
63+ const { socket } = fakeSocket();
64+ render(<App socket={socket} />);
65+
66+ // The panel stays mounted (so its state survives) and hides via CSS.
67+ const workspace = screen.getByRole("complementary", { name: "workspace" });
68+ expect(workspace.className).toContain("workspace-hidden");
69+
70+ fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
71+ expect(workspace.className).not.toContain("workspace-hidden");
72+
73+ fireEvent.click(screen.getByRole("button", { name: "Terminal" }));
74+ await waitFor(() =>
75+ expect(screen.getByTestId("terminal-pane")).toBeDefined(),
76+ );
77+
78+ fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
79+ expect(workspace.className).toContain("workspace-hidden");
2080 });
2181
2282 it("sends a prompt typed in the composer", () => {
@@ -172,6 +232,61 @@ describe("App", () => {
172232 ]);
173233 });
174234
235+ it("keeps a streaming thought open and collapses it when the turn ends", () => {
236+ const { socket } = fakeSocket();
237+ const { container } = render(<App socket={socket} />);
238+
239+ act(() => {
240+ dispatch({
241+ type: "server",
242+ message: {
243+ type: "session_update",
244+ update: {
245+ sessionUpdate: "agent_thought_chunk",
246+ content: { type: "text", text: "streaming thought" },
247+ },
248+ },
249+ });
250+ });
251+
252+ // While streaming: the content is in a live (non-collapsible) block.
253+ expect(container.querySelector(".thought-live")).not.toBeNull();
254+ expect(screen.getByText("💭 Thinking…")).toBeDefined();
255+ expect(screen.getByText("streaming thought")).toBeDefined();
256+
257+ act(() => {
258+ dispatch({
259+ type: "server",
260+ message: { type: "turn_ended", stopReason: "end_turn" },
261+ });
262+ });
263+
264+ // After the turn: a collapsible details block replaces it.
265+ expect(container.querySelector(".thought-live")).toBeNull();
266+ expect(container.querySelector("details.thought")).not.toBeNull();
267+ expect(screen.getByText("💭 Thought for a moment")).toBeDefined();
268+ });
269+
270+ it("shows the spinner with a working phrase during a turn", () => {
271+ const { socket } = fakeSocket();
272+ const { container } = render(<App socket={socket} />);
273+
274+ act(() => {
275+ dispatch({ type: "server", message: { type: "turn_started" } });
276+ });
277+
278+ expect(container.querySelector(".spinner")).not.toBeNull();
279+
280+ act(() => {
281+ dispatch({
282+ type: "server",
283+ message: { type: "turn_ended", stopReason: "end_turn" },
284+ });
285+ });
286+
287+ expect(container.querySelector(".spinner")).toBeNull();
288+ });
289+
175290 it("does not send empty prompts", () => {
176291 const { socket, sent } = fakeSocket();
177292 render(<App socket={socket} />);
@@ -1,9 +1,31 @@
1-import { beforeEach, describe, expect, it } from "vitest";1+import {
2-import { act, fireEvent, render, screen } from "@testing-library/react";2+ act,
3+ fireEvent,
4+ render,
5+ screen,
6+ waitFor,
7+} from "@testing-library/react";
8+import { beforeEach, describe, expect, it, vi } from "vitest";
3 import { App } from "./App";9 import { App } from "./App";
10+import type { ClientMessage } from "./protocol";
4 import { dispatch, resetStore } from "./store";11 import { dispatch, resetStore } from "./store";
12+import { resetTheme } from "./theme";
13+import { resetWorkspace } from "./workspace";
5 import type { OriSocket } from "./ws";14 import type { OriSocket } from "./ws";
6-import type { ClientMessage } from "./protocol";15+
16+vi.mock("./api", () => ({
17+ listFiles: vi.fn().mockResolvedValue({ path: "/w", entries: [] }),
18+ readFile: vi.fn().mockResolvedValue({ path: "/w/x", content: "" }),
19+ writeFile: vi.fn().mockResolvedValue(undefined),
20+ searchFiles: vi.fn().mockResolvedValue({ root: "/w", files: [] }),
21+ listSkills: vi.fn().mockResolvedValue({ skills: [] }),
22+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
23+}));
24+
25+// The real terminal drags xterm and a WebSocket along; a stub is enough here.
26+vi.mock("./components/TerminalPane", () => ({
27+ TerminalPane: () => <div data-testid="terminal-pane" />,
28+}));
7 29
8 function fakeSocket() {30 function fakeSocket() {
9 const sent: ClientMessage[] = [];31 const sent: ClientMessage[] = [];
@@ -17,6 +39,44 @@ function fakeSocket() {
17 describe("App", () => {39 describe("App", () => {
18 beforeEach(() => {40 beforeEach(() => {
19 resetStore();41 resetStore();
42+ resetWorkspace();
43+ resetTheme();
44+ });
45+
46+ it("switches between the light and dark themes from the header", () => {
47+ const { socket } = fakeSocket();
48+ render(<App socket={socket} />);
49+ expect(document.documentElement.dataset.theme).toBe("light");
50+
51+ fireEvent.click(
52+ screen.getByRole("button", { name: "Switch to dark theme" }),
53+ );
54+ expect(document.documentElement.dataset.theme).toBe("dark");
55+
56+ fireEvent.click(
57+ screen.getByRole("button", { name: "Switch to light theme" }),
58+ );
59+ expect(document.documentElement.dataset.theme).toBe("light");
60+ });
61+
62+ it("toggles the workspace panel and switches its tabs", async () => {
63+ const { socket } = fakeSocket();
64+ render(<App socket={socket} />);
65+
66+ // The panel stays mounted (so its state survives) and hides via CSS.
67+ const workspace = screen.getByRole("complementary", { name: "workspace" });
68+ expect(workspace.className).toContain("workspace-hidden");
69+
70+ fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
71+ expect(workspace.className).not.toContain("workspace-hidden");
72+
73+ fireEvent.click(screen.getByRole("button", { name: "Terminal" }));
74+ await waitFor(() =>
75+ expect(screen.getByTestId("terminal-pane")).toBeDefined(),
76+ );
77+
78+ fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
79+ expect(workspace.className).toContain("workspace-hidden");
20 });80 });
21 81
22 it("sends a prompt typed in the composer", () => {82 it("sends a prompt typed in the composer", () => {
@@ -172,6 +232,61 @@ describe("App", () => {
172 ]);232 ]);
173 });233 });
174 234
235+ it("keeps a streaming thought open and collapses it when the turn ends", () => {
236+ const { socket } = fakeSocket();
237+ const { container } = render(<App socket={socket} />);
238+
239+ act(() => {
240+ dispatch({
241+ type: "server",
242+ message: {
243+ type: "session_update",
244+ update: {
245+ sessionUpdate: "agent_thought_chunk",
246+ content: { type: "text", text: "streaming thought" },
247+ },
248+ },
249+ });
250+ });
251+
252+ // While streaming: the content is in a live (non-collapsible) block.
253+ expect(container.querySelector(".thought-live")).not.toBeNull();
254+ expect(screen.getByText("💭 Thinking…")).toBeDefined();
255+ expect(screen.getByText("streaming thought")).toBeDefined();
256+
257+ act(() => {
258+ dispatch({
259+ type: "server",
260+ message: { type: "turn_ended", stopReason: "end_turn" },
261+ });
262+ });
263+
264+ // After the turn: a collapsible details block replaces it.
265+ expect(container.querySelector(".thought-live")).toBeNull();
266+ expect(container.querySelector("details.thought")).not.toBeNull();
267+ expect(screen.getByText("💭 Thought for a moment")).toBeDefined();
268+ });
269+
270+ it("shows the spinner with a working phrase during a turn", () => {
271+ const { socket } = fakeSocket();
272+ const { container } = render(<App socket={socket} />);
273+
274+ act(() => {
275+ dispatch({ type: "server", message: { type: "turn_started" } });
276+ });
277+
278+ expect(container.querySelector(".spinner")).not.toBeNull();
279+
280+ act(() => {
281+ dispatch({
282+ type: "server",
283+ message: { type: "turn_ended", stopReason: "end_turn" },
284+ });
285+ });
286+
287+ expect(container.querySelector(".spinner")).toBeNull();
288+ });
289+
175 it("does not send empty prompts", () => {290 it("does not send empty prompts", () => {
176 const { socket, sent } = fakeSocket();291 const { socket, sent } = fakeSocket();
177 render(<App socket={socket} />);292 render(<App socket={socket} />);
modified ui/src/App.tsx +59 -20
@@ -1,21 +1,28 @@
11 /**
2- * Root component: header with connection/session status, the conversation
3- * thread, and the composer. The WebSocket is injected so tests (and future
2+ * Root component: header with connection/session status and the workspace
3+ * toggle, the chat column (thread, plan, permissions, composer) and the
4+ * workspace panel. The WebSocket is injected so tests (and future
45 * embeddings) can supply a fake.
56 *
67 * @example
78 * <App socket={connectSocket({ url: wsUrl(), dispatch })} />
89 */
910
10-import type { OriSocket } from "./ws";
11-import { useChatState } from "./store";
1211 import { ChatThread } from "./components/ChatThread";
1312 import { PermissionPrompt } from "./components/PermissionPrompt";
1413 import { PlanView } from "./components/PlanView";
1514 import { PromptInput } from "./components/PromptInput";
15+import { Workspace } from "./components/Workspace";
16+import { useChatState } from "./store";
17+import { toggleTheme, useTheme } from "./theme";
18+import { toggleWorkspace, useWorkspace } from "./workspace";
19+import type { OriSocket } from "./ws";
1620
1721 export function App({ socket }: { socket: OriSocket }) {
1822 const state = useChatState();
23+ const workspaceOpen = useWorkspace((workspace) => workspace.open);
24+ const theme = useTheme((state) => state.theme);
25+ const nextTheme = theme === "dark" ? "light" : "dark";
1926
2027 return (
2128 <div className="app">
@@ -25,23 +32,55 @@ export function App({ socket }: { socket: OriSocket }) {
2532 {state.connection}
2633 {state.sessionId ? ` · ${state.sessionId}` : ""}
2734 </span>
35+ <button
36+ type="button"
37+ className="topbar-theme"
38+ onClick={toggleTheme}
39+ aria-label={`Switch to ${nextTheme} theme`}
40+ title={`Switch to ${nextTheme} theme`}
41+ >
42+ {theme === "dark" ? "☀" : "☾"}
43+ </button>
44+ <button
45+ type="button"
46+ className={`topbar-workspace${workspaceOpen ? " topbar-workspace-active" : ""}`}
47+ onClick={toggleWorkspace}
48+ >
49+ Workspace
50+ </button>
2851 </header>
29- <ChatThread thread={state.thread} turnActive={state.turnActive} />
30- <PlanView entries={state.plan} />
31- {state.permissions.map((request) => (
32- <PermissionPrompt
33- key={request.requestId}
34- request={request}
35- onRespond={(requestId, optionId) =>
36- socket.send({ type: "permission_response", requestId, optionId })
37- }
38- />
39- ))}
40- <PromptInput
41- turnActive={state.turnActive}
42- onSend={(text) => socket.send({ type: "prompt", text })}
43- onCancel={() => socket.send({ type: "cancel" })}
44- />
52+ <div className={`app-body${workspaceOpen ? " with-workspace" : ""}`}>
53+ <div className="chat-column">
54+ <ChatThread thread={state.thread} turnActive={state.turnActive} />
55+ <PlanView entries={state.plan} />
56+ {state.permissions.map((request) => (
57+ <PermissionPrompt
58+ key={request.requestId}
59+ request={request}
60+ onRespond={(requestId, optionId) =>
61+ socket.send({
62+ type: "permission_response",
63+ requestId,
64+ optionId,
65+ })
66+ }
67+ />
68+ ))}
69+ <PromptInput
70+ turnActive={state.turnActive}
71+ commands={state.commands}
72+ onSend={(text, attachments) =>
73+ socket.send(
74+ attachments.length > 0
75+ ? { type: "prompt", text, attachments }
76+ : { type: "prompt", text },
77+ )
78+ }
79+ onCancel={() => socket.send({ type: "cancel" })}
80+ />
81+ </div>
82+ <Workspace />
83+ </div>
4584 </div>
4685 );
4786 }
@@ -1,21 +1,28 @@
1 /**1 /**
2- * Root component: header with connection/session status, the conversation2+ * Root component: header with connection/session status and the workspace
3- * thread, and the composer. The WebSocket is injected so tests (and future3+ * toggle, the chat column (thread, plan, permissions, composer) and the
4+ * workspace panel. The WebSocket is injected so tests (and future
4 * embeddings) can supply a fake.5 * embeddings) can supply a fake.
5 *6 *
6 * @example7 * @example
7 * <App socket={connectSocket({ url: wsUrl(), dispatch })} />8 * <App socket={connectSocket({ url: wsUrl(), dispatch })} />
8 */9 */
9 10
10-import type { OriSocket } from "./ws";
11-import { useChatState } from "./store";
12 import { ChatThread } from "./components/ChatThread";11 import { ChatThread } from "./components/ChatThread";
13 import { PermissionPrompt } from "./components/PermissionPrompt";12 import { PermissionPrompt } from "./components/PermissionPrompt";
14 import { PlanView } from "./components/PlanView";13 import { PlanView } from "./components/PlanView";
15 import { PromptInput } from "./components/PromptInput";14 import { PromptInput } from "./components/PromptInput";
15+import { Workspace } from "./components/Workspace";
16+import { useChatState } from "./store";
17+import { toggleTheme, useTheme } from "./theme";
18+import { toggleWorkspace, useWorkspace } from "./workspace";
19+import type { OriSocket } from "./ws";
16 20
17 export function App({ socket }: { socket: OriSocket }) {21 export function App({ socket }: { socket: OriSocket }) {
18 const state = useChatState();22 const state = useChatState();
23+ const workspaceOpen = useWorkspace((workspace) => workspace.open);
24+ const theme = useTheme((state) => state.theme);
25+ const nextTheme = theme === "dark" ? "light" : "dark";
19 26
20 return (27 return (
21 <div className="app">28 <div className="app">
@@ -25,23 +32,55 @@ export function App({ socket }: { socket: OriSocket }) {
25 {state.connection}32 {state.connection}
26 {state.sessionId ? ` · ${state.sessionId}` : ""}33 {state.sessionId ? ` · ${state.sessionId}` : ""}
27 </span>34 </span>
35+ <button
36+ type="button"
37+ className="topbar-theme"
38+ onClick={toggleTheme}
39+ aria-label={`Switch to ${nextTheme} theme`}
40+ title={`Switch to ${nextTheme} theme`}
41+ >
42+ {theme === "dark" ? "☀" : "☾"}
43+ </button>
44+ <button
45+ type="button"
46+ className={`topbar-workspace${workspaceOpen ? " topbar-workspace-active" : ""}`}
47+ onClick={toggleWorkspace}
48+ >
49+ Workspace
50+ </button>
28 </header>51 </header>
29- <ChatThread thread={state.thread} turnActive={state.turnActive} />52+ <div className={`app-body${workspaceOpen ? " with-workspace" : ""}`}>
30- <PlanView entries={state.plan} />53+ <div className="chat-column">
31- {state.permissions.map((request) => (54+ <ChatThread thread={state.thread} turnActive={state.turnActive} />
32- <PermissionPrompt55+ <PlanView entries={state.plan} />
33- key={request.requestId}56+ {state.permissions.map((request) => (
34- request={request}57+ <PermissionPrompt
35- onRespond={(requestId, optionId) =>58+ key={request.requestId}
36- socket.send({ type: "permission_response", requestId, optionId })59+ request={request}
37- }60+ onRespond={(requestId, optionId) =>
38- />61+ socket.send({
39- ))}62+ type: "permission_response",
40- <PromptInput63+ requestId,
41- turnActive={state.turnActive}64+ optionId,
42- onSend={(text) => socket.send({ type: "prompt", text })}65+ })
43- onCancel={() => socket.send({ type: "cancel" })}66+ }
44- />67+ />
68+ ))}
69+ <PromptInput
70+ turnActive={state.turnActive}
71+ commands={state.commands}
72+ onSend={(text, attachments) =>
73+ socket.send(
74+ attachments.length > 0
75+ ? { type: "prompt", text, attachments }
76+ : { type: "prompt", text },
77+ )
78+ }
79+ onCancel={() => socket.send({ type: "cancel" })}
80+ />
81+ </div>
82+ <Workspace />
83+ </div>
45 </div>84 </div>
46 );85 );
47 }86 }
added ui/src/api.ts +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+/**
2+ * Client for the backend's workspace file API (see internal/files and the
3+ * WebSocket/CLI references in docs/). All functions throw an Error carrying
4+ * the server's message when the request fails.
5+ *
6+ * @example
7+ * const { entries } = await listFiles(); // workspace root
8+ * const { content } = await readFile(entries[0].path);
9+ * await writeFile("notes.txt", "hello");
10+ */
11+
12+/** One file matched by the recursive search. */
13+export interface FileHit {
14+ name: string;
15+ /** Absolute path, usable in further API calls. */
16+ path: string;
17+ /** Path relative to the workspace root, with forward slashes. */
18+ relPath: string;
19+}
20+
21+/** One Claude Code skill discovered on disk. */
22+export interface Skill {
23+ name: string;
24+ description: string;
25+ path: string;
26+ source: "project" | "user";
27+}
28+
29+/** One child of a listed directory. */
30+export interface FileEntry {
31+ name: string;
32+ path: string;
33+ isDir: boolean;
34+ size: number;
35+}
36+
37+async function requestJSON<T>(input: string, init?: RequestInit): Promise<T> {
38+ const response = await fetch(input, init);
39+ const payload = (await response.json()) as T & { error?: string };
40+ if (!response.ok) {
41+ throw new Error(
42+ payload.error ?? `request failed with status ${response.status}`,
43+ );
44+ }
45+ return payload;
46+}
47+
48+/** listFiles returns the entries of a directory (workspace root when omitted). */
49+export function listFiles(
50+ path?: string,
51+): Promise<{ path: string; entries: FileEntry[] }> {
52+ const query = path ? `?path=${encodeURIComponent(path)}` : "";
53+ return requestJSON(`/api/files${query}`);
54+}
55+
56+/**
57+ * searchFiles returns the workspace files whose relative path contains the
58+ * query (case-insensitive), recursively, at most `limit` of them.
59+ */
60+export function searchFiles(
61+ query: string,
62+ limit = 50,
63+): Promise<{ root: string; files: FileHit[] }> {
64+ return requestJSON(
65+ `/api/files/search?q=${encodeURIComponent(query)}&limit=${limit}`,
66+ );
67+}
68+
69+/** listSkills returns the skills of the project and of the user. */
70+export function listSkills(): Promise<{ skills: Skill[] }> {
71+ return requestJSON("/api/skills");
72+}
73+
74+/** rawFileUrl is the URL streaming a file's bytes (images, downloads). */
75+export function rawFileUrl(path: string): string {
76+ return `/api/raw?path=${encodeURIComponent(path)}`;
77+}
78+
79+/** readFile returns a text file's content. */
80+export function readFile(
81+ path: string,
82+): Promise<{ path: string; content: string }> {
83+ return requestJSON(`/api/file?path=${encodeURIComponent(path)}`);
84+}
85+
86+/** writeFile saves content to a file, creating parent directories as needed. */
87+export async function writeFile(path: string, content: string): Promise<void> {
88+ await requestJSON(`/api/file?path=${encodeURIComponent(path)}`, {
89+ method: "PUT",
90+ headers: { "Content-Type": "application/json" },
91+ body: JSON.stringify({ content }),
92+ });
93+}
new file mode 100644
@@ -0,0 +1,93 @@
1+/**
2+ * Client for the backend's workspace file API (see internal/files and the
3+ * WebSocket/CLI references in docs/). All functions throw an Error carrying
4+ * the server's message when the request fails.
5+ *
6+ * @example
7+ * const { entries } = await listFiles(); // workspace root
8+ * const { content } = await readFile(entries[0].path);
9+ * await writeFile("notes.txt", "hello");
10+ */
11+
12+/** One file matched by the recursive search. */
13+export interface FileHit {
14+ name: string;
15+ /** Absolute path, usable in further API calls. */
16+ path: string;
17+ /** Path relative to the workspace root, with forward slashes. */
18+ relPath: string;
19+}
20+
21+/** One Claude Code skill discovered on disk. */
22+export interface Skill {
23+ name: string;
24+ description: string;
25+ path: string;
26+ source: "project" | "user";
27+}
28+
29+/** One child of a listed directory. */
30+export interface FileEntry {
31+ name: string;
32+ path: string;
33+ isDir: boolean;
34+ size: number;
35+}
36+
37+async function requestJSON<T>(input: string, init?: RequestInit): Promise<T> {
38+ const response = await fetch(input, init);
39+ const payload = (await response.json()) as T & { error?: string };
40+ if (!response.ok) {
41+ throw new Error(
42+ payload.error ?? `request failed with status ${response.status}`,
43+ );
44+ }
45+ return payload;
46+}
47+
48+/** listFiles returns the entries of a directory (workspace root when omitted). */
49+export function listFiles(
50+ path?: string,
51+): Promise<{ path: string; entries: FileEntry[] }> {
52+ const query = path ? `?path=${encodeURIComponent(path)}` : "";
53+ return requestJSON(`/api/files${query}`);
54+}
55+
56+/**
57+ * searchFiles returns the workspace files whose relative path contains the
58+ * query (case-insensitive), recursively, at most `limit` of them.
59+ */
60+export function searchFiles(
61+ query: string,
62+ limit = 50,
63+): Promise<{ root: string; files: FileHit[] }> {
64+ return requestJSON(
65+ `/api/files/search?q=${encodeURIComponent(query)}&limit=${limit}`,
66+ );
67+}
68+
69+/** listSkills returns the skills of the project and of the user. */
70+export function listSkills(): Promise<{ skills: Skill[] }> {
71+ return requestJSON("/api/skills");
72+}
73+
74+/** rawFileUrl is the URL streaming a file's bytes (images, downloads). */
75+export function rawFileUrl(path: string): string {
76+ return `/api/raw?path=${encodeURIComponent(path)}`;
77+}
78+
79+/** readFile returns a text file's content. */
80+export function readFile(
81+ path: string,
82+): Promise<{ path: string; content: string }> {
83+ return requestJSON(`/api/file?path=${encodeURIComponent(path)}`);
84+}
85+
86+/** writeFile saves content to a file, creating parent directories as needed. */
87+export async function writeFile(path: string, content: string): Promise<void> {
88+ await requestJSON(`/api/file?path=${encodeURIComponent(path)}`, {
89+ method: "PUT",
90+ headers: { "Content-Type": "application/json" },
91+ body: JSON.stringify({ content }),
92+ });
93+}
modified ui/src/app.css +572 -18
@@ -1,9 +1,10 @@
11 /* Ori — agent panel styling, in the spirit of Zed's agent panel:
22 a quiet column of conversation, tool activity and a composer pinned at
3- the bottom. Light and dark follow the system preference. */
3+ the bottom. Light is the default palette; the dark one applies when the
4+ theme store stamps data-theme="dark" on <html> (see ui/src/theme.ts). */
45
56 :root {
6- color-scheme: light dark;
7+ color-scheme: light;
78 font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
89
910 --bg: #fafafa;
@@ -18,19 +19,18 @@
1819 --code-bg: #f2f2f2;
1920 }
2021
21-@media (prefers-color-scheme: dark) {
22- :root {
23- --bg: #1e2025;
24- --panel: #26282e;
25- --text: #e6e6e6;
26- --muted: #9a9a9a;
27- --border: #3a3d45;
28- --accent: #60a5fa;
29- --user-bubble: #2f3a4f;
30- --error: #fca5a5;
31- --error-bg: #3f2222;
32- --code-bg: #1a1c20;
33- }
22+:root[data-theme="dark"] {
23+ color-scheme: dark;
24+ --bg: #1e2025;
25+ --panel: #26282e;
26+ --text: #e6e6e6;
27+ --muted: #9a9a9a;
28+ --border: #3a3d45;
29+ --accent: #60a5fa;
30+ --user-bubble: #2f3a4f;
31+ --error: #fca5a5;
32+ --error-bg: #3f2222;
33+ --code-bg: #1a1c20;
3434 }
3535
3636 body {
@@ -43,8 +43,27 @@ body {
4343 display: flex;
4444 flex-direction: column;
4545 height: 100vh;
46+}
47+
48+.app-body {
49+ flex: 1;
50+ display: flex;
51+ min-height: 0;
52+ justify-content: center;
53+}
54+
55+.chat-column {
56+ display: flex;
57+ flex-direction: column;
58+ flex: 1;
4659 max-width: 52rem;
47- margin: 0 auto;
60+ min-width: 0;
61+}
62+
63+.app-body.with-workspace .chat-column {
64+ max-width: none;
65+ flex: 0 0 42%;
66+ border-right: 1px solid var(--border);
4867 }
4968
5069 /* ---- header ---- */
@@ -86,6 +105,419 @@ body {
86105 color: #eab308;
87106 }
88107
108+.topbar-theme,
109+.topbar-workspace {
110+ border: 1px solid var(--border);
111+ border-radius: 6px;
112+ background: var(--panel);
113+ color: var(--text);
114+ font: inherit;
115+ font-size: 0.8rem;
116+ padding: 0.25rem 0.7rem;
117+ cursor: pointer;
118+}
119+
120+.topbar-workspace-active {
121+ border-color: var(--accent);
122+ color: var(--accent);
123+}
124+
125+.topbar-theme {
126+ margin-left: auto;
127+ padding: 0.25rem 0.5rem;
128+}
129+
130+/* ---- workspace panel ---- */
131+
132+.workspace {
133+ flex: 1;
134+ display: flex;
135+ min-width: 0;
136+ background: var(--bg);
137+}
138+
139+.workspace-hidden {
140+ display: none;
141+}
142+
143+.workspace-pane {
144+ height: 100%;
145+}
146+
147+.workspace-pane-hidden {
148+ display: none;
149+}
150+
151+/* ---- terminal pane ---- */
152+
153+/* The terminal keeps a dark background whatever the app theme, like an IDE. */
154+.terminal {
155+ height: 100%;
156+ padding: 0.4rem;
157+ background: #1e1e1e;
158+}
159+
160+.terminal .xterm {
161+ height: 100%;
162+}
163+
164+.filetree {
165+ /* Width driven by the workspace store (see Workspace.tsx); 14rem default. */
166+ flex: 0 0 var(--filetree-width, 14rem);
167+ overflow-y: auto;
168+ padding: 0.5rem;
169+ font-size: 0.85rem;
170+}
171+
172+/* ---- resize handle between the file tree and the tabs ---- */
173+
174+.resize-handle {
175+ /* The visible line is the 1px border; the padding widens the grab area. */
176+ flex: 0 0 1px;
177+ box-sizing: content-box;
178+ padding: 0 3px;
179+ margin: 0 -3px;
180+ background: var(--border);
181+ background-clip: content-box;
182+ cursor: col-resize;
183+ touch-action: none;
184+ z-index: 1;
185+}
186+
187+.resize-handle:hover,
188+.resize-handle-active {
189+ background-color: var(--accent);
190+}
191+
192+.resize-handle:focus-visible {
193+ outline: 2px solid var(--accent);
194+ outline-offset: -1px;
195+}
196+
197+.filetree-header {
198+ display: flex;
199+ align-items: center;
200+ justify-content: space-between;
201+ color: var(--muted);
202+ font-size: 0.75rem;
203+ text-transform: uppercase;
204+ letter-spacing: 0.05em;
205+ margin-bottom: 0.4rem;
206+}
207+
208+.filetree-refresh {
209+ border: none;
210+ background: none;
211+ color: var(--muted);
212+ cursor: pointer;
213+ font-size: 0.9rem;
214+}
215+
216+.filetree-error {
217+ color: var(--error);
218+ font-size: 0.8rem;
219+}
220+
221+.filetree-loading {
222+ color: var(--muted);
223+ font-style: italic;
224+}
225+
226+.filetree-list {
227+ list-style: none;
228+ margin: 0;
229+ padding: 0;
230+}
231+
232+.filetree-list-nested {
233+ /* VS Code-style indent guide */
234+ margin-left: 0.55rem;
235+ border-left: 1px solid var(--border);
236+}
237+
238+.filetree-entry {
239+ display: flex;
240+ align-items: center;
241+ gap: 0.3rem;
242+ width: 100%;
243+ text-align: left;
244+ border: none;
245+ background: none;
246+ color: var(--text);
247+ font: inherit;
248+ font-size: 0.82rem;
249+ padding: 0.14rem 0.3rem;
250+ border-radius: 4px;
251+ cursor: pointer;
252+ white-space: nowrap;
253+}
254+
255+.filetree-entry:hover {
256+ background: var(--panel);
257+}
258+
259+.filetree-active {
260+ background: var(--user-bubble);
261+}
262+
263+.filetree-chevron {
264+ flex: 0 0 0.7rem;
265+ color: var(--muted);
266+ font-size: 0.8rem;
267+ line-height: 1;
268+ text-align: center;
269+}
270+
271+.filetree-icon {
272+ flex: 0 0 1rem;
273+ width: 1rem;
274+ height: 1rem;
275+}
276+
277+.filetree-name {
278+ overflow: hidden;
279+ text-overflow: ellipsis;
280+}
281+
282+.workspace-main {
283+ flex: 1;
284+ display: flex;
285+ flex-direction: column;
286+ min-width: 0;
287+}
288+
289+.workspace-tabs {
290+ display: flex;
291+ gap: 0.25rem;
292+ border-bottom: 1px solid var(--border);
293+ padding: 0.3rem 0.5rem 0;
294+}
295+
296+.workspace-tab {
297+ border: 1px solid transparent;
298+ border-bottom: none;
299+ border-radius: 6px 6px 0 0;
300+ background: none;
301+ color: var(--muted);
302+ font: inherit;
303+ font-size: 0.85rem;
304+ padding: 0.3rem 0.9rem;
305+ cursor: pointer;
306+}
307+
308+.workspace-tab-active {
309+ border-color: var(--border);
310+ background: var(--panel);
311+ color: var(--text);
312+}
313+
314+.workspace-content {
315+ flex: 1;
316+ min-height: 0;
317+ overflow: auto;
318+ position: relative;
319+}
320+
321+.pane-empty {
322+ color: var(--muted);
323+ font-style: italic;
324+ padding: 1rem;
325+}
326+
327+.pane-error {
328+ color: var(--error);
329+ padding: 1rem;
330+}
331+
332+/* ---- preview pane ---- */
333+
334+.preview {
335+ display: flex;
336+ flex-direction: column;
337+ height: 100%;
338+}
339+
340+.pane-header {
341+ display: flex;
342+ align-items: center;
343+ gap: 0.6rem;
344+ padding: 0.35rem 0.8rem;
345+ border-bottom: 1px solid var(--border);
346+ font-size: 0.85rem;
347+}
348+
349+.pane-path {
350+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
351+ overflow: hidden;
352+ text-overflow: ellipsis;
353+ white-space: nowrap;
354+}
355+
356+.pane-modes {
357+ display: flex;
358+ gap: 0.25rem;
359+ margin-left: auto;
360+}
361+
362+.pane-mode {
363+ border: 1px solid var(--border);
364+ border-radius: 6px;
365+ background: none;
366+ color: var(--muted);
367+ font: inherit;
368+ font-size: 0.75rem;
369+ padding: 0.15rem 0.6rem;
370+ cursor: pointer;
371+}
372+
373+.pane-mode-active {
374+ background: var(--panel);
375+ color: var(--text);
376+ border-color: var(--accent);
377+}
378+
379+.pane-action {
380+ border: 1px solid var(--border);
381+ border-radius: 6px;
382+ background: none;
383+ color: var(--text);
384+ font: inherit;
385+ font-size: 0.75rem;
386+ padding: 0.15rem 0.6rem;
387+ cursor: pointer;
388+}
389+
390+.pane-modes + .pane-action {
391+ margin-left: 0;
392+}
393+
394+.pane-header .pane-action:last-child {
395+ margin-left: auto;
396+}
397+
398+.pane-modes + .pane-action:last-child {
399+ margin-left: 0;
400+}
401+
402+.preview-body {
403+ flex: 1;
404+ min-height: 0;
405+ overflow: auto;
406+}
407+
408+.preview-rendered {
409+ padding: 1rem;
410+ max-width: 48rem;
411+}
412+
413+/* ---- image preview ---- */
414+
415+.image-view {
416+ margin: 0;
417+ padding: 1rem;
418+ display: flex;
419+ flex-direction: column;
420+ gap: 0.5rem;
421+ align-items: flex-start;
422+}
423+
424+.image-view-canvas {
425+ max-width: 100%;
426+ border: 1px solid var(--border);
427+ border-radius: 6px;
428+ --check: rgba(127, 127, 127, 0.18);
429+ background-color: var(--panel);
430+ background-image: linear-gradient(45deg, var(--check) 25%, transparent 25%),
431+ linear-gradient(-45deg, var(--check) 25%, transparent 25%),
432+ linear-gradient(45deg, transparent 75%, var(--check) 75%),
433+ linear-gradient(-45deg, transparent 75%, var(--check) 75%);
434+ background-size: 16px 16px;
435+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
436+}
437+
438+.image-view-canvas img {
439+ display: block;
440+ max-width: 100%;
441+ height: auto;
442+}
443+
444+.image-view-meta {
445+ color: var(--muted);
446+ font-size: 0.8rem;
447+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
448+}
449+
450+/* ---- draw.io preview ---- */
451+
452+.drawio-view {
453+ display: flex;
454+ flex-direction: column;
455+ height: 100%;
456+}
457+
458+.drawio-frame {
459+ flex: 1;
460+ width: 100%;
461+ min-height: 20rem;
462+ border: none;
463+ background: var(--panel);
464+}
465+
466+.drawio-view-loading .drawio-frame {
467+ visibility: hidden;
468+ flex: 0;
469+ min-height: 0;
470+}
471+
472+.drawio-notice {
473+ margin: 1rem;
474+ padding: 0.6rem 0.8rem;
475+ border: 1px solid var(--border);
476+ border-radius: 6px;
477+ color: var(--muted);
478+ font-size: 0.85rem;
479+}
480+
481+/* ---- editor pane ---- */
482+
483+.editor {
484+ display: flex;
485+ flex-direction: column;
486+ height: 100%;
487+}
488+
489+.editor-body {
490+ flex: 1;
491+ min-height: 0;
492+}
493+
494+.pane-saved {
495+ color: #22c55e;
496+ font-size: 0.75rem;
497+}
498+
499+.pane-error-inline {
500+ color: var(--error);
501+ font-size: 0.75rem;
502+ overflow: hidden;
503+ text-overflow: ellipsis;
504+ white-space: nowrap;
505+}
506+
507+.editor .pane-action {
508+ margin-left: auto;
509+}
510+
511+.editor .pane-saved,
512+.editor .pane-error-inline {
513+ margin-left: auto;
514+}
515+
516+.editor .pane-saved + .pane-action,
517+.editor .pane-error-inline + .pane-action {
518+ margin-left: 0.5rem;
519+}
520+
89521 /* ---- thread ---- */
90522
91523 .thread {
@@ -97,13 +529,38 @@ body {
97529 gap: 0.75rem;
98530 }
99531
100-.thread-empty,
101-.thread-working {
532+.thread-empty {
102533 color: var(--muted);
103534 font-size: 0.85rem;
104535 font-style: italic;
105536 }
106537
538+/* ---- working indicator ---- */
539+
540+.working {
541+ display: flex;
542+ align-items: center;
543+ gap: 0.5rem;
544+ color: var(--muted);
545+ font-size: 0.85rem;
546+ font-style: italic;
547+}
548+
549+.spinner {
550+ width: 0.85rem;
551+ height: 0.85rem;
552+ border: 2px solid var(--border);
553+ border-top-color: var(--accent);
554+ border-radius: 50%;
555+ animation: spinner-turn 0.8s linear infinite;
556+}
557+
558+@keyframes spinner-turn {
559+ to {
560+ transform: rotate(360deg);
561+ }
562+}
563+
107564 .message {
108565 border-radius: 8px;
109566 padding: 0.55rem 0.8rem;
@@ -184,6 +641,11 @@ body {
184641 user-select: none;
185642 }
186643
644+.thought-live .thought-summary {
645+ cursor: default;
646+ margin: 0;
647+}
648+
187649 .thought-body {
188650 padding-top: 0.4rem;
189651 }
@@ -280,6 +742,29 @@ body {
280742 font-style: italic;
281743 }
282744
745+.inline-code {
746+ background: var(--code-bg);
747+ border: 1px solid var(--border);
748+ border-radius: 4px;
749+ padding: 0 0.3rem;
750+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
751+ monospace;
752+ font-size: 0.85em;
753+}
754+
755+.tool-output {
756+ background: var(--code-bg);
757+ border: 1px solid var(--border);
758+ border-radius: 6px;
759+ padding: 0.5rem;
760+ overflow-x: auto;
761+ margin: 0.3rem 0;
762+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
763+ monospace;
764+ font-size: 0.8rem;
765+ line-height: 1.4;
766+}
767+
283768 /* ---- plan ---- */
284769
285770 .plan {
@@ -370,6 +855,12 @@ body {
370855 border-top: 1px solid var(--border);
371856 }
372857
858+.composer-field {
859+ flex: 1;
860+ position: relative;
861+ display: flex;
862+}
863+
373864 .composer-input {
374865 flex: 1;
375866 resize: none;
@@ -409,3 +900,66 @@ body {
409900 .composer-stop {
410901 background: #dc2626;
411902 }
903+
904+/* ---- completion popup (@ files, / skills) ---- */
905+
906+.completion {
907+ position: absolute;
908+ left: 0;
909+ right: 0;
910+ bottom: calc(100% + 0.35rem);
911+ max-height: 16rem;
912+ overflow-y: auto;
913+ margin: 0;
914+ padding: 0.25rem;
915+ list-style: none;
916+ background: var(--panel);
917+ border: 1px solid var(--border);
918+ border-radius: 8px;
919+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
920+ font-size: 0.85rem;
921+ z-index: 5;
922+}
923+
924+.completion-item {
925+ display: flex;
926+ align-items: baseline;
927+ gap: 0.5rem;
928+ padding: 0.35rem 0.6rem;
929+ border-radius: 6px;
930+ cursor: pointer;
931+}
932+
933+.completion-item-active {
934+ background: var(--user-bubble);
935+}
936+
937+.completion-label {
938+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
939+ white-space: nowrap;
940+}
941+
942+.completion-description {
943+ flex: 1;
944+ min-width: 0;
945+ color: var(--muted);
946+ overflow: hidden;
947+ text-overflow: ellipsis;
948+ white-space: nowrap;
949+}
950+
951+.completion-source {
952+ margin-left: auto;
953+ font-size: 0.7rem;
954+ text-transform: uppercase;
955+ letter-spacing: 0.04em;
956+ color: var(--muted);
957+ border: 1px solid var(--border);
958+ border-radius: 999px;
959+ padding: 0 0.45rem;
960+}
961+
962+.completion-source-skill {
963+ color: var(--accent);
964+ border-color: var(--accent);
965+}
@@ -1,9 +1,10 @@
1 /* Ori — agent panel styling, in the spirit of Zed's agent panel:1 /* Ori — agent panel styling, in the spirit of Zed's agent panel:
2 a quiet column of conversation, tool activity and a composer pinned at2 a quiet column of conversation, tool activity and a composer pinned at
3- the bottom. Light and dark follow the system preference. */3+ the bottom. Light is the default palette; the dark one applies when the
4+ theme store stamps data-theme="dark" on <html> (see ui/src/theme.ts). */
4 5
5 :root {6 :root {
6- color-scheme: light dark;7+ color-scheme: light;
7 font-family: system-ui, -apple-system, "Segoe UI", sans-serif;8 font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
8 9
9 --bg: #fafafa;10 --bg: #fafafa;
@@ -18,19 +19,18 @@
18 --code-bg: #f2f2f2;19 --code-bg: #f2f2f2;
19 }20 }
20 21
21-@media (prefers-color-scheme: dark) {22+:root[data-theme="dark"] {
22- :root {23+ color-scheme: dark;
23- --bg: #1e2025;24+ --bg: #1e2025;
24- --panel: #26282e;25+ --panel: #26282e;
25- --text: #e6e6e6;26+ --text: #e6e6e6;
26- --muted: #9a9a9a;27+ --muted: #9a9a9a;
27- --border: #3a3d45;28+ --border: #3a3d45;
28- --accent: #60a5fa;29+ --accent: #60a5fa;
29- --user-bubble: #2f3a4f;30+ --user-bubble: #2f3a4f;
30- --error: #fca5a5;31+ --error: #fca5a5;
31- --error-bg: #3f2222;32+ --error-bg: #3f2222;
32- --code-bg: #1a1c20;33+ --code-bg: #1a1c20;
33- }
34 }34 }
35 35
36 body {36 body {
@@ -43,8 +43,27 @@ body {
43 display: flex;43 display: flex;
44 flex-direction: column;44 flex-direction: column;
45 height: 100vh;45 height: 100vh;
46+}
47+
48+.app-body {
49+ flex: 1;
50+ display: flex;
51+ min-height: 0;
52+ justify-content: center;
53+}
54+
55+.chat-column {
56+ display: flex;
57+ flex-direction: column;
58+ flex: 1;
46 max-width: 52rem;59 max-width: 52rem;
47- margin: 0 auto;60+ min-width: 0;
61+}
62+
63+.app-body.with-workspace .chat-column {
64+ max-width: none;
65+ flex: 0 0 42%;
66+ border-right: 1px solid var(--border);
48 }67 }
49 68
50 /* ---- header ---- */69 /* ---- header ---- */
@@ -86,6 +105,419 @@ body {
86 color: #eab308;105 color: #eab308;
87 }106 }
88 107
108+.topbar-theme,
109+.topbar-workspace {
110+ border: 1px solid var(--border);
111+ border-radius: 6px;
112+ background: var(--panel);
113+ color: var(--text);
114+ font: inherit;
115+ font-size: 0.8rem;
116+ padding: 0.25rem 0.7rem;
117+ cursor: pointer;
118+}
119+
120+.topbar-workspace-active {
121+ border-color: var(--accent);
122+ color: var(--accent);
123+}
124+
125+.topbar-theme {
126+ margin-left: auto;
127+ padding: 0.25rem 0.5rem;
128+}
129+
130+/* ---- workspace panel ---- */
131+
132+.workspace {
133+ flex: 1;
134+ display: flex;
135+ min-width: 0;
136+ background: var(--bg);
137+}
138+
139+.workspace-hidden {
140+ display: none;
141+}
142+
143+.workspace-pane {
144+ height: 100%;
145+}
146+
147+.workspace-pane-hidden {
148+ display: none;
149+}
150+
151+/* ---- terminal pane ---- */
152+
153+/* The terminal keeps a dark background whatever the app theme, like an IDE. */
154+.terminal {
155+ height: 100%;
156+ padding: 0.4rem;
157+ background: #1e1e1e;
158+}
159+
160+.terminal .xterm {
161+ height: 100%;
162+}
163+
164+.filetree {
165+ /* Width driven by the workspace store (see Workspace.tsx); 14rem default. */
166+ flex: 0 0 var(--filetree-width, 14rem);
167+ overflow-y: auto;
168+ padding: 0.5rem;
169+ font-size: 0.85rem;
170+}
171+
172+/* ---- resize handle between the file tree and the tabs ---- */
173+
174+.resize-handle {
175+ /* The visible line is the 1px border; the padding widens the grab area. */
176+ flex: 0 0 1px;
177+ box-sizing: content-box;
178+ padding: 0 3px;
179+ margin: 0 -3px;
180+ background: var(--border);
181+ background-clip: content-box;
182+ cursor: col-resize;
183+ touch-action: none;
184+ z-index: 1;
185+}
186+
187+.resize-handle:hover,
188+.resize-handle-active {
189+ background-color: var(--accent);
190+}
191+
192+.resize-handle:focus-visible {
193+ outline: 2px solid var(--accent);
194+ outline-offset: -1px;
195+}
196+
197+.filetree-header {
198+ display: flex;
199+ align-items: center;
200+ justify-content: space-between;
201+ color: var(--muted);
202+ font-size: 0.75rem;
203+ text-transform: uppercase;
204+ letter-spacing: 0.05em;
205+ margin-bottom: 0.4rem;
206+}
207+
208+.filetree-refresh {
209+ border: none;
210+ background: none;
211+ color: var(--muted);
212+ cursor: pointer;
213+ font-size: 0.9rem;
214+}
215+
216+.filetree-error {
217+ color: var(--error);
218+ font-size: 0.8rem;
219+}
220+
221+.filetree-loading {
222+ color: var(--muted);
223+ font-style: italic;
224+}
225+
226+.filetree-list {
227+ list-style: none;
228+ margin: 0;
229+ padding: 0;
230+}
231+
232+.filetree-list-nested {
233+ /* VS Code-style indent guide */
234+ margin-left: 0.55rem;
235+ border-left: 1px solid var(--border);
236+}
237+
238+.filetree-entry {
239+ display: flex;
240+ align-items: center;
241+ gap: 0.3rem;
242+ width: 100%;
243+ text-align: left;
244+ border: none;
245+ background: none;
246+ color: var(--text);
247+ font: inherit;
248+ font-size: 0.82rem;
249+ padding: 0.14rem 0.3rem;
250+ border-radius: 4px;
251+ cursor: pointer;
252+ white-space: nowrap;
253+}
254+
255+.filetree-entry:hover {
256+ background: var(--panel);
257+}
258+
259+.filetree-active {
260+ background: var(--user-bubble);
261+}
262+
263+.filetree-chevron {
264+ flex: 0 0 0.7rem;
265+ color: var(--muted);
266+ font-size: 0.8rem;
267+ line-height: 1;
268+ text-align: center;
269+}
270+
271+.filetree-icon {
272+ flex: 0 0 1rem;
273+ width: 1rem;
274+ height: 1rem;
275+}
276+
277+.filetree-name {
278+ overflow: hidden;
279+ text-overflow: ellipsis;
280+}
281+
282+.workspace-main {
283+ flex: 1;
284+ display: flex;
285+ flex-direction: column;
286+ min-width: 0;
287+}
288+
289+.workspace-tabs {
290+ display: flex;
291+ gap: 0.25rem;
292+ border-bottom: 1px solid var(--border);
293+ padding: 0.3rem 0.5rem 0;
294+}
295+
296+.workspace-tab {
297+ border: 1px solid transparent;
298+ border-bottom: none;
299+ border-radius: 6px 6px 0 0;
300+ background: none;
301+ color: var(--muted);
302+ font: inherit;
303+ font-size: 0.85rem;
304+ padding: 0.3rem 0.9rem;
305+ cursor: pointer;
306+}
307+
308+.workspace-tab-active {
309+ border-color: var(--border);
310+ background: var(--panel);
311+ color: var(--text);
312+}
313+
314+.workspace-content {
315+ flex: 1;
316+ min-height: 0;
317+ overflow: auto;
318+ position: relative;
319+}
320+
321+.pane-empty {
322+ color: var(--muted);
323+ font-style: italic;
324+ padding: 1rem;
325+}
326+
327+.pane-error {
328+ color: var(--error);
329+ padding: 1rem;
330+}
331+
332+/* ---- preview pane ---- */
333+
334+.preview {
335+ display: flex;
336+ flex-direction: column;
337+ height: 100%;
338+}
339+
340+.pane-header {
341+ display: flex;
342+ align-items: center;
343+ gap: 0.6rem;
344+ padding: 0.35rem 0.8rem;
345+ border-bottom: 1px solid var(--border);
346+ font-size: 0.85rem;
347+}
348+
349+.pane-path {
350+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
351+ overflow: hidden;
352+ text-overflow: ellipsis;
353+ white-space: nowrap;
354+}
355+
356+.pane-modes {
357+ display: flex;
358+ gap: 0.25rem;
359+ margin-left: auto;
360+}
361+
362+.pane-mode {
363+ border: 1px solid var(--border);
364+ border-radius: 6px;
365+ background: none;
366+ color: var(--muted);
367+ font: inherit;
368+ font-size: 0.75rem;
369+ padding: 0.15rem 0.6rem;
370+ cursor: pointer;
371+}
372+
373+.pane-mode-active {
374+ background: var(--panel);
375+ color: var(--text);
376+ border-color: var(--accent);
377+}
378+
379+.pane-action {
380+ border: 1px solid var(--border);
381+ border-radius: 6px;
382+ background: none;
383+ color: var(--text);
384+ font: inherit;
385+ font-size: 0.75rem;
386+ padding: 0.15rem 0.6rem;
387+ cursor: pointer;
388+}
389+
390+.pane-modes + .pane-action {
391+ margin-left: 0;
392+}
393+
394+.pane-header .pane-action:last-child {
395+ margin-left: auto;
396+}
397+
398+.pane-modes + .pane-action:last-child {
399+ margin-left: 0;
400+}
401+
402+.preview-body {
403+ flex: 1;
404+ min-height: 0;
405+ overflow: auto;
406+}
407+
408+.preview-rendered {
409+ padding: 1rem;
410+ max-width: 48rem;
411+}
412+
413+/* ---- image preview ---- */
414+
415+.image-view {
416+ margin: 0;
417+ padding: 1rem;
418+ display: flex;
419+ flex-direction: column;
420+ gap: 0.5rem;
421+ align-items: flex-start;
422+}
423+
424+.image-view-canvas {
425+ max-width: 100%;
426+ border: 1px solid var(--border);
427+ border-radius: 6px;
428+ --check: rgba(127, 127, 127, 0.18);
429+ background-color: var(--panel);
430+ background-image: linear-gradient(45deg, var(--check) 25%, transparent 25%),
431+ linear-gradient(-45deg, var(--check) 25%, transparent 25%),
432+ linear-gradient(45deg, transparent 75%, var(--check) 75%),
433+ linear-gradient(-45deg, transparent 75%, var(--check) 75%);
434+ background-size: 16px 16px;
435+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
436+}
437+
438+.image-view-canvas img {
439+ display: block;
440+ max-width: 100%;
441+ height: auto;
442+}
443+
444+.image-view-meta {
445+ color: var(--muted);
446+ font-size: 0.8rem;
447+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
448+}
449+
450+/* ---- draw.io preview ---- */
451+
452+.drawio-view {
453+ display: flex;
454+ flex-direction: column;
455+ height: 100%;
456+}
457+
458+.drawio-frame {
459+ flex: 1;
460+ width: 100%;
461+ min-height: 20rem;
462+ border: none;
463+ background: var(--panel);
464+}
465+
466+.drawio-view-loading .drawio-frame {
467+ visibility: hidden;
468+ flex: 0;
469+ min-height: 0;
470+}
471+
472+.drawio-notice {
473+ margin: 1rem;
474+ padding: 0.6rem 0.8rem;
475+ border: 1px solid var(--border);
476+ border-radius: 6px;
477+ color: var(--muted);
478+ font-size: 0.85rem;
479+}
480+
481+/* ---- editor pane ---- */
482+
483+.editor {
484+ display: flex;
485+ flex-direction: column;
486+ height: 100%;
487+}
488+
489+.editor-body {
490+ flex: 1;
491+ min-height: 0;
492+}
493+
494+.pane-saved {
495+ color: #22c55e;
496+ font-size: 0.75rem;
497+}
498+
499+.pane-error-inline {
500+ color: var(--error);
501+ font-size: 0.75rem;
502+ overflow: hidden;
503+ text-overflow: ellipsis;
504+ white-space: nowrap;
505+}
506+
507+.editor .pane-action {
508+ margin-left: auto;
509+}
510+
511+.editor .pane-saved,
512+.editor .pane-error-inline {
513+ margin-left: auto;
514+}
515+
516+.editor .pane-saved + .pane-action,
517+.editor .pane-error-inline + .pane-action {
518+ margin-left: 0.5rem;
519+}
520+
89 /* ---- thread ---- */521 /* ---- thread ---- */
90 522
91 .thread {523 .thread {
@@ -97,13 +529,38 @@ body {
97 gap: 0.75rem;529 gap: 0.75rem;
98 }530 }
99 531
100-.thread-empty,532+.thread-empty {
101-.thread-working {
102 color: var(--muted);533 color: var(--muted);
103 font-size: 0.85rem;534 font-size: 0.85rem;
104 font-style: italic;535 font-style: italic;
105 }536 }
106 537
538+/* ---- working indicator ---- */
539+
540+.working {
541+ display: flex;
542+ align-items: center;
543+ gap: 0.5rem;
544+ color: var(--muted);
545+ font-size: 0.85rem;
546+ font-style: italic;
547+}
548+
549+.spinner {
550+ width: 0.85rem;
551+ height: 0.85rem;
552+ border: 2px solid var(--border);
553+ border-top-color: var(--accent);
554+ border-radius: 50%;
555+ animation: spinner-turn 0.8s linear infinite;
556+}
557+
558+@keyframes spinner-turn {
559+ to {
560+ transform: rotate(360deg);
561+ }
562+}
563+
107 .message {564 .message {
108 border-radius: 8px;565 border-radius: 8px;
109 padding: 0.55rem 0.8rem;566 padding: 0.55rem 0.8rem;
@@ -184,6 +641,11 @@ body {
184 user-select: none;641 user-select: none;
185 }642 }
186 643
644+.thought-live .thought-summary {
645+ cursor: default;
646+ margin: 0;
647+}
648+
187 .thought-body {649 .thought-body {
188 padding-top: 0.4rem;650 padding-top: 0.4rem;
189 }651 }
@@ -280,6 +742,29 @@ body {
280 font-style: italic;742 font-style: italic;
281 }743 }
282 744
745+.inline-code {
746+ background: var(--code-bg);
747+ border: 1px solid var(--border);
748+ border-radius: 4px;
749+ padding: 0 0.3rem;
750+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
751+ monospace;
752+ font-size: 0.85em;
753+}
754+
755+.tool-output {
756+ background: var(--code-bg);
757+ border: 1px solid var(--border);
758+ border-radius: 6px;
759+ padding: 0.5rem;
760+ overflow-x: auto;
761+ margin: 0.3rem 0;
762+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
763+ monospace;
764+ font-size: 0.8rem;
765+ line-height: 1.4;
766+}
767+
283 /* ---- plan ---- */768 /* ---- plan ---- */
284 769
285 .plan {770 .plan {
@@ -370,6 +855,12 @@ body {
370 border-top: 1px solid var(--border);855 border-top: 1px solid var(--border);
371 }856 }
372 857
858+.composer-field {
859+ flex: 1;
860+ position: relative;
861+ display: flex;
862+}
863+
373 .composer-input {864 .composer-input {
374 flex: 1;865 flex: 1;
375 resize: none;866 resize: none;
@@ -409,3 +900,66 @@ body {
409 .composer-stop {900 .composer-stop {
410 background: #dc2626;901 background: #dc2626;
411 }902 }
903+
904+/* ---- completion popup (@ files, / skills) ---- */
905+
906+.completion {
907+ position: absolute;
908+ left: 0;
909+ right: 0;
910+ bottom: calc(100% + 0.35rem);
911+ max-height: 16rem;
912+ overflow-y: auto;
913+ margin: 0;
914+ padding: 0.25rem;
915+ list-style: none;
916+ background: var(--panel);
917+ border: 1px solid var(--border);
918+ border-radius: 8px;
919+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
920+ font-size: 0.85rem;
921+ z-index: 5;
922+}
923+
924+.completion-item {
925+ display: flex;
926+ align-items: baseline;
927+ gap: 0.5rem;
928+ padding: 0.35rem 0.6rem;
929+ border-radius: 6px;
930+ cursor: pointer;
931+}
932+
933+.completion-item-active {
934+ background: var(--user-bubble);
935+}
936+
937+.completion-label {
938+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
939+ white-space: nowrap;
940+}
941+
942+.completion-description {
943+ flex: 1;
944+ min-width: 0;
945+ color: var(--muted);
946+ overflow: hidden;
947+ text-overflow: ellipsis;
948+ white-space: nowrap;
949+}
950+
951+.completion-source {
952+ margin-left: auto;
953+ font-size: 0.7rem;
954+ text-transform: uppercase;
955+ letter-spacing: 0.04em;
956+ color: var(--muted);
957+ border: 1px solid var(--border);
958+ border-radius: 999px;
959+ padding: 0 0.45rem;
960+}
961+
962+.completion-source-skill {
963+ color: var(--accent);
964+ border-color: var(--accent);
965+}
added ui/src/components/AsciiDocView.test.tsx +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+import { render, screen, waitFor } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import { AsciiDocView } from "./AsciiDocView";
4+
5+// Deliberately NOT mocked: this exercises the real @asciidoctor/core module
6+// resolution and conversion — the exact integration that once broke in
7+// production while every mocked test stayed green.
8+describe("AsciiDocView (real converter)", () => {
9+ it("renders a document with title, section and list", async () => {
10+ render(
11+ <AsciiDocView
12+ content={
13+ "= The Title\n\n== Section\n\n* item one\n* item two\n\nSome *bold* text."
14+ }
15+ />,
16+ );
17+
18+ await waitFor(() => expect(screen.getByText("The Title")).toBeDefined(), {
19+ timeout: 15000,
20+ });
21+ expect(screen.getByText("Section")).toBeDefined();
22+ expect(screen.getByText("item one")).toBeDefined();
23+ expect(screen.getByText("bold")).toBeDefined();
24+ }, 20000);
25+
26+ it("re-renders when the content changes", async () => {
27+ const { rerender } = render(<AsciiDocView content="= First" />);
28+ await waitFor(() => expect(screen.getByText("First")).toBeDefined(), {
29+ timeout: 15000,
30+ });
31+
32+ rerender(<AsciiDocView content="= Second" />);
33+ await waitFor(() => expect(screen.getByText("Second")).toBeDefined());
34+ }, 20000);
35+});
new file mode 100644
@@ -0,0 +1,35 @@
1+import { render, screen, waitFor } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import { AsciiDocView } from "./AsciiDocView";
4+
5+// Deliberately NOT mocked: this exercises the real @asciidoctor/core module
6+// resolution and conversion — the exact integration that once broke in
7+// production while every mocked test stayed green.
8+describe("AsciiDocView (real converter)", () => {
9+ it("renders a document with title, section and list", async () => {
10+ render(
11+ <AsciiDocView
12+ content={
13+ "= The Title\n\n== Section\n\n* item one\n* item two\n\nSome *bold* text."
14+ }
15+ />,
16+ );
17+
18+ await waitFor(() => expect(screen.getByText("The Title")).toBeDefined(), {
19+ timeout: 15000,
20+ });
21+ expect(screen.getByText("Section")).toBeDefined();
22+ expect(screen.getByText("item one")).toBeDefined();
23+ expect(screen.getByText("bold")).toBeDefined();
24+ }, 20000);
25+
26+ it("re-renders when the content changes", async () => {
27+ const { rerender } = render(<AsciiDocView content="= First" />);
28+ await waitFor(() => expect(screen.getByText("First")).toBeDefined(), {
29+ timeout: 15000,
30+ });
31+
32+ rerender(<AsciiDocView content="= Second" />);
33+ await waitFor(() => expect(screen.getByText("Second")).toBeDefined());
34+ }, 20000);
35+});
added ui/src/components/AsciiDocView.tsx +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+/**
2+ * AsciiDocView renders an AsciiDoc document to HTML with Asciidoctor,
3+ * lazy-loading the (heavy) converter on first use.
4+ *
5+ * The converted HTML comes from files in the user's own workspace, rendered
6+ * for the user who owns them — the same trust model as opening the file in
7+ * any editor's preview.
8+ *
9+ * @example
10+ * <AsciiDocView content="= Title" />
11+ */
12+
13+import { useEffect, useState } from "react";
14+
15+export function AsciiDocView({ content }: { content: string }) {
16+ const [html, setHtml] = useState<string | null>(null);
17+ const [error, setError] = useState<string | null>(null);
18+
19+ useEffect(() => {
20+ let cancelled = false;
21+ // @asciidoctor/core v4 exposes an async convert() as a named export
22+ // (the v3 synchronous asciidoctor() factory is gone).
23+ import("@asciidoctor/core")
24+ .then(({ convert }) =>
25+ convert(content, {
26+ safe: "safe",
27+ attributes: { showtitle: true, icons: "font" },
28+ }),
29+ )
30+ .then((converted) => {
31+ if (!cancelled) {
32+ setHtml(String(converted));
33+ }
34+ })
35+ .catch((cause: Error) => {
36+ if (!cancelled) {
37+ setError(`AsciiDoc rendering failed: ${cause.message}`);
38+ }
39+ });
40+ return () => {
41+ cancelled = true;
42+ };
43+ }, [content]);
44+
45+ if (error) {
46+ return <p className="pane-error">{error}</p>;
47+ }
48+ if (html === null) {
49+ return <p className="pane-empty">rendering</p>;
50+ }
51+ return (
52+ <div
53+ className="markdown asciidoc"
54+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Asciidoctor's output over the user's own workspace files, converted in safe mode — the accepted pattern for a local document preview.
55+ dangerouslySetInnerHTML={{ __html: html }}
56+ />
57+ );
58+}
new file mode 100644
@@ -0,0 +1,58 @@
1+/**
2+ * AsciiDocView renders an AsciiDoc document to HTML with Asciidoctor,
3+ * lazy-loading the (heavy) converter on first use.
4+ *
5+ * The converted HTML comes from files in the user's own workspace, rendered
6+ * for the user who owns them — the same trust model as opening the file in
7+ * any editor's preview.
8+ *
9+ * @example
10+ * <AsciiDocView content="= Title" />
11+ */
12+
13+import { useEffect, useState } from "react";
14+
15+export function AsciiDocView({ content }: { content: string }) {
16+ const [html, setHtml] = useState<string | null>(null);
17+ const [error, setError] = useState<string | null>(null);
18+
19+ useEffect(() => {
20+ let cancelled = false;
21+ // @asciidoctor/core v4 exposes an async convert() as a named export
22+ // (the v3 synchronous asciidoctor() factory is gone).
23+ import("@asciidoctor/core")
24+ .then(({ convert }) =>
25+ convert(content, {
26+ safe: "safe",
27+ attributes: { showtitle: true, icons: "font" },
28+ }),
29+ )
30+ .then((converted) => {
31+ if (!cancelled) {
32+ setHtml(String(converted));
33+ }
34+ })
35+ .catch((cause: Error) => {
36+ if (!cancelled) {
37+ setError(`AsciiDoc rendering failed: ${cause.message}`);
38+ }
39+ });
40+ return () => {
41+ cancelled = true;
42+ };
43+ }, [content]);
44+
45+ if (error) {
46+ return <p className="pane-error">{error}</p>;
47+ }
48+ if (html === null) {
49+ return <p className="pane-empty">rendering</p>;
50+ }
51+ return (
52+ <div
53+ className="markdown asciidoc"
54+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Asciidoctor's output over the user's own workspace files, converted in safe mode — the accepted pattern for a local document preview.
55+ dangerouslySetInnerHTML={{ __html: html }}
56+ />
57+ );
58+}
modified ui/src/components/ChatThread.tsx +29 -11
@@ -10,6 +10,7 @@ import { useEffect, useRef } from "react";
1010 import type { ThreadItem } from "../reducer";
1111 import { Markdown } from "./Markdown";
1212 import { ToolCallCard } from "./ToolCallCard";
13+import { WorkingIndicator } from "./WorkingIndicator";
1314
1415 interface ChatThreadProps {
1516 thread: ThreadItem[];
@@ -36,9 +37,7 @@ export function ChatThread({ thread, turnActive }: ChatThreadProps) {
3637 // biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity.
3738 <ThreadEntry key={index} item={item} />
3839 ))}
39- {turnActive && (
40- <output className="thread-working">the agent is working</output>
41- )}
40+ {turnActive && <WorkingIndicator />}
4241 <div ref={endRef} />
4342 </section>
4443 );
@@ -55,14 +54,7 @@ function ThreadEntry({ item }: { item: ThreadItem }) {
5554 </div>
5655 );
5756 case "thought":
58- return (
59- <details className="thought">
60- <summary className="thought-summary">💭 Thinking</summary>
61- <div className="thought-body">
62- <Markdown text={item.text} />
63- </div>
64- </details>
65- );
57+ return <ThoughtEntry text={item.text} closed={item.closed} />;
6658 case "tool":
6759 return <ToolCallCard call={item.call} />;
6860 case "error":
@@ -73,3 +65,29 @@ function ThreadEntry({ item }: { item: ThreadItem }) {
7365 );
7466 }
7567 }
68+
69+/**
70+ * ThoughtEntry renders one thinking block: while the thought streams its
71+ * content stays visible; once the turn ends it collapses into a reopenable
72+ * summary.
73+ */
74+function ThoughtEntry({ text, closed }: { text: string; closed: boolean }) {
75+ if (!closed) {
76+ return (
77+ <div className="thought thought-live">
78+ <p className="thought-summary">💭 Thinking</p>
79+ <div className="thought-body">
80+ <Markdown text={text} />
81+ </div>
82+ </div>
83+ );
84+ }
85+ return (
86+ <details className="thought">
87+ <summary className="thought-summary">💭 Thought for a moment</summary>
88+ <div className="thought-body">
89+ <Markdown text={text} />
90+ </div>
91+ </details>
92+ );
93+}
@@ -10,6 +10,7 @@ import { useEffect, useRef } from "react";
10 import type { ThreadItem } from "../reducer";10 import type { ThreadItem } from "../reducer";
11 import { Markdown } from "./Markdown";11 import { Markdown } from "./Markdown";
12 import { ToolCallCard } from "./ToolCallCard";12 import { ToolCallCard } from "./ToolCallCard";
13+import { WorkingIndicator } from "./WorkingIndicator";
13 14
14 interface ChatThreadProps {15 interface ChatThreadProps {
15 thread: ThreadItem[];16 thread: ThreadItem[];
@@ -36,9 +37,7 @@ export function ChatThread({ thread, turnActive }: ChatThreadProps) {
36 // biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity.37 // biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity.
37 <ThreadEntry key={index} item={item} />38 <ThreadEntry key={index} item={item} />
38 ))}39 ))}
39- {turnActive && (40+ {turnActive && <WorkingIndicator />}
40- <output className="thread-working">the agent is working</output>
41- )}
42 <div ref={endRef} />41 <div ref={endRef} />
43 </section>42 </section>
44 );43 );
@@ -55,14 +54,7 @@ function ThreadEntry({ item }: { item: ThreadItem }) {
55 </div>54 </div>
56 );55 );
57 case "thought":56 case "thought":
58- return (57+ return <ThoughtEntry text={item.text} closed={item.closed} />;
59- <details className="thought">
60- <summary className="thought-summary">💭 Thinking</summary>
61- <div className="thought-body">
62- <Markdown text={item.text} />
63- </div>
64- </details>
65- );
66 case "tool":58 case "tool":
67 return <ToolCallCard call={item.call} />;59 return <ToolCallCard call={item.call} />;
68 case "error":60 case "error":
@@ -73,3 +65,29 @@ function ThreadEntry({ item }: { item: ThreadItem }) {
73 );65 );
74 }66 }
75 }67 }
68+
69+/**
70+ * ThoughtEntry renders one thinking block: while the thought streams its
71+ * content stays visible; once the turn ends it collapses into a reopenable
72+ * summary.
73+ */
74+function ThoughtEntry({ text, closed }: { text: string; closed: boolean }) {
75+ if (!closed) {
76+ return (
77+ <div className="thought thought-live">
78+ <p className="thought-summary">💭 Thinking</p>
79+ <div className="thought-body">
80+ <Markdown text={text} />
81+ </div>
82+ </div>
83+ );
84+ }
85+ return (
86+ <details className="thought">
87+ <summary className="thought-summary">💭 Thought for a moment</summary>
88+ <div className="thought-body">
89+ <Markdown text={text} />
90+ </div>
91+ </details>
92+ );
93+}
added ui/src/components/CodeView.tsx +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+/**
2+ * CodeView shows read-only source code with Monaco's syntax highlighting,
3+ * lazy-loading Monaco on first use.
4+ *
5+ * @example
6+ * <CodeView content="package main" language="go" />
7+ */
8+
9+import { lazy, Suspense } from "react";
10+
11+const MonacoViewer = lazy(() => import("./MonacoViewer"));
12+
13+export function CodeView({
14+ content,
15+ language,
16+}: { content: string; language: string }) {
17+ return (
18+ <Suspense fallback={<p className="pane-empty">loading viewer</p>}>
19+ <MonacoViewer content={content} language={language} readOnly />
20+ </Suspense>
21+ );
22+}
new file mode 100644
@@ -0,0 +1,22 @@
1+/**
2+ * CodeView shows read-only source code with Monaco's syntax highlighting,
3+ * lazy-loading Monaco on first use.
4+ *
5+ * @example
6+ * <CodeView content="package main" language="go" />
7+ */
8+
9+import { lazy, Suspense } from "react";
10+
11+const MonacoViewer = lazy(() => import("./MonacoViewer"));
12+
13+export function CodeView({
14+ content,
15+ language,
16+}: { content: string; language: string }) {
17+ return (
18+ <Suspense fallback={<p className="pane-empty">loading viewer</p>}>
19+ <MonacoViewer content={content} language={language} readOnly />
20+ </Suspense>
21+ );
22+}
added ui/src/components/CompletionPopup.tsx +74 -0
new file mode 100644
@@ -0,0 +1,74 @@
1+/**
2+ * CompletionPopup is the list shown above the composer while an "@" or "/"
3+ * selector is active: label, description, and a source tag for skills and
4+ * agent commands. Selection happens with the keyboard (handled by the
5+ * composer) or the mouse.
6+ *
7+ * @example
8+ * <CompletionPopup items={items} active={0} onHover={setActive} onPick={pick} />
9+ */
10+
11+import type { CompletionItem } from "../mentions";
12+
13+interface CompletionPopupProps {
14+ items: CompletionItem[];
15+ active: number;
16+ onHover: (index: number) => void;
17+ onPick: (item: CompletionItem) => void;
18+}
19+
20+const sourceLabels: Record<CompletionItem["source"], string> = {
21+ file: "",
22+ skill: "skill",
23+ command: "command",
24+};
25+
26+// The popup is a combobox listbox driven from the textarea (which carries
27+// aria-activedescendant), not a form <select>: hence the explicit roles on
28+// focusable-but-unreachable divs.
29+export function CompletionPopup({
30+ items,
31+ active,
32+ onHover,
33+ onPick,
34+}: CompletionPopupProps) {
35+ return (
36+ <div
37+ className="completion"
38+ // biome-ignore lint/a11y/useSemanticElements: a <select> cannot host rich rows nor stay open while typing
39+ role="listbox"
40+ tabIndex={-1}
41+ aria-label={items[0]?.source === "file" ? "files" : "skills"}
42+ >
43+ {items.map((item, index) => (
44+ <div
45+ key={item.id}
46+ id={`completion-${index}`}
47+ // biome-ignore lint/a11y/useSemanticElements: see the listbox above
48+ role="option"
49+ tabIndex={-1}
50+ aria-selected={index === active}
51+ className={`completion-item${index === active ? " completion-item-active" : ""}`}
52+ onMouseEnter={() => onHover(index)}
53+ // Fire before the textarea loses focus on click.
54+ onMouseDown={(event) => {
55+ event.preventDefault();
56+ onPick(item);
57+ }}
58+ >
59+ <span className="completion-label">{item.label}</span>
60+ {item.description && (
61+ <span className="completion-description">{item.description}</span>
62+ )}
63+ {sourceLabels[item.source] && (
64+ <span
65+ className={`completion-source completion-source-${item.source}`}
66+ >
67+ {sourceLabels[item.source]}
68+ </span>
69+ )}
70+ </div>
71+ ))}
72+ </div>
73+ );
74+}
new file mode 100644
@@ -0,0 +1,74 @@
1+/**
2+ * CompletionPopup is the list shown above the composer while an "@" or "/"
3+ * selector is active: label, description, and a source tag for skills and
4+ * agent commands. Selection happens with the keyboard (handled by the
5+ * composer) or the mouse.
6+ *
7+ * @example
8+ * <CompletionPopup items={items} active={0} onHover={setActive} onPick={pick} />
9+ */
10+
11+import type { CompletionItem } from "../mentions";
12+
13+interface CompletionPopupProps {
14+ items: CompletionItem[];
15+ active: number;
16+ onHover: (index: number) => void;
17+ onPick: (item: CompletionItem) => void;
18+}
19+
20+const sourceLabels: Record<CompletionItem["source"], string> = {
21+ file: "",
22+ skill: "skill",
23+ command: "command",
24+};
25+
26+// The popup is a combobox listbox driven from the textarea (which carries
27+// aria-activedescendant), not a form <select>: hence the explicit roles on
28+// focusable-but-unreachable divs.
29+export function CompletionPopup({
30+ items,
31+ active,
32+ onHover,
33+ onPick,
34+}: CompletionPopupProps) {
35+ return (
36+ <div
37+ className="completion"
38+ // biome-ignore lint/a11y/useSemanticElements: a <select> cannot host rich rows nor stay open while typing
39+ role="listbox"
40+ tabIndex={-1}
41+ aria-label={items[0]?.source === "file" ? "files" : "skills"}
42+ >
43+ {items.map((item, index) => (
44+ <div
45+ key={item.id}
46+ id={`completion-${index}`}
47+ // biome-ignore lint/a11y/useSemanticElements: see the listbox above
48+ role="option"
49+ tabIndex={-1}
50+ aria-selected={index === active}
51+ className={`completion-item${index === active ? " completion-item-active" : ""}`}
52+ onMouseEnter={() => onHover(index)}
53+ // Fire before the textarea loses focus on click.
54+ onMouseDown={(event) => {
55+ event.preventDefault();
56+ onPick(item);
57+ }}
58+ >
59+ <span className="completion-label">{item.label}</span>
60+ {item.description && (
61+ <span className="completion-description">{item.description}</span>
62+ )}
63+ {sourceLabels[item.source] && (
64+ <span
65+ className={`completion-source completion-source-${item.source}`}
66+ >
67+ {sourceLabels[item.source]}
68+ </span>
69+ )}
70+ </div>
71+ ))}
72+ </div>
73+ );
74+}
added ui/src/components/DrawioView.test.tsx +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+import { act, render, screen } from "@testing-library/react";
2+import { describe, expect, it, vi } from "vitest";
3+import { DRAWIO_EMBED_URL, DrawioView } from "./DrawioView";
4+
5+/** viewerSays simulates one postMessage frame coming from the iframe. */
6+function viewerSays(frame: HTMLIFrameElement, payload: unknown) {
7+ act(() => {
8+ window.dispatchEvent(
9+ new MessageEvent("message", {
10+ data: typeof payload === "string" ? payload : JSON.stringify(payload),
11+ source: frame.contentWindow,
12+ }),
13+ );
14+ });
15+}
16+
17+describe("DrawioView", () => {
18+ it("points the iframe at the read-only embed viewer", () => {
19+ render(<DrawioView xml="<mxfile/>" />);
20+ const frame = screen.getByTitle("draw.io diagram") as HTMLIFrameElement;
21+ expect(frame.getAttribute("src")).toBe(DRAWIO_EMBED_URL);
22+ expect(DRAWIO_EMBED_URL).toContain("embed.diagrams.net");
23+ expect(DRAWIO_EMBED_URL).toContain("proto=json");
24+ expect(screen.getByText("loading viewer…")).toBeDefined();
25+ });
26+
27+ it("answers the viewer's init with a load action carrying the XML", () => {
28+ render(<DrawioView xml="<mxfile><diagram/></mxfile>" timeoutMs={60000} />);
29+ const frame = screen.getByTitle("draw.io diagram") as HTMLIFrameElement;
30+ const target = frame.contentWindow;
31+ if (!target) {
32+ throw new Error("jsdom iframe has no contentWindow");
33+ }
34+ const posted = vi.spyOn(target, "postMessage");
35+
36+ viewerSays(frame, "not json");
37+ expect(posted).not.toHaveBeenCalled();
38+
39+ viewerSays(frame, { event: "init" });
40+ expect(posted).toHaveBeenCalledTimes(1);
41+ expect(JSON.parse(String(posted.mock.calls[0][0]))).toEqual({
42+ action: "load",
43+ xml: "<mxfile><diagram/></mxfile>",
44+ autosave: 0,
45+ });
46+ expect(screen.queryByText("loading viewer…")).toBeNull();
47+ expect(frame.hidden).toBe(false);
48+ });
49+
50+ it("ignores messages from other windows", () => {
51+ render(<DrawioView xml="<mxfile/>" timeoutMs={60000} />);
52+ act(() => {
53+ window.dispatchEvent(
54+ new MessageEvent("message", {
55+ data: JSON.stringify({ event: "init" }),
56+ source: window,
57+ }),
58+ );
59+ });
60+ expect(screen.getByText("loading viewer…")).toBeDefined();
61+ });
62+
63+ it("falls back to a notice when the viewer never answers", async () => {
64+ render(<DrawioView xml="<mxfile/>" timeoutMs={20} />);
65+ await act(async () => {
66+ await new Promise((resolve) => setTimeout(resolve, 60));
67+ });
68+ expect(screen.getByText(/is not reachable/)).toBeDefined();
69+ expect(
70+ (screen.getByTitle("draw.io diagram") as HTMLIFrameElement).hidden,
71+ ).toBe(true);
72+ });
73+});
new file mode 100644
@@ -0,0 +1,73 @@
1+import { act, render, screen } from "@testing-library/react";
2+import { describe, expect, it, vi } from "vitest";
3+import { DRAWIO_EMBED_URL, DrawioView } from "./DrawioView";
4+
5+/** viewerSays simulates one postMessage frame coming from the iframe. */
6+function viewerSays(frame: HTMLIFrameElement, payload: unknown) {
7+ act(() => {
8+ window.dispatchEvent(
9+ new MessageEvent("message", {
10+ data: typeof payload === "string" ? payload : JSON.stringify(payload),
11+ source: frame.contentWindow,
12+ }),
13+ );
14+ });
15+}
16+
17+describe("DrawioView", () => {
18+ it("points the iframe at the read-only embed viewer", () => {
19+ render(<DrawioView xml="<mxfile/>" />);
20+ const frame = screen.getByTitle("draw.io diagram") as HTMLIFrameElement;
21+ expect(frame.getAttribute("src")).toBe(DRAWIO_EMBED_URL);
22+ expect(DRAWIO_EMBED_URL).toContain("embed.diagrams.net");
23+ expect(DRAWIO_EMBED_URL).toContain("proto=json");
24+ expect(screen.getByText("loading viewer…")).toBeDefined();
25+ });
26+
27+ it("answers the viewer's init with a load action carrying the XML", () => {
28+ render(<DrawioView xml="<mxfile><diagram/></mxfile>" timeoutMs={60000} />);
29+ const frame = screen.getByTitle("draw.io diagram") as HTMLIFrameElement;
30+ const target = frame.contentWindow;
31+ if (!target) {
32+ throw new Error("jsdom iframe has no contentWindow");
33+ }
34+ const posted = vi.spyOn(target, "postMessage");
35+
36+ viewerSays(frame, "not json");
37+ expect(posted).not.toHaveBeenCalled();
38+
39+ viewerSays(frame, { event: "init" });
40+ expect(posted).toHaveBeenCalledTimes(1);
41+ expect(JSON.parse(String(posted.mock.calls[0][0]))).toEqual({
42+ action: "load",
43+ xml: "<mxfile><diagram/></mxfile>",
44+ autosave: 0,
45+ });
46+ expect(screen.queryByText("loading viewer…")).toBeNull();
47+ expect(frame.hidden).toBe(false);
48+ });
49+
50+ it("ignores messages from other windows", () => {
51+ render(<DrawioView xml="<mxfile/>" timeoutMs={60000} />);
52+ act(() => {
53+ window.dispatchEvent(
54+ new MessageEvent("message", {
55+ data: JSON.stringify({ event: "init" }),
56+ source: window,
57+ }),
58+ );
59+ });
60+ expect(screen.getByText("loading viewer…")).toBeDefined();
61+ });
62+
63+ it("falls back to a notice when the viewer never answers", async () => {
64+ render(<DrawioView xml="<mxfile/>" timeoutMs={20} />);
65+ await act(async () => {
66+ await new Promise((resolve) => setTimeout(resolve, 60));
67+ });
68+ expect(screen.getByText(/is not reachable/)).toBeDefined();
69+ expect(
70+ (screen.getByTitle("draw.io diagram") as HTMLIFrameElement).hidden,
71+ ).toBe(true);
72+ });
73+});
added ui/src/components/DrawioView.tsx +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+/**
2+ * DrawioView renders a .drawio / .dio diagram with the diagrams.net embed
3+ * viewer, loaded in an iframe and fed the XML over its postMessage protocol
4+ * (`init` → `load`). There is no offline renderer for draw.io's shape
5+ * library that ori could bundle, so an isolated sandbox without access to
6+ * embed.diagrams.net gets a notice and the XML source instead.
7+ *
8+ * @example
9+ * <DrawioView xml={content} onUnavailable={() => setMode("source")} />
10+ */
11+
12+import { useEffect, useRef, useState } from "react";
13+
14+/** Read-only embed: lightbox chrome (zoom, pages, layers), no editor UI. */
15+export const DRAWIO_EMBED_URL =
16+ "https://embed.diagrams.net/?embed=1&proto=json&spin=1&lightbox=1&nav=1&chrome=0";
17+
18+/** How long to wait for the viewer's `init` before declaring it unreachable. */
19+const DEFAULT_TIMEOUT_MS = 8000;
20+
21+interface DrawioViewProps {
22+ xml: string;
23+ /** Override for tests; defaults to DRAWIO_EMBED_URL. */
24+ embedUrl?: string;
25+ /** Override for tests; defaults to 8 s. */
26+ timeoutMs?: number;
27+}
28+
29+type ViewerStatus = "loading" | "ready" | "unavailable";
30+
31+export function DrawioView({
32+ xml,
33+ embedUrl = DRAWIO_EMBED_URL,
34+ timeoutMs = DEFAULT_TIMEOUT_MS,
35+}: DrawioViewProps) {
36+ const frameRef = useRef<HTMLIFrameElement>(null);
37+ const [status, setStatus] = useState<ViewerStatus>("loading");
38+
39+ useEffect(() => {
40+ const frame = frameRef.current;
41+ if (!frame) {
42+ return;
43+ }
44+ const timer = setTimeout(() => {
45+ setStatus((current) => (current === "ready" ? current : "unavailable"));
46+ }, timeoutMs);
47+
48+ const onMessage = (event: MessageEvent) => {
49+ if (event.source !== frame.contentWindow) {
50+ return;
51+ }
52+ const message = parseViewerMessage(event.data);
53+ if (message?.event === "init") {
54+ frame.contentWindow?.postMessage(
55+ JSON.stringify({ action: "load", xml, autosave: 0 }),
56+ "*",
57+ );
58+ }
59+ if (message?.event === "init" || message?.event === "load") {
60+ clearTimeout(timer);
61+ setStatus("ready");
62+ }
63+ };
64+ window.addEventListener("message", onMessage);
65+ return () => {
66+ clearTimeout(timer);
67+ window.removeEventListener("message", onMessage);
68+ };
69+ }, [xml, timeoutMs]);
70+
71+ return (
72+ <div className={`drawio-view drawio-view-${status}`}>
73+ {status === "unavailable" && (
74+ <p className="drawio-notice">
75+ The diagram viewer (embed.diagrams.net) is not reachable from this
76+ browser; use the <strong>Source</strong> view to read the diagram XML.
77+ </p>
78+ )}
79+ {status === "loading" && <p className="pane-empty">loading viewer</p>}
80+ <iframe
81+ ref={frameRef}
82+ className="drawio-frame"
83+ title="draw.io diagram"
84+ src={embedUrl}
85+ sandbox="allow-scripts allow-same-origin allow-popups"
86+ hidden={status === "unavailable"}
87+ />
88+ </div>
89+ );
90+}
91+
92+/** parseViewerMessage decodes one embed-protocol frame; anything else is null. */
93+function parseViewerMessage(data: unknown): { event?: string } | null {
94+ if (typeof data !== "string" || !data.startsWith("{")) {
95+ return null;
96+ }
97+ try {
98+ return JSON.parse(data) as { event?: string };
99+ } catch {
100+ return null;
101+ }
102+}
new file mode 100644
@@ -0,0 +1,102 @@
1+/**
2+ * DrawioView renders a .drawio / .dio diagram with the diagrams.net embed
3+ * viewer, loaded in an iframe and fed the XML over its postMessage protocol
4+ * (`init` → `load`). There is no offline renderer for draw.io's shape
5+ * library that ori could bundle, so an isolated sandbox without access to
6+ * embed.diagrams.net gets a notice and the XML source instead.
7+ *
8+ * @example
9+ * <DrawioView xml={content} onUnavailable={() => setMode("source")} />
10+ */
11+
12+import { useEffect, useRef, useState } from "react";
13+
14+/** Read-only embed: lightbox chrome (zoom, pages, layers), no editor UI. */
15+export const DRAWIO_EMBED_URL =
16+ "https://embed.diagrams.net/?embed=1&proto=json&spin=1&lightbox=1&nav=1&chrome=0";
17+
18+/** How long to wait for the viewer's `init` before declaring it unreachable. */
19+const DEFAULT_TIMEOUT_MS = 8000;
20+
21+interface DrawioViewProps {
22+ xml: string;
23+ /** Override for tests; defaults to DRAWIO_EMBED_URL. */
24+ embedUrl?: string;
25+ /** Override for tests; defaults to 8 s. */
26+ timeoutMs?: number;
27+}
28+
29+type ViewerStatus = "loading" | "ready" | "unavailable";
30+
31+export function DrawioView({
32+ xml,
33+ embedUrl = DRAWIO_EMBED_URL,
34+ timeoutMs = DEFAULT_TIMEOUT_MS,
35+}: DrawioViewProps) {
36+ const frameRef = useRef<HTMLIFrameElement>(null);
37+ const [status, setStatus] = useState<ViewerStatus>("loading");
38+
39+ useEffect(() => {
40+ const frame = frameRef.current;
41+ if (!frame) {
42+ return;
43+ }
44+ const timer = setTimeout(() => {
45+ setStatus((current) => (current === "ready" ? current : "unavailable"));
46+ }, timeoutMs);
47+
48+ const onMessage = (event: MessageEvent) => {
49+ if (event.source !== frame.contentWindow) {
50+ return;
51+ }
52+ const message = parseViewerMessage(event.data);
53+ if (message?.event === "init") {
54+ frame.contentWindow?.postMessage(
55+ JSON.stringify({ action: "load", xml, autosave: 0 }),
56+ "*",
57+ );
58+ }
59+ if (message?.event === "init" || message?.event === "load") {
60+ clearTimeout(timer);
61+ setStatus("ready");
62+ }
63+ };
64+ window.addEventListener("message", onMessage);
65+ return () => {
66+ clearTimeout(timer);
67+ window.removeEventListener("message", onMessage);
68+ };
69+ }, [xml, timeoutMs]);
70+
71+ return (
72+ <div className={`drawio-view drawio-view-${status}`}>
73+ {status === "unavailable" && (
74+ <p className="drawio-notice">
75+ The diagram viewer (embed.diagrams.net) is not reachable from this
76+ browser; use the <strong>Source</strong> view to read the diagram XML.
77+ </p>
78+ )}
79+ {status === "loading" && <p className="pane-empty">loading viewer</p>}
80+ <iframe
81+ ref={frameRef}
82+ className="drawio-frame"
83+ title="draw.io diagram"
84+ src={embedUrl}
85+ sandbox="allow-scripts allow-same-origin allow-popups"
86+ hidden={status === "unavailable"}
87+ />
88+ </div>
89+ );
90+}
91+
92+/** parseViewerMessage decodes one embed-protocol frame; anything else is null. */
93+function parseViewerMessage(data: unknown): { event?: string } | null {
94+ if (typeof data !== "string" || !data.startsWith("{")) {
95+ return null;
96+ }
97+ try {
98+ return JSON.parse(data) as { event?: string };
99+ } catch {
100+ return null;
101+ }
102+}
added ui/src/components/EditorPane.test.tsx +103 -0
new file mode 100644
@@ -0,0 +1,103 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { EditorPane } from "./EditorPane";
4+
5+const { readFileMock, writeFileMock } = vi.hoisted(() => ({
6+ readFileMock: vi.fn(),
7+ writeFileMock: vi.fn(),
8+}));
9+
10+vi.mock("../api", () => ({
11+ readFile: readFileMock,
12+ writeFile: writeFileMock,
13+}));
14+
15+// Monaco is replaced by a plain textarea driving the same onChange contract.
16+vi.mock("./MonacoViewer", () => ({
17+ default: ({
18+ content,
19+ onChange,
20+ }: { content: string; onChange?: (value: string) => void }) => (
21+ <textarea
22+ aria-label="editor"
23+ value={content}
24+ onChange={(event) => onChange?.(event.target.value)}
25+ />
26+ ),
27+}));
28+
29+async function openEditorOn(path: string, content: string) {
30+ readFileMock.mockResolvedValue({ path, content });
31+ render(<EditorPane path={path} />);
32+ return await screen.findByLabelText("editor");
33+}
34+
35+describe("EditorPane", () => {
36+ beforeEach(() => {
37+ readFileMock.mockReset();
38+ writeFileMock.mockReset();
39+ });
40+
41+ it("asks for a file when none is open", () => {
42+ render(<EditorPane path={null} />);
43+ expect(screen.getByText(/Double-click a file/)).toBeDefined();
44+ });
45+
46+ it("loads the file into the editor with Save disabled", async () => {
47+ const editor = await openEditorOn("/w/main.go", "package main");
48+ expect((editor as HTMLTextAreaElement).value).toBe("package main");
49+ expect(
50+ (screen.getByRole("button", { name: "Save" }) as HTMLButtonElement)
51+ .disabled,
52+ ).toBe(true);
53+ });
54+
55+ it("marks the file dirty on edit and saves through the API", async () => {
56+ writeFileMock.mockResolvedValue(undefined);
57+ const editor = await openEditorOn("/w/main.go", "package main");
58+
59+ fireEvent.change(editor, { target: { value: "package main // edited" } });
60+ expect(screen.getByText(/●/)).toBeDefined();
61+
62+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
63+ await waitFor(() =>
64+ expect(writeFileMock).toHaveBeenCalledWith(
65+ "/w/main.go",
66+ "package main // edited",
67+ ),
68+ );
69+ await waitFor(() => expect(screen.getByText("saved")).toBeDefined());
70+ expect(screen.queryByText(/●/)).toBeNull();
71+ });
72+
73+ it("saves with Ctrl+S", async () => {
74+ writeFileMock.mockResolvedValue(undefined);
75+ const editor = await openEditorOn("/w/notes.txt", "a");
76+
77+ fireEvent.change(editor, { target: { value: "ab" } });
78+ fireEvent.keyDown(editor, { key: "s", ctrlKey: true });
79+
80+ await waitFor(() =>
81+ expect(writeFileMock).toHaveBeenCalledWith("/w/notes.txt", "ab"),
82+ );
83+ });
84+
85+ it("keeps the dirty state and shows the error when saving fails", async () => {
86+ writeFileMock.mockRejectedValue(new Error("disk full"));
87+ const editor = await openEditorOn("/w/notes.txt", "a");
88+
89+ fireEvent.change(editor, { target: { value: "ab" } });
90+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
91+
92+ await waitFor(() => expect(screen.getByText("disk full")).toBeDefined());
93+ expect(screen.getByText(/●/)).toBeDefined();
94+ });
95+
96+ it("surfaces read errors", async () => {
97+ readFileMock.mockRejectedValue(new Error("not a text file"));
98+ render(<EditorPane path="/w/blob.bin" />);
99+ await waitFor(() =>
100+ expect(screen.getByText("not a text file")).toBeDefined(),
101+ );
102+ });
103+});
new file mode 100644
@@ -0,0 +1,103 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { EditorPane } from "./EditorPane";
4+
5+const { readFileMock, writeFileMock } = vi.hoisted(() => ({
6+ readFileMock: vi.fn(),
7+ writeFileMock: vi.fn(),
8+}));
9+
10+vi.mock("../api", () => ({
11+ readFile: readFileMock,
12+ writeFile: writeFileMock,
13+}));
14+
15+// Monaco is replaced by a plain textarea driving the same onChange contract.
16+vi.mock("./MonacoViewer", () => ({
17+ default: ({
18+ content,
19+ onChange,
20+ }: { content: string; onChange?: (value: string) => void }) => (
21+ <textarea
22+ aria-label="editor"
23+ value={content}
24+ onChange={(event) => onChange?.(event.target.value)}
25+ />
26+ ),
27+}));
28+
29+async function openEditorOn(path: string, content: string) {
30+ readFileMock.mockResolvedValue({ path, content });
31+ render(<EditorPane path={path} />);
32+ return await screen.findByLabelText("editor");
33+}
34+
35+describe("EditorPane", () => {
36+ beforeEach(() => {
37+ readFileMock.mockReset();
38+ writeFileMock.mockReset();
39+ });
40+
41+ it("asks for a file when none is open", () => {
42+ render(<EditorPane path={null} />);
43+ expect(screen.getByText(/Double-click a file/)).toBeDefined();
44+ });
45+
46+ it("loads the file into the editor with Save disabled", async () => {
47+ const editor = await openEditorOn("/w/main.go", "package main");
48+ expect((editor as HTMLTextAreaElement).value).toBe("package main");
49+ expect(
50+ (screen.getByRole("button", { name: "Save" }) as HTMLButtonElement)
51+ .disabled,
52+ ).toBe(true);
53+ });
54+
55+ it("marks the file dirty on edit and saves through the API", async () => {
56+ writeFileMock.mockResolvedValue(undefined);
57+ const editor = await openEditorOn("/w/main.go", "package main");
58+
59+ fireEvent.change(editor, { target: { value: "package main // edited" } });
60+ expect(screen.getByText(/●/)).toBeDefined();
61+
62+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
63+ await waitFor(() =>
64+ expect(writeFileMock).toHaveBeenCalledWith(
65+ "/w/main.go",
66+ "package main // edited",
67+ ),
68+ );
69+ await waitFor(() => expect(screen.getByText("saved")).toBeDefined());
70+ expect(screen.queryByText(/●/)).toBeNull();
71+ });
72+
73+ it("saves with Ctrl+S", async () => {
74+ writeFileMock.mockResolvedValue(undefined);
75+ const editor = await openEditorOn("/w/notes.txt", "a");
76+
77+ fireEvent.change(editor, { target: { value: "ab" } });
78+ fireEvent.keyDown(editor, { key: "s", ctrlKey: true });
79+
80+ await waitFor(() =>
81+ expect(writeFileMock).toHaveBeenCalledWith("/w/notes.txt", "ab"),
82+ );
83+ });
84+
85+ it("keeps the dirty state and shows the error when saving fails", async () => {
86+ writeFileMock.mockRejectedValue(new Error("disk full"));
87+ const editor = await openEditorOn("/w/notes.txt", "a");
88+
89+ fireEvent.change(editor, { target: { value: "ab" } });
90+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
91+
92+ await waitFor(() => expect(screen.getByText("disk full")).toBeDefined());
93+ expect(screen.getByText(/●/)).toBeDefined();
94+ });
95+
96+ it("surfaces read errors", async () => {
97+ readFileMock.mockRejectedValue(new Error("not a text file"));
98+ render(<EditorPane path="/w/blob.bin" />);
99+ await waitFor(() =>
100+ expect(screen.getByText("not a text file")).toBeDefined(),
101+ );
102+ });
103+});
added ui/src/components/EditorPane.tsx +147 -0
new file mode 100644
@@ -0,0 +1,147 @@
1+/**
2+ * EditorPane edits a workspace file with Monaco: dirty indicator, Save
3+ * button and Ctrl/Cmd+S, saving through the backend file API.
4+ *
5+ * @example
6+ * <EditorPane path="src/main.go" />
7+ */
8+
9+import { lazy, Suspense, useCallback, useEffect, useState } from "react";
10+import { readFile, writeFile } from "../api";
11+import { languageForPath } from "../lang";
12+
13+const MonacoViewer = lazy(() => import("./MonacoViewer"));
14+
15+type SaveStatus = "idle" | "saving" | "saved";
16+
17+export function EditorPane({ path }: { path: string | null }) {
18+ const [original, setOriginal] = useState<string | null>(null);
19+ const [text, setText] = useState("");
20+ const [error, setError] = useState<string | null>(null);
21+ const [status, setStatus] = useState<SaveStatus>("idle");
22+
23+ useEffect(() => {
24+ if (!path) {
25+ return;
26+ }
27+ setOriginal(null);
28+ setError(null);
29+ setStatus("idle");
30+ readFile(path)
31+ .then((file) => {
32+ setOriginal(file.content);
33+ setText(file.content);
34+ })
35+ .catch((cause: Error) => setError(cause.message));
36+ }, [path]);
37+
38+ const dirty = original !== null && text !== original;
39+
40+ const save = useCallback(async () => {
41+ if (!path || original === null || text === original) {
42+ return;
43+ }
44+ setStatus("saving");
45+ setError(null);
46+ try {
47+ await writeFile(path, text);
48+ setOriginal(text);
49+ setStatus("saved");
50+ } catch (cause) {
51+ setError((cause as Error).message);
52+ setStatus("idle");
53+ }
54+ }, [path, text, original]);
55+
56+ const placeholder = editorPlaceholder(path, original, error);
57+ if (placeholder) {
58+ return placeholder;
59+ }
60+
61+ return (
62+ // The shortcut is handled on the wrapper so it works wherever the
63+ // focus sits inside the pane, Monaco included.
64+ <div
65+ className="editor"
66+ onKeyDown={(event) => {
67+ if ((event.ctrlKey || event.metaKey) && event.key === "s") {
68+ event.preventDefault();
69+ void save();
70+ }
71+ }}
72+ >
73+ <EditorHeader
74+ path={path as string}
75+ dirty={dirty}
76+ status={status}
77+ error={error}
78+ onSave={save}
79+ />
80+ <div className="editor-body">
81+ <Suspense fallback={<p className="pane-empty">loading editor</p>}>
82+ <MonacoViewer
83+ content={text}
84+ language={languageForPath(path as string)}
85+ onChange={(value) => setText(value ?? "")}
86+ />
87+ </Suspense>
88+ </div>
89+ </div>
90+ );
91+}
92+
93+/** editorPlaceholder covers the no-file / error / loading states. */
94+function editorPlaceholder(
95+ path: string | null,
96+ original: string | null,
97+ error: string | null,
98+) {
99+ if (!path) {
100+ return (
101+ <p className="pane-empty">Double-click a file in the tree to edit it.</p>
102+ );
103+ }
104+ if (error && original === null) {
105+ return <p className="pane-error">{error}</p>;
106+ }
107+ if (original === null) {
108+ return <p className="pane-empty">loading</p>;
109+ }
110+ return null;
111+}
112+
113+/** EditorHeader shows the file name, dirty marker, save state and button. */
114+function EditorHeader({
115+ path,
116+ dirty,
117+ status,
118+ error,
119+ onSave,
120+}: {
121+ path: string;
122+ dirty: boolean;
123+ status: SaveStatus;
124+ error: string | null;
125+ onSave: () => Promise<void>;
126+}) {
127+ return (
128+ <div className="pane-header">
129+ <span className="pane-path" title={path}>
130+ {path.split("/").pop()}
131+ {dirty ? " ●" : ""}
132+ </span>
133+ {error && <span className="pane-error-inline">{error}</span>}
134+ {status === "saved" && !dirty && (
135+ <span className="pane-saved">saved</span>
136+ )}
137+ <button
138+ type="button"
139+ className="pane-action"
140+ disabled={!dirty || status === "saving"}
141+ onClick={() => void onSave()}
142+ >
143+ {status === "saving" ? "Saving…" : "Save"}
144+ </button>
145+ </div>
146+ );
147+}
new file mode 100644
@@ -0,0 +1,147 @@
1+/**
2+ * EditorPane edits a workspace file with Monaco: dirty indicator, Save
3+ * button and Ctrl/Cmd+S, saving through the backend file API.
4+ *
5+ * @example
6+ * <EditorPane path="src/main.go" />
7+ */
8+
9+import { lazy, Suspense, useCallback, useEffect, useState } from "react";
10+import { readFile, writeFile } from "../api";
11+import { languageForPath } from "../lang";
12+
13+const MonacoViewer = lazy(() => import("./MonacoViewer"));
14+
15+type SaveStatus = "idle" | "saving" | "saved";
16+
17+export function EditorPane({ path }: { path: string | null }) {
18+ const [original, setOriginal] = useState<string | null>(null);
19+ const [text, setText] = useState("");
20+ const [error, setError] = useState<string | null>(null);
21+ const [status, setStatus] = useState<SaveStatus>("idle");
22+
23+ useEffect(() => {
24+ if (!path) {
25+ return;
26+ }
27+ setOriginal(null);
28+ setError(null);
29+ setStatus("idle");
30+ readFile(path)
31+ .then((file) => {
32+ setOriginal(file.content);
33+ setText(file.content);
34+ })
35+ .catch((cause: Error) => setError(cause.message));
36+ }, [path]);
37+
38+ const dirty = original !== null && text !== original;
39+
40+ const save = useCallback(async () => {
41+ if (!path || original === null || text === original) {
42+ return;
43+ }
44+ setStatus("saving");
45+ setError(null);
46+ try {
47+ await writeFile(path, text);
48+ setOriginal(text);
49+ setStatus("saved");
50+ } catch (cause) {
51+ setError((cause as Error).message);
52+ setStatus("idle");
53+ }
54+ }, [path, text, original]);
55+
56+ const placeholder = editorPlaceholder(path, original, error);
57+ if (placeholder) {
58+ return placeholder;
59+ }
60+
61+ return (
62+ // The shortcut is handled on the wrapper so it works wherever the
63+ // focus sits inside the pane, Monaco included.
64+ <div
65+ className="editor"
66+ onKeyDown={(event) => {
67+ if ((event.ctrlKey || event.metaKey) && event.key === "s") {
68+ event.preventDefault();
69+ void save();
70+ }
71+ }}
72+ >
73+ <EditorHeader
74+ path={path as string}
75+ dirty={dirty}
76+ status={status}
77+ error={error}
78+ onSave={save}
79+ />
80+ <div className="editor-body">
81+ <Suspense fallback={<p className="pane-empty">loading editor</p>}>
82+ <MonacoViewer
83+ content={text}
84+ language={languageForPath(path as string)}
85+ onChange={(value) => setText(value ?? "")}
86+ />
87+ </Suspense>
88+ </div>
89+ </div>
90+ );
91+}
92+
93+/** editorPlaceholder covers the no-file / error / loading states. */
94+function editorPlaceholder(
95+ path: string | null,
96+ original: string | null,
97+ error: string | null,
98+) {
99+ if (!path) {
100+ return (
101+ <p className="pane-empty">Double-click a file in the tree to edit it.</p>
102+ );
103+ }
104+ if (error && original === null) {
105+ return <p className="pane-error">{error}</p>;
106+ }
107+ if (original === null) {
108+ return <p className="pane-empty">loading</p>;
109+ }
110+ return null;
111+}
112+
113+/** EditorHeader shows the file name, dirty marker, save state and button. */
114+function EditorHeader({
115+ path,
116+ dirty,
117+ status,
118+ error,
119+ onSave,
120+}: {
121+ path: string;
122+ dirty: boolean;
123+ status: SaveStatus;
124+ error: string | null;
125+ onSave: () => Promise<void>;
126+}) {
127+ return (
128+ <div className="pane-header">
129+ <span className="pane-path" title={path}>
130+ {path.split("/").pop()}
131+ {dirty ? " ●" : ""}
132+ </span>
133+ {error && <span className="pane-error-inline">{error}</span>}
134+ {status === "saved" && !dirty && (
135+ <span className="pane-saved">saved</span>
136+ )}
137+ <button
138+ type="button"
139+ className="pane-action"
140+ disabled={!dirty || status === "saving"}
141+ onClick={() => void onSave()}
142+ >
143+ {status === "saving" ? "Saving…" : "Save"}
144+ </button>
145+ </div>
146+ );
147+}
added ui/src/components/FileTree.test.tsx +110 -0
new file mode 100644
@@ -0,0 +1,110 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import type { FileEntry } from "../api";
4+import { resetWorkspace, useWorkspace } from "../workspace";
5+import { FileTree } from "./FileTree";
6+
7+const { listFilesMock } = vi.hoisted(() => ({ listFilesMock: vi.fn() }));
8+
9+vi.mock("../api", () => ({
10+ listFiles: listFilesMock,
11+}));
12+
13+const rootEntries: FileEntry[] = [
14+ { name: "src", path: "/w/src", isDir: true, size: 0 },
15+ { name: "README.md", path: "/w/README.md", isDir: false, size: 7 },
16+];
17+
18+describe("FileTree", () => {
19+ beforeEach(() => {
20+ resetWorkspace();
21+ listFilesMock.mockReset();
22+ });
23+
24+ it("lists the workspace root", async () => {
25+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
26+ render(<FileTree />);
27+
28+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
29+ expect(screen.getByText(/src/)).toBeDefined();
30+ expect(listFilesMock.mock.calls[0]).toEqual([]);
31+ });
32+
33+ it("expands a directory lazily", async () => {
34+ listFilesMock.mockImplementation((path?: string) =>
35+ Promise.resolve(
36+ path === "/w/src"
37+ ? {
38+ path: "/w/src",
39+ entries: [
40+ {
41+ name: "main.go",
42+ path: "/w/src/main.go",
43+ isDir: false,
44+ size: 3,
45+ },
46+ ],
47+ }
48+ : { path: "/w", entries: rootEntries },
49+ ),
50+ );
51+ render(<FileTree />);
52+ await waitFor(() => expect(screen.getByText(/src/)).toBeDefined());
53+
54+ fireEvent.click(screen.getByText(/src/));
55+ await waitFor(() => expect(screen.getByText("main.go")).toBeDefined());
56+ expect(listFilesMock).toHaveBeenCalledWith("/w/src");
57+ });
58+
59+ it("opens a file in preview on click and in editor on double-click", async () => {
60+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
61+ render(<FileTree />);
62+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
63+
64+ fireEvent.click(screen.getByText("README.md"));
65+ expect(useWorkspace.getState()).toMatchObject({
66+ tab: "preview",
67+ previewPath: "/w/README.md",
68+ });
69+
70+ fireEvent.dblClick(screen.getByText("README.md"));
71+ expect(useWorkspace.getState()).toMatchObject({
72+ tab: "editor",
73+ editorPath: "/w/README.md",
74+ });
75+ });
76+
77+ it("shows Material icons for files and directories", async () => {
78+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
79+ const { container } = render(<FileTree />);
80+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
81+
82+ const icons = Array.from(
83+ container.querySelectorAll("img.filetree-icon"),
84+ ).map((img) => img.getAttribute("src"));
85+ expect(icons).toHaveLength(2);
86+ for (const src of icons) {
87+ expect(src).toMatch(/^\/material-icons\/.+\.svg$/);
88+ }
89+ });
90+
91+ it("highlights the file open in the preview", async () => {
92+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
93+ const { container } = render(<FileTree />);
94+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
95+
96+ expect(container.querySelector(".filetree-active")).toBeNull();
97+ fireEvent.click(screen.getByText("README.md"));
98+ await waitFor(() =>
99+ expect(container.querySelector(".filetree-active")).not.toBeNull(),
100+ );
101+ });
102+
103+ it("surfaces API errors", async () => {
104+ listFilesMock.mockRejectedValue(new Error("permission denied"));
105+ render(<FileTree />);
106+ await waitFor(() =>
107+ expect(screen.getByText("permission denied")).toBeDefined(),
108+ );
109+ });
110+});
new file mode 100644
@@ -0,0 +1,110 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import type { FileEntry } from "../api";
4+import { resetWorkspace, useWorkspace } from "../workspace";
5+import { FileTree } from "./FileTree";
6+
7+const { listFilesMock } = vi.hoisted(() => ({ listFilesMock: vi.fn() }));
8+
9+vi.mock("../api", () => ({
10+ listFiles: listFilesMock,
11+}));
12+
13+const rootEntries: FileEntry[] = [
14+ { name: "src", path: "/w/src", isDir: true, size: 0 },
15+ { name: "README.md", path: "/w/README.md", isDir: false, size: 7 },
16+];
17+
18+describe("FileTree", () => {
19+ beforeEach(() => {
20+ resetWorkspace();
21+ listFilesMock.mockReset();
22+ });
23+
24+ it("lists the workspace root", async () => {
25+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
26+ render(<FileTree />);
27+
28+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
29+ expect(screen.getByText(/src/)).toBeDefined();
30+ expect(listFilesMock.mock.calls[0]).toEqual([]);
31+ });
32+
33+ it("expands a directory lazily", async () => {
34+ listFilesMock.mockImplementation((path?: string) =>
35+ Promise.resolve(
36+ path === "/w/src"
37+ ? {
38+ path: "/w/src",
39+ entries: [
40+ {
41+ name: "main.go",
42+ path: "/w/src/main.go",
43+ isDir: false,
44+ size: 3,
45+ },
46+ ],
47+ }
48+ : { path: "/w", entries: rootEntries },
49+ ),
50+ );
51+ render(<FileTree />);
52+ await waitFor(() => expect(screen.getByText(/src/)).toBeDefined());
53+
54+ fireEvent.click(screen.getByText(/src/));
55+ await waitFor(() => expect(screen.getByText("main.go")).toBeDefined());
56+ expect(listFilesMock).toHaveBeenCalledWith("/w/src");
57+ });
58+
59+ it("opens a file in preview on click and in editor on double-click", async () => {
60+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
61+ render(<FileTree />);
62+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
63+
64+ fireEvent.click(screen.getByText("README.md"));
65+ expect(useWorkspace.getState()).toMatchObject({
66+ tab: "preview",
67+ previewPath: "/w/README.md",
68+ });
69+
70+ fireEvent.dblClick(screen.getByText("README.md"));
71+ expect(useWorkspace.getState()).toMatchObject({
72+ tab: "editor",
73+ editorPath: "/w/README.md",
74+ });
75+ });
76+
77+ it("shows Material icons for files and directories", async () => {
78+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
79+ const { container } = render(<FileTree />);
80+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
81+
82+ const icons = Array.from(
83+ container.querySelectorAll("img.filetree-icon"),
84+ ).map((img) => img.getAttribute("src"));
85+ expect(icons).toHaveLength(2);
86+ for (const src of icons) {
87+ expect(src).toMatch(/^\/material-icons\/.+\.svg$/);
88+ }
89+ });
90+
91+ it("highlights the file open in the preview", async () => {
92+ listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries });
93+ const { container } = render(<FileTree />);
94+ await waitFor(() => expect(screen.getByText("README.md")).toBeDefined());
95+
96+ expect(container.querySelector(".filetree-active")).toBeNull();
97+ fireEvent.click(screen.getByText("README.md"));
98+ await waitFor(() =>
99+ expect(container.querySelector(".filetree-active")).not.toBeNull(),
100+ );
101+ });
102+
103+ it("surfaces API errors", async () => {
104+ listFilesMock.mockRejectedValue(new Error("permission denied"));
105+ render(<FileTree />);
106+ await waitFor(() =>
107+ expect(screen.getByText("permission denied")).toBeDefined(),
108+ );
109+ });
110+});
added ui/src/components/FileTree.tsx +161 -0
new file mode 100644
@@ -0,0 +1,161 @@
1+/**
2+ * FileTree lists the workspace files VS Code style: Material icons, chevrons
3+ * on directories, indent guides, and the previewed file highlighted.
4+ * Directories load lazily. Clicking a file opens it in the preview tab;
5+ * double-clicking opens it in the editor.
6+ *
7+ * Icons are the Material Icon Theme SVGs, served from /material-icons
8+ * (copied into public/ by ui/scripts/copy-icons.mjs).
9+ *
10+ * @example
11+ * <FileTree />
12+ */
13+
14+import { useCallback, useEffect, useState } from "react";
15+import {
16+ getIconForDirectoryPath,
17+ getIconForFilePath,
18+ getIconUrlByName,
19+ isMaterialIconName,
20+} from "vscode-material-icons";
21+import { type FileEntry, listFiles } from "../api";
22+import { openEditor, openPreview, useWorkspace } from "../workspace";
23+
24+const ICONS_URL = "/material-icons";
25+
26+export function FileTree() {
27+ const [entries, setEntries] = useState<FileEntry[] | null>(null);
28+ const [error, setError] = useState<string | null>(null);
29+
30+ const refresh = useCallback(() => {
31+ listFiles()
32+ .then((result) => {
33+ setEntries(result.entries);
34+ setError(null);
35+ })
36+ .catch((cause: Error) => setError(cause.message));
37+ }, []);
38+
39+ useEffect(() => {
40+ refresh();
41+ }, [refresh]);
42+
43+ return (
44+ <nav className="filetree" aria-label="workspace files">
45+ <div className="filetree-header">
46+ <span>Files</span>
47+ <button
48+ type="button"
49+ className="filetree-refresh"
50+ onClick={refresh}
51+ title="Refresh"
52+ >
53+
54+ </button>
55+ </div>
56+ {error && <p className="filetree-error">{error}</p>}
57+ {entries === null && !error && (
58+ <p className="filetree-loading">loading</p>
59+ )}
60+ {entries && <EntryList entries={entries} depth={0} />}
61+ </nav>
62+ );
63+}
64+
65+function EntryList({
66+ entries,
67+ depth,
68+}: { entries: FileEntry[]; depth: number }) {
69+ return (
70+ <ul className={`filetree-list${depth > 0 ? " filetree-list-nested" : ""}`}>
71+ {entries.map((entry) => (
72+ <li key={entry.path}>
73+ {entry.isDir ? (
74+ <DirectoryNode entry={entry} depth={depth} />
75+ ) : (
76+ <FileNode entry={entry} depth={depth} />
77+ )}
78+ </li>
79+ ))}
80+ </ul>
81+ );
82+}
83+
84+/** materialIconUrl resolves the Material icon for an entry (open-folder aware). */
85+function materialIconUrl(entry: FileEntry, expanded: boolean): string {
86+ let name = entry.isDir
87+ ? getIconForDirectoryPath(entry.name)
88+ : getIconForFilePath(entry.name);
89+ if (entry.isDir && expanded) {
90+ const openName = `${name}-open`;
91+ if (isMaterialIconName(openName)) {
92+ name = openName;
93+ }
94+ }
95+ return getIconUrlByName(name, ICONS_URL);
96+}
97+
98+function DirectoryNode({ entry, depth }: { entry: FileEntry; depth: number }) {
99+ const [children, setChildren] = useState<FileEntry[] | null>(null);
100+ const [expanded, setExpanded] = useState(false);
101+
102+ const toggle = () => {
103+ const next = !expanded;
104+ setExpanded(next);
105+ if (next && children === null) {
106+ listFiles(entry.path)
107+ .then((result) => setChildren(result.entries))
108+ .catch(() => setChildren([]));
109+ }
110+ };
111+
112+ return (
113+ <>
114+ <button
115+ type="button"
116+ className="filetree-entry filetree-dir"
117+ style={{ paddingLeft: `${depth * 0.75}rem` }}
118+ onClick={toggle}
119+ >
120+ <span className="filetree-chevron" aria-hidden>
121+ {expanded ? "⌄" : "›"}
122+ </span>
123+ <img
124+ className="filetree-icon"
125+ src={materialIconUrl(entry, expanded)}
126+ alt=""
127+ />
128+ <span className="filetree-name">{entry.name}</span>
129+ </button>
130+ {expanded && children && (
131+ <EntryList entries={children} depth={depth + 1} />
132+ )}
133+ </>
134+ );
135+}
136+
137+function FileNode({ entry, depth }: { entry: FileEntry; depth: number }) {
138+ const active = useWorkspace(
139+ (workspace) =>
140+ workspace.previewPath === entry.path ||
141+ workspace.editorPath === entry.path,
142+ );
143+ return (
144+ <button
145+ type="button"
146+ className={`filetree-entry filetree-file${active ? " filetree-active" : ""}`}
147+ style={{ paddingLeft: `${depth * 0.75}rem` }}
148+ title="Click: preview — double-click: edit"
149+ onClick={() => openPreview(entry.path)}
150+ onDoubleClick={() => openEditor(entry.path)}
151+ >
152+ <span className="filetree-chevron" aria-hidden />
153+ <img
154+ className="filetree-icon"
155+ src={materialIconUrl(entry, false)}
156+ alt=""
157+ />
158+ <span className="filetree-name">{entry.name}</span>
159+ </button>
160+ );
161+}
new file mode 100644
@@ -0,0 +1,161 @@
1+/**
2+ * FileTree lists the workspace files VS Code style: Material icons, chevrons
3+ * on directories, indent guides, and the previewed file highlighted.
4+ * Directories load lazily. Clicking a file opens it in the preview tab;
5+ * double-clicking opens it in the editor.
6+ *
7+ * Icons are the Material Icon Theme SVGs, served from /material-icons
8+ * (copied into public/ by ui/scripts/copy-icons.mjs).
9+ *
10+ * @example
11+ * <FileTree />
12+ */
13+
14+import { useCallback, useEffect, useState } from "react";
15+import {
16+ getIconForDirectoryPath,
17+ getIconForFilePath,
18+ getIconUrlByName,
19+ isMaterialIconName,
20+} from "vscode-material-icons";
21+import { type FileEntry, listFiles } from "../api";
22+import { openEditor, openPreview, useWorkspace } from "../workspace";
23+
24+const ICONS_URL = "/material-icons";
25+
26+export function FileTree() {
27+ const [entries, setEntries] = useState<FileEntry[] | null>(null);
28+ const [error, setError] = useState<string | null>(null);
29+
30+ const refresh = useCallback(() => {
31+ listFiles()
32+ .then((result) => {
33+ setEntries(result.entries);
34+ setError(null);
35+ })
36+ .catch((cause: Error) => setError(cause.message));
37+ }, []);
38+
39+ useEffect(() => {
40+ refresh();
41+ }, [refresh]);
42+
43+ return (
44+ <nav className="filetree" aria-label="workspace files">
45+ <div className="filetree-header">
46+ <span>Files</span>
47+ <button
48+ type="button"
49+ className="filetree-refresh"
50+ onClick={refresh}
51+ title="Refresh"
52+ >
53+
54+ </button>
55+ </div>
56+ {error && <p className="filetree-error">{error}</p>}
57+ {entries === null && !error && (
58+ <p className="filetree-loading">loading</p>
59+ )}
60+ {entries && <EntryList entries={entries} depth={0} />}
61+ </nav>
62+ );
63+}
64+
65+function EntryList({
66+ entries,
67+ depth,
68+}: { entries: FileEntry[]; depth: number }) {
69+ return (
70+ <ul className={`filetree-list${depth > 0 ? " filetree-list-nested" : ""}`}>
71+ {entries.map((entry) => (
72+ <li key={entry.path}>
73+ {entry.isDir ? (
74+ <DirectoryNode entry={entry} depth={depth} />
75+ ) : (
76+ <FileNode entry={entry} depth={depth} />
77+ )}
78+ </li>
79+ ))}
80+ </ul>
81+ );
82+}
83+
84+/** materialIconUrl resolves the Material icon for an entry (open-folder aware). */
85+function materialIconUrl(entry: FileEntry, expanded: boolean): string {
86+ let name = entry.isDir
87+ ? getIconForDirectoryPath(entry.name)
88+ : getIconForFilePath(entry.name);
89+ if (entry.isDir && expanded) {
90+ const openName = `${name}-open`;
91+ if (isMaterialIconName(openName)) {
92+ name = openName;
93+ }
94+ }
95+ return getIconUrlByName(name, ICONS_URL);
96+}
97+
98+function DirectoryNode({ entry, depth }: { entry: FileEntry; depth: number }) {
99+ const [children, setChildren] = useState<FileEntry[] | null>(null);
100+ const [expanded, setExpanded] = useState(false);
101+
102+ const toggle = () => {
103+ const next = !expanded;
104+ setExpanded(next);
105+ if (next && children === null) {
106+ listFiles(entry.path)
107+ .then((result) => setChildren(result.entries))
108+ .catch(() => setChildren([]));
109+ }
110+ };
111+
112+ return (
113+ <>
114+ <button
115+ type="button"
116+ className="filetree-entry filetree-dir"
117+ style={{ paddingLeft: `${depth * 0.75}rem` }}
118+ onClick={toggle}
119+ >
120+ <span className="filetree-chevron" aria-hidden>
121+ {expanded ? "⌄" : "›"}
122+ </span>
123+ <img
124+ className="filetree-icon"
125+ src={materialIconUrl(entry, expanded)}
126+ alt=""
127+ />
128+ <span className="filetree-name">{entry.name}</span>
129+ </button>
130+ {expanded && children && (
131+ <EntryList entries={children} depth={depth + 1} />
132+ )}
133+ </>
134+ );
135+}
136+
137+function FileNode({ entry, depth }: { entry: FileEntry; depth: number }) {
138+ const active = useWorkspace(
139+ (workspace) =>
140+ workspace.previewPath === entry.path ||
141+ workspace.editorPath === entry.path,
142+ );
143+ return (
144+ <button
145+ type="button"
146+ className={`filetree-entry filetree-file${active ? " filetree-active" : ""}`}
147+ style={{ paddingLeft: `${depth * 0.75}rem` }}
148+ title="Click: preview — double-click: edit"
149+ onClick={() => openPreview(entry.path)}
150+ onDoubleClick={() => openEditor(entry.path)}
151+ >
152+ <span className="filetree-chevron" aria-hidden />
153+ <img
154+ className="filetree-icon"
155+ src={materialIconUrl(entry, false)}
156+ alt=""
157+ />
158+ <span className="filetree-name">{entry.name}</span>
159+ </button>
160+ );
161+}
added ui/src/components/ImageView.tsx +41 -0
new file mode 100644
@@ -0,0 +1,41 @@
1+/**
2+ * ImageView shows a workspace image (png, jpeg, gif, webp, svg, bmp, ico,
3+ * avif) at its natural size, capped to the pane width, over a checkerboard
4+ * that reveals transparency. The bytes stream from GET /api/raw, and the
5+ * natural dimensions appear once the browser has decoded the image.
6+ *
7+ * @example
8+ * <ImageView path="/work/docs/logo.png" />
9+ */
10+
11+import { useState } from "react";
12+import { rawFileUrl } from "../api";
13+
14+export function ImageView({ path }: { path: string }) {
15+ const [size, setSize] = useState<{ width: number; height: number } | null>(
16+ null,
17+ );
18+ const [failed, setFailed] = useState(false);
19+
20+ if (failed) {
21+ return <p className="pane-error">the image could not be loaded</p>;
22+ }
23+ return (
24+ <figure className="image-view">
25+ <div className="image-view-canvas">
26+ <img
27+ src={rawFileUrl(path)}
28+ alt={path.split("/").pop() ?? path}
29+ onLoad={(event) => {
30+ const img = event.currentTarget;
31+ setSize({ width: img.naturalWidth, height: img.naturalHeight });
32+ }}
33+ onError={() => setFailed(true)}
34+ />
35+ </div>
36+ <figcaption className="image-view-meta">
37+ {size ? `${size.width} × ${size.height} px` : "loading…"}
38+ </figcaption>
39+ </figure>
40+ );
41+}
new file mode 100644
@@ -0,0 +1,41 @@
1+/**
2+ * ImageView shows a workspace image (png, jpeg, gif, webp, svg, bmp, ico,
3+ * avif) at its natural size, capped to the pane width, over a checkerboard
4+ * that reveals transparency. The bytes stream from GET /api/raw, and the
5+ * natural dimensions appear once the browser has decoded the image.
6+ *
7+ * @example
8+ * <ImageView path="/work/docs/logo.png" />
9+ */
10+
11+import { useState } from "react";
12+import { rawFileUrl } from "../api";
13+
14+export function ImageView({ path }: { path: string }) {
15+ const [size, setSize] = useState<{ width: number; height: number } | null>(
16+ null,
17+ );
18+ const [failed, setFailed] = useState(false);
19+
20+ if (failed) {
21+ return <p className="pane-error">the image could not be loaded</p>;
22+ }
23+ return (
24+ <figure className="image-view">
25+ <div className="image-view-canvas">
26+ <img
27+ src={rawFileUrl(path)}
28+ alt={path.split("/").pop() ?? path}
29+ onLoad={(event) => {
30+ const img = event.currentTarget;
31+ setSize({ width: img.naturalWidth, height: img.naturalHeight });
32+ }}
33+ onError={() => setFailed(true)}
34+ />
35+ </div>
36+ <figcaption className="image-view-meta">
37+ {size ? `${size.width} × ${size.height} px` : "loading…"}
38+ </figcaption>
39+ </figure>
40+ );
41+}
added ui/src/components/InlineText.test.tsx +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+import { render, screen } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import { InlineText } from "./InlineText";
4+
5+describe("InlineText", () => {
6+ it("wraps backtick spans in monospace code elements", () => {
7+ const { container } = render(<InlineText text="Run `ls .memory/` now" />);
8+
9+ const code = container.querySelector("code.inline-code");
10+ expect(code?.textContent).toBe("ls .memory/");
11+ expect(container.textContent).toBe("Run ls .memory/ now");
12+ });
13+
14+ it("handles several code spans", () => {
15+ const { container } = render(<InlineText text="`a` puis `b`" />);
16+ const codes = container.querySelectorAll("code.inline-code");
17+ expect(codes).toHaveLength(2);
18+ expect(codes[0].textContent).toBe("a");
19+ expect(codes[1].textContent).toBe("b");
20+ });
21+
22+ it("renders plain text untouched", () => {
23+ const { container } = render(<InlineText text="no code here" />);
24+ expect(container.querySelector("code")).toBeNull();
25+ expect(screen.getByText("no code here")).toBeDefined();
26+ });
27+
28+ it("leaves an unpaired backtick's tail as plain text", () => {
29+ const { container } = render(<InlineText text="odd `tick" />);
30+ // "tick" follows an unclosed backtick: rendered as a code span is
31+ // acceptable, but the full text must survive.
32+ expect(container.textContent).toBe("odd tick");
33+ });
34+});
new file mode 100644
@@ -0,0 +1,34 @@
1+import { render, screen } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import { InlineText } from "./InlineText";
4+
5+describe("InlineText", () => {
6+ it("wraps backtick spans in monospace code elements", () => {
7+ const { container } = render(<InlineText text="Run `ls .memory/` now" />);
8+
9+ const code = container.querySelector("code.inline-code");
10+ expect(code?.textContent).toBe("ls .memory/");
11+ expect(container.textContent).toBe("Run ls .memory/ now");
12+ });
13+
14+ it("handles several code spans", () => {
15+ const { container } = render(<InlineText text="`a` puis `b`" />);
16+ const codes = container.querySelectorAll("code.inline-code");
17+ expect(codes).toHaveLength(2);
18+ expect(codes[0].textContent).toBe("a");
19+ expect(codes[1].textContent).toBe("b");
20+ });
21+
22+ it("renders plain text untouched", () => {
23+ const { container } = render(<InlineText text="no code here" />);
24+ expect(container.querySelector("code")).toBeNull();
25+ expect(screen.getByText("no code here")).toBeDefined();
26+ });
27+
28+ it("leaves an unpaired backtick's tail as plain text", () => {
29+ const { container } = render(<InlineText text="odd `tick" />);
30+ // "tick" follows an unclosed backtick: rendered as a code span is
31+ // acceptable, but the full text must survive.
32+ expect(container.textContent).toBe("odd tick");
33+ });
34+});
added ui/src/components/InlineText.tsx +27 -0
new file mode 100644
@@ -0,0 +1,27 @@
1+/**
2+ * InlineText renders a one-line string in which `backtick` spans become
3+ * monospace <code> elements — for tool call titles, permission titles and
4+ * plan entries, where full markdown would be overkill but agents routinely
5+ * quote commands and paths.
6+ *
7+ * @example
8+ * <InlineText text="Run `ls .memory/`" />
9+ */
10+
11+export function InlineText({ text }: { text: string }) {
12+ // Even segments are plain text, odd segments were between backticks.
13+ const parts = text.split("`");
14+ return (
15+ <>
16+ {parts.map((part, index) =>
17+ index % 2 === 1 ? (
18+ <code key={`${index}:${part}`} className="inline-code">
19+ {part}
20+ </code>
21+ ) : (
22+ part
23+ ),
24+ )}
25+ </>
26+ );
27+}
new file mode 100644
@@ -0,0 +1,27 @@
1+/**
2+ * InlineText renders a one-line string in which `backtick` spans become
3+ * monospace <code> elements — for tool call titles, permission titles and
4+ * plan entries, where full markdown would be overkill but agents routinely
5+ * quote commands and paths.
6+ *
7+ * @example
8+ * <InlineText text="Run `ls .memory/`" />
9+ */
10+
11+export function InlineText({ text }: { text: string }) {
12+ // Even segments are plain text, odd segments were between backticks.
13+ const parts = text.split("`");
14+ return (
15+ <>
16+ {parts.map((part, index) =>
17+ index % 2 === 1 ? (
18+ <code key={`${index}:${part}`} className="inline-code">
19+ {part}
20+ </code>
21+ ) : (
22+ part
23+ ),
24+ )}
25+ </>
26+ );
27+}
added ui/src/components/MonacoViewer.tsx +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+/**
2+ * MonacoViewer wraps the Monaco editor for both the read-only code preview
3+ * and the editable editor pane. It is the only module importing Monaco, and
4+ * is meant to be lazy-loaded (see CodeView / EditorPane).
5+ *
6+ * @example
7+ * <MonacoViewer content="package main" language="go" readOnly />
8+ */
9+
10+import Editor, { type OnChange, type OnMount } from "@monaco-editor/react";
11+import "../monaco-setup";
12+import { useTheme } from "../theme";
13+
14+interface MonacoViewerProps {
15+ content: string;
16+ language: string;
17+ readOnly?: boolean;
18+ /** Called with the new text on every edit (editable mode). */
19+ onChange?: OnChange;
20+ /** Gives access to the editor instance (key bindings, focus). */
21+ onMount?: OnMount;
22+}
23+
24+export default function MonacoViewer({
25+ content,
26+ language,
27+ readOnly = false,
28+ onChange,
29+ onMount,
30+}: MonacoViewerProps) {
31+ // Monaco has its own palettes; pick the one matching the app theme.
32+ const theme =
33+ useTheme((state) => state.theme) === "dark" ? "vs-dark" : "light";
34+ return (
35+ <Editor
36+ height="100%"
37+ language={language}
38+ value={content}
39+ theme={theme}
40+ onChange={onChange}
41+ onMount={onMount}
42+ options={{
43+ readOnly,
44+ minimap: { enabled: false },
45+ scrollBeyondLastLine: false,
46+ fontSize: 13,
47+ automaticLayout: true,
48+ }}
49+ />
50+ );
51+}
new file mode 100644
@@ -0,0 +1,51 @@
1+/**
2+ * MonacoViewer wraps the Monaco editor for both the read-only code preview
3+ * and the editable editor pane. It is the only module importing Monaco, and
4+ * is meant to be lazy-loaded (see CodeView / EditorPane).
5+ *
6+ * @example
7+ * <MonacoViewer content="package main" language="go" readOnly />
8+ */
9+
10+import Editor, { type OnChange, type OnMount } from "@monaco-editor/react";
11+import "../monaco-setup";
12+import { useTheme } from "../theme";
13+
14+interface MonacoViewerProps {
15+ content: string;
16+ language: string;
17+ readOnly?: boolean;
18+ /** Called with the new text on every edit (editable mode). */
19+ onChange?: OnChange;
20+ /** Gives access to the editor instance (key bindings, focus). */
21+ onMount?: OnMount;
22+}
23+
24+export default function MonacoViewer({
25+ content,
26+ language,
27+ readOnly = false,
28+ onChange,
29+ onMount,
30+}: MonacoViewerProps) {
31+ // Monaco has its own palettes; pick the one matching the app theme.
32+ const theme =
33+ useTheme((state) => state.theme) === "dark" ? "vs-dark" : "light";
34+ return (
35+ <Editor
36+ height="100%"
37+ language={language}
38+ value={content}
39+ theme={theme}
40+ onChange={onChange}
41+ onMount={onMount}
42+ options={{
43+ readOnly,
44+ minimap: { enabled: false },
45+ scrollBeyondLastLine: false,
46+ fontSize: 13,
47+ automaticLayout: true,
48+ }}
49+ />
50+ );
51+}
modified ui/src/components/PermissionPrompt.tsx +4 -1
@@ -12,6 +12,7 @@
1212 */
1313
1414 import type { PermissionRequestState } from "../reducer";
15+import { InlineText } from "./InlineText";
1516
1617 interface PermissionPromptProps {
1718 request: PermissionRequestState;
@@ -28,7 +29,9 @@ export function PermissionPrompt({
2829 role="alertdialog"
2930 aria-label="permission request"
3031 >
31- <p className="permission-title">🔐 {request.title}</p>
32+ <p className="permission-title">
33+ 🔐 <InlineText text={request.title} />
34+ </p>
3235 <div className="permission-options">
3336 {request.options.map((option) => (
3437 <button
@@ -12,6 +12,7 @@
12 */12 */
13 13
14 import type { PermissionRequestState } from "../reducer";14 import type { PermissionRequestState } from "../reducer";
15+import { InlineText } from "./InlineText";
15 16
16 interface PermissionPromptProps {17 interface PermissionPromptProps {
17 request: PermissionRequestState;18 request: PermissionRequestState;
@@ -28,7 +29,9 @@ export function PermissionPrompt({
28 role="alertdialog"29 role="alertdialog"
29 aria-label="permission request"30 aria-label="permission request"
30 >31 >
31- <p className="permission-title">🔐 {request.title}</p>32+ <p className="permission-title">
33+ 🔐 <InlineText text={request.title} />
34+ </p>
32 <div className="permission-options">35 <div className="permission-options">
33 {request.options.map((option) => (36 {request.options.map((option) => (
34 <button37 <button
modified ui/src/components/PlanView.tsx +2 -1
@@ -7,6 +7,7 @@
77 */
88
99 import type { AcpPlanEntry } from "../protocol";
10+import { InlineText } from "./InlineText";
1011
1112 const statusMarks: Record<string, string> = {
1213 pending: "○",
@@ -29,7 +30,7 @@ export function PlanView({ entries }: { entries: AcpPlanEntry[] }) {
2930 // biome-ignore lint/suspicious/noArrayIndexKey: ACP plan entries carry no id and the list is replaced wholesale on every update.
3031 <li key={index} className={`plan-entry plan-entry-${entry.status}`}>
3132 <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "}
32- {entry.content}
33+ <InlineText text={entry.content} />
3334 </li>
3435 ))}
3536 </ul>
@@ -7,6 +7,7 @@
7 */7 */
8 8
9 import type { AcpPlanEntry } from "../protocol";9 import type { AcpPlanEntry } from "../protocol";
10+import { InlineText } from "./InlineText";
10 11
11 const statusMarks: Record<string, string> = {12 const statusMarks: Record<string, string> = {
12 pending: "○",13 pending: "○",
@@ -29,7 +30,7 @@ export function PlanView({ entries }: { entries: AcpPlanEntry[] }) {
29 // biome-ignore lint/suspicious/noArrayIndexKey: ACP plan entries carry no id and the list is replaced wholesale on every update.30 // biome-ignore lint/suspicious/noArrayIndexKey: ACP plan entries carry no id and the list is replaced wholesale on every update.
30 <li key={index} className={`plan-entry plan-entry-${entry.status}`}>31 <li key={index} className={`plan-entry plan-entry-${entry.status}`}>
31 <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "}32 <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "}
32- {entry.content}33+ <InlineText text={entry.content} />
33 </li>34 </li>
34 ))}35 ))}
35 </ul>36 </ul>
added ui/src/components/PreviewPane.test.tsx +146 -0
new file mode 100644
@@ -0,0 +1,146 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { resetWorkspace, useWorkspace } from "../workspace";
4+import { PreviewPane } from "./PreviewPane";
5+
6+const { readFileMock } = vi.hoisted(() => ({ readFileMock: vi.fn() }));
7+
8+vi.mock("../api", () => ({
9+ readFile: readFileMock,
10+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
11+}));
12+
13+vi.mock("./DrawioView", () => ({
14+ DrawioView: ({ xml }: { xml: string }) => (
15+ <div data-testid="drawio-view">{xml}</div>
16+ ),
17+}));
18+
19+// The heavy viewers are replaced by inspectable stubs.
20+vi.mock("./CodeView", () => ({
21+ CodeView: ({ content, language }: { content: string; language: string }) => (
22+ <pre data-testid="code-view" data-language={language}>
23+ {content}
24+ </pre>
25+ ),
26+}));
27+
28+vi.mock("./AsciiDocView", () => ({
29+ AsciiDocView: ({ content }: { content: string }) => (
30+ <div data-testid="asciidoc-view">{content}</div>
31+ ),
32+}));
33+
34+describe("PreviewPane", () => {
35+ beforeEach(() => {
36+ resetWorkspace();
37+ readFileMock.mockReset();
38+ });
39+
40+ it("asks for a selection when no file is open", () => {
41+ render(<PreviewPane path={null} />);
42+ expect(screen.getByText(/Select a file/)).toBeDefined();
43+ });
44+
45+ it("previews code with the detected language", async () => {
46+ readFileMock.mockResolvedValue({
47+ path: "/w/main.go",
48+ content: "package main",
49+ });
50+ render(<PreviewPane path="/w/main.go" />);
51+
52+ const view = await screen.findByTestId("code-view");
53+ expect(view.getAttribute("data-language")).toBe("go");
54+ expect(view.textContent).toBe("package main");
55+ // Code has no rendered form: no mode toggle.
56+ expect(screen.queryByRole("button", { name: "Rendered" })).toBeNull();
57+ });
58+
59+ it("renders markdown by default and toggles to source", async () => {
60+ readFileMock.mockResolvedValue({
61+ path: "/w/README.md",
62+ content: "# Title",
63+ });
64+ render(<PreviewPane path="/w/README.md" />);
65+
66+ expect(await screen.findByRole("heading", { name: "Title" })).toBeDefined();
67+
68+ fireEvent.click(screen.getByRole("button", { name: "Source" }));
69+ const view = await screen.findByTestId("code-view");
70+ expect(view.getAttribute("data-language")).toBe("markdown");
71+ expect(view.textContent).toBe("# Title");
72+ });
73+
74+ it("renders asciidoc through the AsciiDoc viewer", async () => {
75+ readFileMock.mockResolvedValue({ path: "/w/book.adoc", content: "= Book" });
76+ render(<PreviewPane path="/w/book.adoc" />);
77+
78+ expect(await screen.findByTestId("asciidoc-view")).toBeDefined();
79+ });
80+
81+ it("sends the file to the editor with the Edit button", async () => {
82+ readFileMock.mockResolvedValue({
83+ path: "/w/main.go",
84+ content: "package main",
85+ });
86+ render(<PreviewPane path="/w/main.go" />);
87+ await screen.findByTestId("code-view");
88+
89+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
90+ expect(useWorkspace.getState()).toMatchObject({
91+ tab: "editor",
92+ editorPath: "/w/main.go",
93+ });
94+ });
95+
96+ it("shows images from the raw endpoint without reading them as text", async () => {
97+ render(<PreviewPane path="/w/docs/logo.png" />);
98+
99+ const img = (await screen.findByRole("img", {
100+ name: "logo.png",
101+ })) as HTMLImageElement;
102+ expect(img.getAttribute("src")).toBe(
103+ "/api/raw?path=%2Fw%2Fdocs%2Flogo.png",
104+ );
105+ expect(readFileMock).not.toHaveBeenCalled();
106+ // Images have no text form: no toggle, no Edit button.
107+ expect(screen.queryByRole("button", { name: "Source" })).toBeNull();
108+ expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
109+
110+ Object.defineProperty(img, "naturalWidth", { value: 640 });
111+ Object.defineProperty(img, "naturalHeight", { value: 480 });
112+ fireEvent.load(img);
113+ expect(screen.getByText("640 × 480 px")).toBeDefined();
114+ });
115+
116+ it("treats .drawio.svg exports as images", async () => {
117+ render(<PreviewPane path="/w/docs/flow.drawio.svg" />);
118+ expect(
119+ await screen.findByRole("img", { name: "flow.drawio.svg" }),
120+ ).toBeDefined();
121+ });
122+
123+ it("renders draw.io diagrams through the viewer and toggles to the XML source", async () => {
124+ readFileMock.mockResolvedValue({
125+ path: "/w/docs/arch.drawio",
126+ content: "<mxfile><diagram/></mxfile>",
127+ });
128+ render(<PreviewPane path="/w/docs/arch.drawio" />);
129+
130+ const view = await screen.findByTestId("drawio-view");
131+ expect(view.textContent).toBe("<mxfile><diagram/></mxfile>");
132+
133+ fireEvent.click(screen.getByRole("button", { name: "Source" }));
134+ const code = await screen.findByTestId("code-view");
135+ expect(code.getAttribute("data-language")).toBe("xml");
136+ expect(code.textContent).toBe("<mxfile><diagram/></mxfile>");
137+ });
138+
139+ it("surfaces read errors", async () => {
140+ readFileMock.mockRejectedValue(new Error("not a text file"));
141+ render(<PreviewPane path="/w/blob.bin" />);
142+ await waitFor(() =>
143+ expect(screen.getByText("not a text file")).toBeDefined(),
144+ );
145+ });
146+});
new file mode 100644
@@ -0,0 +1,146 @@
1+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { resetWorkspace, useWorkspace } from "../workspace";
4+import { PreviewPane } from "./PreviewPane";
5+
6+const { readFileMock } = vi.hoisted(() => ({ readFileMock: vi.fn() }));
7+
8+vi.mock("../api", () => ({
9+ readFile: readFileMock,
10+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
11+}));
12+
13+vi.mock("./DrawioView", () => ({
14+ DrawioView: ({ xml }: { xml: string }) => (
15+ <div data-testid="drawio-view">{xml}</div>
16+ ),
17+}));
18+
19+// The heavy viewers are replaced by inspectable stubs.
20+vi.mock("./CodeView", () => ({
21+ CodeView: ({ content, language }: { content: string; language: string }) => (
22+ <pre data-testid="code-view" data-language={language}>
23+ {content}
24+ </pre>
25+ ),
26+}));
27+
28+vi.mock("./AsciiDocView", () => ({
29+ AsciiDocView: ({ content }: { content: string }) => (
30+ <div data-testid="asciidoc-view">{content}</div>
31+ ),
32+}));
33+
34+describe("PreviewPane", () => {
35+ beforeEach(() => {
36+ resetWorkspace();
37+ readFileMock.mockReset();
38+ });
39+
40+ it("asks for a selection when no file is open", () => {
41+ render(<PreviewPane path={null} />);
42+ expect(screen.getByText(/Select a file/)).toBeDefined();
43+ });
44+
45+ it("previews code with the detected language", async () => {
46+ readFileMock.mockResolvedValue({
47+ path: "/w/main.go",
48+ content: "package main",
49+ });
50+ render(<PreviewPane path="/w/main.go" />);
51+
52+ const view = await screen.findByTestId("code-view");
53+ expect(view.getAttribute("data-language")).toBe("go");
54+ expect(view.textContent).toBe("package main");
55+ // Code has no rendered form: no mode toggle.
56+ expect(screen.queryByRole("button", { name: "Rendered" })).toBeNull();
57+ });
58+
59+ it("renders markdown by default and toggles to source", async () => {
60+ readFileMock.mockResolvedValue({
61+ path: "/w/README.md",
62+ content: "# Title",
63+ });
64+ render(<PreviewPane path="/w/README.md" />);
65+
66+ expect(await screen.findByRole("heading", { name: "Title" })).toBeDefined();
67+
68+ fireEvent.click(screen.getByRole("button", { name: "Source" }));
69+ const view = await screen.findByTestId("code-view");
70+ expect(view.getAttribute("data-language")).toBe("markdown");
71+ expect(view.textContent).toBe("# Title");
72+ });
73+
74+ it("renders asciidoc through the AsciiDoc viewer", async () => {
75+ readFileMock.mockResolvedValue({ path: "/w/book.adoc", content: "= Book" });
76+ render(<PreviewPane path="/w/book.adoc" />);
77+
78+ expect(await screen.findByTestId("asciidoc-view")).toBeDefined();
79+ });
80+
81+ it("sends the file to the editor with the Edit button", async () => {
82+ readFileMock.mockResolvedValue({
83+ path: "/w/main.go",
84+ content: "package main",
85+ });
86+ render(<PreviewPane path="/w/main.go" />);
87+ await screen.findByTestId("code-view");
88+
89+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
90+ expect(useWorkspace.getState()).toMatchObject({
91+ tab: "editor",
92+ editorPath: "/w/main.go",
93+ });
94+ });
95+
96+ it("shows images from the raw endpoint without reading them as text", async () => {
97+ render(<PreviewPane path="/w/docs/logo.png" />);
98+
99+ const img = (await screen.findByRole("img", {
100+ name: "logo.png",
101+ })) as HTMLImageElement;
102+ expect(img.getAttribute("src")).toBe(
103+ "/api/raw?path=%2Fw%2Fdocs%2Flogo.png",
104+ );
105+ expect(readFileMock).not.toHaveBeenCalled();
106+ // Images have no text form: no toggle, no Edit button.
107+ expect(screen.queryByRole("button", { name: "Source" })).toBeNull();
108+ expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
109+
110+ Object.defineProperty(img, "naturalWidth", { value: 640 });
111+ Object.defineProperty(img, "naturalHeight", { value: 480 });
112+ fireEvent.load(img);
113+ expect(screen.getByText("640 × 480 px")).toBeDefined();
114+ });
115+
116+ it("treats .drawio.svg exports as images", async () => {
117+ render(<PreviewPane path="/w/docs/flow.drawio.svg" />);
118+ expect(
119+ await screen.findByRole("img", { name: "flow.drawio.svg" }),
120+ ).toBeDefined();
121+ });
122+
123+ it("renders draw.io diagrams through the viewer and toggles to the XML source", async () => {
124+ readFileMock.mockResolvedValue({
125+ path: "/w/docs/arch.drawio",
126+ content: "<mxfile><diagram/></mxfile>",
127+ });
128+ render(<PreviewPane path="/w/docs/arch.drawio" />);
129+
130+ const view = await screen.findByTestId("drawio-view");
131+ expect(view.textContent).toBe("<mxfile><diagram/></mxfile>");
132+
133+ fireEvent.click(screen.getByRole("button", { name: "Source" }));
134+ const code = await screen.findByTestId("code-view");
135+ expect(code.getAttribute("data-language")).toBe("xml");
136+ expect(code.textContent).toBe("<mxfile><diagram/></mxfile>");
137+ });
138+
139+ it("surfaces read errors", async () => {
140+ readFileMock.mockRejectedValue(new Error("not a text file"));
141+ render(<PreviewPane path="/w/blob.bin" />);
142+ await waitFor(() =>
143+ expect(screen.getByText("not a text file")).toBeDefined(),
144+ );
145+ });
146+});
added ui/src/components/PreviewPane.tsx +159 -0
new file mode 100644
@@ -0,0 +1,159 @@
1+/**
2+ * PreviewPane shows a file from the workspace: rendered Markdown or AsciiDoc
3+ * (with a Rendered/Source toggle), images at natural size, draw.io diagrams
4+ * through the diagrams.net viewer (with the XML as Source), and
5+ * syntax-highlighted read-only code for everything else.
6+ *
7+ * @example
8+ * <PreviewPane path="README.md" />
9+ */
10+
11+import { useEffect, useState } from "react";
12+import { readFile } from "../api";
13+import { type DocumentKind, kindForPath, languageForPath } from "../lang";
14+import { openEditor } from "../workspace";
15+import { AsciiDocView } from "./AsciiDocView";
16+import { CodeView } from "./CodeView";
17+import { DrawioView } from "./DrawioView";
18+import { ImageView } from "./ImageView";
19+import { Markdown } from "./Markdown";
20+
21+type PreviewMode = "rendered" | "source";
22+
23+/** Kinds with both a rendered form and a source form. */
24+const toggleableKinds: ReadonlySet<DocumentKind> = new Set([
25+ "markdown",
26+ "asciidoc",
27+ "drawio",
28+]);
29+
30+/**
31+ * usePreviewFile loads the file behind path (images excepted: the browser
32+ * fetches their bytes itself) and resets the view mode whenever the path
33+ * changes. `content` stays null while loading.
34+ */
35+function usePreviewFile(path: string | null) {
36+ const [content, setContent] = useState<string | null>(null);
37+ const [error, setError] = useState<string | null>(null);
38+ const [mode, setMode] = useState<PreviewMode>("rendered");
39+
40+ useEffect(() => {
41+ if (!path) {
42+ return;
43+ }
44+ setContent(null);
45+ setError(null);
46+ setMode("rendered");
47+ if (kindForPath(path) === "image") {
48+ return;
49+ }
50+ readFile(path)
51+ .then((file) => setContent(file.content))
52+ .catch((cause: Error) => setError(cause.message));
53+ }, [path]);
54+
55+ return { content, error, mode, setMode };
56+}
57+
58+export function PreviewPane({ path }: { path: string | null }) {
59+ const { content, error, mode, setMode } = usePreviewFile(path);
60+ const kind = path ? kindForPath(path) : "code";
61+
62+ if (!path) {
63+ return (
64+ <p className="pane-empty">Select a file in the tree to preview it.</p>
65+ );
66+ }
67+ if (error) {
68+ return <p className="pane-error">{error}</p>;
69+ }
70+ if (kind !== "image" && content === null) {
71+ return <p className="pane-empty">loading</p>;
72+ }
73+
74+ const toggleable = toggleableKinds.has(kind);
75+
76+ return (
77+ <div className="preview">
78+ <div className="pane-header">
79+ <span className="pane-path" title={path}>
80+ {path.split("/").pop()}
81+ </span>
82+ {toggleable && (
83+ <div className="pane-modes">
84+ <ModeButton current={mode} mode="rendered" onSelect={setMode} />
85+ <ModeButton current={mode} mode="source" onSelect={setMode} />
86+ </div>
87+ )}
88+ {kind !== "image" && (
89+ <button
90+ type="button"
91+ className="pane-action"
92+ onClick={() => openEditor(path)}
93+ >
94+ Edit
95+ </button>
96+ )}
97+ </div>
98+ <div className="preview-body">
99+ <PreviewContent
100+ path={path}
101+ kind={kind}
102+ content={content ?? ""}
103+ mode={toggleable ? mode : "rendered"}
104+ />
105+ </div>
106+ </div>
107+ );
108+}
109+
110+function ModeButton({
111+ current,
112+ mode,
113+ onSelect,
114+}: {
115+ current: PreviewMode;
116+ mode: PreviewMode;
117+ onSelect: (mode: PreviewMode) => void;
118+}) {
119+ return (
120+ <button
121+ type="button"
122+ className={`pane-mode${current === mode ? " pane-mode-active" : ""}`}
123+ onClick={() => onSelect(mode)}
124+ >
125+ {mode === "rendered" ? "Rendered" : "Source"}
126+ </button>
127+ );
128+}
129+
130+function PreviewContent({
131+ path,
132+ kind,
133+ content,
134+ mode,
135+}: {
136+ path: string;
137+ kind: DocumentKind;
138+ content: string;
139+ mode: PreviewMode;
140+}) {
141+ if (kind === "image") {
142+ return <ImageView path={path} />;
143+ }
144+ if (mode === "source" || kind === "code") {
145+ return <CodeView content={content} language={languageForPath(path)} />;
146+ }
147+ if (kind === "drawio") {
148+ return <DrawioView xml={content} />;
149+ }
150+ return (
151+ <div className="preview-rendered">
152+ {kind === "markdown" ? (
153+ <Markdown text={content} />
154+ ) : (
155+ <AsciiDocView content={content} />
156+ )}
157+ </div>
158+ );
159+}
new file mode 100644
@@ -0,0 +1,159 @@
1+/**
2+ * PreviewPane shows a file from the workspace: rendered Markdown or AsciiDoc
3+ * (with a Rendered/Source toggle), images at natural size, draw.io diagrams
4+ * through the diagrams.net viewer (with the XML as Source), and
5+ * syntax-highlighted read-only code for everything else.
6+ *
7+ * @example
8+ * <PreviewPane path="README.md" />
9+ */
10+
11+import { useEffect, useState } from "react";
12+import { readFile } from "../api";
13+import { type DocumentKind, kindForPath, languageForPath } from "../lang";
14+import { openEditor } from "../workspace";
15+import { AsciiDocView } from "./AsciiDocView";
16+import { CodeView } from "./CodeView";
17+import { DrawioView } from "./DrawioView";
18+import { ImageView } from "./ImageView";
19+import { Markdown } from "./Markdown";
20+
21+type PreviewMode = "rendered" | "source";
22+
23+/** Kinds with both a rendered form and a source form. */
24+const toggleableKinds: ReadonlySet<DocumentKind> = new Set([
25+ "markdown",
26+ "asciidoc",
27+ "drawio",
28+]);
29+
30+/**
31+ * usePreviewFile loads the file behind path (images excepted: the browser
32+ * fetches their bytes itself) and resets the view mode whenever the path
33+ * changes. `content` stays null while loading.
34+ */
35+function usePreviewFile(path: string | null) {
36+ const [content, setContent] = useState<string | null>(null);
37+ const [error, setError] = useState<string | null>(null);
38+ const [mode, setMode] = useState<PreviewMode>("rendered");
39+
40+ useEffect(() => {
41+ if (!path) {
42+ return;
43+ }
44+ setContent(null);
45+ setError(null);
46+ setMode("rendered");
47+ if (kindForPath(path) === "image") {
48+ return;
49+ }
50+ readFile(path)
51+ .then((file) => setContent(file.content))
52+ .catch((cause: Error) => setError(cause.message));
53+ }, [path]);
54+
55+ return { content, error, mode, setMode };
56+}
57+
58+export function PreviewPane({ path }: { path: string | null }) {
59+ const { content, error, mode, setMode } = usePreviewFile(path);
60+ const kind = path ? kindForPath(path) : "code";
61+
62+ if (!path) {
63+ return (
64+ <p className="pane-empty">Select a file in the tree to preview it.</p>
65+ );
66+ }
67+ if (error) {
68+ return <p className="pane-error">{error}</p>;
69+ }
70+ if (kind !== "image" && content === null) {
71+ return <p className="pane-empty">loading</p>;
72+ }
73+
74+ const toggleable = toggleableKinds.has(kind);
75+
76+ return (
77+ <div className="preview">
78+ <div className="pane-header">
79+ <span className="pane-path" title={path}>
80+ {path.split("/").pop()}
81+ </span>
82+ {toggleable && (
83+ <div className="pane-modes">
84+ <ModeButton current={mode} mode="rendered" onSelect={setMode} />
85+ <ModeButton current={mode} mode="source" onSelect={setMode} />
86+ </div>
87+ )}
88+ {kind !== "image" && (
89+ <button
90+ type="button"
91+ className="pane-action"
92+ onClick={() => openEditor(path)}
93+ >
94+ Edit
95+ </button>
96+ )}
97+ </div>
98+ <div className="preview-body">
99+ <PreviewContent
100+ path={path}
101+ kind={kind}
102+ content={content ?? ""}
103+ mode={toggleable ? mode : "rendered"}
104+ />
105+ </div>
106+ </div>
107+ );
108+}
109+
110+function ModeButton({
111+ current,
112+ mode,
113+ onSelect,
114+}: {
115+ current: PreviewMode;
116+ mode: PreviewMode;
117+ onSelect: (mode: PreviewMode) => void;
118+}) {
119+ return (
120+ <button
121+ type="button"
122+ className={`pane-mode${current === mode ? " pane-mode-active" : ""}`}
123+ onClick={() => onSelect(mode)}
124+ >
125+ {mode === "rendered" ? "Rendered" : "Source"}
126+ </button>
127+ );
128+}
129+
130+function PreviewContent({
131+ path,
132+ kind,
133+ content,
134+ mode,
135+}: {
136+ path: string;
137+ kind: DocumentKind;
138+ content: string;
139+ mode: PreviewMode;
140+}) {
141+ if (kind === "image") {
142+ return <ImageView path={path} />;
143+ }
144+ if (mode === "source" || kind === "code") {
145+ return <CodeView content={content} language={languageForPath(path)} />;
146+ }
147+ if (kind === "drawio") {
148+ return <DrawioView xml={content} />;
149+ }
150+ return (
151+ <div className="preview-rendered">
152+ {kind === "markdown" ? (
153+ <Markdown text={content} />
154+ ) : (
155+ <AsciiDocView content={content} />
156+ )}
157+ </div>
158+ );
159+}
added ui/src/components/PromptInput.test.tsx +224 -0
new file mode 100644
@@ -0,0 +1,224 @@
1+import { act, fireEvent, render, screen } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { PromptInput } from "./PromptInput";
4+
5+const { searchFilesMock, listSkillsMock } = vi.hoisted(() => ({
6+ searchFilesMock: vi.fn(),
7+ listSkillsMock: vi.fn(),
8+}));
9+
10+vi.mock("../api", () => ({
11+ searchFiles: searchFilesMock,
12+ listSkills: listSkillsMock,
13+}));
14+
15+function type(textarea: HTMLTextAreaElement, value: string) {
16+ fireEvent.change(textarea, { target: { value } });
17+}
18+
19+async function settle() {
20+ // The file search is debounced (120 ms); flush timers and promises.
21+ await act(async () => {
22+ await new Promise((resolve) => setTimeout(resolve, 150));
23+ });
24+}
25+
26+describe("PromptInput", () => {
27+ beforeEach(() => {
28+ searchFilesMock.mockReset();
29+ listSkillsMock.mockReset();
30+ searchFilesMock.mockResolvedValue({
31+ root: "/w",
32+ files: [
33+ { name: "main.go", path: "/w/src/main.go", relPath: "src/main.go" },
34+ {
35+ name: "main_test.go",
36+ path: "/w/src/main_test.go",
37+ relPath: "src/main_test.go",
38+ },
39+ ],
40+ });
41+ listSkillsMock.mockResolvedValue({
42+ skills: [
43+ {
44+ name: "quality",
45+ description: "Measure code quality",
46+ path: "/p",
47+ source: "project",
48+ },
49+ ],
50+ });
51+ });
52+
53+ it("sends the trimmed text on Enter and clears the field", () => {
54+ const onSend = vi.fn();
55+ render(
56+ <PromptInput
57+ turnActive={false}
58+ commands={[]}
59+ onSend={onSend}
60+ onCancel={() => {}}
61+ />,
62+ );
63+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
64+
65+ type(input, " hello ");
66+ fireEvent.keyDown(input, { key: "Enter" });
67+ expect(onSend).toHaveBeenCalledWith("hello", []);
68+ expect(input.value).toBe("");
69+ });
70+
71+ it("opens the file popup on @, navigates with arrows and inserts the pick as an attachment", async () => {
72+ const onSend = vi.fn();
73+ render(
74+ <PromptInput
75+ turnActive={false}
76+ commands={[]}
77+ onSend={onSend}
78+ onCancel={() => {}}
79+ />,
80+ );
81+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
82+
83+ type(input, "look at @ma");
84+ await settle();
85+ expect(searchFilesMock).toHaveBeenCalledWith("ma", 30);
86+ const options = screen.getAllByRole("option");
87+ expect(options.map((o) => o.textContent)).toEqual([
88+ "src/main.go",
89+ "src/main_test.go",
90+ ]);
91+ expect(options[0].getAttribute("aria-selected")).toBe("true");
92+
93+ fireEvent.keyDown(input, { key: "ArrowDown" });
94+ expect(screen.getAllByRole("option")[1].getAttribute("aria-selected")).toBe(
95+ "true",
96+ );
97+ fireEvent.keyDown(input, { key: "ArrowUp" });
98+ expect(screen.getAllByRole("option")[0].getAttribute("aria-selected")).toBe(
99+ "true",
100+ );
101+
102+ // Enter picks instead of sending while the popup is open.
103+ fireEvent.keyDown(input, { key: "Enter" });
104+ expect(onSend).not.toHaveBeenCalled();
105+ expect(input.value).toBe("look at @src/main.go ");
106+ expect(screen.queryByRole("listbox")).toBeNull();
107+
108+ type(input, "look at @src/main.go please");
109+ fireEvent.keyDown(input, { key: "Enter" });
110+ expect(onSend).toHaveBeenCalledWith("look at @src/main.go please", [
111+ { path: "/w/src/main.go", name: "src/main.go" },
112+ ]);
113+ });
114+
115+ it("drops an attachment whose mention was deleted before sending", async () => {
116+ const onSend = vi.fn();
117+ render(
118+ <PromptInput
119+ turnActive={false}
120+ commands={[]}
121+ onSend={onSend}
122+ onCancel={() => {}}
123+ />,
124+ );
125+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
126+
127+ type(input, "@ma");
128+ await settle();
129+ fireEvent.keyDown(input, { key: "Tab" });
130+ expect(input.value).toBe("@src/main.go ");
131+
132+ type(input, "no mention any more");
133+ fireEvent.keyDown(input, { key: "Enter" });
134+ expect(onSend).toHaveBeenCalledWith("no mention any more", []);
135+ });
136+
137+ it("closes the popup with Escape and does not reopen for the same trigger", async () => {
138+ render(
139+ <PromptInput
140+ turnActive={false}
141+ commands={[]}
142+ onSend={() => {}}
143+ onCancel={() => {}}
144+ />,
145+ );
146+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
147+
148+ type(input, "@ma");
149+ await settle();
150+ expect(screen.getByRole("listbox")).toBeDefined();
151+ fireEvent.keyDown(input, { key: "Escape" });
152+ expect(screen.queryByRole("listbox")).toBeNull();
153+
154+ type(input, "@mai");
155+ await settle();
156+ expect(screen.queryByRole("listbox")).toBeNull();
157+ });
158+
159+ it("does not open the popup for an @ inside a word", async () => {
160+ render(
161+ <PromptInput
162+ turnActive={false}
163+ commands={[]}
164+ onSend={() => {}}
165+ onCancel={() => {}}
166+ />,
167+ );
168+ type(screen.getByLabelText("prompt") as HTMLTextAreaElement, "me@example");
169+ await settle();
170+ expect(searchFilesMock).not.toHaveBeenCalled();
171+ expect(screen.queryByRole("listbox")).toBeNull();
172+ });
173+
174+ it("opens the skill popup on a leading slash, merging skills and agent commands", async () => {
175+ const onSend = vi.fn();
176+ render(
177+ <PromptInput
178+ turnActive={false}
179+ commands={[{ name: "compact", description: "Summarise the thread" }]}
180+ onSend={onSend}
181+ onCancel={() => {}}
182+ />,
183+ );
184+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
185+
186+ type(input, "/");
187+ await settle();
188+ const options = screen.getAllByRole("option");
189+ expect(
190+ options.map((o) => o.querySelector(".completion-label")?.textContent),
191+ ).toEqual(["compact", "quality"]);
192+ expect(options[0].querySelector(".completion-source")?.textContent).toBe(
193+ "command",
194+ );
195+ expect(options[1].querySelector(".completion-source")?.textContent).toBe(
196+ "skill",
197+ );
198+ expect(screen.getByText("Measure code quality")).toBeDefined();
199+
200+ type(input, "/qua");
201+ expect(screen.getAllByRole("option")).toHaveLength(1);
202+
203+ fireEvent.mouseDown(screen.getByRole("option"));
204+ expect(input.value).toBe("/quality ");
205+
206+ type(input, "/quality src");
207+ fireEvent.keyDown(input, { key: "Enter" });
208+ expect(onSend).toHaveBeenCalledWith("/quality src", []);
209+ });
210+
211+ it("shows Stop instead of Send while a turn runs", () => {
212+ const onCancel = vi.fn();
213+ render(
214+ <PromptInput
215+ turnActive={true}
216+ commands={[]}
217+ onSend={() => {}}
218+ onCancel={onCancel}
219+ />,
220+ );
221+ fireEvent.click(screen.getByRole("button", { name: "Stop" }));
222+ expect(onCancel).toHaveBeenCalled();
223+ });
224+});
new file mode 100644
@@ -0,0 +1,224 @@
1+import { act, fireEvent, render, screen } from "@testing-library/react";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { PromptInput } from "./PromptInput";
4+
5+const { searchFilesMock, listSkillsMock } = vi.hoisted(() => ({
6+ searchFilesMock: vi.fn(),
7+ listSkillsMock: vi.fn(),
8+}));
9+
10+vi.mock("../api", () => ({
11+ searchFiles: searchFilesMock,
12+ listSkills: listSkillsMock,
13+}));
14+
15+function type(textarea: HTMLTextAreaElement, value: string) {
16+ fireEvent.change(textarea, { target: { value } });
17+}
18+
19+async function settle() {
20+ // The file search is debounced (120 ms); flush timers and promises.
21+ await act(async () => {
22+ await new Promise((resolve) => setTimeout(resolve, 150));
23+ });
24+}
25+
26+describe("PromptInput", () => {
27+ beforeEach(() => {
28+ searchFilesMock.mockReset();
29+ listSkillsMock.mockReset();
30+ searchFilesMock.mockResolvedValue({
31+ root: "/w",
32+ files: [
33+ { name: "main.go", path: "/w/src/main.go", relPath: "src/main.go" },
34+ {
35+ name: "main_test.go",
36+ path: "/w/src/main_test.go",
37+ relPath: "src/main_test.go",
38+ },
39+ ],
40+ });
41+ listSkillsMock.mockResolvedValue({
42+ skills: [
43+ {
44+ name: "quality",
45+ description: "Measure code quality",
46+ path: "/p",
47+ source: "project",
48+ },
49+ ],
50+ });
51+ });
52+
53+ it("sends the trimmed text on Enter and clears the field", () => {
54+ const onSend = vi.fn();
55+ render(
56+ <PromptInput
57+ turnActive={false}
58+ commands={[]}
59+ onSend={onSend}
60+ onCancel={() => {}}
61+ />,
62+ );
63+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
64+
65+ type(input, " hello ");
66+ fireEvent.keyDown(input, { key: "Enter" });
67+ expect(onSend).toHaveBeenCalledWith("hello", []);
68+ expect(input.value).toBe("");
69+ });
70+
71+ it("opens the file popup on @, navigates with arrows and inserts the pick as an attachment", async () => {
72+ const onSend = vi.fn();
73+ render(
74+ <PromptInput
75+ turnActive={false}
76+ commands={[]}
77+ onSend={onSend}
78+ onCancel={() => {}}
79+ />,
80+ );
81+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
82+
83+ type(input, "look at @ma");
84+ await settle();
85+ expect(searchFilesMock).toHaveBeenCalledWith("ma", 30);
86+ const options = screen.getAllByRole("option");
87+ expect(options.map((o) => o.textContent)).toEqual([
88+ "src/main.go",
89+ "src/main_test.go",
90+ ]);
91+ expect(options[0].getAttribute("aria-selected")).toBe("true");
92+
93+ fireEvent.keyDown(input, { key: "ArrowDown" });
94+ expect(screen.getAllByRole("option")[1].getAttribute("aria-selected")).toBe(
95+ "true",
96+ );
97+ fireEvent.keyDown(input, { key: "ArrowUp" });
98+ expect(screen.getAllByRole("option")[0].getAttribute("aria-selected")).toBe(
99+ "true",
100+ );
101+
102+ // Enter picks instead of sending while the popup is open.
103+ fireEvent.keyDown(input, { key: "Enter" });
104+ expect(onSend).not.toHaveBeenCalled();
105+ expect(input.value).toBe("look at @src/main.go ");
106+ expect(screen.queryByRole("listbox")).toBeNull();
107+
108+ type(input, "look at @src/main.go please");
109+ fireEvent.keyDown(input, { key: "Enter" });
110+ expect(onSend).toHaveBeenCalledWith("look at @src/main.go please", [
111+ { path: "/w/src/main.go", name: "src/main.go" },
112+ ]);
113+ });
114+
115+ it("drops an attachment whose mention was deleted before sending", async () => {
116+ const onSend = vi.fn();
117+ render(
118+ <PromptInput
119+ turnActive={false}
120+ commands={[]}
121+ onSend={onSend}
122+ onCancel={() => {}}
123+ />,
124+ );
125+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
126+
127+ type(input, "@ma");
128+ await settle();
129+ fireEvent.keyDown(input, { key: "Tab" });
130+ expect(input.value).toBe("@src/main.go ");
131+
132+ type(input, "no mention any more");
133+ fireEvent.keyDown(input, { key: "Enter" });
134+ expect(onSend).toHaveBeenCalledWith("no mention any more", []);
135+ });
136+
137+ it("closes the popup with Escape and does not reopen for the same trigger", async () => {
138+ render(
139+ <PromptInput
140+ turnActive={false}
141+ commands={[]}
142+ onSend={() => {}}
143+ onCancel={() => {}}
144+ />,
145+ );
146+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
147+
148+ type(input, "@ma");
149+ await settle();
150+ expect(screen.getByRole("listbox")).toBeDefined();
151+ fireEvent.keyDown(input, { key: "Escape" });
152+ expect(screen.queryByRole("listbox")).toBeNull();
153+
154+ type(input, "@mai");
155+ await settle();
156+ expect(screen.queryByRole("listbox")).toBeNull();
157+ });
158+
159+ it("does not open the popup for an @ inside a word", async () => {
160+ render(
161+ <PromptInput
162+ turnActive={false}
163+ commands={[]}
164+ onSend={() => {}}
165+ onCancel={() => {}}
166+ />,
167+ );
168+ type(screen.getByLabelText("prompt") as HTMLTextAreaElement, "me@example");
169+ await settle();
170+ expect(searchFilesMock).not.toHaveBeenCalled();
171+ expect(screen.queryByRole("listbox")).toBeNull();
172+ });
173+
174+ it("opens the skill popup on a leading slash, merging skills and agent commands", async () => {
175+ const onSend = vi.fn();
176+ render(
177+ <PromptInput
178+ turnActive={false}
179+ commands={[{ name: "compact", description: "Summarise the thread" }]}
180+ onSend={onSend}
181+ onCancel={() => {}}
182+ />,
183+ );
184+ const input = screen.getByLabelText("prompt") as HTMLTextAreaElement;
185+
186+ type(input, "/");
187+ await settle();
188+ const options = screen.getAllByRole("option");
189+ expect(
190+ options.map((o) => o.querySelector(".completion-label")?.textContent),
191+ ).toEqual(["compact", "quality"]);
192+ expect(options[0].querySelector(".completion-source")?.textContent).toBe(
193+ "command",
194+ );
195+ expect(options[1].querySelector(".completion-source")?.textContent).toBe(
196+ "skill",
197+ );
198+ expect(screen.getByText("Measure code quality")).toBeDefined();
199+
200+ type(input, "/qua");
201+ expect(screen.getAllByRole("option")).toHaveLength(1);
202+
203+ fireEvent.mouseDown(screen.getByRole("option"));
204+ expect(input.value).toBe("/quality ");
205+
206+ type(input, "/quality src");
207+ fireEvent.keyDown(input, { key: "Enter" });
208+ expect(onSend).toHaveBeenCalledWith("/quality src", []);
209+ });
210+
211+ it("shows Stop instead of Send while a turn runs", () => {
212+ const onCancel = vi.fn();
213+ render(
214+ <PromptInput
215+ turnActive={true}
216+ commands={[]}
217+ onSend={() => {}}
218+ onCancel={onCancel}
219+ />,
220+ );
221+ fireEvent.click(screen.getByRole("button", { name: "Stop" }));
222+ expect(onCancel).toHaveBeenCalled();
223+ });
224+});
modified ui/src/components/PromptInput.tsx +130 -18
@@ -1,34 +1,98 @@
11 /**
22 * PromptInput is the message composer: Enter sends (Shift+Enter for a new
33 * line); while a turn runs the send button becomes a stop button, matching
4- * the behaviour of Zed's agent panel.
4+ * the behaviour of Zed's agent panel. Typing "@" opens the workspace file
5+ * selector (the pick is sent as an ACP resource_link attachment) and a
6+ * leading "/" opens the skill / command selector.
57 *
68 * @example
7- * <PromptInput turnActive={false} onSend={(t) => console.log(t)} onCancel={() => {}} />
9+ * <PromptInput turnActive={false} commands={[]} onSend={(t, files) => …} onCancel={() => {}} />
810 */
911
10-import { useState } from "react";
12+import { type KeyboardEvent, useEffect, useRef, useState } from "react";
13+import {
14+ type CompletionItem,
15+ activeAttachments,
16+ applyCompletion,
17+} from "../mentions";
18+import type { AcpAvailableCommand, PromptAttachment } from "../protocol";
19+import { useCompletion } from "../useCompletion";
20+import { CompletionPopup } from "./CompletionPopup";
1121
1222 interface PromptInputProps {
1323 turnActive: boolean;
14- onSend: (text: string) => void;
24+ /** Slash commands announced by the agent, merged into the "/" popup. */
25+ commands: AcpAvailableCommand[];
26+ onSend: (text: string, attachments: PromptAttachment[]) => void;
1527 onCancel: () => void;
1628 }
1729
1830 export function PromptInput({
1931 turnActive,
32+ commands,
2033 onSend,
2134 onCancel,
2235 }: PromptInputProps) {
2336 const [text, setText] = useState("");
37+ const [caret, setCaret] = useState(0);
38+ const [attachments, setAttachments] = useState<PromptAttachment[]>([]);
39+ const textareaRef = useRef<HTMLTextAreaElement>(null);
40+ const pendingCaret = useRef<number | null>(null);
41+ const completion = useCompletion({ text, caret, commands });
42+
43+ // After a completion the caret must land right after the insertion; the
44+ // pending value is consumed on the render that follows the text update.
45+ useEffect(() => {
46+ if (pendingCaret.current !== null && textareaRef.current) {
47+ textareaRef.current.setSelectionRange(
48+ pendingCaret.current,
49+ pendingCaret.current,
50+ );
51+ pendingCaret.current = null;
52+ }
53+ });
54+
55+ const syncCaret = () => {
56+ setCaret(textareaRef.current?.selectionStart ?? text.length);
57+ };
58+
59+ const pick = (item: CompletionItem) => {
60+ if (!completion.trigger) {
61+ return;
62+ }
63+ const next = applyCompletion(text, completion.trigger, item.insert);
64+ setText(next.text);
65+ setCaret(next.caret);
66+ pendingCaret.current = next.caret;
67+ if (item.attachment) {
68+ setAttachments((current) => [
69+ ...current,
70+ item.attachment as PromptAttachment,
71+ ]);
72+ }
73+ textareaRef.current?.focus();
74+ };
2475
2576 const submit = () => {
2677 const trimmed = text.trim();
2778 if (trimmed === "" || turnActive) {
2879 return;
2980 }
30- onSend(trimmed);
81+ onSend(trimmed, activeAttachments(trimmed, attachments));
3182 setText("");
83+ setCaret(0);
84+ setAttachments([]);
85+ };
86+
87+ const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
88+ if (completion.open && handleCompletionKey(event, completion, pick)) {
89+ event.preventDefault();
90+ return;
91+ }
92+ if (event.key === "Enter" && !event.shiftKey) {
93+ event.preventDefault();
94+ submit();
95+ }
3296 };
3397
3498 return (
@@ -39,20 +103,36 @@ export function PromptInput({
39103 submit();
40104 }}
41105 >
42- <textarea
43- className="composer-input"
44- placeholder="Message the agent…"
45- aria-label="prompt"
46- value={text}
47- rows={3}
48- onChange={(event) => setText(event.target.value)}
49- onKeyDown={(event) => {
50- if (event.key === "Enter" && !event.shiftKey) {
51- event.preventDefault();
52- submit();
106+ <div className="composer-field">
107+ {completion.open && (
108+ <CompletionPopup
109+ items={completion.items}
110+ active={completion.active}
111+ onHover={completion.setActive}
112+ onPick={pick}
113+ />
114+ )}
115+ <textarea
116+ ref={textareaRef}
117+ className="composer-input"
118+ placeholder="Message the agent… (@ file, / skill)"
119+ aria-label="prompt"
120+ aria-autocomplete="list"
121+ aria-expanded={completion.open}
122+ aria-activedescendant={
123+ completion.open ? `completion-${completion.active}` : undefined
53124 }
54- }}
55- />
125+ value={text}
126+ rows={3}
127+ onChange={(event) => {
128+ setText(event.target.value);
129+ setCaret(event.target.selectionStart);
130+ }}
131+ onKeyDown={onKeyDown}
132+ onKeyUp={syncCaret}
133+ onClick={syncCaret}
134+ />
135+ </div>
56136 {turnActive ? (
57137 <button type="button" className="composer-stop" onClick={onCancel}>
58138 Stop
@@ -69,3 +149,35 @@ export function PromptInput({
69149 </form>
70150 );
71151 }
152+
153+/**
154+ * handleCompletionKey applies the popup's keyboard contract and tells
155+ * whether the key was consumed: arrows move, Enter/Tab pick, Escape closes.
156+ */
157+function handleCompletionKey(
158+ event: KeyboardEvent<HTMLTextAreaElement>,
159+ completion: ReturnType<typeof useCompletion>,
160+ pick: (item: CompletionItem) => void,
161+): boolean {
162+ switch (event.key) {
163+ case "ArrowDown":
164+ completion.moveActive(1);
165+ return true;
166+ case "ArrowUp":
167+ completion.moveActive(-1);
168+ return true;
169+ case "Enter":
170+ case "Tab": {
171+ const item = completion.items[completion.active];
172+ if (item) {
173+ pick(item);
174+ }
175+ return true;
176+ }
177+ case "Escape":
178+ completion.dismiss();
179+ return true;
180+ default:
181+ return false;
182+ }
183+}
@@ -1,34 +1,98 @@
1 /**1 /**
2 * PromptInput is the message composer: Enter sends (Shift+Enter for a new2 * PromptInput is the message composer: Enter sends (Shift+Enter for a new
3 * line); while a turn runs the send button becomes a stop button, matching3 * line); while a turn runs the send button becomes a stop button, matching
4- * the behaviour of Zed's agent panel.4+ * the behaviour of Zed's agent panel. Typing "@" opens the workspace file
5+ * selector (the pick is sent as an ACP resource_link attachment) and a
6+ * leading "/" opens the skill / command selector.
5 *7 *
6 * @example8 * @example
7- * <PromptInput turnActive={false} onSend={(t) => console.log(t)} onCancel={() => {}} />9+ * <PromptInput turnActive={false} commands={[]} onSend={(t, files) => …} onCancel={() => {}} />
8 */10 */
9 11
10-import { useState } from "react";12+import { type KeyboardEvent, useEffect, useRef, useState } from "react";
13+import {
14+ type CompletionItem,
15+ activeAttachments,
16+ applyCompletion,
17+} from "../mentions";
18+import type { AcpAvailableCommand, PromptAttachment } from "../protocol";
19+import { useCompletion } from "../useCompletion";
20+import { CompletionPopup } from "./CompletionPopup";
11 21
12 interface PromptInputProps {22 interface PromptInputProps {
13 turnActive: boolean;23 turnActive: boolean;
14- onSend: (text: string) => void;24+ /** Slash commands announced by the agent, merged into the "/" popup. */
25+ commands: AcpAvailableCommand[];
26+ onSend: (text: string, attachments: PromptAttachment[]) => void;
15 onCancel: () => void;27 onCancel: () => void;
16 }28 }
17 29
18 export function PromptInput({30 export function PromptInput({
19 turnActive,31 turnActive,
32+ commands,
20 onSend,33 onSend,
21 onCancel,34 onCancel,
22 }: PromptInputProps) {35 }: PromptInputProps) {
23 const [text, setText] = useState("");36 const [text, setText] = useState("");
37+ const [caret, setCaret] = useState(0);
38+ const [attachments, setAttachments] = useState<PromptAttachment[]>([]);
39+ const textareaRef = useRef<HTMLTextAreaElement>(null);
40+ const pendingCaret = useRef<number | null>(null);
41+ const completion = useCompletion({ text, caret, commands });
42+
43+ // After a completion the caret must land right after the insertion; the
44+ // pending value is consumed on the render that follows the text update.
45+ useEffect(() => {
46+ if (pendingCaret.current !== null && textareaRef.current) {
47+ textareaRef.current.setSelectionRange(
48+ pendingCaret.current,
49+ pendingCaret.current,
50+ );
51+ pendingCaret.current = null;
52+ }
53+ });
54+
55+ const syncCaret = () => {
56+ setCaret(textareaRef.current?.selectionStart ?? text.length);
57+ };
58+
59+ const pick = (item: CompletionItem) => {
60+ if (!completion.trigger) {
61+ return;
62+ }
63+ const next = applyCompletion(text, completion.trigger, item.insert);
64+ setText(next.text);
65+ setCaret(next.caret);
66+ pendingCaret.current = next.caret;
67+ if (item.attachment) {
68+ setAttachments((current) => [
69+ ...current,
70+ item.attachment as PromptAttachment,
71+ ]);
72+ }
73+ textareaRef.current?.focus();
74+ };
24 75
25 const submit = () => {76 const submit = () => {
26 const trimmed = text.trim();77 const trimmed = text.trim();
27 if (trimmed === "" || turnActive) {78 if (trimmed === "" || turnActive) {
28 return;79 return;
29 }80 }
30- onSend(trimmed);81+ onSend(trimmed, activeAttachments(trimmed, attachments));
31 setText("");82 setText("");
83+ setCaret(0);
84+ setAttachments([]);
85+ };
86+
87+ const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
88+ if (completion.open && handleCompletionKey(event, completion, pick)) {
89+ event.preventDefault();
90+ return;
91+ }
92+ if (event.key === "Enter" && !event.shiftKey) {
93+ event.preventDefault();
94+ submit();
95+ }
32 };96 };
33 97
34 return (98 return (
@@ -39,20 +103,36 @@ export function PromptInput({
39 submit();103 submit();
40 }}104 }}
41 >105 >
42- <textarea106+ <div className="composer-field">
43- className="composer-input"107+ {completion.open && (
44- placeholder="Message the agent…"108+ <CompletionPopup
45- aria-label="prompt"109+ items={completion.items}
46- value={text}110+ active={completion.active}
47- rows={3}111+ onHover={completion.setActive}
48- onChange={(event) => setText(event.target.value)}112+ onPick={pick}
49- onKeyDown={(event) => {113+ />
50- if (event.key === "Enter" && !event.shiftKey) {114+ )}
51- event.preventDefault();115+ <textarea
52- submit();116+ ref={textareaRef}
117+ className="composer-input"
118+ placeholder="Message the agent… (@ file, / skill)"
119+ aria-label="prompt"
120+ aria-autocomplete="list"
121+ aria-expanded={completion.open}
122+ aria-activedescendant={
123+ completion.open ? `completion-${completion.active}` : undefined
53 }124 }
54- }}125+ value={text}
55- />126+ rows={3}
127+ onChange={(event) => {
128+ setText(event.target.value);
129+ setCaret(event.target.selectionStart);
130+ }}
131+ onKeyDown={onKeyDown}
132+ onKeyUp={syncCaret}
133+ onClick={syncCaret}
134+ />
135+ </div>
56 {turnActive ? (136 {turnActive ? (
57 <button type="button" className="composer-stop" onClick={onCancel}>137 <button type="button" className="composer-stop" onClick={onCancel}>
58 Stop138 Stop
@@ -69,3 +149,35 @@ export function PromptInput({
69 </form>149 </form>
70 );150 );
71 }151 }
152+
153+/**
154+ * handleCompletionKey applies the popup's keyboard contract and tells
155+ * whether the key was consumed: arrows move, Enter/Tab pick, Escape closes.
156+ */
157+function handleCompletionKey(
158+ event: KeyboardEvent<HTMLTextAreaElement>,
159+ completion: ReturnType<typeof useCompletion>,
160+ pick: (item: CompletionItem) => void,
161+): boolean {
162+ switch (event.key) {
163+ case "ArrowDown":
164+ completion.moveActive(1);
165+ return true;
166+ case "ArrowUp":
167+ completion.moveActive(-1);
168+ return true;
169+ case "Enter":
170+ case "Tab": {
171+ const item = completion.items[completion.active];
172+ if (item) {
173+ pick(item);
174+ }
175+ return true;
176+ }
177+ case "Escape":
178+ completion.dismiss();
179+ return true;
180+ default:
181+ return false;
182+ }
183+}
added ui/src/components/ResizeHandle.test.tsx +115 -0
new file mode 100644
@@ -0,0 +1,115 @@
1+import { fireEvent, render, screen } from "@testing-library/react";
2+import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3+import {
4+ KEYBOARD_RESIZE_STEP,
5+ ResizeHandle,
6+ widthAfterKey,
7+} from "./ResizeHandle";
8+
9+// jsdom has no PointerEvent; a MouseEvent carrying a pointerId is enough for
10+// React's pointer handlers and gives the tests real clientX values.
11+beforeAll(() => {
12+ if (typeof window.PointerEvent === "undefined") {
13+ class PointerEventPolyfill extends MouseEvent {
14+ readonly pointerId: number;
15+ constructor(type: string, init: PointerEventInit = {}) {
16+ super(type, init);
17+ this.pointerId = init.pointerId ?? 0;
18+ }
19+ }
20+ Object.defineProperty(window, "PointerEvent", {
21+ value: PointerEventPolyfill,
22+ configurable: true,
23+ });
24+ }
25+});
26+
27+const onResize = vi.fn();
28+const onReset = vi.fn();
29+
30+function renderHandle(width = 200) {
31+ render(
32+ <ResizeHandle
33+ label="Resize the file tree"
34+ width={width}
35+ min={120}
36+ max={800}
37+ onResize={onResize}
38+ onReset={onReset}
39+ />,
40+ );
41+ return screen.getByRole("separator", { name: "Resize the file tree" });
42+}
43+
44+describe("ResizeHandle", () => {
45+ beforeEach(() => {
46+ onResize.mockReset();
47+ onReset.mockReset();
48+ });
49+
50+ it("exposes the width through the separator pattern", () => {
51+ const handle = renderHandle(300);
52+ expect(handle.getAttribute("aria-orientation")).toBe("vertical");
53+ expect(handle.getAttribute("aria-valuenow")).toBe("300");
54+ expect(handle.getAttribute("aria-valuemin")).toBe("120");
55+ expect(handle.getAttribute("aria-valuemax")).toBe("800");
56+ });
57+
58+ it("reports the width while dragging and stops on pointer up", () => {
59+ const handle = renderHandle(200);
60+ fireEvent.pointerDown(handle, { clientX: 500, pointerId: 1 });
61+ expect(handle.classList.contains("resize-handle-active")).toBe(true);
62+
63+ fireEvent.pointerMove(handle, { clientX: 540, pointerId: 1 });
64+ fireEvent.pointerMove(handle, { clientX: 470, pointerId: 1 });
65+ expect(onResize.mock.calls).toEqual([[240], [170]]);
66+
67+ fireEvent.pointerUp(handle, { clientX: 470, pointerId: 1 });
68+ expect(handle.classList.contains("resize-handle-active")).toBe(false);
69+ fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 });
70+ expect(onResize).toHaveBeenCalledTimes(2);
71+ });
72+
73+ it("ignores pointer moves that did not start on the handle", () => {
74+ const handle = renderHandle(200);
75+ fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 });
76+ expect(onResize).not.toHaveBeenCalled();
77+ });
78+
79+ it("resizes step by step with the arrow keys and jumps with Home / End", () => {
80+ const handle = renderHandle(200);
81+ fireEvent.keyDown(handle, { key: "ArrowRight" });
82+ fireEvent.keyDown(handle, { key: "ArrowLeft" });
83+ fireEvent.keyDown(handle, { key: "Home" });
84+ fireEvent.keyDown(handle, { key: "End" });
85+ expect(onResize.mock.calls).toEqual([
86+ [200 + KEYBOARD_RESIZE_STEP],
87+ [200 - KEYBOARD_RESIZE_STEP],
88+ [120],
89+ [800],
90+ ]);
91+ });
92+
93+ it("leaves other keys alone", () => {
94+ const handle = renderHandle(200);
95+ fireEvent.keyDown(handle, { key: "Enter" });
96+ fireEvent.keyDown(handle, { key: "Tab" });
97+ expect(onResize).not.toHaveBeenCalled();
98+ });
99+
100+ it("resets on double-click", () => {
101+ const handle = renderHandle(200);
102+ fireEvent.doubleClick(handle);
103+ expect(onReset).toHaveBeenCalledTimes(1);
104+ });
105+});
106+
107+describe("widthAfterKey", () => {
108+ it("maps the resize keys and returns null otherwise", () => {
109+ expect(widthAfterKey("ArrowRight", 200, 120, 800)).toBe(216);
110+ expect(widthAfterKey("ArrowLeft", 200, 120, 800)).toBe(184);
111+ expect(widthAfterKey("Home", 200, 120, 800)).toBe(120);
112+ expect(widthAfterKey("End", 200, 120, 800)).toBe(800);
113+ expect(widthAfterKey("a", 200, 120, 800)).toBeNull();
114+ });
115+});
new file mode 100644
@@ -0,0 +1,115 @@
1+import { fireEvent, render, screen } from "@testing-library/react";
2+import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3+import {
4+ KEYBOARD_RESIZE_STEP,
5+ ResizeHandle,
6+ widthAfterKey,
7+} from "./ResizeHandle";
8+
9+// jsdom has no PointerEvent; a MouseEvent carrying a pointerId is enough for
10+// React's pointer handlers and gives the tests real clientX values.
11+beforeAll(() => {
12+ if (typeof window.PointerEvent === "undefined") {
13+ class PointerEventPolyfill extends MouseEvent {
14+ readonly pointerId: number;
15+ constructor(type: string, init: PointerEventInit = {}) {
16+ super(type, init);
17+ this.pointerId = init.pointerId ?? 0;
18+ }
19+ }
20+ Object.defineProperty(window, "PointerEvent", {
21+ value: PointerEventPolyfill,
22+ configurable: true,
23+ });
24+ }
25+});
26+
27+const onResize = vi.fn();
28+const onReset = vi.fn();
29+
30+function renderHandle(width = 200) {
31+ render(
32+ <ResizeHandle
33+ label="Resize the file tree"
34+ width={width}
35+ min={120}
36+ max={800}
37+ onResize={onResize}
38+ onReset={onReset}
39+ />,
40+ );
41+ return screen.getByRole("separator", { name: "Resize the file tree" });
42+}
43+
44+describe("ResizeHandle", () => {
45+ beforeEach(() => {
46+ onResize.mockReset();
47+ onReset.mockReset();
48+ });
49+
50+ it("exposes the width through the separator pattern", () => {
51+ const handle = renderHandle(300);
52+ expect(handle.getAttribute("aria-orientation")).toBe("vertical");
53+ expect(handle.getAttribute("aria-valuenow")).toBe("300");
54+ expect(handle.getAttribute("aria-valuemin")).toBe("120");
55+ expect(handle.getAttribute("aria-valuemax")).toBe("800");
56+ });
57+
58+ it("reports the width while dragging and stops on pointer up", () => {
59+ const handle = renderHandle(200);
60+ fireEvent.pointerDown(handle, { clientX: 500, pointerId: 1 });
61+ expect(handle.classList.contains("resize-handle-active")).toBe(true);
62+
63+ fireEvent.pointerMove(handle, { clientX: 540, pointerId: 1 });
64+ fireEvent.pointerMove(handle, { clientX: 470, pointerId: 1 });
65+ expect(onResize.mock.calls).toEqual([[240], [170]]);
66+
67+ fireEvent.pointerUp(handle, { clientX: 470, pointerId: 1 });
68+ expect(handle.classList.contains("resize-handle-active")).toBe(false);
69+ fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 });
70+ expect(onResize).toHaveBeenCalledTimes(2);
71+ });
72+
73+ it("ignores pointer moves that did not start on the handle", () => {
74+ const handle = renderHandle(200);
75+ fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 });
76+ expect(onResize).not.toHaveBeenCalled();
77+ });
78+
79+ it("resizes step by step with the arrow keys and jumps with Home / End", () => {
80+ const handle = renderHandle(200);
81+ fireEvent.keyDown(handle, { key: "ArrowRight" });
82+ fireEvent.keyDown(handle, { key: "ArrowLeft" });
83+ fireEvent.keyDown(handle, { key: "Home" });
84+ fireEvent.keyDown(handle, { key: "End" });
85+ expect(onResize.mock.calls).toEqual([
86+ [200 + KEYBOARD_RESIZE_STEP],
87+ [200 - KEYBOARD_RESIZE_STEP],
88+ [120],
89+ [800],
90+ ]);
91+ });
92+
93+ it("leaves other keys alone", () => {
94+ const handle = renderHandle(200);
95+ fireEvent.keyDown(handle, { key: "Enter" });
96+ fireEvent.keyDown(handle, { key: "Tab" });
97+ expect(onResize).not.toHaveBeenCalled();
98+ });
99+
100+ it("resets on double-click", () => {
101+ const handle = renderHandle(200);
102+ fireEvent.doubleClick(handle);
103+ expect(onReset).toHaveBeenCalledTimes(1);
104+ });
105+});
106+
107+describe("widthAfterKey", () => {
108+ it("maps the resize keys and returns null otherwise", () => {
109+ expect(widthAfterKey("ArrowRight", 200, 120, 800)).toBe(216);
110+ expect(widthAfterKey("ArrowLeft", 200, 120, 800)).toBe(184);
111+ expect(widthAfterKey("Home", 200, 120, 800)).toBe(120);
112+ expect(widthAfterKey("End", 200, 120, 800)).toBe(800);
113+ expect(widthAfterKey("a", 200, 120, 800)).toBeNull();
114+ });
115+});
added ui/src/components/ResizeHandle.tsx +147 -0
new file mode 100644
@@ -0,0 +1,147 @@
1+/**
2+ * ResizeHandle is the thin vertical grip between two columns. Dragging it
3+ * with the pointer resizes the column on its left; the arrow keys resize it
4+ * step by step, Home / End jump to the bounds, and a double-click restores
5+ * the default width. It exposes the ARIA separator pattern so the current
6+ * width is readable by assistive technology.
7+ *
8+ * The handle owns no width itself: it reports the wanted width through
9+ * `onResize` and the parent decides (and clamps) what to apply.
10+ *
11+ * @example
12+ * <ResizeHandle
13+ * label="Resize the file tree"
14+ * width={width}
15+ * min={120}
16+ * max={800}
17+ * onResize={setFileTreeWidth}
18+ * onReset={resetFileTreeWidth}
19+ * />
20+ */
21+
22+import {
23+ type KeyboardEvent,
24+ type PointerEvent as ReactPointerEvent,
25+ useRef,
26+ useState,
27+} from "react";
28+
29+/** Pixels moved per arrow-key press. */
30+export const KEYBOARD_RESIZE_STEP = 16;
31+
32+export interface ResizeHandleProps {
33+ /** Accessible name of the separator. */
34+ label: string;
35+ /** Current width of the resized column, in pixels. */
36+ width: number;
37+ /** Narrowest width the parent accepts, in pixels. */
38+ min: number;
39+ /** Widest width the parent accepts, in pixels. */
40+ max: number;
41+ /** Called with the wanted width whenever the user drags or presses a key. */
42+ onResize: (width: number) => void;
43+ /** Called on double-click to restore the default width. */
44+ onReset: () => void;
45+}
46+
47+interface DragOrigin {
48+ pointerX: number;
49+ width: number;
50+}
51+
52+export function ResizeHandle({
53+ label,
54+ width,
55+ min,
56+ max,
57+ onResize,
58+ onReset,
59+}: ResizeHandleProps) {
60+ // The origin of the current drag; null when the pointer is up.
61+ const dragOrigin = useRef<DragOrigin | null>(null);
62+ const [dragging, setDragging] = useState(false);
63+
64+ const startDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
65+ // Prevents the browser from starting a text selection while dragging.
66+ event.preventDefault();
67+ dragOrigin.current = { pointerX: event.clientX, width };
68+ // Capture keeps the move/up events coming even when the pointer
69+ // leaves the thin handle (jsdom lacks the API, hence the guard).
70+ event.currentTarget.setPointerCapture?.(event.pointerId);
71+ setDragging(true);
72+ };
73+
74+ const moveDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
75+ const origin = dragOrigin.current;
76+ if (origin === null) {
77+ return;
78+ }
79+ onResize(origin.width + event.clientX - origin.pointerX);
80+ };
81+
82+ const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
83+ if (dragOrigin.current === null) {
84+ return;
85+ }
86+ dragOrigin.current = null;
87+ event.currentTarget.releasePointerCapture?.(event.pointerId);
88+ setDragging(false);
89+ };
90+
91+ const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {
92+ const wanted = widthAfterKey(event.key, width, min, max);
93+ if (wanted === null) {
94+ return;
95+ }
96+ event.preventDefault();
97+ onResize(wanted);
98+ };
99+
100+ return (
101+ <div
102+ role="separator"
103+ aria-label={label}
104+ aria-orientation="vertical"
105+ aria-valuenow={width}
106+ aria-valuemin={min}
107+ aria-valuemax={max}
108+ tabIndex={0}
109+ className={`resize-handle${dragging ? " resize-handle-active" : ""}`}
110+ onPointerDown={startDrag}
111+ onPointerMove={moveDrag}
112+ onPointerUp={endDrag}
113+ onPointerCancel={endDrag}
114+ onKeyDown={handleKey}
115+ onDoubleClick={onReset}
116+ />
117+ );
118+}
119+
120+/**
121+ * widthAfterKey maps a key press to the wanted width, or null when the key
122+ * is not a resize key so the event keeps its default behaviour.
123+ *
124+ * @example
125+ * widthAfterKey("ArrowRight", 200, 120, 800); // 216
126+ * widthAfterKey("Home", 200, 120, 800); // 120
127+ * widthAfterKey("Enter", 200, 120, 800); // null
128+ */
129+export function widthAfterKey(
130+ key: string,
131+ width: number,
132+ min: number,
133+ max: number,
134+): number | null {
135+ switch (key) {
136+ case "ArrowLeft":
137+ return width - KEYBOARD_RESIZE_STEP;
138+ case "ArrowRight":
139+ return width + KEYBOARD_RESIZE_STEP;
140+ case "Home":
141+ return min;
142+ case "End":
143+ return max;
144+ default:
145+ return null;
146+ }
147+}
new file mode 100644
@@ -0,0 +1,147 @@
1+/**
2+ * ResizeHandle is the thin vertical grip between two columns. Dragging it
3+ * with the pointer resizes the column on its left; the arrow keys resize it
4+ * step by step, Home / End jump to the bounds, and a double-click restores
5+ * the default width. It exposes the ARIA separator pattern so the current
6+ * width is readable by assistive technology.
7+ *
8+ * The handle owns no width itself: it reports the wanted width through
9+ * `onResize` and the parent decides (and clamps) what to apply.
10+ *
11+ * @example
12+ * <ResizeHandle
13+ * label="Resize the file tree"
14+ * width={width}
15+ * min={120}
16+ * max={800}
17+ * onResize={setFileTreeWidth}
18+ * onReset={resetFileTreeWidth}
19+ * />
20+ */
21+
22+import {
23+ type KeyboardEvent,
24+ type PointerEvent as ReactPointerEvent,
25+ useRef,
26+ useState,
27+} from "react";
28+
29+/** Pixels moved per arrow-key press. */
30+export const KEYBOARD_RESIZE_STEP = 16;
31+
32+export interface ResizeHandleProps {
33+ /** Accessible name of the separator. */
34+ label: string;
35+ /** Current width of the resized column, in pixels. */
36+ width: number;
37+ /** Narrowest width the parent accepts, in pixels. */
38+ min: number;
39+ /** Widest width the parent accepts, in pixels. */
40+ max: number;
41+ /** Called with the wanted width whenever the user drags or presses a key. */
42+ onResize: (width: number) => void;
43+ /** Called on double-click to restore the default width. */
44+ onReset: () => void;
45+}
46+
47+interface DragOrigin {
48+ pointerX: number;
49+ width: number;
50+}
51+
52+export function ResizeHandle({
53+ label,
54+ width,
55+ min,
56+ max,
57+ onResize,
58+ onReset,
59+}: ResizeHandleProps) {
60+ // The origin of the current drag; null when the pointer is up.
61+ const dragOrigin = useRef<DragOrigin | null>(null);
62+ const [dragging, setDragging] = useState(false);
63+
64+ const startDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
65+ // Prevents the browser from starting a text selection while dragging.
66+ event.preventDefault();
67+ dragOrigin.current = { pointerX: event.clientX, width };
68+ // Capture keeps the move/up events coming even when the pointer
69+ // leaves the thin handle (jsdom lacks the API, hence the guard).
70+ event.currentTarget.setPointerCapture?.(event.pointerId);
71+ setDragging(true);
72+ };
73+
74+ const moveDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
75+ const origin = dragOrigin.current;
76+ if (origin === null) {
77+ return;
78+ }
79+ onResize(origin.width + event.clientX - origin.pointerX);
80+ };
81+
82+ const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
83+ if (dragOrigin.current === null) {
84+ return;
85+ }
86+ dragOrigin.current = null;
87+ event.currentTarget.releasePointerCapture?.(event.pointerId);
88+ setDragging(false);
89+ };
90+
91+ const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {
92+ const wanted = widthAfterKey(event.key, width, min, max);
93+ if (wanted === null) {
94+ return;
95+ }
96+ event.preventDefault();
97+ onResize(wanted);
98+ };
99+
100+ return (
101+ <div
102+ role="separator"
103+ aria-label={label}
104+ aria-orientation="vertical"
105+ aria-valuenow={width}
106+ aria-valuemin={min}
107+ aria-valuemax={max}
108+ tabIndex={0}
109+ className={`resize-handle${dragging ? " resize-handle-active" : ""}`}
110+ onPointerDown={startDrag}
111+ onPointerMove={moveDrag}
112+ onPointerUp={endDrag}
113+ onPointerCancel={endDrag}
114+ onKeyDown={handleKey}
115+ onDoubleClick={onReset}
116+ />
117+ );
118+}
119+
120+/**
121+ * widthAfterKey maps a key press to the wanted width, or null when the key
122+ * is not a resize key so the event keeps its default behaviour.
123+ *
124+ * @example
125+ * widthAfterKey("ArrowRight", 200, 120, 800); // 216
126+ * widthAfterKey("Home", 200, 120, 800); // 120
127+ * widthAfterKey("Enter", 200, 120, 800); // null
128+ */
129+export function widthAfterKey(
130+ key: string,
131+ width: number,
132+ min: number,
133+ max: number,
134+): number | null {
135+ switch (key) {
136+ case "ArrowLeft":
137+ return width - KEYBOARD_RESIZE_STEP;
138+ case "ArrowRight":
139+ return width + KEYBOARD_RESIZE_STEP;
140+ case "Home":
141+ return min;
142+ case "End":
143+ return max;
144+ default:
145+ return null;
146+ }
147+}
added ui/src/components/TerminalPane.test.tsx +134 -0
new file mode 100644
@@ -0,0 +1,134 @@
1+import { render, waitFor } from "@testing-library/react";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+import { TerminalPane, terminalWsUrl } from "./TerminalPane";
4+
5+const { terminalInstances, TerminalMock, FitAddonMock } = vi.hoisted(() => {
6+ class TerminalStub {
7+ cols = 80;
8+ rows = 24;
9+ opened: HTMLElement | null = null;
10+ written: unknown[] = [];
11+ disposed = false;
12+ dataHandler: ((data: string) => void) | null = null;
13+
14+ loadAddon() {}
15+ open(element: HTMLElement) {
16+ this.opened = element;
17+ }
18+ write(data: unknown) {
19+ this.written.push(data);
20+ }
21+ onData(handler: (data: string) => void) {
22+ this.dataHandler = handler;
23+ return { dispose: () => {} };
24+ }
25+ onResize() {
26+ return { dispose: () => {} };
27+ }
28+ dispose() {
29+ this.disposed = true;
30+ }
31+ }
32+ const instances: TerminalStub[] = [];
33+ return {
34+ terminalInstances: instances,
35+ TerminalMock: class extends TerminalStub {
36+ constructor() {
37+ super();
38+ instances.push(this);
39+ }
40+ },
41+ FitAddonMock: class {
42+ fit() {}
43+ },
44+ };
45+});
46+
47+vi.mock("@xterm/xterm", () => ({ Terminal: TerminalMock }));
48+vi.mock("@xterm/addon-fit", () => ({ FitAddon: FitAddonMock }));
49+vi.mock("@xterm/xterm/css/xterm.css", () => ({}));
50+
51+class FakeWebSocket {
52+ static OPEN = 1;
53+ static instances: FakeWebSocket[] = [];
54+ url: string;
55+ readyState = FakeWebSocket.OPEN;
56+ binaryType = "";
57+ sent: string[] = [];
58+ onmessage: ((event: { data: ArrayBuffer }) => void) | null = null;
59+ onopen: (() => void) | null = null;
60+ onclose: (() => void) | null = null;
61+
62+ constructor(url: string) {
63+ this.url = url;
64+ FakeWebSocket.instances.push(this);
65+ }
66+ send(data: string) {
67+ this.sent.push(data);
68+ }
69+ close() {}
70+}
71+
72+describe("TerminalPane", () => {
73+ beforeEach(() => {
74+ terminalInstances.length = 0;
75+ FakeWebSocket.instances.length = 0;
76+ vi.stubGlobal("WebSocket", FakeWebSocket);
77+ vi.stubGlobal(
78+ "ResizeObserver",
79+ class {
80+ observe() {}
81+ disconnect() {}
82+ },
83+ );
84+ });
85+
86+ afterEach(() => {
87+ vi.unstubAllGlobals();
88+ });
89+
90+ it("opens xterm in the pane and connects to /ws/terminal", async () => {
91+ render(<TerminalPane />);
92+
93+ await waitFor(() => expect(terminalInstances).toHaveLength(1));
94+ expect(terminalInstances[0].opened).not.toBeNull();
95+
96+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
97+ expect(FakeWebSocket.instances[0].url).toBe(terminalWsUrl());
98+ expect(FakeWebSocket.instances[0].binaryType).toBe("arraybuffer");
99+ });
100+
101+ it("announces its size on open and forwards keystrokes as input frames", async () => {
102+ render(<TerminalPane />);
103+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
104+ const socket = FakeWebSocket.instances[0];
105+
106+ socket.onopen?.();
107+ expect(socket.sent[0]).toBe(
108+ JSON.stringify({ type: "resize", cols: 80, rows: 24 }),
109+ );
110+
111+ terminalInstances[0].dataHandler?.("ls\r");
112+ expect(socket.sent[1]).toBe(
113+ JSON.stringify({ type: "input", data: "ls\r" }),
114+ );
115+ });
116+
117+ it("writes incoming binary frames to the terminal", async () => {
118+ render(<TerminalPane />);
119+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
120+
121+ FakeWebSocket.instances[0].onmessage?.({
122+ data: new TextEncoder().encode("hello").buffer as ArrayBuffer,
123+ });
124+ expect(terminalInstances[0].written).toHaveLength(1);
125+ });
126+
127+ it("disposes the terminal on unmount", async () => {
128+ const { unmount } = render(<TerminalPane />);
129+ await waitFor(() => expect(terminalInstances).toHaveLength(1));
130+
131+ unmount();
132+ await waitFor(() => expect(terminalInstances[0].disposed).toBe(true));
133+ });
134+});
new file mode 100644
@@ -0,0 +1,134 @@
1+import { render, waitFor } from "@testing-library/react";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+import { TerminalPane, terminalWsUrl } from "./TerminalPane";
4+
5+const { terminalInstances, TerminalMock, FitAddonMock } = vi.hoisted(() => {
6+ class TerminalStub {
7+ cols = 80;
8+ rows = 24;
9+ opened: HTMLElement | null = null;
10+ written: unknown[] = [];
11+ disposed = false;
12+ dataHandler: ((data: string) => void) | null = null;
13+
14+ loadAddon() {}
15+ open(element: HTMLElement) {
16+ this.opened = element;
17+ }
18+ write(data: unknown) {
19+ this.written.push(data);
20+ }
21+ onData(handler: (data: string) => void) {
22+ this.dataHandler = handler;
23+ return { dispose: () => {} };
24+ }
25+ onResize() {
26+ return { dispose: () => {} };
27+ }
28+ dispose() {
29+ this.disposed = true;
30+ }
31+ }
32+ const instances: TerminalStub[] = [];
33+ return {
34+ terminalInstances: instances,
35+ TerminalMock: class extends TerminalStub {
36+ constructor() {
37+ super();
38+ instances.push(this);
39+ }
40+ },
41+ FitAddonMock: class {
42+ fit() {}
43+ },
44+ };
45+});
46+
47+vi.mock("@xterm/xterm", () => ({ Terminal: TerminalMock }));
48+vi.mock("@xterm/addon-fit", () => ({ FitAddon: FitAddonMock }));
49+vi.mock("@xterm/xterm/css/xterm.css", () => ({}));
50+
51+class FakeWebSocket {
52+ static OPEN = 1;
53+ static instances: FakeWebSocket[] = [];
54+ url: string;
55+ readyState = FakeWebSocket.OPEN;
56+ binaryType = "";
57+ sent: string[] = [];
58+ onmessage: ((event: { data: ArrayBuffer }) => void) | null = null;
59+ onopen: (() => void) | null = null;
60+ onclose: (() => void) | null = null;
61+
62+ constructor(url: string) {
63+ this.url = url;
64+ FakeWebSocket.instances.push(this);
65+ }
66+ send(data: string) {
67+ this.sent.push(data);
68+ }
69+ close() {}
70+}
71+
72+describe("TerminalPane", () => {
73+ beforeEach(() => {
74+ terminalInstances.length = 0;
75+ FakeWebSocket.instances.length = 0;
76+ vi.stubGlobal("WebSocket", FakeWebSocket);
77+ vi.stubGlobal(
78+ "ResizeObserver",
79+ class {
80+ observe() {}
81+ disconnect() {}
82+ },
83+ );
84+ });
85+
86+ afterEach(() => {
87+ vi.unstubAllGlobals();
88+ });
89+
90+ it("opens xterm in the pane and connects to /ws/terminal", async () => {
91+ render(<TerminalPane />);
92+
93+ await waitFor(() => expect(terminalInstances).toHaveLength(1));
94+ expect(terminalInstances[0].opened).not.toBeNull();
95+
96+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
97+ expect(FakeWebSocket.instances[0].url).toBe(terminalWsUrl());
98+ expect(FakeWebSocket.instances[0].binaryType).toBe("arraybuffer");
99+ });
100+
101+ it("announces its size on open and forwards keystrokes as input frames", async () => {
102+ render(<TerminalPane />);
103+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
104+ const socket = FakeWebSocket.instances[0];
105+
106+ socket.onopen?.();
107+ expect(socket.sent[0]).toBe(
108+ JSON.stringify({ type: "resize", cols: 80, rows: 24 }),
109+ );
110+
111+ terminalInstances[0].dataHandler?.("ls\r");
112+ expect(socket.sent[1]).toBe(
113+ JSON.stringify({ type: "input", data: "ls\r" }),
114+ );
115+ });
116+
117+ it("writes incoming binary frames to the terminal", async () => {
118+ render(<TerminalPane />);
119+ await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
120+
121+ FakeWebSocket.instances[0].onmessage?.({
122+ data: new TextEncoder().encode("hello").buffer as ArrayBuffer,
123+ });
124+ expect(terminalInstances[0].written).toHaveLength(1);
125+ });
126+
127+ it("disposes the terminal on unmount", async () => {
128+ const { unmount } = render(<TerminalPane />);
129+ await waitFor(() => expect(terminalInstances).toHaveLength(1));
130+
131+ unmount();
132+ await waitFor(() => expect(terminalInstances[0].disposed).toBe(true));
133+ });
134+});
added ui/src/components/TerminalPane.tsx +94 -0
new file mode 100644
@@ -0,0 +1,94 @@
1+/**
2+ * TerminalPane is an interactive shell in the browser: xterm.js on the front,
3+ * a real PTY on the backend, joined by the /ws/terminal WebSocket (raw output
4+ * as binary frames, input and resizes as JSON — see the protocol reference).
5+ *
6+ * The pane is kept mounted (hidden, not unmounted) while other tabs are
7+ * active, so the shell session survives tab switches.
8+ *
9+ * @example
10+ * <TerminalPane />
11+ */
12+
13+import { useEffect, useRef } from "react";
14+import "@xterm/xterm/css/xterm.css";
15+
16+/** terminalWsUrl derives the terminal endpoint from the page's origin. */
17+export function terminalWsUrl(): string {
18+ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
19+ return `${scheme}://${window.location.host}/ws/terminal`;
20+}
21+
22+export function TerminalPane() {
23+ const containerRef = useRef<HTMLDivElement>(null);
24+
25+ useEffect(() => {
26+ const container = containerRef.current;
27+ if (!container) {
28+ return;
29+ }
30+ let disposed = false;
31+ let cleanup: (() => void) | undefined;
32+
33+ // xterm is heavy: loaded on first use only.
34+ Promise.all([import("@xterm/xterm"), import("@xterm/addon-fit")]).then(
35+ ([{ Terminal }, { FitAddon }]) => {
36+ if (disposed) {
37+ return;
38+ }
39+ // Always dark, IDE-style, whatever the app theme.
40+ const term = new Terminal({
41+ fontSize: 13,
42+ cursorBlink: true,
43+ theme: { background: "#1e1e1e" },
44+ });
45+ const fit = new FitAddon();
46+ term.loadAddon(fit);
47+ term.open(container);
48+ fit.fit();
49+
50+ const socket = new WebSocket(terminalWsUrl());
51+ socket.binaryType = "arraybuffer";
52+ socket.onmessage = (event) =>
53+ term.write(new Uint8Array(event.data as ArrayBuffer));
54+ socket.onopen = () =>
55+ socket.send(
56+ JSON.stringify({
57+ type: "resize",
58+ cols: term.cols,
59+ rows: term.rows,
60+ }),
61+ );
62+ socket.onclose = () => term.write("\r\n[terminal session closed]\r\n");
63+
64+ const inputSubscription = term.onData((data) => {
65+ if (socket.readyState === WebSocket.OPEN) {
66+ socket.send(JSON.stringify({ type: "input", data }));
67+ }
68+ });
69+ const resizeSubscription = term.onResize(({ cols, rows }) => {
70+ if (socket.readyState === WebSocket.OPEN) {
71+ socket.send(JSON.stringify({ type: "resize", cols, rows }));
72+ }
73+ });
74+ const observer = new ResizeObserver(() => fit.fit());
75+ observer.observe(container);
76+
77+ cleanup = () => {
78+ observer.disconnect();
79+ inputSubscription.dispose();
80+ resizeSubscription.dispose();
81+ socket.close();
82+ term.dispose();
83+ };
84+ },
85+ );
86+
87+ return () => {
88+ disposed = true;
89+ cleanup?.();
90+ };
91+ }, []);
92+
93+ return <div className="terminal" ref={containerRef} />;
94+}
new file mode 100644
@@ -0,0 +1,94 @@
1+/**
2+ * TerminalPane is an interactive shell in the browser: xterm.js on the front,
3+ * a real PTY on the backend, joined by the /ws/terminal WebSocket (raw output
4+ * as binary frames, input and resizes as JSON — see the protocol reference).
5+ *
6+ * The pane is kept mounted (hidden, not unmounted) while other tabs are
7+ * active, so the shell session survives tab switches.
8+ *
9+ * @example
10+ * <TerminalPane />
11+ */
12+
13+import { useEffect, useRef } from "react";
14+import "@xterm/xterm/css/xterm.css";
15+
16+/** terminalWsUrl derives the terminal endpoint from the page's origin. */
17+export function terminalWsUrl(): string {
18+ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
19+ return `${scheme}://${window.location.host}/ws/terminal`;
20+}
21+
22+export function TerminalPane() {
23+ const containerRef = useRef<HTMLDivElement>(null);
24+
25+ useEffect(() => {
26+ const container = containerRef.current;
27+ if (!container) {
28+ return;
29+ }
30+ let disposed = false;
31+ let cleanup: (() => void) | undefined;
32+
33+ // xterm is heavy: loaded on first use only.
34+ Promise.all([import("@xterm/xterm"), import("@xterm/addon-fit")]).then(
35+ ([{ Terminal }, { FitAddon }]) => {
36+ if (disposed) {
37+ return;
38+ }
39+ // Always dark, IDE-style, whatever the app theme.
40+ const term = new Terminal({
41+ fontSize: 13,
42+ cursorBlink: true,
43+ theme: { background: "#1e1e1e" },
44+ });
45+ const fit = new FitAddon();
46+ term.loadAddon(fit);
47+ term.open(container);
48+ fit.fit();
49+
50+ const socket = new WebSocket(terminalWsUrl());
51+ socket.binaryType = "arraybuffer";
52+ socket.onmessage = (event) =>
53+ term.write(new Uint8Array(event.data as ArrayBuffer));
54+ socket.onopen = () =>
55+ socket.send(
56+ JSON.stringify({
57+ type: "resize",
58+ cols: term.cols,
59+ rows: term.rows,
60+ }),
61+ );
62+ socket.onclose = () => term.write("\r\n[terminal session closed]\r\n");
63+
64+ const inputSubscription = term.onData((data) => {
65+ if (socket.readyState === WebSocket.OPEN) {
66+ socket.send(JSON.stringify({ type: "input", data }));
67+ }
68+ });
69+ const resizeSubscription = term.onResize(({ cols, rows }) => {
70+ if (socket.readyState === WebSocket.OPEN) {
71+ socket.send(JSON.stringify({ type: "resize", cols, rows }));
72+ }
73+ });
74+ const observer = new ResizeObserver(() => fit.fit());
75+ observer.observe(container);
76+
77+ cleanup = () => {
78+ observer.disconnect();
79+ inputSubscription.dispose();
80+ resizeSubscription.dispose();
81+ socket.close();
82+ term.dispose();
83+ };
84+ },
85+ );
86+
87+ return () => {
88+ disposed = true;
89+ cleanup?.();
90+ };
91+ }, []);
92+
93+ return <div className="terminal" ref={containerRef} />;
94+}
added ui/src/components/ToolCallCard.test.tsx +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+import { render, screen } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import type { ToolCall } from "../reducer";
4+import { ToolCallCard } from "./ToolCallCard";
5+
6+function call(overrides: Partial<ToolCall>): ToolCall {
7+ return {
8+ toolCallId: "c1",
9+ title: "A tool call",
10+ toolKind: "other",
11+ status: "completed",
12+ contents: [],
13+ locations: [],
14+ ...overrides,
15+ };
16+}
17+
18+describe("ToolCallCard", () => {
19+ it("renders execute output verbatim in a monospace block", () => {
20+ const { container } = render(
21+ <ToolCallCard
22+ call={call({
23+ toolKind: "execute",
24+ contents: [
25+ {
26+ type: "content",
27+ content: { type: "text", text: "* main\n remote/origin" },
28+ },
29+ ],
30+ })}
31+ />,
32+ );
33+
34+ const output = container.querySelector("pre.tool-output");
35+ expect(output).not.toBeNull();
36+ // Verbatim: the "* " prefix must not have become a markdown list.
37+ expect(output?.textContent).toBe("* main\n remote/origin");
38+ expect(container.querySelector("ul li")).toBeNull();
39+ });
40+
41+ it("renders backticked commands in the title as monospace code", () => {
42+ const { container } = render(
43+ <ToolCallCard
44+ call={call({ toolKind: "execute", title: "Run `ls .memory/`" })}
45+ />,
46+ );
47+
48+ const code = container.querySelector(".tool-title code.inline-code");
49+ expect(code?.textContent).toBe("ls .memory/");
50+ });
51+
52+ it("renders non-execute text content as markdown", () => {
53+ const { container } = render(
54+ <ToolCallCard
55+ call={call({
56+ toolKind: "read",
57+ contents: [
58+ {
59+ type: "content",
60+ content: { type: "text", text: "**bold** result" },
61+ },
62+ ],
63+ })}
64+ />,
65+ );
66+
67+ expect(container.querySelector("pre.tool-output")).toBeNull();
68+ expect(screen.getByText("bold")).toBeDefined();
69+ });
70+});
new file mode 100644
@@ -0,0 +1,70 @@
1+import { render, screen } from "@testing-library/react";
2+import { describe, expect, it } from "vitest";
3+import type { ToolCall } from "../reducer";
4+import { ToolCallCard } from "./ToolCallCard";
5+
6+function call(overrides: Partial<ToolCall>): ToolCall {
7+ return {
8+ toolCallId: "c1",
9+ title: "A tool call",
10+ toolKind: "other",
11+ status: "completed",
12+ contents: [],
13+ locations: [],
14+ ...overrides,
15+ };
16+}
17+
18+describe("ToolCallCard", () => {
19+ it("renders execute output verbatim in a monospace block", () => {
20+ const { container } = render(
21+ <ToolCallCard
22+ call={call({
23+ toolKind: "execute",
24+ contents: [
25+ {
26+ type: "content",
27+ content: { type: "text", text: "* main\n remote/origin" },
28+ },
29+ ],
30+ })}
31+ />,
32+ );
33+
34+ const output = container.querySelector("pre.tool-output");
35+ expect(output).not.toBeNull();
36+ // Verbatim: the "* " prefix must not have become a markdown list.
37+ expect(output?.textContent).toBe("* main\n remote/origin");
38+ expect(container.querySelector("ul li")).toBeNull();
39+ });
40+
41+ it("renders backticked commands in the title as monospace code", () => {
42+ const { container } = render(
43+ <ToolCallCard
44+ call={call({ toolKind: "execute", title: "Run `ls .memory/`" })}
45+ />,
46+ );
47+
48+ const code = container.querySelector(".tool-title code.inline-code");
49+ expect(code?.textContent).toBe("ls .memory/");
50+ });
51+
52+ it("renders non-execute text content as markdown", () => {
53+ const { container } = render(
54+ <ToolCallCard
55+ call={call({
56+ toolKind: "read",
57+ contents: [
58+ {
59+ type: "content",
60+ content: { type: "text", text: "**bold** result" },
61+ },
62+ ],
63+ })}
64+ />,
65+ );
66+
67+ expect(container.querySelector("pre.tool-output")).toBeNull();
68+ expect(screen.getByText("bold")).toBeDefined();
69+ });
70+});
modified ui/src/components/ToolCallCard.tsx +30 -6
@@ -9,6 +9,7 @@
99 */
1010
1111 import type { ToolCall } from "../reducer";
12+import { InlineText } from "./InlineText";
1213 import type { AcpToolCallContent } from "../protocol";
1314 import { Markdown } from "./Markdown";
1415
@@ -39,7 +40,9 @@ export function ToolCallCard({ call }: { call: ToolCall }) {
3940 <span className="tool-icon" aria-hidden>
4041 {kindIcons[call.toolKind] ?? kindIcons.other}
4142 </span>
42- <span className="tool-title">{call.title}</span>
43+ <span className="tool-title">
44+ <InlineText text={call.title} />
45+ </span>
4346 <span className={`tool-status tool-status-${call.status}`}>
4447 {statusLabels[call.status] ?? call.status}
4548 </span>
@@ -60,19 +63,22 @@ export function ToolCallCard({ call }: { call: ToolCall }) {
6063 )}
6164 {call.contents.map((content, index) => (
6265 // biome-ignore lint/suspicious/noArrayIndexKey: tool-call contents carry no id and the list is replaced wholesale on every update.
63- <ToolContent key={index} content={content} />
66+ <ToolContent key={index} content={content} toolKind={call.toolKind} />
6467 ))}
6568 </div>
6669 </details>
6770 );
6871 }
6972
70-function ToolContent({ content }: { content: AcpToolCallContent }) {
73+function ToolContent({
74+ content,
75+ toolKind,
76+}: { content: AcpToolCallContent; toolKind: string }) {
7177 switch (content.type) {
7278 case "content":
73- return content.content?.text ? (
74- <Markdown text={content.content.text} />
75- ) : null;
79+ return (
80+ <TextToolContent text={content.content?.text} toolKind={toolKind} />
81+ );
7682 case "diff":
7783 return (
7884 <div className="tool-diff">
@@ -94,6 +100,24 @@ function ToolContent({ content }: { content: AcpToolCallContent }) {
94100 }
95101 }
96102
103+/**
104+ * TextToolContent renders a tool call's textual output. System-command
105+ * output (kind "execute") is verbatim text: it goes into a monospace block,
106+ * never through the markdown pipeline.
107+ */
108+function TextToolContent({
109+ text,
110+ toolKind,
111+}: { text: string | undefined; toolKind: string }) {
112+ if (!text) {
113+ return null;
114+ }
115+ if (toolKind === "execute") {
116+ return <pre className="tool-output">{text}</pre>;
117+ }
118+ return <Markdown text={text} />;
119+}
120+
97121 function prefixLines(text: string, prefix: string): string {
98122 return text
99123 .split("\n")
@@ -9,6 +9,7 @@
9 */9 */
10 10
11 import type { ToolCall } from "../reducer";11 import type { ToolCall } from "../reducer";
12+import { InlineText } from "./InlineText";
12 import type { AcpToolCallContent } from "../protocol";13 import type { AcpToolCallContent } from "../protocol";
13 import { Markdown } from "./Markdown";14 import { Markdown } from "./Markdown";
14 15
@@ -39,7 +40,9 @@ export function ToolCallCard({ call }: { call: ToolCall }) {
39 <span className="tool-icon" aria-hidden>40 <span className="tool-icon" aria-hidden>
40 {kindIcons[call.toolKind] ?? kindIcons.other}41 {kindIcons[call.toolKind] ?? kindIcons.other}
41 </span>42 </span>
42- <span className="tool-title">{call.title}</span>43+ <span className="tool-title">
44+ <InlineText text={call.title} />
45+ </span>
43 <span className={`tool-status tool-status-${call.status}`}>46 <span className={`tool-status tool-status-${call.status}`}>
44 {statusLabels[call.status] ?? call.status}47 {statusLabels[call.status] ?? call.status}
45 </span>48 </span>
@@ -60,19 +63,22 @@ export function ToolCallCard({ call }: { call: ToolCall }) {
60 )}63 )}
61 {call.contents.map((content, index) => (64 {call.contents.map((content, index) => (
62 // biome-ignore lint/suspicious/noArrayIndexKey: tool-call contents carry no id and the list is replaced wholesale on every update.65 // biome-ignore lint/suspicious/noArrayIndexKey: tool-call contents carry no id and the list is replaced wholesale on every update.
63- <ToolContent key={index} content={content} />66+ <ToolContent key={index} content={content} toolKind={call.toolKind} />
64 ))}67 ))}
65 </div>68 </div>
66 </details>69 </details>
67 );70 );
68 }71 }
69 72
70-function ToolContent({ content }: { content: AcpToolCallContent }) {73+function ToolContent({
74+ content,
75+ toolKind,
76+}: { content: AcpToolCallContent; toolKind: string }) {
71 switch (content.type) {77 switch (content.type) {
72 case "content":78 case "content":
73- return content.content?.text ? (79+ return (
74- <Markdown text={content.content.text} />80+ <TextToolContent text={content.content?.text} toolKind={toolKind} />
75- ) : null;81+ );
76 case "diff":82 case "diff":
77 return (83 return (
78 <div className="tool-diff">84 <div className="tool-diff">
@@ -94,6 +100,24 @@ function ToolContent({ content }: { content: AcpToolCallContent }) {
94 }100 }
95 }101 }
96 102
103+/**
104+ * TextToolContent renders a tool call's textual output. System-command
105+ * output (kind "execute") is verbatim text: it goes into a monospace block,
106+ * never through the markdown pipeline.
107+ */
108+function TextToolContent({
109+ text,
110+ toolKind,
111+}: { text: string | undefined; toolKind: string }) {
112+ if (!text) {
113+ return null;
114+ }
115+ if (toolKind === "execute") {
116+ return <pre className="tool-output">{text}</pre>;
117+ }
118+ return <Markdown text={text} />;
119+}
120+
97 function prefixLines(text: string, prefix: string): string {121 function prefixLines(text: string, prefix: string): string {
98 return text122 return text
99 .split("\n")123 .split("\n")
added ui/src/components/WorkingIndicator.test.tsx +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+import { act, render, screen } from "@testing-library/react";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+import {
4+ phraseIntervalMs,
5+ WorkingIndicator,
6+ workingPhrases,
7+} from "./WorkingIndicator";
8+
9+describe("WorkingIndicator", () => {
10+ beforeEach(() => {
11+ vi.useFakeTimers();
12+ });
13+
14+ afterEach(() => {
15+ vi.useRealTimers();
16+ });
17+
18+ it("shows the first working phrase with a spinner", () => {
19+ const { container } = render(<WorkingIndicator />);
20+ expect(screen.getByText(workingPhrases[0])).toBeDefined();
21+ expect(container.querySelector(".spinner")).not.toBeNull();
22+ });
23+
24+ it("rotates to the next phrase after the interval", () => {
25+ render(<WorkingIndicator />);
26+ act(() => {
27+ vi.advanceTimersByTime(phraseIntervalMs);
28+ });
29+ expect(screen.getByText(workingPhrases[1])).toBeDefined();
30+ });
31+
32+ it("wraps around to the first phrase after the full cycle", () => {
33+ render(<WorkingIndicator />);
34+ act(() => {
35+ vi.advanceTimersByTime(phraseIntervalMs * workingPhrases.length);
36+ });
37+ expect(screen.getByText(workingPhrases[0])).toBeDefined();
38+ });
39+});
new file mode 100644
@@ -0,0 +1,39 @@
1+import { act, render, screen } from "@testing-library/react";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+import {
4+ phraseIntervalMs,
5+ WorkingIndicator,
6+ workingPhrases,
7+} from "./WorkingIndicator";
8+
9+describe("WorkingIndicator", () => {
10+ beforeEach(() => {
11+ vi.useFakeTimers();
12+ });
13+
14+ afterEach(() => {
15+ vi.useRealTimers();
16+ });
17+
18+ it("shows the first working phrase with a spinner", () => {
19+ const { container } = render(<WorkingIndicator />);
20+ expect(screen.getByText(workingPhrases[0])).toBeDefined();
21+ expect(container.querySelector(".spinner")).not.toBeNull();
22+ });
23+
24+ it("rotates to the next phrase after the interval", () => {
25+ render(<WorkingIndicator />);
26+ act(() => {
27+ vi.advanceTimersByTime(phraseIntervalMs);
28+ });
29+ expect(screen.getByText(workingPhrases[1])).toBeDefined();
30+ });
31+
32+ it("wraps around to the first phrase after the full cycle", () => {
33+ render(<WorkingIndicator />);
34+ act(() => {
35+ vi.advanceTimersByTime(phraseIntervalMs * workingPhrases.length);
36+ });
37+ expect(screen.getByText(workingPhrases[0])).toBeDefined();
38+ });
39+});
added ui/src/components/WorkingIndicator.tsx +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
1+/**
2+ * WorkingIndicator shows that the agent is busy: a spinner next to a whimsical
3+ * phrase that rotates every few seconds, in the spirit of Claude's own
4+ * "Pondering…" status line.
5+ *
6+ * @example
7+ * {turnActive && <WorkingIndicator />}
8+ */
9+
10+import { useEffect, useState } from "react";
11+
12+/** The rotating status phrases; exported so tests can assert against them. */
13+export const workingPhrases = [
14+ "Pondering…",
15+ "Connecting the dots…",
16+ "Brewing ideas…",
17+ "Reading between the lines…",
18+ "Sketching a plan…",
19+ "Turning gears…",
20+ "Chasing a thought…",
21+ "Polishing the answer…",
22+];
23+
24+/** How long each phrase stays on screen, in milliseconds. */
25+export const phraseIntervalMs = 2500;
26+
27+export function WorkingIndicator() {
28+ const [index, setIndex] = useState(0);
29+
30+ useEffect(() => {
31+ const timer = setInterval(
32+ () => setIndex((current) => (current + 1) % workingPhrases.length),
33+ phraseIntervalMs,
34+ );
35+ return () => clearInterval(timer);
36+ }, []);
37+
38+ return (
39+ <output className="working">
40+ <span className="spinner" aria-hidden />
41+ <span className="working-phrase">{workingPhrases[index]}</span>
42+ </output>
43+ );
44+}
new file mode 100644
@@ -0,0 +1,44 @@
1+/**
2+ * WorkingIndicator shows that the agent is busy: a spinner next to a whimsical
3+ * phrase that rotates every few seconds, in the spirit of Claude's own
4+ * "Pondering…" status line.
5+ *
6+ * @example
7+ * {turnActive && <WorkingIndicator />}
8+ */
9+
10+import { useEffect, useState } from "react";
11+
12+/** The rotating status phrases; exported so tests can assert against them. */
13+export const workingPhrases = [
14+ "Pondering…",
15+ "Connecting the dots…",
16+ "Brewing ideas…",
17+ "Reading between the lines…",
18+ "Sketching a plan…",
19+ "Turning gears…",
20+ "Chasing a thought…",
21+ "Polishing the answer…",
22+];
23+
24+/** How long each phrase stays on screen, in milliseconds. */
25+export const phraseIntervalMs = 2500;
26+
27+export function WorkingIndicator() {
28+ const [index, setIndex] = useState(0);
29+
30+ useEffect(() => {
31+ const timer = setInterval(
32+ () => setIndex((current) => (current + 1) % workingPhrases.length),
33+ phraseIntervalMs,
34+ );
35+ return () => clearInterval(timer);
36+ }, []);
37+
38+ return (
39+ <output className="working">
40+ <span className="spinner" aria-hidden />
41+ <span className="working-phrase">{workingPhrases[index]}</span>
42+ </output>
43+ );
44+}
added ui/src/components/Workspace.test.tsx +67 -0
new file mode 100644
@@ -0,0 +1,67 @@
1+import {
2+ act,
3+ fireEvent,
4+ render,
5+ screen,
6+ waitFor,
7+} from "@testing-library/react";
8+import { beforeEach, describe, expect, it, vi } from "vitest";
9+import {
10+ DEFAULT_FILE_TREE_WIDTH,
11+ resetWorkspace,
12+ setFileTreeWidth,
13+ toggleWorkspace,
14+ useWorkspace,
15+} from "../workspace";
16+import { Workspace } from "./Workspace";
17+
18+vi.mock("../api", () => ({
19+ listFiles: vi.fn().mockResolvedValue({ path: "/w", entries: [] }),
20+ readFile: vi.fn().mockResolvedValue({ path: "/w/x", content: "" }),
21+ writeFile: vi.fn().mockResolvedValue(undefined),
22+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
23+}));
24+
25+// The real terminal drags xterm and a WebSocket along; a stub is enough here.
26+vi.mock("./TerminalPane", () => ({
27+ TerminalPane: () => <div data-testid="terminal-pane" />,
28+}));
29+
30+/** Waits for the (mocked) file listing to settle so React stays quiet. */
31+function treeLoaded() {
32+ return waitFor(() => expect(screen.queryByText("loading…")).toBeNull());
33+}
34+
35+describe("Workspace", () => {
36+ beforeEach(() => {
37+ localStorage.clear();
38+ resetWorkspace();
39+ toggleWorkspace();
40+ });
41+
42+ it("applies the file tree width as a CSS custom property", async () => {
43+ render(<Workspace />);
44+ await treeLoaded();
45+ const panel = screen.getByRole("complementary", { name: "workspace" });
46+ expect(panel.style.getPropertyValue("--filetree-width")).toBe(
47+ `${DEFAULT_FILE_TREE_WIDTH}px`,
48+ );
49+
50+ act(() => setFileTreeWidth(360));
51+ expect(panel.style.getPropertyValue("--filetree-width")).toBe("360px");
52+ });
53+
54+ it("resizes the file tree from the handle and restores it on double-click", async () => {
55+ render(<Workspace />);
56+ await treeLoaded();
57+ const handle = screen.getByRole("separator", {
58+ name: "Resize the file tree",
59+ });
60+
61+ fireEvent.keyDown(handle, { key: "End" });
62+ expect(useWorkspace.getState().fileTreeWidth).toBe(800);
63+
64+ fireEvent.doubleClick(handle);
65+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
66+ });
67+});
new file mode 100644
@@ -0,0 +1,67 @@
1+import {
2+ act,
3+ fireEvent,
4+ render,
5+ screen,
6+ waitFor,
7+} from "@testing-library/react";
8+import { beforeEach, describe, expect, it, vi } from "vitest";
9+import {
10+ DEFAULT_FILE_TREE_WIDTH,
11+ resetWorkspace,
12+ setFileTreeWidth,
13+ toggleWorkspace,
14+ useWorkspace,
15+} from "../workspace";
16+import { Workspace } from "./Workspace";
17+
18+vi.mock("../api", () => ({
19+ listFiles: vi.fn().mockResolvedValue({ path: "/w", entries: [] }),
20+ readFile: vi.fn().mockResolvedValue({ path: "/w/x", content: "" }),
21+ writeFile: vi.fn().mockResolvedValue(undefined),
22+ rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
23+}));
24+
25+// The real terminal drags xterm and a WebSocket along; a stub is enough here.
26+vi.mock("./TerminalPane", () => ({
27+ TerminalPane: () => <div data-testid="terminal-pane" />,
28+}));
29+
30+/** Waits for the (mocked) file listing to settle so React stays quiet. */
31+function treeLoaded() {
32+ return waitFor(() => expect(screen.queryByText("loading…")).toBeNull());
33+}
34+
35+describe("Workspace", () => {
36+ beforeEach(() => {
37+ localStorage.clear();
38+ resetWorkspace();
39+ toggleWorkspace();
40+ });
41+
42+ it("applies the file tree width as a CSS custom property", async () => {
43+ render(<Workspace />);
44+ await treeLoaded();
45+ const panel = screen.getByRole("complementary", { name: "workspace" });
46+ expect(panel.style.getPropertyValue("--filetree-width")).toBe(
47+ `${DEFAULT_FILE_TREE_WIDTH}px`,
48+ );
49+
50+ act(() => setFileTreeWidth(360));
51+ expect(panel.style.getPropertyValue("--filetree-width")).toBe("360px");
52+ });
53+
54+ it("resizes the file tree from the handle and restores it on double-click", async () => {
55+ render(<Workspace />);
56+ await treeLoaded();
57+ const handle = screen.getByRole("separator", {
58+ name: "Resize the file tree",
59+ });
60+
61+ fireEvent.keyDown(handle, { key: "End" });
62+ expect(useWorkspace.getState().fileTreeWidth).toBe(800);
63+
64+ fireEvent.doubleClick(handle);
65+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
66+ });
67+});
added ui/src/components/Workspace.tsx +107 -0
new file mode 100644
@@ -0,0 +1,107 @@
1+/**
2+ * Workspace is the right-hand panel: the file tree next to the Preview /
3+ * Editor / Terminal tabs. Panes are hidden rather than unmounted when
4+ * another tab is active — the terminal's shell session and the editor's
5+ * unsaved state survive tab switches (and the panel being collapsed).
6+ * The file tree column is resizable: its width lives in the workspace
7+ * store and reaches the CSS through the `--filetree-width` custom property.
8+ *
9+ * @example
10+ * <Workspace /> // visibility is driven by the workspace store
11+ */
12+
13+import { type CSSProperties, type ReactNode, useEffect, useState } from "react";
14+import {
15+ MAX_FILE_TREE_WIDTH,
16+ MIN_FILE_TREE_WIDTH,
17+ type WorkspaceTab,
18+ resetFileTreeWidth,
19+ setFileTreeWidth,
20+ showTab,
21+ useWorkspace,
22+} from "../workspace";
23+import { EditorPane } from "./EditorPane";
24+import { FileTree } from "./FileTree";
25+import { PreviewPane } from "./PreviewPane";
26+import { ResizeHandle } from "./ResizeHandle";
27+import { TerminalPane } from "./TerminalPane";
28+
29+const tabs: { id: WorkspaceTab; label: string }[] = [
30+ { id: "preview", label: "Preview" },
31+ { id: "editor", label: "Editor" },
32+ { id: "terminal", label: "Terminal" },
33+];
34+
35+export function Workspace() {
36+ const workspace = useWorkspace();
37+ // The terminal (and its shell) starts on first activation only.
38+ const [terminalStarted, setTerminalStarted] = useState(false);
39+ useEffect(() => {
40+ if (workspace.open && workspace.tab === "terminal") {
41+ setTerminalStarted(true);
42+ }
43+ }, [workspace.open, workspace.tab]);
44+
45+ const style = {
46+ "--filetree-width": `${workspace.fileTreeWidth}px`,
47+ } as CSSProperties;
48+
49+ return (
50+ <aside
51+ className={`workspace${workspace.open ? "" : " workspace-hidden"}`}
52+ aria-label="workspace"
53+ style={style}
54+ >
55+ <FileTree />
56+ <ResizeHandle
57+ label="Resize the file tree"
58+ width={workspace.fileTreeWidth}
59+ min={MIN_FILE_TREE_WIDTH}
60+ max={MAX_FILE_TREE_WIDTH}
61+ onResize={setFileTreeWidth}
62+ onReset={resetFileTreeWidth}
63+ />
64+ <div className="workspace-main">
65+ <nav className="workspace-tabs" aria-label="workspace tabs">
66+ {tabs.map((tab) => (
67+ <button
68+ key={tab.id}
69+ type="button"
70+ className={`workspace-tab${workspace.tab === tab.id ? " workspace-tab-active" : ""}`}
71+ onClick={() => showTab(tab.id)}
72+ >
73+ {tab.label}
74+ </button>
75+ ))}
76+ </nav>
77+ <div className="workspace-content">
78+ <Pane visible={workspace.tab === "preview"}>
79+ <PreviewPane path={workspace.previewPath} />
80+ </Pane>
81+ <Pane visible={workspace.tab === "editor"}>
82+ <EditorPane path={workspace.editorPath} />
83+ </Pane>
84+ <Pane visible={workspace.tab === "terminal"}>
85+ {terminalStarted ? (
86+ <TerminalPane />
87+ ) : (
88+ <p className="pane-empty">starting</p>
89+ )}
90+ </Pane>
91+ </div>
92+ </div>
93+ </aside>
94+ );
95+}
96+
97+/** Pane hides its children without unmounting them. */
98+function Pane({
99+ visible,
100+ children,
101+}: { visible: boolean; children: ReactNode }) {
102+ return (
103+ <div className={`workspace-pane${visible ? "" : " workspace-pane-hidden"}`}>
104+ {children}
105+ </div>
106+ );
107+}
new file mode 100644
@@ -0,0 +1,107 @@
1+/**
2+ * Workspace is the right-hand panel: the file tree next to the Preview /
3+ * Editor / Terminal tabs. Panes are hidden rather than unmounted when
4+ * another tab is active — the terminal's shell session and the editor's
5+ * unsaved state survive tab switches (and the panel being collapsed).
6+ * The file tree column is resizable: its width lives in the workspace
7+ * store and reaches the CSS through the `--filetree-width` custom property.
8+ *
9+ * @example
10+ * <Workspace /> // visibility is driven by the workspace store
11+ */
12+
13+import { type CSSProperties, type ReactNode, useEffect, useState } from "react";
14+import {
15+ MAX_FILE_TREE_WIDTH,
16+ MIN_FILE_TREE_WIDTH,
17+ type WorkspaceTab,
18+ resetFileTreeWidth,
19+ setFileTreeWidth,
20+ showTab,
21+ useWorkspace,
22+} from "../workspace";
23+import { EditorPane } from "./EditorPane";
24+import { FileTree } from "./FileTree";
25+import { PreviewPane } from "./PreviewPane";
26+import { ResizeHandle } from "./ResizeHandle";
27+import { TerminalPane } from "./TerminalPane";
28+
29+const tabs: { id: WorkspaceTab; label: string }[] = [
30+ { id: "preview", label: "Preview" },
31+ { id: "editor", label: "Editor" },
32+ { id: "terminal", label: "Terminal" },
33+];
34+
35+export function Workspace() {
36+ const workspace = useWorkspace();
37+ // The terminal (and its shell) starts on first activation only.
38+ const [terminalStarted, setTerminalStarted] = useState(false);
39+ useEffect(() => {
40+ if (workspace.open && workspace.tab === "terminal") {
41+ setTerminalStarted(true);
42+ }
43+ }, [workspace.open, workspace.tab]);
44+
45+ const style = {
46+ "--filetree-width": `${workspace.fileTreeWidth}px`,
47+ } as CSSProperties;
48+
49+ return (
50+ <aside
51+ className={`workspace${workspace.open ? "" : " workspace-hidden"}`}
52+ aria-label="workspace"
53+ style={style}
54+ >
55+ <FileTree />
56+ <ResizeHandle
57+ label="Resize the file tree"
58+ width={workspace.fileTreeWidth}
59+ min={MIN_FILE_TREE_WIDTH}
60+ max={MAX_FILE_TREE_WIDTH}
61+ onResize={setFileTreeWidth}
62+ onReset={resetFileTreeWidth}
63+ />
64+ <div className="workspace-main">
65+ <nav className="workspace-tabs" aria-label="workspace tabs">
66+ {tabs.map((tab) => (
67+ <button
68+ key={tab.id}
69+ type="button"
70+ className={`workspace-tab${workspace.tab === tab.id ? " workspace-tab-active" : ""}`}
71+ onClick={() => showTab(tab.id)}
72+ >
73+ {tab.label}
74+ </button>
75+ ))}
76+ </nav>
77+ <div className="workspace-content">
78+ <Pane visible={workspace.tab === "preview"}>
79+ <PreviewPane path={workspace.previewPath} />
80+ </Pane>
81+ <Pane visible={workspace.tab === "editor"}>
82+ <EditorPane path={workspace.editorPath} />
83+ </Pane>
84+ <Pane visible={workspace.tab === "terminal"}>
85+ {terminalStarted ? (
86+ <TerminalPane />
87+ ) : (
88+ <p className="pane-empty">starting</p>
89+ )}
90+ </Pane>
91+ </div>
92+ </div>
93+ </aside>
94+ );
95+}
96+
97+/** Pane hides its children without unmounting them. */
98+function Pane({
99+ visible,
100+ children,
101+}: { visible: boolean; children: ReactNode }) {
102+ return (
103+ <div className={`workspace-pane${visible ? "" : " workspace-pane-hidden"}`}>
104+ {children}
105+ </div>
106+ );
107+}
added ui/src/lang.test.ts +61 -0
new file mode 100644
@@ -0,0 +1,61 @@
1+import { describe, expect, it } from "vitest";
2+import { kindForPath, languageForPath } from "./lang";
3+
4+describe("languageForPath", () => {
5+ it("maps common extensions to Monaco language ids", () => {
6+ expect(languageForPath("/w/main.go")).toBe("go");
7+ expect(languageForPath("src/App.tsx")).toBe("typescript");
8+ expect(languageForPath("script.py")).toBe("python");
9+ expect(languageForPath("style.css")).toBe("css");
10+ expect(languageForPath("run.sh")).toBe("shell");
11+ expect(languageForPath("doc.md")).toBe("markdown");
12+ });
13+
14+ it("recognises well-known extensionless files", () => {
15+ expect(languageForPath("/w/Makefile")).toBe("makefile");
16+ expect(languageForPath("/w/Dockerfile")).toBe("dockerfile");
17+ });
18+
19+ it("falls back to plaintext", () => {
20+ expect(languageForPath("notes.xyz")).toBe("plaintext");
21+ expect(languageForPath("LICENSE")).toBe("plaintext");
22+ expect(languageForPath(".gitignore")).toBe("plaintext");
23+ });
24+});
25+
26+describe("kindForPath", () => {
27+ it("detects markdown and asciidoc", () => {
28+ expect(kindForPath("README.md")).toBe("markdown");
29+ expect(kindForPath("book.adoc")).toBe("asciidoc");
30+ expect(kindForPath("book.asciidoc")).toBe("asciidoc");
31+ });
32+
33+ it("detects images, including draw.io exports", () => {
34+ for (const name of [
35+ "a.png",
36+ "b.JPG",
37+ "c.jpeg",
38+ "d.gif",
39+ "e.webp",
40+ "f.svg",
41+ "g.bmp",
42+ "h.ico",
43+ "i.avif",
44+ "diagram.drawio.svg",
45+ "diagram.drawio.png",
46+ ]) {
47+ expect(kindForPath(name), name).toBe("image");
48+ }
49+ });
50+
51+ it("detects draw.io XML diagrams", () => {
52+ expect(kindForPath("docs/diagrams/packages.drawio")).toBe("drawio");
53+ expect(kindForPath("flow.dio")).toBe("drawio");
54+ expect(languageForPath("flow.drawio")).toBe("xml");
55+ });
56+
57+ it("treats everything else as code", () => {
58+ expect(kindForPath("main.go")).toBe("code");
59+ expect(kindForPath("LICENSE")).toBe("code");
60+ });
61+});
new file mode 100644
@@ -0,0 +1,61 @@
1+import { describe, expect, it } from "vitest";
2+import { kindForPath, languageForPath } from "./lang";
3+
4+describe("languageForPath", () => {
5+ it("maps common extensions to Monaco language ids", () => {
6+ expect(languageForPath("/w/main.go")).toBe("go");
7+ expect(languageForPath("src/App.tsx")).toBe("typescript");
8+ expect(languageForPath("script.py")).toBe("python");
9+ expect(languageForPath("style.css")).toBe("css");
10+ expect(languageForPath("run.sh")).toBe("shell");
11+ expect(languageForPath("doc.md")).toBe("markdown");
12+ });
13+
14+ it("recognises well-known extensionless files", () => {
15+ expect(languageForPath("/w/Makefile")).toBe("makefile");
16+ expect(languageForPath("/w/Dockerfile")).toBe("dockerfile");
17+ });
18+
19+ it("falls back to plaintext", () => {
20+ expect(languageForPath("notes.xyz")).toBe("plaintext");
21+ expect(languageForPath("LICENSE")).toBe("plaintext");
22+ expect(languageForPath(".gitignore")).toBe("plaintext");
23+ });
24+});
25+
26+describe("kindForPath", () => {
27+ it("detects markdown and asciidoc", () => {
28+ expect(kindForPath("README.md")).toBe("markdown");
29+ expect(kindForPath("book.adoc")).toBe("asciidoc");
30+ expect(kindForPath("book.asciidoc")).toBe("asciidoc");
31+ });
32+
33+ it("detects images, including draw.io exports", () => {
34+ for (const name of [
35+ "a.png",
36+ "b.JPG",
37+ "c.jpeg",
38+ "d.gif",
39+ "e.webp",
40+ "f.svg",
41+ "g.bmp",
42+ "h.ico",
43+ "i.avif",
44+ "diagram.drawio.svg",
45+ "diagram.drawio.png",
46+ ]) {
47+ expect(kindForPath(name), name).toBe("image");
48+ }
49+ });
50+
51+ it("detects draw.io XML diagrams", () => {
52+ expect(kindForPath("docs/diagrams/packages.drawio")).toBe("drawio");
53+ expect(kindForPath("flow.dio")).toBe("drawio");
54+ expect(languageForPath("flow.drawio")).toBe("xml");
55+ });
56+
57+ it("treats everything else as code", () => {
58+ expect(kindForPath("main.go")).toBe("code");
59+ expect(kindForPath("LICENSE")).toBe("code");
60+ });
61+});
added ui/src/lang.ts +101 -0
new file mode 100644
@@ -0,0 +1,101 @@
1+/**
2+ * File-type detection for the preview and editor panels: maps a file path to
3+ * a Monaco language id and to the kind of preview it deserves.
4+ *
5+ * @example
6+ * languageForPath("main.go"); // "go"
7+ * kindForPath("README.md"); // "markdown"
8+ */
9+
10+/**
11+ * How the preview pane should treat a file: a rendered document, a bitmap or
12+ * SVG shown as an image, a draw.io diagram, or code.
13+ */
14+export type DocumentKind =
15+ | "markdown"
16+ | "asciidoc"
17+ | "image"
18+ | "drawio"
19+ | "code";
20+
21+const imageExtensions = new Set([
22+ "avif",
23+ "bmp",
24+ "gif",
25+ "ico",
26+ "jpeg",
27+ "jpg",
28+ "png",
29+ "svg",
30+ "webp",
31+]);
32+
33+/** draw.io diagrams stored as XML (.drawio.svg/.drawio.png are plain images). */
34+const drawioExtensions = new Set(["drawio", "dio"]);
35+
36+const languageByExtension: Record<string, string> = {
37+ c: "c",
38+ cjs: "javascript",
39+ cpp: "cpp",
40+ css: "css",
41+ go: "go",
42+ h: "c",
43+ hpp: "cpp",
44+ html: "html",
45+ java: "java",
46+ js: "javascript",
47+ json: "json",
48+ jsx: "javascript",
49+ kt: "kotlin",
50+ md: "markdown",
51+ mjs: "javascript",
52+ php: "php",
53+ py: "python",
54+ rb: "ruby",
55+ rs: "rust",
56+ sh: "shell",
57+ bash: "shell",
58+ sql: "sql",
59+ svg: "xml",
60+ drawio: "xml",
61+ dio: "xml",
62+ swift: "swift",
63+ toml: "ini",
64+ ts: "typescript",
65+ tsx: "typescript",
66+ xml: "xml",
67+ yaml: "yaml",
68+ yml: "yaml",
69+};
70+
71+function extensionOf(path: string): string {
72+ const name = path.split("/").pop() ?? path;
73+ const dot = name.lastIndexOf(".");
74+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
75+}
76+
77+/** languageForPath returns the Monaco language id for a file (default: plaintext). */
78+export function languageForPath(path: string): string {
79+ if (path.endsWith("Makefile") || path.endsWith("Dockerfile")) {
80+ return path.endsWith("Makefile") ? "makefile" : "dockerfile";
81+ }
82+ return languageByExtension[extensionOf(path)] ?? "plaintext";
83+}
84+
85+/** kindForPath tells the preview pane whether a file has a rendered form. */
86+export function kindForPath(path: string): DocumentKind {
87+ const extension = extensionOf(path);
88+ if (extension === "md" || extension === "markdown") {
89+ return "markdown";
90+ }
91+ if (extension === "adoc" || extension === "asciidoc") {
92+ return "asciidoc";
93+ }
94+ if (imageExtensions.has(extension)) {
95+ return "image";
96+ }
97+ if (drawioExtensions.has(extension)) {
98+ return "drawio";
99+ }
100+ return "code";
101+}
new file mode 100644
@@ -0,0 +1,101 @@
1+/**
2+ * File-type detection for the preview and editor panels: maps a file path to
3+ * a Monaco language id and to the kind of preview it deserves.
4+ *
5+ * @example
6+ * languageForPath("main.go"); // "go"
7+ * kindForPath("README.md"); // "markdown"
8+ */
9+
10+/**
11+ * How the preview pane should treat a file: a rendered document, a bitmap or
12+ * SVG shown as an image, a draw.io diagram, or code.
13+ */
14+export type DocumentKind =
15+ | "markdown"
16+ | "asciidoc"
17+ | "image"
18+ | "drawio"
19+ | "code";
20+
21+const imageExtensions = new Set([
22+ "avif",
23+ "bmp",
24+ "gif",
25+ "ico",
26+ "jpeg",
27+ "jpg",
28+ "png",
29+ "svg",
30+ "webp",
31+]);
32+
33+/** draw.io diagrams stored as XML (.drawio.svg/.drawio.png are plain images). */
34+const drawioExtensions = new Set(["drawio", "dio"]);
35+
36+const languageByExtension: Record<string, string> = {
37+ c: "c",
38+ cjs: "javascript",
39+ cpp: "cpp",
40+ css: "css",
41+ go: "go",
42+ h: "c",
43+ hpp: "cpp",
44+ html: "html",
45+ java: "java",
46+ js: "javascript",
47+ json: "json",
48+ jsx: "javascript",
49+ kt: "kotlin",
50+ md: "markdown",
51+ mjs: "javascript",
52+ php: "php",
53+ py: "python",
54+ rb: "ruby",
55+ rs: "rust",
56+ sh: "shell",
57+ bash: "shell",
58+ sql: "sql",
59+ svg: "xml",
60+ drawio: "xml",
61+ dio: "xml",
62+ swift: "swift",
63+ toml: "ini",
64+ ts: "typescript",
65+ tsx: "typescript",
66+ xml: "xml",
67+ yaml: "yaml",
68+ yml: "yaml",
69+};
70+
71+function extensionOf(path: string): string {
72+ const name = path.split("/").pop() ?? path;
73+ const dot = name.lastIndexOf(".");
74+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
75+}
76+
77+/** languageForPath returns the Monaco language id for a file (default: plaintext). */
78+export function languageForPath(path: string): string {
79+ if (path.endsWith("Makefile") || path.endsWith("Dockerfile")) {
80+ return path.endsWith("Makefile") ? "makefile" : "dockerfile";
81+ }
82+ return languageByExtension[extensionOf(path)] ?? "plaintext";
83+}
84+
85+/** kindForPath tells the preview pane whether a file has a rendered form. */
86+export function kindForPath(path: string): DocumentKind {
87+ const extension = extensionOf(path);
88+ if (extension === "md" || extension === "markdown") {
89+ return "markdown";
90+ }
91+ if (extension === "adoc" || extension === "asciidoc") {
92+ return "asciidoc";
93+ }
94+ if (imageExtensions.has(extension)) {
95+ return "image";
96+ }
97+ if (drawioExtensions.has(extension)) {
98+ return "drawio";
99+ }
100+ return "code";
101+}
modified ui/src/main.tsx +3 -0
@@ -2,6 +2,9 @@ import { StrictMode } from "react";
22 import { createRoot } from "react-dom/client";
33 import { App } from "./App";
44 import { dispatch } from "./store";
5+// Imported for its side effect: stamps the remembered theme on <html>
6+// before the first render, so the page does not flash the default palette.
7+import "./theme";
58 import { connectSocket, wsUrl } from "./ws";
69 import "./app.css";
710
@@ -2,6 +2,9 @@ import { StrictMode } from "react";
2 import { createRoot } from "react-dom/client";2 import { createRoot } from "react-dom/client";
3 import { App } from "./App";3 import { App } from "./App";
4 import { dispatch } from "./store";4 import { dispatch } from "./store";
5+// Imported for its side effect: stamps the remembered theme on <html>
6+// before the first render, so the page does not flash the default palette.
7+import "./theme";
5 import { connectSocket, wsUrl } from "./ws";8 import { connectSocket, wsUrl } from "./ws";
6 import "./app.css";9 import "./app.css";
7 10
added ui/src/mentions.test.ts +143 -0
new file mode 100644
@@ -0,0 +1,143 @@
1+import { describe, expect, it } from "vitest";
2+import {
3+ activeAttachments,
4+ applyCompletion,
5+ detectTrigger,
6+ fileItem,
7+ matchesQuery,
8+ skillItems,
9+} from "./mentions";
10+
11+describe("detectTrigger", () => {
12+ it("opens the file selector on @ at the start or after a space", () => {
13+ expect(detectTrigger("@", 1)).toEqual({
14+ kind: "file",
15+ query: "",
16+ start: 0,
17+ });
18+ expect(detectTrigger("look at @src/ma", 15)).toEqual({
19+ kind: "file",
20+ query: "src/ma",
21+ start: 8,
22+ });
23+ });
24+
25+ it("ignores @ glued to a word or followed by whitespace", () => {
26+ expect(detectTrigger("mail me@example", 15)).toBeNull();
27+ expect(detectTrigger("@src done", 9)).toBeNull();
28+ });
29+
30+ it("only looks before the caret", () => {
31+ expect(detectTrigger("@a @b", 2)).toEqual({
32+ kind: "file",
33+ query: "a",
34+ start: 0,
35+ });
36+ });
37+
38+ it("opens the skill selector on a leading slash only", () => {
39+ expect(detectTrigger("/", 1)).toEqual({
40+ kind: "skill",
41+ query: "",
42+ start: 0,
43+ });
44+ expect(detectTrigger("/qua", 4)).toEqual({
45+ kind: "skill",
46+ query: "qua",
47+ start: 0,
48+ });
49+ expect(detectTrigger("/quality now", 12)).toBeNull();
50+ expect(detectTrigger("see /path", 9)).toBeNull();
51+ });
52+});
53+
54+describe("applyCompletion", () => {
55+ it("replaces the trigger and query, adds a space and moves the caret", () => {
56+ const trigger = detectTrigger("look at @src/ma please", 15);
57+ if (!trigger) {
58+ throw new Error("expected a trigger");
59+ }
60+ expect(
61+ applyCompletion("look at @src/ma please", trigger, "@src/main.go"),
62+ ).toEqual({
63+ text: "look at @src/main.go please",
64+ caret: "look at @src/main.go ".length,
65+ });
66+ });
67+
68+ it("handles the slash trigger at the start", () => {
69+ expect(
70+ applyCompletion(
71+ "/qu",
72+ { kind: "skill", query: "qu", start: 0 },
73+ "/quality",
74+ ),
75+ ).toEqual({
76+ text: "/quality ",
77+ caret: 9,
78+ });
79+ });
80+});
81+
82+describe("skillItems", () => {
83+ it("merges skills and commands, skills winning, sorted by name", () => {
84+ const items = skillItems(
85+ [
86+ {
87+ name: "quality",
88+ description: "Measure quality",
89+ path: "/p",
90+ source: "project",
91+ },
92+ {
93+ name: "review",
94+ description: "Skill review",
95+ path: "/p2",
96+ source: "user",
97+ },
98+ ],
99+ [
100+ { name: "review", description: "Command review" },
101+ { name: "compact", description: "Summarise" },
102+ ],
103+ );
104+ expect(items.map((i) => [i.label, i.source, i.insert])).toEqual([
105+ ["compact", "command", "/compact"],
106+ ["quality", "skill", "/quality"],
107+ ["review", "skill", "/review"],
108+ ]);
109+ });
110+
111+ it("filters on label or description, case-insensitively", () => {
112+ const [item] = skillItems(
113+ [],
114+ [{ name: "compact", description: "Summarise the thread" }],
115+ );
116+ expect(matchesQuery(item, "COMP")).toBe(true);
117+ expect(matchesQuery(item, "thread")).toBe(true);
118+ expect(matchesQuery(item, "zzz")).toBe(false);
119+ expect(matchesQuery(item, "")).toBe(true);
120+ });
121+});
122+
123+describe("fileItem / activeAttachments", () => {
124+ it("builds an @ insertion carrying the attachment", () => {
125+ expect(
126+ fileItem({ path: "/w/src/main.go", relPath: "src/main.go" }),
127+ ).toMatchObject({
128+ insert: "@src/main.go",
129+ attachment: { path: "/w/src/main.go", name: "src/main.go" },
130+ });
131+ });
132+
133+ it("drops attachments no longer mentioned and duplicates", () => {
134+ const attachments = [
135+ { path: "/w/a.go", name: "a.go" },
136+ { path: "/w/b.go", name: "b.go" },
137+ { path: "/w/a.go", name: "a.go" },
138+ ];
139+ expect(activeAttachments("see @a.go", attachments)).toEqual([
140+ { path: "/w/a.go", name: "a.go" },
141+ ]);
142+ });
143+});
new file mode 100644
@@ -0,0 +1,143 @@
1+import { describe, expect, it } from "vitest";
2+import {
3+ activeAttachments,
4+ applyCompletion,
5+ detectTrigger,
6+ fileItem,
7+ matchesQuery,
8+ skillItems,
9+} from "./mentions";
10+
11+describe("detectTrigger", () => {
12+ it("opens the file selector on @ at the start or after a space", () => {
13+ expect(detectTrigger("@", 1)).toEqual({
14+ kind: "file",
15+ query: "",
16+ start: 0,
17+ });
18+ expect(detectTrigger("look at @src/ma", 15)).toEqual({
19+ kind: "file",
20+ query: "src/ma",
21+ start: 8,
22+ });
23+ });
24+
25+ it("ignores @ glued to a word or followed by whitespace", () => {
26+ expect(detectTrigger("mail me@example", 15)).toBeNull();
27+ expect(detectTrigger("@src done", 9)).toBeNull();
28+ });
29+
30+ it("only looks before the caret", () => {
31+ expect(detectTrigger("@a @b", 2)).toEqual({
32+ kind: "file",
33+ query: "a",
34+ start: 0,
35+ });
36+ });
37+
38+ it("opens the skill selector on a leading slash only", () => {
39+ expect(detectTrigger("/", 1)).toEqual({
40+ kind: "skill",
41+ query: "",
42+ start: 0,
43+ });
44+ expect(detectTrigger("/qua", 4)).toEqual({
45+ kind: "skill",
46+ query: "qua",
47+ start: 0,
48+ });
49+ expect(detectTrigger("/quality now", 12)).toBeNull();
50+ expect(detectTrigger("see /path", 9)).toBeNull();
51+ });
52+});
53+
54+describe("applyCompletion", () => {
55+ it("replaces the trigger and query, adds a space and moves the caret", () => {
56+ const trigger = detectTrigger("look at @src/ma please", 15);
57+ if (!trigger) {
58+ throw new Error("expected a trigger");
59+ }
60+ expect(
61+ applyCompletion("look at @src/ma please", trigger, "@src/main.go"),
62+ ).toEqual({
63+ text: "look at @src/main.go please",
64+ caret: "look at @src/main.go ".length,
65+ });
66+ });
67+
68+ it("handles the slash trigger at the start", () => {
69+ expect(
70+ applyCompletion(
71+ "/qu",
72+ { kind: "skill", query: "qu", start: 0 },
73+ "/quality",
74+ ),
75+ ).toEqual({
76+ text: "/quality ",
77+ caret: 9,
78+ });
79+ });
80+});
81+
82+describe("skillItems", () => {
83+ it("merges skills and commands, skills winning, sorted by name", () => {
84+ const items = skillItems(
85+ [
86+ {
87+ name: "quality",
88+ description: "Measure quality",
89+ path: "/p",
90+ source: "project",
91+ },
92+ {
93+ name: "review",
94+ description: "Skill review",
95+ path: "/p2",
96+ source: "user",
97+ },
98+ ],
99+ [
100+ { name: "review", description: "Command review" },
101+ { name: "compact", description: "Summarise" },
102+ ],
103+ );
104+ expect(items.map((i) => [i.label, i.source, i.insert])).toEqual([
105+ ["compact", "command", "/compact"],
106+ ["quality", "skill", "/quality"],
107+ ["review", "skill", "/review"],
108+ ]);
109+ });
110+
111+ it("filters on label or description, case-insensitively", () => {
112+ const [item] = skillItems(
113+ [],
114+ [{ name: "compact", description: "Summarise the thread" }],
115+ );
116+ expect(matchesQuery(item, "COMP")).toBe(true);
117+ expect(matchesQuery(item, "thread")).toBe(true);
118+ expect(matchesQuery(item, "zzz")).toBe(false);
119+ expect(matchesQuery(item, "")).toBe(true);
120+ });
121+});
122+
123+describe("fileItem / activeAttachments", () => {
124+ it("builds an @ insertion carrying the attachment", () => {
125+ expect(
126+ fileItem({ path: "/w/src/main.go", relPath: "src/main.go" }),
127+ ).toMatchObject({
128+ insert: "@src/main.go",
129+ attachment: { path: "/w/src/main.go", name: "src/main.go" },
130+ });
131+ });
132+
133+ it("drops attachments no longer mentioned and duplicates", () => {
134+ const attachments = [
135+ { path: "/w/a.go", name: "a.go" },
136+ { path: "/w/b.go", name: "b.go" },
137+ { path: "/w/a.go", name: "a.go" },
138+ ];
139+ expect(activeAttachments("see @a.go", attachments)).toEqual([
140+ { path: "/w/a.go", name: "a.go" },
141+ ]);
142+ });
143+});
added ui/src/mentions.ts +153 -0
new file mode 100644
@@ -0,0 +1,153 @@
1+/**
2+ * Pure logic behind the composer's "@" file selector and "/" skill selector:
3+ * detecting a trigger at the caret, applying a completion, merging skills
4+ * and agent commands, and keeping only the attachments still mentioned.
5+ *
6+ * @example
7+ * detectTrigger("look at @src/ma", 15); // { kind: "file", query: "src/ma", start: 8 }
8+ * applyCompletion("look at @src/ma", trigger, "@src/main.go");
9+ * // { text: "look at @src/main.go ", caret: 21 }
10+ */
11+
12+import type { Skill } from "./api";
13+import type { AcpAvailableCommand, PromptAttachment } from "./protocol";
14+
15+/** An active selector: what the user typed after "@" or "/", and where. */
16+export interface Trigger {
17+ kind: "file" | "skill";
18+ /** Text typed after the trigger character, up to the caret. */
19+ query: string;
20+ /** Index of the trigger character in the text. */
21+ start: number;
22+}
23+
24+/** One row of the completion popup. */
25+export interface CompletionItem {
26+ /** Stable identifier (path for files, name for skills/commands). */
27+ id: string;
28+ /** Main label. */
29+ label: string;
30+ /** Secondary line (description, or nothing for files). */
31+ description: string;
32+ /** Where it comes from; shown as a tag for skills and commands. */
33+ source: "file" | "skill" | "command";
34+ /** Text inserted in place of the trigger and its query. */
35+ insert: string;
36+ /** For files: the attachment to send with the prompt. */
37+ attachment?: PromptAttachment;
38+}
39+
40+/**
41+ * detectTrigger finds the selector active at the caret: "/" at the very
42+ * start of the text (Claude Code slash-command style), or "@" preceded by a
43+ * whitespace or the start, with no whitespace between it and the caret.
44+ */
45+export function detectTrigger(text: string, caret: number): Trigger | null {
46+ const before = text.slice(0, caret);
47+ const slash = /^\/(\S*)$/.exec(before);
48+ if (slash) {
49+ return { kind: "skill", query: slash[1], start: 0 };
50+ }
51+ const at = before.lastIndexOf("@");
52+ if (at < 0) {
53+ return null;
54+ }
55+ const query = before.slice(at + 1);
56+ if (/\s/.test(query)) {
57+ return null;
58+ }
59+ if (at > 0 && !/\s/.test(before[at - 1])) {
60+ return null;
61+ }
62+ return { kind: "file", query, start: at };
63+}
64+
65+/**
66+ * applyCompletion replaces the trigger and its query with `insert` followed
67+ * by a space, and returns the new text and caret position.
68+ */
69+export function applyCompletion(
70+ text: string,
71+ trigger: Trigger,
72+ insert: string,
73+): { text: string; caret: number } {
74+ const caret = trigger.start + trigger.query.length + 1;
75+ const head = text.slice(0, trigger.start);
76+ const tail = text.slice(caret);
77+ const inserted = `${head}${insert} `;
78+ return { text: inserted + tail, caret: inserted.length };
79+}
80+
81+/** matchesQuery is the popup's filter: case-insensitive substring on label or description. */
82+export function matchesQuery(item: CompletionItem, query: string): boolean {
83+ const needle = query.toLowerCase();
84+ return (
85+ needle === "" ||
86+ item.label.toLowerCase().includes(needle) ||
87+ item.description.toLowerCase().includes(needle)
88+ );
89+}
90+
91+/**
92+ * skillItems merges the skills discovered on disk with the commands the
93+ * agent announced, deduplicated by name (a skill wins over a command of the
94+ * same name, since it carries the richer description), sorted by name.
95+ */
96+export function skillItems(
97+ skills: Skill[],
98+ commands: AcpAvailableCommand[],
99+): CompletionItem[] {
100+ const byName = new Map<string, CompletionItem>();
101+ for (const command of commands) {
102+ byName.set(command.name, {
103+ id: command.name,
104+ label: command.name,
105+ description: command.description,
106+ source: "command",
107+ insert: `/${command.name}`,
108+ });
109+ }
110+ for (const skill of skills) {
111+ byName.set(skill.name, {
112+ id: skill.name,
113+ label: skill.name,
114+ description: skill.description,
115+ source: "skill",
116+ insert: `/${skill.name}`,
117+ });
118+ }
119+ return [...byName.values()].sort((a, b) => a.label.localeCompare(b.label));
120+}
121+
122+/** fileItem turns a search hit into a popup row inserting "@<relative path>". */
123+export function fileItem(hit: {
124+ path: string;
125+ relPath: string;
126+}): CompletionItem {
127+ return {
128+ id: hit.path,
129+ label: hit.relPath,
130+ description: "",
131+ source: "file",
132+ insert: `@${hit.relPath}`,
133+ attachment: { path: hit.path, name: hit.relPath },
134+ };
135+}
136+
137+/**
138+ * activeAttachments keeps the attachments whose "@name" mention is still
139+ * present in the text — the user may have deleted one before sending.
140+ */
141+export function activeAttachments(
142+ text: string,
143+ attachments: PromptAttachment[],
144+): PromptAttachment[] {
145+ const seen = new Set<string>();
146+ return attachments.filter((attachment) => {
147+ if (seen.has(attachment.path) || !text.includes(`@${attachment.name}`)) {
148+ return false;
149+ }
150+ seen.add(attachment.path);
151+ return true;
152+ });
153+}
new file mode 100644
@@ -0,0 +1,153 @@
1+/**
2+ * Pure logic behind the composer's "@" file selector and "/" skill selector:
3+ * detecting a trigger at the caret, applying a completion, merging skills
4+ * and agent commands, and keeping only the attachments still mentioned.
5+ *
6+ * @example
7+ * detectTrigger("look at @src/ma", 15); // { kind: "file", query: "src/ma", start: 8 }
8+ * applyCompletion("look at @src/ma", trigger, "@src/main.go");
9+ * // { text: "look at @src/main.go ", caret: 21 }
10+ */
11+
12+import type { Skill } from "./api";
13+import type { AcpAvailableCommand, PromptAttachment } from "./protocol";
14+
15+/** An active selector: what the user typed after "@" or "/", and where. */
16+export interface Trigger {
17+ kind: "file" | "skill";
18+ /** Text typed after the trigger character, up to the caret. */
19+ query: string;
20+ /** Index of the trigger character in the text. */
21+ start: number;
22+}
23+
24+/** One row of the completion popup. */
25+export interface CompletionItem {
26+ /** Stable identifier (path for files, name for skills/commands). */
27+ id: string;
28+ /** Main label. */
29+ label: string;
30+ /** Secondary line (description, or nothing for files). */
31+ description: string;
32+ /** Where it comes from; shown as a tag for skills and commands. */
33+ source: "file" | "skill" | "command";
34+ /** Text inserted in place of the trigger and its query. */
35+ insert: string;
36+ /** For files: the attachment to send with the prompt. */
37+ attachment?: PromptAttachment;
38+}
39+
40+/**
41+ * detectTrigger finds the selector active at the caret: "/" at the very
42+ * start of the text (Claude Code slash-command style), or "@" preceded by a
43+ * whitespace or the start, with no whitespace between it and the caret.
44+ */
45+export function detectTrigger(text: string, caret: number): Trigger | null {
46+ const before = text.slice(0, caret);
47+ const slash = /^\/(\S*)$/.exec(before);
48+ if (slash) {
49+ return { kind: "skill", query: slash[1], start: 0 };
50+ }
51+ const at = before.lastIndexOf("@");
52+ if (at < 0) {
53+ return null;
54+ }
55+ const query = before.slice(at + 1);
56+ if (/\s/.test(query)) {
57+ return null;
58+ }
59+ if (at > 0 && !/\s/.test(before[at - 1])) {
60+ return null;
61+ }
62+ return { kind: "file", query, start: at };
63+}
64+
65+/**
66+ * applyCompletion replaces the trigger and its query with `insert` followed
67+ * by a space, and returns the new text and caret position.
68+ */
69+export function applyCompletion(
70+ text: string,
71+ trigger: Trigger,
72+ insert: string,
73+): { text: string; caret: number } {
74+ const caret = trigger.start + trigger.query.length + 1;
75+ const head = text.slice(0, trigger.start);
76+ const tail = text.slice(caret);
77+ const inserted = `${head}${insert} `;
78+ return { text: inserted + tail, caret: inserted.length };
79+}
80+
81+/** matchesQuery is the popup's filter: case-insensitive substring on label or description. */
82+export function matchesQuery(item: CompletionItem, query: string): boolean {
83+ const needle = query.toLowerCase();
84+ return (
85+ needle === "" ||
86+ item.label.toLowerCase().includes(needle) ||
87+ item.description.toLowerCase().includes(needle)
88+ );
89+}
90+
91+/**
92+ * skillItems merges the skills discovered on disk with the commands the
93+ * agent announced, deduplicated by name (a skill wins over a command of the
94+ * same name, since it carries the richer description), sorted by name.
95+ */
96+export function skillItems(
97+ skills: Skill[],
98+ commands: AcpAvailableCommand[],
99+): CompletionItem[] {
100+ const byName = new Map<string, CompletionItem>();
101+ for (const command of commands) {
102+ byName.set(command.name, {
103+ id: command.name,
104+ label: command.name,
105+ description: command.description,
106+ source: "command",
107+ insert: `/${command.name}`,
108+ });
109+ }
110+ for (const skill of skills) {
111+ byName.set(skill.name, {
112+ id: skill.name,
113+ label: skill.name,
114+ description: skill.description,
115+ source: "skill",
116+ insert: `/${skill.name}`,
117+ });
118+ }
119+ return [...byName.values()].sort((a, b) => a.label.localeCompare(b.label));
120+}
121+
122+/** fileItem turns a search hit into a popup row inserting "@<relative path>". */
123+export function fileItem(hit: {
124+ path: string;
125+ relPath: string;
126+}): CompletionItem {
127+ return {
128+ id: hit.path,
129+ label: hit.relPath,
130+ description: "",
131+ source: "file",
132+ insert: `@${hit.relPath}`,
133+ attachment: { path: hit.path, name: hit.relPath },
134+ };
135+}
136+
137+/**
138+ * activeAttachments keeps the attachments whose "@name" mention is still
139+ * present in the text — the user may have deleted one before sending.
140+ */
141+export function activeAttachments(
142+ text: string,
143+ attachments: PromptAttachment[],
144+): PromptAttachment[] {
145+ const seen = new Set<string>();
146+ return attachments.filter((attachment) => {
147+ if (seen.has(attachment.path) || !text.includes(`@${attachment.name}`)) {
148+ return false;
149+ }
150+ seen.add(attachment.path);
151+ return true;
152+ });
153+}
added ui/src/monaco-setup.ts +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+/**
2+ * Bundles Monaco with the application instead of the default CDN loader, so
3+ * ori keeps working without internet access. Imported only by MonacoViewer,
4+ * which is itself lazy-loaded: none of this weight reaches the initial
5+ * bundle.
6+ */
7+
8+import { loader } from "@monaco-editor/react";
9+import * as monaco from "monaco-editor";
10+import editorWorker from "monaco-editor/editor/editor.worker.js?worker";
11+import cssWorker from "monaco-editor/language/css/css.worker.js?worker";
12+import htmlWorker from "monaco-editor/language/html/html.worker.js?worker";
13+import jsonWorker from "monaco-editor/language/json/json.worker.js?worker";
14+import tsWorker from "monaco-editor/language/typescript/ts.worker.js?worker";
15+
16+self.MonacoEnvironment = {
17+ getWorker(_workerId: string, label: string) {
18+ switch (label) {
19+ case "json":
20+ return new jsonWorker();
21+ case "css":
22+ case "scss":
23+ case "less":
24+ return new cssWorker();
25+ case "html":
26+ case "handlebars":
27+ case "razor":
28+ return new htmlWorker();
29+ case "typescript":
30+ case "javascript":
31+ return new tsWorker();
32+ default:
33+ return new editorWorker();
34+ }
35+ },
36+};
37+
38+loader.config({ monaco });
new file mode 100644
@@ -0,0 +1,38 @@
1+/**
2+ * Bundles Monaco with the application instead of the default CDN loader, so
3+ * ori keeps working without internet access. Imported only by MonacoViewer,
4+ * which is itself lazy-loaded: none of this weight reaches the initial
5+ * bundle.
6+ */
7+
8+import { loader } from "@monaco-editor/react";
9+import * as monaco from "monaco-editor";
10+import editorWorker from "monaco-editor/editor/editor.worker.js?worker";
11+import cssWorker from "monaco-editor/language/css/css.worker.js?worker";
12+import htmlWorker from "monaco-editor/language/html/html.worker.js?worker";
13+import jsonWorker from "monaco-editor/language/json/json.worker.js?worker";
14+import tsWorker from "monaco-editor/language/typescript/ts.worker.js?worker";
15+
16+self.MonacoEnvironment = {
17+ getWorker(_workerId: string, label: string) {
18+ switch (label) {
19+ case "json":
20+ return new jsonWorker();
21+ case "css":
22+ case "scss":
23+ case "less":
24+ return new cssWorker();
25+ case "html":
26+ case "handlebars":
27+ case "razor":
28+ return new htmlWorker();
29+ case "typescript":
30+ case "javascript":
31+ return new tsWorker();
32+ default:
33+ return new editorWorker();
34+ }
35+ },
36+};
37+
38+loader.config({ monaco });
modified ui/src/protocol.ts +20 -1
@@ -42,6 +42,13 @@ export interface AcpPermissionOption {
4242 kind: string;
4343 }
4444
45+/** One slash command announced by the agent (available_commands_update). */
46+export interface AcpAvailableCommand {
47+ name: string;
48+ description: string;
49+ input?: { hint?: string } | null;
50+}
51+
4552 /** The ACP payload of a session/request_permission call. */
4653 export interface AcpPermissionRequest {
4754 sessionId?: string;
@@ -68,9 +75,20 @@ export interface AcpSessionUpdate {
6875 status?: string;
6976 locations?: AcpToolCallLocation[];
7077 entries?: AcpPlanEntry[];
78+ availableCommands?: AcpAvailableCommand[];
7179 [key: string]: unknown;
7280 }
7381
82+/**
83+ * A workspace file mentioned in a prompt with "@": the server turns it into
84+ * an ACP resource_link block next to the text. `path` is absolute (as
85+ * returned by the file search), `name` the relative path shown in the text.
86+ */
87+export interface PromptAttachment {
88+ path: string;
89+ name: string;
90+}
91+
7492 /** A message sent by the server over the WebSocket. */
7593 export interface ServerMessage {
7694 type:
@@ -88,13 +106,14 @@ export interface ServerMessage {
88106 requestId?: string;
89107 request?: AcpPermissionRequest;
90108 text?: string;
109+ attachments?: PromptAttachment[];
91110 stopReason?: string;
92111 message?: string;
93112 }
94113
95114 /** A message the browser sends to the server. */
96115 export type ClientMessage =
97- | { type: "prompt"; text: string }
116+ | { type: "prompt"; text: string; attachments?: PromptAttachment[] }
98117 | { type: "cancel" }
99118 | {
100119 type: "permission_response";
@@ -42,6 +42,13 @@ export interface AcpPermissionOption {
42 kind: string;42 kind: string;
43 }43 }
44 44
45+/** One slash command announced by the agent (available_commands_update). */
46+export interface AcpAvailableCommand {
47+ name: string;
48+ description: string;
49+ input?: { hint?: string } | null;
50+}
51+
45 /** The ACP payload of a session/request_permission call. */52 /** The ACP payload of a session/request_permission call. */
46 export interface AcpPermissionRequest {53 export interface AcpPermissionRequest {
47 sessionId?: string;54 sessionId?: string;
@@ -68,9 +75,20 @@ export interface AcpSessionUpdate {
68 status?: string;75 status?: string;
69 locations?: AcpToolCallLocation[];76 locations?: AcpToolCallLocation[];
70 entries?: AcpPlanEntry[];77 entries?: AcpPlanEntry[];
78+ availableCommands?: AcpAvailableCommand[];
71 [key: string]: unknown;79 [key: string]: unknown;
72 }80 }
73 81
82+/**
83+ * A workspace file mentioned in a prompt with "@": the server turns it into
84+ * an ACP resource_link block next to the text. `path` is absolute (as
85+ * returned by the file search), `name` the relative path shown in the text.
86+ */
87+export interface PromptAttachment {
88+ path: string;
89+ name: string;
90+}
91+
74 /** A message sent by the server over the WebSocket. */92 /** A message sent by the server over the WebSocket. */
75 export interface ServerMessage {93 export interface ServerMessage {
76 type:94 type:
@@ -88,13 +106,14 @@ export interface ServerMessage {
88 requestId?: string;106 requestId?: string;
89 request?: AcpPermissionRequest;107 request?: AcpPermissionRequest;
90 text?: string;108 text?: string;
109+ attachments?: PromptAttachment[];
91 stopReason?: string;110 stopReason?: string;
92 message?: string;111 message?: string;
93 }112 }
94 113
95 /** A message the browser sends to the server. */114 /** A message the browser sends to the server. */
96 export type ClientMessage =115 export type ClientMessage =
97- | { type: "prompt"; text: string }116+ | { type: "prompt"; text: string; attachments?: PromptAttachment[] }
98 | { type: "cancel" }117 | { type: "cancel" }
99 | {118 | {
100 type: "permission_response";119 type: "permission_response";
modified ui/src/reducer.test.ts +23 -0
@@ -275,6 +275,29 @@ describe("reduce", () => {
275275 expect(state.plan).toEqual([]);
276276 });
277277
278+ it("stores the agent's available commands and clears them on hello", () => {
279+ let state = reduce(initialState, {
280+ type: "server",
281+ message: {
282+ type: "session_update",
283+ update: {
284+ sessionUpdate: "available_commands_update",
285+ availableCommands: [
286+ { name: "review", description: "Review changes" },
287+ { name: "compact", description: "Summarise", input: { hint: "x" } },
288+ ],
289+ },
290+ },
291+ });
292+ expect(state.commands.map((c) => c.name)).toEqual(["review", "compact"]);
293+
294+ state = reduce(state, {
295+ type: "server",
296+ message: { type: "hello", sessionId: "s2" },
297+ });
298+ expect(state.commands).toEqual([]);
299+ });
300+
278301 it("tracks connection status changes", () => {
279302 const state = reduce(initialState, {
280303 type: "connection",
@@ -275,6 +275,29 @@ describe("reduce", () => {
275 expect(state.plan).toEqual([]);275 expect(state.plan).toEqual([]);
276 });276 });
277 277
278+ it("stores the agent's available commands and clears them on hello", () => {
279+ let state = reduce(initialState, {
280+ type: "server",
281+ message: {
282+ type: "session_update",
283+ update: {
284+ sessionUpdate: "available_commands_update",
285+ availableCommands: [
286+ { name: "review", description: "Review changes" },
287+ { name: "compact", description: "Summarise", input: { hint: "x" } },
288+ ],
289+ },
290+ },
291+ });
292+ expect(state.commands.map((c) => c.name)).toEqual(["review", "compact"]);
293+
294+ state = reduce(state, {
295+ type: "server",
296+ message: { type: "hello", sessionId: "s2" },
297+ });
298+ expect(state.commands).toEqual([]);
299+ });
300+
278 it("tracks connection status changes", () => {301 it("tracks connection status changes", () => {
279 const state = reduce(initialState, {302 const state = reduce(initialState, {
280 type: "connection",303 type: "connection",
modified ui/src/reducer.ts +11 -3
@@ -5,6 +5,7 @@
55 */
66
77 import type {
8+ AcpAvailableCommand,
89 AcpContentBlock,
910 AcpPlanEntry,
1011 AcpPermissionOption,
@@ -48,6 +49,8 @@ export interface ChatState {
4849 thread: ThreadItem[];
4950 plan: AcpPlanEntry[];
5051 permissions: PermissionRequestState[];
52+ /** Slash commands the agent announced (available_commands_update). */
53+ commands: AcpAvailableCommand[];
5154 }
5255
5356 export const initialState: ChatState = {
@@ -57,6 +60,7 @@ export const initialState: ChatState = {
5760 thread: [],
5861 plan: [],
5962 permissions: [],
63+ commands: [],
6064 };
6165
6266 /** Everything that can change the state. */
@@ -95,6 +99,7 @@ const serverHandlers: Record<string, MessageHandler> = {
9599 thread: [],
96100 plan: [],
97101 permissions: [],
102+ commands: [],
98103 }),
99104 user_message: (state, msg) =>
100105 withItem(state, { kind: "user", text: msg.text ?? "" }),
@@ -115,9 +120,8 @@ const serverHandlers: Record<string, MessageHandler> = {
115120 };
116121
117122 /**
118- * One handler per ACP update kind; the rest (available_commands_update,
119- * current_mode_update, ...) are not rendered yet, and ignoring them keeps the
120- * UI forward-compatible.
123+ * One handler per ACP update kind; the rest (current_mode_update, ...) are
124+ * not rendered yet, and ignoring them keeps the UI forward-compatible.
121125 */
122126 const updateHandlers: Record<string, MessageHandler> = {
123127 agent_message_chunk: (state, msg) =>
@@ -127,6 +131,10 @@ const updateHandlers: Record<string, MessageHandler> = {
127131 tool_call: upsertToolCall,
128132 tool_call_update: upsertToolCall,
129133 plan: (state, msg) => ({ ...state, plan: msg.update?.entries ?? [] }),
134+ available_commands_update: (state, msg) => ({
135+ ...state,
136+ commands: msg.update?.availableCommands ?? [],
137+ }),
130138 };
131139
132140 function reduceUpdate(state: ChatState, msg: ServerMessage): ChatState {
@@ -5,6 +5,7 @@
5 */5 */
6 6
7 import type {7 import type {
8+ AcpAvailableCommand,
8 AcpContentBlock,9 AcpContentBlock,
9 AcpPlanEntry,10 AcpPlanEntry,
10 AcpPermissionOption,11 AcpPermissionOption,
@@ -48,6 +49,8 @@ export interface ChatState {
48 thread: ThreadItem[];49 thread: ThreadItem[];
49 plan: AcpPlanEntry[];50 plan: AcpPlanEntry[];
50 permissions: PermissionRequestState[];51 permissions: PermissionRequestState[];
52+ /** Slash commands the agent announced (available_commands_update). */
53+ commands: AcpAvailableCommand[];
51 }54 }
52 55
53 export const initialState: ChatState = {56 export const initialState: ChatState = {
@@ -57,6 +60,7 @@ export const initialState: ChatState = {
57 thread: [],60 thread: [],
58 plan: [],61 plan: [],
59 permissions: [],62 permissions: [],
63+ commands: [],
60 };64 };
61 65
62 /** Everything that can change the state. */66 /** Everything that can change the state. */
@@ -95,6 +99,7 @@ const serverHandlers: Record<string, MessageHandler> = {
95 thread: [],99 thread: [],
96 plan: [],100 plan: [],
97 permissions: [],101 permissions: [],
102+ commands: [],
98 }),103 }),
99 user_message: (state, msg) =>104 user_message: (state, msg) =>
100 withItem(state, { kind: "user", text: msg.text ?? "" }),105 withItem(state, { kind: "user", text: msg.text ?? "" }),
@@ -115,9 +120,8 @@ const serverHandlers: Record<string, MessageHandler> = {
115 };120 };
116 121
117 /**122 /**
118- * One handler per ACP update kind; the rest (available_commands_update,123+ * One handler per ACP update kind; the rest (current_mode_update, ...) are
119- * current_mode_update, ...) are not rendered yet, and ignoring them keeps the124+ * not rendered yet, and ignoring them keeps the UI forward-compatible.
120- * UI forward-compatible.
121 */125 */
122 const updateHandlers: Record<string, MessageHandler> = {126 const updateHandlers: Record<string, MessageHandler> = {
123 agent_message_chunk: (state, msg) =>127 agent_message_chunk: (state, msg) =>
@@ -127,6 +131,10 @@ const updateHandlers: Record<string, MessageHandler> = {
127 tool_call: upsertToolCall,131 tool_call: upsertToolCall,
128 tool_call_update: upsertToolCall,132 tool_call_update: upsertToolCall,
129 plan: (state, msg) => ({ ...state, plan: msg.update?.entries ?? [] }),133 plan: (state, msg) => ({ ...state, plan: msg.update?.entries ?? [] }),
134+ available_commands_update: (state, msg) => ({
135+ ...state,
136+ commands: msg.update?.availableCommands ?? [],
137+ }),
130 };138 };
131 139
132 function reduceUpdate(state: ChatState, msg: ServerMessage): ChatState {140 function reduceUpdate(state: ChatState, msg: ServerMessage): ChatState {
added ui/src/theme.test.ts +54 -0
new file mode 100644
@@ -0,0 +1,54 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import {
3+ DEFAULT_THEME,
4+ THEME_KEY,
5+ isTheme,
6+ resetTheme,
7+ setTheme,
8+ toggleTheme,
9+ useTheme,
10+} from "./theme";
11+
12+describe("theme store", () => {
13+ beforeEach(() => {
14+ localStorage.clear();
15+ resetTheme();
16+ });
17+
18+ it("starts light and stamps the document", () => {
19+ expect(useTheme.getState().theme).toBe(DEFAULT_THEME);
20+ expect(document.documentElement.dataset.theme).toBe("light");
21+ });
22+
23+ it("setTheme applies, stamps and remembers the theme", () => {
24+ setTheme("dark");
25+ expect(useTheme.getState().theme).toBe("dark");
26+ expect(document.documentElement.dataset.theme).toBe("dark");
27+ expect(localStorage.getItem(THEME_KEY)).toBe("dark");
28+ });
29+
30+ it("toggleTheme alternates between light and dark", () => {
31+ toggleTheme();
32+ expect(useTheme.getState().theme).toBe("dark");
33+ toggleTheme();
34+ expect(useTheme.getState().theme).toBe("light");
35+ expect(document.documentElement.dataset.theme).toBe("light");
36+ });
37+
38+ it("resetTheme goes back to light", () => {
39+ setTheme("dark");
40+ resetTheme();
41+ expect(useTheme.getState().theme).toBe("light");
42+ expect(localStorage.getItem(THEME_KEY)).toBe("light");
43+ });
44+});
45+
46+describe("isTheme", () => {
47+ it("accepts the two themes and rejects anything else", () => {
48+ expect(isTheme("light")).toBe(true);
49+ expect(isTheme("dark")).toBe(true);
50+ expect(isTheme("system")).toBe(false);
51+ expect(isTheme(null)).toBe(false);
52+ expect(isTheme(42)).toBe(false);
53+ });
54+});
new file mode 100644
@@ -0,0 +1,54 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import {
3+ DEFAULT_THEME,
4+ THEME_KEY,
5+ isTheme,
6+ resetTheme,
7+ setTheme,
8+ toggleTheme,
9+ useTheme,
10+} from "./theme";
11+
12+describe("theme store", () => {
13+ beforeEach(() => {
14+ localStorage.clear();
15+ resetTheme();
16+ });
17+
18+ it("starts light and stamps the document", () => {
19+ expect(useTheme.getState().theme).toBe(DEFAULT_THEME);
20+ expect(document.documentElement.dataset.theme).toBe("light");
21+ });
22+
23+ it("setTheme applies, stamps and remembers the theme", () => {
24+ setTheme("dark");
25+ expect(useTheme.getState().theme).toBe("dark");
26+ expect(document.documentElement.dataset.theme).toBe("dark");
27+ expect(localStorage.getItem(THEME_KEY)).toBe("dark");
28+ });
29+
30+ it("toggleTheme alternates between light and dark", () => {
31+ toggleTheme();
32+ expect(useTheme.getState().theme).toBe("dark");
33+ toggleTheme();
34+ expect(useTheme.getState().theme).toBe("light");
35+ expect(document.documentElement.dataset.theme).toBe("light");
36+ });
37+
38+ it("resetTheme goes back to light", () => {
39+ setTheme("dark");
40+ resetTheme();
41+ expect(useTheme.getState().theme).toBe("light");
42+ expect(localStorage.getItem(THEME_KEY)).toBe("light");
43+ });
44+});
45+
46+describe("isTheme", () => {
47+ it("accepts the two themes and rejects anything else", () => {
48+ expect(isTheme("light")).toBe(true);
49+ expect(isTheme("dark")).toBe(true);
50+ expect(isTheme("system")).toBe(false);
51+ expect(isTheme(null)).toBe(false);
52+ expect(isTheme(42)).toBe(false);
53+ });
54+});
added ui/src/theme.ts +87 -0
new file mode 100644
@@ -0,0 +1,87 @@
1+/**
2+ * Colour theme of the app: light (the default) or dark. The choice is kept
3+ * apart from the chat and workspace state because it is a per-browser
4+ * preference, remembered in localStorage and applied to the document as a
5+ * `data-theme` attribute that the stylesheet keys its palettes on.
6+ *
7+ * @example
8+ * toggleTheme(); // light → dark, dark → light
9+ * useTheme((t) => t.theme); // inside a component
10+ * document.documentElement.dataset.theme; // "dark"
11+ */
12+
13+import { create } from "zustand";
14+
15+export type Theme = "light" | "dark";
16+
17+/** Theme used until the user picks one. */
18+export const DEFAULT_THEME: Theme = "light";
19+/** localStorage key under which the chosen theme is remembered. */
20+export const THEME_KEY = "ori.theme";
21+
22+export interface ThemeState {
23+ /** The active theme. */
24+ theme: Theme;
25+}
26+
27+/** isTheme narrows an arbitrary value (e.g. read from storage) to a Theme. */
28+export function isTheme(value: unknown): value is Theme {
29+ return value === "light" || value === "dark";
30+}
31+
32+/** React hook over the theme store. */
33+export const useTheme = create<ThemeState>(() => ({ theme: loadTheme() }));
34+
35+/**
36+ * setTheme activates a theme, reflects it on the document and remembers it.
37+ *
38+ * @example
39+ * setTheme("dark");
40+ * useTheme.getState().theme; // "dark"
41+ */
42+export function setTheme(theme: Theme): void {
43+ useTheme.setState({ theme });
44+ applyTheme(theme);
45+ saveTheme(theme);
46+}
47+
48+/** toggleTheme switches between light and dark. */
49+export function toggleTheme(): void {
50+ setTheme(useTheme.getState().theme === "dark" ? "light" : "dark");
51+}
52+
53+/** resetTheme restores the default theme and forgets the stored one; meant for tests. */
54+export function resetTheme(): void {
55+ setTheme(DEFAULT_THEME);
56+}
57+
58+/** applyTheme stamps the theme on <html> so the CSS palettes switch. */
59+function applyTheme(theme: Theme): void {
60+ document.documentElement.dataset.theme = theme;
61+}
62+
63+/**
64+ * loadTheme reads the remembered theme. Storage can be missing or throw
65+ * (private browsing, blocked site data), and the stored value may be stale,
66+ * so any failure or unknown value yields the default.
67+ */
68+function loadTheme(): Theme {
69+ try {
70+ const stored = globalThis.localStorage?.getItem(THEME_KEY);
71+ return isTheme(stored) ? stored : DEFAULT_THEME;
72+ } catch {
73+ return DEFAULT_THEME;
74+ }
75+}
76+
77+function saveTheme(theme: Theme): void {
78+ try {
79+ globalThis.localStorage?.setItem(THEME_KEY, theme);
80+ } catch {
81+ // Storage unavailable: the theme still applies for this page.
82+ }
83+}
84+
85+// Stamp the document as soon as the module loads, before React renders, so
86+// the first paint already uses the remembered theme.
87+applyTheme(useTheme.getState().theme);
new file mode 100644
@@ -0,0 +1,87 @@
1+/**
2+ * Colour theme of the app: light (the default) or dark. The choice is kept
3+ * apart from the chat and workspace state because it is a per-browser
4+ * preference, remembered in localStorage and applied to the document as a
5+ * `data-theme` attribute that the stylesheet keys its palettes on.
6+ *
7+ * @example
8+ * toggleTheme(); // light → dark, dark → light
9+ * useTheme((t) => t.theme); // inside a component
10+ * document.documentElement.dataset.theme; // "dark"
11+ */
12+
13+import { create } from "zustand";
14+
15+export type Theme = "light" | "dark";
16+
17+/** Theme used until the user picks one. */
18+export const DEFAULT_THEME: Theme = "light";
19+/** localStorage key under which the chosen theme is remembered. */
20+export const THEME_KEY = "ori.theme";
21+
22+export interface ThemeState {
23+ /** The active theme. */
24+ theme: Theme;
25+}
26+
27+/** isTheme narrows an arbitrary value (e.g. read from storage) to a Theme. */
28+export function isTheme(value: unknown): value is Theme {
29+ return value === "light" || value === "dark";
30+}
31+
32+/** React hook over the theme store. */
33+export const useTheme = create<ThemeState>(() => ({ theme: loadTheme() }));
34+
35+/**
36+ * setTheme activates a theme, reflects it on the document and remembers it.
37+ *
38+ * @example
39+ * setTheme("dark");
40+ * useTheme.getState().theme; // "dark"
41+ */
42+export function setTheme(theme: Theme): void {
43+ useTheme.setState({ theme });
44+ applyTheme(theme);
45+ saveTheme(theme);
46+}
47+
48+/** toggleTheme switches between light and dark. */
49+export function toggleTheme(): void {
50+ setTheme(useTheme.getState().theme === "dark" ? "light" : "dark");
51+}
52+
53+/** resetTheme restores the default theme and forgets the stored one; meant for tests. */
54+export function resetTheme(): void {
55+ setTheme(DEFAULT_THEME);
56+}
57+
58+/** applyTheme stamps the theme on <html> so the CSS palettes switch. */
59+function applyTheme(theme: Theme): void {
60+ document.documentElement.dataset.theme = theme;
61+}
62+
63+/**
64+ * loadTheme reads the remembered theme. Storage can be missing or throw
65+ * (private browsing, blocked site data), and the stored value may be stale,
66+ * so any failure or unknown value yields the default.
67+ */
68+function loadTheme(): Theme {
69+ try {
70+ const stored = globalThis.localStorage?.getItem(THEME_KEY);
71+ return isTheme(stored) ? stored : DEFAULT_THEME;
72+ } catch {
73+ return DEFAULT_THEME;
74+ }
75+}
76+
77+function saveTheme(theme: Theme): void {
78+ try {
79+ globalThis.localStorage?.setItem(THEME_KEY, theme);
80+ } catch {
81+ // Storage unavailable: the theme still applies for this page.
82+ }
83+}
84+
85+// Stamp the document as soon as the module loads, before React renders, so
86+// the first paint already uses the remembered theme.
87+applyTheme(useTheme.getState().theme);
added ui/src/useCompletion.ts +154 -0
new file mode 100644
@@ -0,0 +1,154 @@
1+/**
2+ * useCompletion drives the composer's "@" and "/" popups: it derives the
3+ * active trigger from the text and caret, fetches file hits or skills,
4+ * filters them, and exposes the keyboard/selection state.
5+ *
6+ * @example
7+ * const completion = useCompletion({ text, caret, commands });
8+ * if (completion.open) { … render completion.items[completion.active] … }
9+ */
10+
11+import { useEffect, useMemo, useState } from "react";
12+import { listSkills, searchFiles, type Skill } from "./api";
13+import {
14+ type CompletionItem,
15+ type Trigger,
16+ detectTrigger,
17+ fileItem,
18+ matchesQuery,
19+ skillItems,
20+} from "./mentions";
21+import type { AcpAvailableCommand } from "./protocol";
22+
23+/** Delay before a file search runs, so fast typing sends one request. */
24+const SEARCH_DEBOUNCE_MS = 120;
25+const MAX_FILE_HITS = 30;
26+
27+interface CompletionOptions {
28+ text: string;
29+ caret: number;
30+ commands: AcpAvailableCommand[];
31+}
32+
33+export interface Completion {
34+ /** True when a popup with at least one item should show. */
35+ open: boolean;
36+ trigger: Trigger | null;
37+ items: CompletionItem[];
38+ /** Index of the highlighted item. */
39+ active: number;
40+ setActive: (index: number) => void;
41+ moveActive: (delta: number) => void;
42+ /** Closes the popup until the trigger changes. */
43+ dismiss: () => void;
44+}
45+
46+export function useCompletion({
47+ text,
48+ caret,
49+ commands,
50+}: CompletionOptions): Completion {
51+ const trigger = useMemo(() => detectTrigger(text, caret), [text, caret]);
52+ const triggerKey = trigger ? `${trigger.kind}:${trigger.start}` : "";
53+
54+ const fileHits = useFileSearch(
55+ trigger?.kind === "file" ? trigger.query : null,
56+ );
57+ const skills = useSkills(trigger?.kind === "skill");
58+
59+ const items = useMemo(() => {
60+ if (!trigger) {
61+ return [];
62+ }
63+ if (trigger.kind === "file") {
64+ return fileHits.map(fileItem);
65+ }
66+ return skillItems(skills, commands).filter((item) =>
67+ matchesQuery(item, trigger.query),
68+ );
69+ }, [trigger, fileHits, skills, commands]);
70+
71+ // The highlighted index is remembered per item list: a new trigger or a
72+ // changed list starts again from the first item.
73+ const listKey = `${triggerKey}:${items.length}`;
74+ const [highlight, setHighlight] = useState({ key: "", index: 0 });
75+ const [dismissedKey, setDismissedKey] = useState("");
76+ const active = highlight.key === listKey ? highlight.index : 0;
77+ const setActive = (index: number) => setHighlight({ key: listKey, index });
78+
79+ const open =
80+ trigger !== null && items.length > 0 && dismissedKey !== triggerKey;
81+
82+ return {
83+ open,
84+ trigger,
85+ items,
86+ active,
87+ setActive,
88+ moveActive: (delta) => {
89+ if (items.length > 0) {
90+ setActive((active + delta + items.length) % items.length);
91+ }
92+ },
93+ dismiss: () => setDismissedKey(triggerKey),
94+ };
95+}
96+
97+/**
98+ * useFileSearch queries the backend for `query` (debounced) and ignores
99+ * stale answers; a null query clears the hits.
100+ */
101+function useFileSearch(query: string | null) {
102+ const [hits, setHits] = useState<{ path: string; relPath: string }[]>([]);
103+ useEffect(() => {
104+ if (query === null) {
105+ setHits([]);
106+ return;
107+ }
108+ let stale = false;
109+ const timer = setTimeout(() => {
110+ searchFiles(query, MAX_FILE_HITS)
111+ .then((result) => {
112+ if (!stale) {
113+ setHits(result.files);
114+ }
115+ })
116+ .catch(() => {
117+ if (!stale) {
118+ setHits([]);
119+ }
120+ });
121+ }, SEARCH_DEBOUNCE_MS);
122+ return () => {
123+ stale = true;
124+ clearTimeout(timer);
125+ };
126+ }, [query]);
127+ return hits;
128+}
129+
130+/** useSkills loads the skill list once, the first time the "/" popup opens. */
131+function useSkills(enabled: boolean) {
132+ const [skills, setSkills] = useState<Skill[] | null>(null);
133+ useEffect(() => {
134+ if (!enabled || skills !== null) {
135+ return;
136+ }
137+ let cancelled = false;
138+ listSkills()
139+ .then((result) => {
140+ if (!cancelled) {
141+ setSkills(result.skills);
142+ }
143+ })
144+ .catch(() => {
145+ if (!cancelled) {
146+ setSkills([]);
147+ }
148+ });
149+ return () => {
150+ cancelled = true;
151+ };
152+ }, [enabled, skills]);
153+ return skills ?? [];
154+}
new file mode 100644
@@ -0,0 +1,154 @@
1+/**
2+ * useCompletion drives the composer's "@" and "/" popups: it derives the
3+ * active trigger from the text and caret, fetches file hits or skills,
4+ * filters them, and exposes the keyboard/selection state.
5+ *
6+ * @example
7+ * const completion = useCompletion({ text, caret, commands });
8+ * if (completion.open) { … render completion.items[completion.active] … }
9+ */
10+
11+import { useEffect, useMemo, useState } from "react";
12+import { listSkills, searchFiles, type Skill } from "./api";
13+import {
14+ type CompletionItem,
15+ type Trigger,
16+ detectTrigger,
17+ fileItem,
18+ matchesQuery,
19+ skillItems,
20+} from "./mentions";
21+import type { AcpAvailableCommand } from "./protocol";
22+
23+/** Delay before a file search runs, so fast typing sends one request. */
24+const SEARCH_DEBOUNCE_MS = 120;
25+const MAX_FILE_HITS = 30;
26+
27+interface CompletionOptions {
28+ text: string;
29+ caret: number;
30+ commands: AcpAvailableCommand[];
31+}
32+
33+export interface Completion {
34+ /** True when a popup with at least one item should show. */
35+ open: boolean;
36+ trigger: Trigger | null;
37+ items: CompletionItem[];
38+ /** Index of the highlighted item. */
39+ active: number;
40+ setActive: (index: number) => void;
41+ moveActive: (delta: number) => void;
42+ /** Closes the popup until the trigger changes. */
43+ dismiss: () => void;
44+}
45+
46+export function useCompletion({
47+ text,
48+ caret,
49+ commands,
50+}: CompletionOptions): Completion {
51+ const trigger = useMemo(() => detectTrigger(text, caret), [text, caret]);
52+ const triggerKey = trigger ? `${trigger.kind}:${trigger.start}` : "";
53+
54+ const fileHits = useFileSearch(
55+ trigger?.kind === "file" ? trigger.query : null,
56+ );
57+ const skills = useSkills(trigger?.kind === "skill");
58+
59+ const items = useMemo(() => {
60+ if (!trigger) {
61+ return [];
62+ }
63+ if (trigger.kind === "file") {
64+ return fileHits.map(fileItem);
65+ }
66+ return skillItems(skills, commands).filter((item) =>
67+ matchesQuery(item, trigger.query),
68+ );
69+ }, [trigger, fileHits, skills, commands]);
70+
71+ // The highlighted index is remembered per item list: a new trigger or a
72+ // changed list starts again from the first item.
73+ const listKey = `${triggerKey}:${items.length}`;
74+ const [highlight, setHighlight] = useState({ key: "", index: 0 });
75+ const [dismissedKey, setDismissedKey] = useState("");
76+ const active = highlight.key === listKey ? highlight.index : 0;
77+ const setActive = (index: number) => setHighlight({ key: listKey, index });
78+
79+ const open =
80+ trigger !== null && items.length > 0 && dismissedKey !== triggerKey;
81+
82+ return {
83+ open,
84+ trigger,
85+ items,
86+ active,
87+ setActive,
88+ moveActive: (delta) => {
89+ if (items.length > 0) {
90+ setActive((active + delta + items.length) % items.length);
91+ }
92+ },
93+ dismiss: () => setDismissedKey(triggerKey),
94+ };
95+}
96+
97+/**
98+ * useFileSearch queries the backend for `query` (debounced) and ignores
99+ * stale answers; a null query clears the hits.
100+ */
101+function useFileSearch(query: string | null) {
102+ const [hits, setHits] = useState<{ path: string; relPath: string }[]>([]);
103+ useEffect(() => {
104+ if (query === null) {
105+ setHits([]);
106+ return;
107+ }
108+ let stale = false;
109+ const timer = setTimeout(() => {
110+ searchFiles(query, MAX_FILE_HITS)
111+ .then((result) => {
112+ if (!stale) {
113+ setHits(result.files);
114+ }
115+ })
116+ .catch(() => {
117+ if (!stale) {
118+ setHits([]);
119+ }
120+ });
121+ }, SEARCH_DEBOUNCE_MS);
122+ return () => {
123+ stale = true;
124+ clearTimeout(timer);
125+ };
126+ }, [query]);
127+ return hits;
128+}
129+
130+/** useSkills loads the skill list once, the first time the "/" popup opens. */
131+function useSkills(enabled: boolean) {
132+ const [skills, setSkills] = useState<Skill[] | null>(null);
133+ useEffect(() => {
134+ if (!enabled || skills !== null) {
135+ return;
136+ }
137+ let cancelled = false;
138+ listSkills()
139+ .then((result) => {
140+ if (!cancelled) {
141+ setSkills(result.skills);
142+ }
143+ })
144+ .catch(() => {
145+ if (!cancelled) {
146+ setSkills([]);
147+ }
148+ });
149+ return () => {
150+ cancelled = true;
151+ };
152+ }, [enabled, skills]);
153+ return skills ?? [];
154+}
added ui/src/workspace.test.ts +112 -0
new file mode 100644
@@ -0,0 +1,112 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import {
3+ DEFAULT_FILE_TREE_WIDTH,
4+ FILE_TREE_WIDTH_KEY,
5+ MAX_FILE_TREE_WIDTH,
6+ MIN_FILE_TREE_WIDTH,
7+ clampFileTreeWidth,
8+ openEditor,
9+ openPreview,
10+ resetFileTreeWidth,
11+ resetWorkspace,
12+ setFileTreeWidth,
13+ showTab,
14+ toggleWorkspace,
15+ useWorkspace,
16+} from "./workspace";
17+
18+describe("workspace store", () => {
19+ beforeEach(() => {
20+ localStorage.clear();
21+ resetWorkspace();
22+ });
23+
24+ it("starts closed on the preview tab", () => {
25+ const state = useWorkspace.getState();
26+ expect(state.open).toBe(false);
27+ expect(state.tab).toBe("preview");
28+ expect(state.previewPath).toBeNull();
29+ expect(state.editorPath).toBeNull();
30+ });
31+
32+ it("toggles the panel", () => {
33+ toggleWorkspace();
34+ expect(useWorkspace.getState().open).toBe(true);
35+ toggleWorkspace();
36+ expect(useWorkspace.getState().open).toBe(false);
37+ });
38+
39+ it("opens the panel when a tab is shown", () => {
40+ showTab("terminal");
41+ const state = useWorkspace.getState();
42+ expect(state.open).toBe(true);
43+ expect(state.tab).toBe("terminal");
44+ });
45+
46+ it("openPreview targets the preview tab with the file", () => {
47+ openPreview("/work/readme.md");
48+ const state = useWorkspace.getState();
49+ expect(state).toMatchObject({
50+ open: true,
51+ tab: "preview",
52+ previewPath: "/work/readme.md",
53+ });
54+ });
55+
56+ it("openEditor targets the editor tab and keeps the preview file", () => {
57+ openPreview("/work/a.md");
58+ openEditor("/work/b.go");
59+ const state = useWorkspace.getState();
60+ expect(state).toMatchObject({
61+ open: true,
62+ tab: "editor",
63+ editorPath: "/work/b.go",
64+ previewPath: "/work/a.md",
65+ });
66+ });
67+
68+ it("starts with the default file tree width", () => {
69+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
70+ });
71+
72+ it("setFileTreeWidth applies the width and remembers it", () => {
73+ setFileTreeWidth(320);
74+ expect(useWorkspace.getState().fileTreeWidth).toBe(320);
75+ expect(localStorage.getItem(FILE_TREE_WIDTH_KEY)).toBe("320");
76+ });
77+
78+ it("setFileTreeWidth keeps the width within the bounds", () => {
79+ setFileTreeWidth(10);
80+ expect(useWorkspace.getState().fileTreeWidth).toBe(MIN_FILE_TREE_WIDTH);
81+ setFileTreeWidth(5000);
82+ expect(useWorkspace.getState().fileTreeWidth).toBe(MAX_FILE_TREE_WIDTH);
83+ });
84+
85+ it("resetFileTreeWidth goes back to the default", () => {
86+ setFileTreeWidth(400);
87+ resetFileTreeWidth();
88+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
89+ expect(localStorage.getItem(FILE_TREE_WIDTH_KEY)).toBe(
90+ String(DEFAULT_FILE_TREE_WIDTH),
91+ );
92+ });
93+});
94+
95+describe("clampFileTreeWidth", () => {
96+ it("rounds and keeps in-range values", () => {
97+ expect(clampFileTreeWidth(300.6)).toBe(301);
98+ });
99+
100+ it("clamps below the minimum and above the maximum", () => {
101+ expect(clampFileTreeWidth(0)).toBe(MIN_FILE_TREE_WIDTH);
102+ expect(clampFileTreeWidth(-50)).toBe(MIN_FILE_TREE_WIDTH);
103+ expect(clampFileTreeWidth(10_000)).toBe(MAX_FILE_TREE_WIDTH);
104+ });
105+
106+ it("falls back to the default for NaN and infinities", () => {
107+ expect(clampFileTreeWidth(Number.NaN)).toBe(DEFAULT_FILE_TREE_WIDTH);
108+ expect(clampFileTreeWidth(Number.POSITIVE_INFINITY)).toBe(
109+ DEFAULT_FILE_TREE_WIDTH,
110+ );
111+ });
112+});
new file mode 100644
@@ -0,0 +1,112 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import {
3+ DEFAULT_FILE_TREE_WIDTH,
4+ FILE_TREE_WIDTH_KEY,
5+ MAX_FILE_TREE_WIDTH,
6+ MIN_FILE_TREE_WIDTH,
7+ clampFileTreeWidth,
8+ openEditor,
9+ openPreview,
10+ resetFileTreeWidth,
11+ resetWorkspace,
12+ setFileTreeWidth,
13+ showTab,
14+ toggleWorkspace,
15+ useWorkspace,
16+} from "./workspace";
17+
18+describe("workspace store", () => {
19+ beforeEach(() => {
20+ localStorage.clear();
21+ resetWorkspace();
22+ });
23+
24+ it("starts closed on the preview tab", () => {
25+ const state = useWorkspace.getState();
26+ expect(state.open).toBe(false);
27+ expect(state.tab).toBe("preview");
28+ expect(state.previewPath).toBeNull();
29+ expect(state.editorPath).toBeNull();
30+ });
31+
32+ it("toggles the panel", () => {
33+ toggleWorkspace();
34+ expect(useWorkspace.getState().open).toBe(true);
35+ toggleWorkspace();
36+ expect(useWorkspace.getState().open).toBe(false);
37+ });
38+
39+ it("opens the panel when a tab is shown", () => {
40+ showTab("terminal");
41+ const state = useWorkspace.getState();
42+ expect(state.open).toBe(true);
43+ expect(state.tab).toBe("terminal");
44+ });
45+
46+ it("openPreview targets the preview tab with the file", () => {
47+ openPreview("/work/readme.md");
48+ const state = useWorkspace.getState();
49+ expect(state).toMatchObject({
50+ open: true,
51+ tab: "preview",
52+ previewPath: "/work/readme.md",
53+ });
54+ });
55+
56+ it("openEditor targets the editor tab and keeps the preview file", () => {
57+ openPreview("/work/a.md");
58+ openEditor("/work/b.go");
59+ const state = useWorkspace.getState();
60+ expect(state).toMatchObject({
61+ open: true,
62+ tab: "editor",
63+ editorPath: "/work/b.go",
64+ previewPath: "/work/a.md",
65+ });
66+ });
67+
68+ it("starts with the default file tree width", () => {
69+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
70+ });
71+
72+ it("setFileTreeWidth applies the width and remembers it", () => {
73+ setFileTreeWidth(320);
74+ expect(useWorkspace.getState().fileTreeWidth).toBe(320);
75+ expect(localStorage.getItem(FILE_TREE_WIDTH_KEY)).toBe("320");
76+ });
77+
78+ it("setFileTreeWidth keeps the width within the bounds", () => {
79+ setFileTreeWidth(10);
80+ expect(useWorkspace.getState().fileTreeWidth).toBe(MIN_FILE_TREE_WIDTH);
81+ setFileTreeWidth(5000);
82+ expect(useWorkspace.getState().fileTreeWidth).toBe(MAX_FILE_TREE_WIDTH);
83+ });
84+
85+ it("resetFileTreeWidth goes back to the default", () => {
86+ setFileTreeWidth(400);
87+ resetFileTreeWidth();
88+ expect(useWorkspace.getState().fileTreeWidth).toBe(DEFAULT_FILE_TREE_WIDTH);
89+ expect(localStorage.getItem(FILE_TREE_WIDTH_KEY)).toBe(
90+ String(DEFAULT_FILE_TREE_WIDTH),
91+ );
92+ });
93+});
94+
95+describe("clampFileTreeWidth", () => {
96+ it("rounds and keeps in-range values", () => {
97+ expect(clampFileTreeWidth(300.6)).toBe(301);
98+ });
99+
100+ it("clamps below the minimum and above the maximum", () => {
101+ expect(clampFileTreeWidth(0)).toBe(MIN_FILE_TREE_WIDTH);
102+ expect(clampFileTreeWidth(-50)).toBe(MIN_FILE_TREE_WIDTH);
103+ expect(clampFileTreeWidth(10_000)).toBe(MAX_FILE_TREE_WIDTH);
104+ });
105+
106+ it("falls back to the default for NaN and infinities", () => {
107+ expect(clampFileTreeWidth(Number.NaN)).toBe(DEFAULT_FILE_TREE_WIDTH);
108+ expect(clampFileTreeWidth(Number.POSITIVE_INFINITY)).toBe(
109+ DEFAULT_FILE_TREE_WIDTH,
110+ );
111+ });
112+});
added ui/src/workspace.ts +136 -0
new file mode 100644
@@ -0,0 +1,136 @@
1+/**
2+ * State of the workspace panel (file tree + Preview / Editor / Terminal
3+ * tabs), kept apart from the chat state: the two evolve independently.
4+ *
5+ * @example
6+ * openPreview("README.md"); // opens the panel on the preview tab
7+ * useWorkspace((w) => w.tab); // inside a component
8+ */
9+
10+import { create } from "zustand";
11+
12+export type WorkspaceTab = "preview" | "editor" | "terminal";
13+
14+/** Default width of the file tree column, in pixels (14rem at 16px). */
15+export const DEFAULT_FILE_TREE_WIDTH = 224;
16+/** Narrowest width the file tree column can be dragged to, in pixels. */
17+export const MIN_FILE_TREE_WIDTH = 120;
18+/** Widest width the file tree column can be dragged to, in pixels. */
19+export const MAX_FILE_TREE_WIDTH = 800;
20+/** localStorage key under which the chosen width is remembered. */
21+export const FILE_TREE_WIDTH_KEY = "ori.fileTreeWidth";
22+
23+export interface WorkspaceState {
24+ /** Whether the right-hand panel is visible. */
25+ open: boolean;
26+ /** The active tab. */
27+ tab: WorkspaceTab;
28+ /** File shown in the preview tab, if any. */
29+ previewPath: string | null;
30+ /** File opened in the editor tab, if any. */
31+ editorPath: string | null;
32+ /** Width of the file tree column, in pixels, always within the bounds. */
33+ fileTreeWidth: number;
34+}
35+
36+const initialWorkspace: WorkspaceState = {
37+ open: false,
38+ tab: "preview",
39+ previewPath: null,
40+ editorPath: null,
41+ fileTreeWidth: loadFileTreeWidth(),
42+};
43+
44+/** React hook over the workspace store. */
45+export const useWorkspace = create<WorkspaceState>(() => initialWorkspace);
46+
47+/** toggleWorkspace shows or hides the panel. */
48+export function toggleWorkspace(): void {
49+ useWorkspace.setState((state) => ({ open: !state.open }));
50+}
51+
52+/** showTab activates a tab (and opens the panel if needed). */
53+export function showTab(tab: WorkspaceTab): void {
54+ useWorkspace.setState({ open: true, tab });
55+}
56+
57+/** openPreview shows a file in the preview tab. */
58+export function openPreview(path: string): void {
59+ useWorkspace.setState({ open: true, tab: "preview", previewPath: path });
60+}
61+
62+/** openEditor opens a file in the editor tab. */
63+export function openEditor(path: string): void {
64+ useWorkspace.setState({ open: true, tab: "editor", editorPath: path });
65+}
66+
67+/**
68+ * clampFileTreeWidth brings a width back within the allowed bounds; a
69+ * non-finite value (NaN, Infinity) falls back to the default width.
70+ *
71+ * @example
72+ * clampFileTreeWidth(50); // 120
73+ * clampFileTreeWidth(300); // 300
74+ * clampFileTreeWidth(NaN); // 224
75+ */
76+export function clampFileTreeWidth(width: number): number {
77+ if (!Number.isFinite(width)) {
78+ return DEFAULT_FILE_TREE_WIDTH;
79+ }
80+ return Math.min(
81+ MAX_FILE_TREE_WIDTH,
82+ Math.max(MIN_FILE_TREE_WIDTH, Math.round(width)),
83+ );
84+}
85+
86+/**
87+ * setFileTreeWidth resizes the file tree column and remembers the width in
88+ * localStorage so it survives a page reload.
89+ *
90+ * @example
91+ * setFileTreeWidth(320);
92+ * useWorkspace.getState().fileTreeWidth; // 320
93+ */
94+export function setFileTreeWidth(width: number): void {
95+ const clamped = clampFileTreeWidth(width);
96+ useWorkspace.setState({ fileTreeWidth: clamped });
97+ saveFileTreeWidth(clamped);
98+}
99+
100+/** resetFileTreeWidth restores the default width (and forgets the stored one). */
101+export function resetFileTreeWidth(): void {
102+ setFileTreeWidth(DEFAULT_FILE_TREE_WIDTH);
103+}
104+
105+/**
106+ * loadFileTreeWidth reads the remembered width. Storage can be missing or
107+ * throw (private browsing, blocked site data, non-browser test runs), so any
108+ * failure or unusable value yields the default.
109+ */
110+function loadFileTreeWidth(): number {
111+ try {
112+ const stored = globalThis.localStorage?.getItem(FILE_TREE_WIDTH_KEY);
113+ if (stored === null || stored === undefined) {
114+ return DEFAULT_FILE_TREE_WIDTH;
115+ }
116+ return clampFileTreeWidth(Number(stored));
117+ } catch {
118+ return DEFAULT_FILE_TREE_WIDTH;
119+ }
120+}
121+
122+function saveFileTreeWidth(width: number): void {
123+ try {
124+ globalThis.localStorage?.setItem(FILE_TREE_WIDTH_KEY, String(width));
125+ } catch {
126+ // Storage unavailable: the width still applies for this page.
127+ }
128+}
129+
130+/** resetWorkspace restores the initial state; meant for tests. */
131+export function resetWorkspace(): void {
132+ useWorkspace.setState(
133+ { ...initialWorkspace, fileTreeWidth: DEFAULT_FILE_TREE_WIDTH },
134+ true,
135+ );
136+}
new file mode 100644
@@ -0,0 +1,136 @@
1+/**
2+ * State of the workspace panel (file tree + Preview / Editor / Terminal
3+ * tabs), kept apart from the chat state: the two evolve independently.
4+ *
5+ * @example
6+ * openPreview("README.md"); // opens the panel on the preview tab
7+ * useWorkspace((w) => w.tab); // inside a component
8+ */
9+
10+import { create } from "zustand";
11+
12+export type WorkspaceTab = "preview" | "editor" | "terminal";
13+
14+/** Default width of the file tree column, in pixels (14rem at 16px). */
15+export const DEFAULT_FILE_TREE_WIDTH = 224;
16+/** Narrowest width the file tree column can be dragged to, in pixels. */
17+export const MIN_FILE_TREE_WIDTH = 120;
18+/** Widest width the file tree column can be dragged to, in pixels. */
19+export const MAX_FILE_TREE_WIDTH = 800;
20+/** localStorage key under which the chosen width is remembered. */
21+export const FILE_TREE_WIDTH_KEY = "ori.fileTreeWidth";
22+
23+export interface WorkspaceState {
24+ /** Whether the right-hand panel is visible. */
25+ open: boolean;
26+ /** The active tab. */
27+ tab: WorkspaceTab;
28+ /** File shown in the preview tab, if any. */
29+ previewPath: string | null;
30+ /** File opened in the editor tab, if any. */
31+ editorPath: string | null;
32+ /** Width of the file tree column, in pixels, always within the bounds. */
33+ fileTreeWidth: number;
34+}
35+
36+const initialWorkspace: WorkspaceState = {
37+ open: false,
38+ tab: "preview",
39+ previewPath: null,
40+ editorPath: null,
41+ fileTreeWidth: loadFileTreeWidth(),
42+};
43+
44+/** React hook over the workspace store. */
45+export const useWorkspace = create<WorkspaceState>(() => initialWorkspace);
46+
47+/** toggleWorkspace shows or hides the panel. */
48+export function toggleWorkspace(): void {
49+ useWorkspace.setState((state) => ({ open: !state.open }));
50+}
51+
52+/** showTab activates a tab (and opens the panel if needed). */
53+export function showTab(tab: WorkspaceTab): void {
54+ useWorkspace.setState({ open: true, tab });
55+}
56+
57+/** openPreview shows a file in the preview tab. */
58+export function openPreview(path: string): void {
59+ useWorkspace.setState({ open: true, tab: "preview", previewPath: path });
60+}
61+
62+/** openEditor opens a file in the editor tab. */
63+export function openEditor(path: string): void {
64+ useWorkspace.setState({ open: true, tab: "editor", editorPath: path });
65+}
66+
67+/**
68+ * clampFileTreeWidth brings a width back within the allowed bounds; a
69+ * non-finite value (NaN, Infinity) falls back to the default width.
70+ *
71+ * @example
72+ * clampFileTreeWidth(50); // 120
73+ * clampFileTreeWidth(300); // 300
74+ * clampFileTreeWidth(NaN); // 224
75+ */
76+export function clampFileTreeWidth(width: number): number {
77+ if (!Number.isFinite(width)) {
78+ return DEFAULT_FILE_TREE_WIDTH;
79+ }
80+ return Math.min(
81+ MAX_FILE_TREE_WIDTH,
82+ Math.max(MIN_FILE_TREE_WIDTH, Math.round(width)),
83+ );
84+}
85+
86+/**
87+ * setFileTreeWidth resizes the file tree column and remembers the width in
88+ * localStorage so it survives a page reload.
89+ *
90+ * @example
91+ * setFileTreeWidth(320);
92+ * useWorkspace.getState().fileTreeWidth; // 320
93+ */
94+export function setFileTreeWidth(width: number): void {
95+ const clamped = clampFileTreeWidth(width);
96+ useWorkspace.setState({ fileTreeWidth: clamped });
97+ saveFileTreeWidth(clamped);
98+}
99+
100+/** resetFileTreeWidth restores the default width (and forgets the stored one). */
101+export function resetFileTreeWidth(): void {
102+ setFileTreeWidth(DEFAULT_FILE_TREE_WIDTH);
103+}
104+
105+/**
106+ * loadFileTreeWidth reads the remembered width. Storage can be missing or
107+ * throw (private browsing, blocked site data, non-browser test runs), so any
108+ * failure or unusable value yields the default.
109+ */
110+function loadFileTreeWidth(): number {
111+ try {
112+ const stored = globalThis.localStorage?.getItem(FILE_TREE_WIDTH_KEY);
113+ if (stored === null || stored === undefined) {
114+ return DEFAULT_FILE_TREE_WIDTH;
115+ }
116+ return clampFileTreeWidth(Number(stored));
117+ } catch {
118+ return DEFAULT_FILE_TREE_WIDTH;
119+ }
120+}
121+
122+function saveFileTreeWidth(width: number): void {
123+ try {
124+ globalThis.localStorage?.setItem(FILE_TREE_WIDTH_KEY, String(width));
125+ } catch {
126+ // Storage unavailable: the width still applies for this page.
127+ }
128+}
129+
130+/** resetWorkspace restores the initial state; meant for tests. */
131+export function resetWorkspace(): void {
132+ useWorkspace.setState(
133+ { ...initialWorkspace, fileTreeWidth: DEFAULT_FILE_TREE_WIDTH },
134+ true,
135+ );
136+}
modified ui/tsconfig.json +1 -1
@@ -12,7 +12,7 @@
1212 "skipLibCheck": true,
1313 "isolatedModules": true,
1414 "noEmit": true,
15- "types": ["vitest/globals"]
15+ "types": ["vitest/globals", "vite/client"]
1616 },
1717 "include": ["src", "vite.config.ts"]
1818 }
@@ -12,7 +12,7 @@
12 "skipLibCheck": true,12 "skipLibCheck": true,
13 "isolatedModules": true,13 "isolatedModules": true,
14 "noEmit": true,14 "noEmit": true,
15- "types": ["vitest/globals"]15+ "types": ["vitest/globals", "vite/client"]
16 },16 },
17 "include": ["src", "vite.config.ts"]17 "include": ["src", "vite.config.ts"]
18 }18 }
modified ui/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1-{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/protocol.ts","./src/reducer.test.ts","./src/reducer.ts","./src/store.ts","./src/ws.test.ts","./src/ws.ts","./src/components/ChatThread.tsx","./src/components/Markdown.tsx","./src/components/PermissionPrompt.tsx","./src/components/PlanView.tsx","./src/components/PromptInput.tsx","./src/components/ToolCallCard.tsx","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
1+{"root":["./src/App.test.tsx","./src/App.tsx","./src/api.ts","./src/lang.test.ts","./src/lang.ts","./src/main.tsx","./src/mentions.test.ts","./src/mentions.ts","./src/monaco-setup.ts","./src/protocol.ts","./src/reducer.test.ts","./src/reducer.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/useCompletion.ts","./src/workspace.test.ts","./src/workspace.ts","./src/ws.test.ts","./src/ws.ts","./src/components/AsciiDocView.test.tsx","./src/components/AsciiDocView.tsx","./src/components/ChatThread.tsx","./src/components/CodeView.tsx","./src/components/CompletionPopup.tsx","./src/components/DrawioView.test.tsx","./src/components/DrawioView.tsx","./src/components/EditorPane.test.tsx","./src/components/EditorPane.tsx","./src/components/FileTree.test.tsx","./src/components/FileTree.tsx","./src/components/ImageView.tsx","./src/components/InlineText.test.tsx","./src/components/InlineText.tsx","./src/components/Markdown.tsx","./src/components/MonacoViewer.tsx","./src/components/PermissionPrompt.tsx","./src/components/PlanView.tsx","./src/components/PreviewPane.test.tsx","./src/components/PreviewPane.tsx","./src/components/PromptInput.test.tsx","./src/components/PromptInput.tsx","./src/components/ResizeHandle.test.tsx","./src/components/ResizeHandle.tsx","./src/components/TerminalPane.test.tsx","./src/components/TerminalPane.tsx","./src/components/ToolCallCard.test.tsx","./src/components/ToolCallCard.tsx","./src/components/WorkingIndicator.test.tsx","./src/components/WorkingIndicator.tsx","./src/components/Workspace.test.tsx","./src/components/Workspace.tsx","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
@@ -1 +1 @@
1-{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/protocol.ts","./src/reducer.test.ts","./src/reducer.ts","./src/store.ts","./src/ws.test.ts","./src/ws.ts","./src/components/ChatThread.tsx","./src/components/Markdown.tsx","./src/components/PermissionPrompt.tsx","./src/components/PlanView.tsx","./src/components/PromptInput.tsx","./src/components/ToolCallCard.tsx","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file\ No newline at end of file
1+{"root":["./src/App.test.tsx","./src/App.tsx","./src/api.ts","./src/lang.test.ts","./src/lang.ts","./src/main.tsx","./src/mentions.test.ts","./src/mentions.ts","./src/monaco-setup.ts","./src/protocol.ts","./src/reducer.test.ts","./src/reducer.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/useCompletion.ts","./src/workspace.test.ts","./src/workspace.ts","./src/ws.test.ts","./src/ws.ts","./src/components/AsciiDocView.test.tsx","./src/components/AsciiDocView.tsx","./src/components/ChatThread.tsx","./src/components/CodeView.tsx","./src/components/CompletionPopup.tsx","./src/components/DrawioView.test.tsx","./src/components/DrawioView.tsx","./src/components/EditorPane.test.tsx","./src/components/EditorPane.tsx","./src/components/FileTree.test.tsx","./src/components/FileTree.tsx","./src/components/ImageView.tsx","./src/components/InlineText.test.tsx","./src/components/InlineText.tsx","./src/components/Markdown.tsx","./src/components/MonacoViewer.tsx","./src/components/PermissionPrompt.tsx","./src/components/PlanView.tsx","./src/components/PreviewPane.test.tsx","./src/components/PreviewPane.tsx","./src/components/PromptInput.test.tsx","./src/components/PromptInput.tsx","./src/components/ResizeHandle.test.tsx","./src/components/ResizeHandle.tsx","./src/components/TerminalPane.test.tsx","./src/components/TerminalPane.tsx","./src/components/ToolCallCard.test.tsx","./src/components/ToolCallCard.tsx","./src/components/WorkingIndicator.test.tsx","./src/components/WorkingIndicator.tsx","./src/components/Workspace.test.tsx","./src/components/Workspace.tsx","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file\ No newline at end of file
modified ui/vite.config.ts +1 -0
@@ -10,6 +10,7 @@ export default defineConfig({
1010 server: {
1111 proxy: {
1212 "/ws": { target: "ws://localhost:8888", ws: true },
13+ "/api": { target: "http://localhost:8888" },
1314 "/healthz": { target: "http://localhost:8888" },
1415 },
1516 },
@@ -10,6 +10,7 @@ export default defineConfig({
10 server: {10 server: {
11 proxy: {11 proxy: {
12 "/ws": { target: "ws://localhost:8888", ws: true },12 "/ws": { target: "ws://localhost:8888", ws: true },
13+ "/api": { target: "http://localhost:8888" },
13 "/healthz": { target: "http://localhost:8888" },14 "/healthz": { target: "http://localhost:8888" },
14 },15 },
15 },16 },