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>
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 | + | |
| 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 | + | ||
| 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, "&").replace(/</g, "<").replace(/>/g, ">"); | |
| 98 | +const xmlAttr = (s) => | |
| 99 | + s | |
| 100 | + .replace(/&/g, "&") | |
| 101 | + .replace(/</g, "<") | |
| 102 | + .replace(/>/g, ">") | |
| 103 | + .replace(/"/g, """) | |
| 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, "&#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, "&").replace(/</g, "<").replace(/>/g, ">"); | ||
| 98 | +const xmlAttr = (s) => | ||
| 99 | + s | ||
| 100 | + .replace(/&/g, "&") | ||
| 101 | + .replace(/</g, "<") | ||
| 102 | + .replace(/>/g, ">") | ||
| 103 | + .replace(/"/g, """) | ||
| 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, "&#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 @@ | ||
| 1 | 1 | # Go |
| 2 | 2 | /bin/ |
| 3 | 3 | *.test |
| 4 | +bin | |
| 4 | 5 | |
| 5 | 6 | # Node / Vite |
| 6 | 7 | node_modules/ |
| 7 | 8 | ui/dist/* |
| 8 | 9 | !ui/dist/.gitkeep |
| 10 | +# copied from vscode-material-icons by ui/scripts/copy-icons.mjs | |
| 11 | +ui/public/material-icons/ | |
| 9 | 12 | |
| 10 | 13 | # Editors & OS |
| 11 | 14 | .DS_Store |
| 12 | 15 | *.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 | # Go | 1 | # Go |
| 2 | /bin/ | 2 | /bin/ |
| 3 | *.test | 3 | *.test |
| 4 | +bin | ||
| 4 | 5 | ||
| 5 | # Node / Vite | 6 | # Node / Vite |
| 6 | node_modules/ | 7 | node_modules/ |
| 7 | ui/dist/* | 8 | ui/dist/* |
| 8 | !ui/dist/.gitkeep | 9 | !ui/dist/.gitkeep |
| 10 | +# copied from vscode-material-icons by ui/scripts/copy-icons.mjs | ||
| 11 | +ui/public/material-icons/ | ||
| 9 | 12 | ||
| 10 | # Editors & OS | 13 | # Editors & OS |
| 11 | .DS_Store | 14 | .DS_Store |
| 12 | *.log | 15 | *.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 @@ | ||
| 2 | 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 | 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 | 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"} | |
| @@ -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 | |
| 2 | 2 | |
| 3 | 3 | - **Gate**: ✅ **PASS** |
| 4 | -- **Commit**: `4edda86` on `feat/acp-webapp` | |
| 4 | +- **Commit**: `2434cc7` on `feat/acp-webapp` | |
| 5 | 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 | 8 | ## Lint issues (`qlty check`) |
| 9 | 9 | |
| @@ -19,7 +19,7 @@ _none_ | ||
| 19 | 19 | |
| 20 | 20 | ## Code smells (`qlty smells`) |
| 21 | 21 | |
| 22 | -Total: **0** (vs previous: -2) | |
| 22 | +Total: **0** (vs previous: ±0) | |
| 23 | 23 | |
| 24 | 24 | _none_ |
| 25 | 25 | |
| @@ -27,40 +27,46 @@ _none_ | ||
| 27 | 27 | |
| 28 | 28 | | metric | total | vs previous | |
| 29 | 29 | |---|---|---| |
| 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 | | |
| 35 | 35 | | lcom | 0 | ±0 | |
| 36 | -| lines | 2068 | +4 | | |
| 37 | -| loc | 1451 | -2 | | |
| 36 | +| lines | 5671 | +101 | | |
| 37 | +| loc | 4082 | +52 | | |
| 38 | 38 | |
| 39 | 39 | ### Most complex files |
| 40 | 40 | |
| 41 | 41 | | file | complex | cyclo | loc | |
| 42 | 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 | | |
| 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 | | |
| 46 | 55 | | internal/agent/agent.go | 15 | 31 | 121 | |
| 56 | +| ori-desktop/internal/settings/settings.go | 15 | 31 | 82 | | |
| 47 | 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 | 59 | ## Trend |
| 60 | 60 | |
| 61 | 61 | | run | timestamp | error | warning | smells | complex | gate | |
| 62 | 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 | | |
| 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:21Z | 1 | +# 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 files | 39 | ### 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 | ## Trend | 59 | ## 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 2 |
| 3 | 3 | title: Create an architecture diagram |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-17T09:36:53.046Z |
| 9 | -updatedAt: 2026-09-17T09:36:53.046Z | |
| 9 | +updatedAt: 2026-09-17T22:23:54.249Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | body: "" |
| 12 | 12 | 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: 2 | 2 | id: 2 |
| 3 | title: Create an architecture diagram | 3 | title: Create an architecture diagram |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-17T09:36:53.046Z | 8 | createdAt: 2026-09-17T09:36:53.046Z |
| 9 | -updatedAt: 2026-09-17T09:36:53.046Z | 9 | +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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 4 |
| 3 | 3 | title: Specifications |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-17T17:58:21.934Z |
| 9 | -updatedAt: 2026-09-17T18:04:56.702Z | |
| 9 | +updatedAt: 2026-09-17T19:39:19.515Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | body: | |
| 12 | 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) |
| @@ -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: 4 | 2 | id: 4 |
| 3 | title: Specifications | 3 | title: Specifications |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-17T17:58:21.934Z | 8 | createdAt: 2026-09-17T17:58:21.934Z |
| 9 | -updatedAt: 2026-09-17T18:04:56.702Z | 9 | +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 @@ | ||
| 9 | 9 | "files.autoSave": "afterDelay", |
| 10 | 10 | "files.autoSaveDelay": 1000, |
| 11 | 11 | "workbench.tree.indent": 20, |
| 12 | - "window.zoomLevel": 1.0, | |
| 12 | + "window.zoomLevel": 0.5, | |
| 13 | 13 | "[markdown]": { |
| 14 | 14 | "editor.unicodeHighlight.ambiguousCharacters": false, |
| 15 | 15 | "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 @@ | ||
| 1 | 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 clean | |
| 3 | +.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 | 5 | help: ## Show this help |
| 6 | 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 | 34 | dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal) |
| 35 | 35 | 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 | 46 | clean: ## Remove build artifacts |
| 38 | 47 | rm -rf bin ui/dist/* |
| 39 | 48 | @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 clean | 3 | +.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 help | 5 | 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 dev | 35 | 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 artifacts | 46 | clean: ## Remove build artifacts |
| 38 | rm -rf bin ui/dist/* | 47 | rm -rf bin ui/dist/* |
| 39 | @touch ui/dist/.gitkeep | 48 | @touch ui/dist/.gitkeep |
modified
README.md +17 -0 | @@ -1,2 +1,19 @@ | ||
| 1 | 1 | # Ori |
| 2 | 2 | > 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 | # Ori | 1 | # Ori |
| 2 | > The origin from which many voices are called, and through which they are known | 2 | > 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 ( | ||
| 11 | 11 | "os" |
| 12 | 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 | 17 | 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 ( | ||
| 14 | 14 | "github.com/coder/websocket" |
| 15 | 15 | "github.com/coder/websocket/wsjson" |
| 16 | 16 | |
| 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" | |
| 20 | 20 | ) |
| 21 | 21 | |
| 22 | 22 | // TestEndToEnd exercises the full backend chain exactly as production wires |
| @@ -28,7 +28,7 @@ func TestEndToEnd(t *testing.T) { | ||
| 28 | 28 | } |
| 29 | 29 | |
| 30 | 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 | 32 | if output, err := build.CombinedOutput(); err != nil { |
| 33 | 33 | t.Fatalf("building the mock agent failed: %v\n%s", err, output) |
| 34 | 34 | } |
| @@ -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 wires | 22 | // 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 ( | ||
| 13 | 13 | "net/http" |
| 14 | 14 | "os" |
| 15 | 15 | |
| 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" | |
| 21 | 24 | ) |
| 22 | 25 | |
| 23 | 26 | func main() { |
| @@ -48,11 +51,21 @@ func run(args []string) error { | ||
| 48 | 51 | } |
| 49 | 52 | defer func() { _ = session.Close() }() |
| 50 | 53 | relay.SetSession(session) |
| 54 | + relay.SetWorkspaceRoot(cfg.Cwd) | |
| 51 | 55 | logger.Info("agent session ready", "sessionId", session.ID()) |
| 52 | 56 | |
| 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) | |
| 56 | 69 | |
| 57 | 70 | logger.Info("ori listening", "addr", cfg.Addr) |
| 58 | 71 | 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="<mxfile host="Electron" agent="make-diagram.mjs" type="device"><diagram name="Ori Architecture" id="diagram-1"><mxGraphModel dx="1000" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="3" value="Browser&amp;#10;React SPA" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=0;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="20" y="20" width="220" height="110" as="geometry"/></mxCell><mxCell id="4" value="UI Components&amp;#10;· Agent Panel&amp;#10;· Workspace Panel&amp;#10;· File Tree&amp;#10;· Monaco Editor&amp;#10;· Terminal (xterm.js)" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="20" y="150" width="220" height="150" as="geometry"/></mxCell><mxCell id="5" value="httpserver&amp;#10;Serves SPA + /ws" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="20" width="280" height="90" as="geometry"/></mxCell><mxCell id="6" value="bridge&amp;#10;· WebSocket ⇄ ACP&amp;#10;· Event broadcast&amp;#10;· Permission routing&amp;#10;· History replay" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="140" width="280" height="130" as="geometry"/></mxCell><mxCell id="7" value="agent&amp;#10;ACP session&amp;#10;lifecycle" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="300" width="135" height="90" as="geometry"/></mxCell><mxCell id="8" value="files&amp;#10;Workspace&amp;#10;file API" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="515" y="300" width="135" height="90" as="geometry"/></mxCell><mxCell id="9" value="terminal&amp;#10;PTY per&amp;#10;connection" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="410" width="135" height="90" as="geometry"/></mxCell><mxCell id="10" value="mockagent&amp;#10;Demo agent&amp;#10;(testing)" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="515" y="410" width="135" height="90" as="geometry"/></mxCell><mxCell id="11" value="ACP Agent&amp;#10;(claude-code-acp / Gemini CLI)&amp;#10;&amp;#10;JSON-RPC over stdio&amp;#10;session_update · permission_request" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#0062FF;fontColor=#0062FF;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="800" y="230" width="400" height="130" as="geometry"/></mxCell><mxCell id="12" value="Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call." style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=15;fontStyle=0;strokeWidth=2;dashed=1;" vertex="1" parent="1"><mxGeometry x="20" y="560" width="1180" height="60" as="geometry"/></mxCell><mxCell id="13" value="1. load SPA" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="14" value="2. WebSocket&amp;#10;/ws" style="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;" edge="1" parent="1" source="3" target="6"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="15" value="3. prompt +&amp;#10;permission&amp;#10;responses" style="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;" edge="1" parent="1" source="4" target="6"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="16" value="4. session_update&amp;#10;events (replay)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;" edge="1" parent="1" source="6" target="4"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="310" y="230"/><mxPoint x="280" y="230"/></Array></mxGeometry></mxCell><mxCell id="17" value="5. ACP&amp;#10;lifecycle" style="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;" edge="1" parent="1" source="6" target="7"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="18" value="6. stdio&amp;#10;JSON-RPC" style="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;" edge="1" parent="1" source="7" target="11"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="19" value="7. updates +&amp;#10;permissions" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;" edge="1" parent="1" source="11" target="6"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="720" y="200"/><mxPoint x="650" y="200"/></Array></mxGeometry></mxCell><mxCell id="20" value="file ops" style="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;" edge="1" parent="1" source="4" target="8"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="21" value="shell I/O" style="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;" edge="1" parent="1" source="4" target="9"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell></root></mxGraphModel></diagram></mxfile>"> | |
| 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="<mxfile host="Electron" agent="make-diagram.mjs" type="device"><diagram name="Ori Architecture" id="diagram-1"><mxGraphModel dx="1000" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="3" value="Browser&amp;#10;React SPA" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=0;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="20" y="20" width="220" height="110" as="geometry"/></mxCell><mxCell id="4" value="UI Components&amp;#10;· Agent Panel&amp;#10;· Workspace Panel&amp;#10;· File Tree&amp;#10;· Monaco Editor&amp;#10;· Terminal (xterm.js)" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="20" y="150" width="220" height="150" as="geometry"/></mxCell><mxCell id="5" value="httpserver&amp;#10;Serves SPA + /ws" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="20" width="280" height="90" as="geometry"/></mxCell><mxCell id="6" value="bridge&amp;#10;· WebSocket ⇄ ACP&amp;#10;· Event broadcast&amp;#10;· Permission routing&amp;#10;· History replay" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="140" width="280" height="130" as="geometry"/></mxCell><mxCell id="7" value="agent&amp;#10;ACP session&amp;#10;lifecycle" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="300" width="135" height="90" as="geometry"/></mxCell><mxCell id="8" value="files&amp;#10;Workspace&amp;#10;file API" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="515" y="300" width="135" height="90" as="geometry"/></mxCell><mxCell id="9" value="terminal&amp;#10;PTY per&amp;#10;connection" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#15202b;strokeColor=#15202b;fontColor=#fdfdfb;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="370" y="410" width="135" height="90" as="geometry"/></mxCell><mxCell id="10" value="mockagent&amp;#10;Demo agent&amp;#10;(testing)" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#e8ebda;strokeColor=#15202b;fontColor=#15202b;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="515" y="410" width="135" height="90" as="geometry"/></mxCell><mxCell id="11" value="ACP Agent&amp;#10;(claude-code-acp / Gemini CLI)&amp;#10;&amp;#10;JSON-RPC over stdio&amp;#10;session_update · permission_request" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#0062FF;fontColor=#0062FF;fontSize=17;fontStyle=1;strokeWidth=2;" vertex="1" parent="1"><mxGeometry x="800" y="230" width="400" height="130" as="geometry"/></mxCell><mxCell id="12" value="Go backend bridges browser WebSockets to ACP stdio, broadcasting agent updates to all connected browsers while routing permissions back to the waiting call." style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#fdfdfb;strokeColor=#15202b;fontColor=#15202b;fontSize=15;fontStyle=0;strokeWidth=2;dashed=1;" vertex="1" parent="1"><mxGeometry x="20" y="560" width="1180" height="60" as="geometry"/></mxCell><mxCell id="13" value="1. load SPA" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#15202b;fontSize=14;fontColor=#15202b;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="14" value="2. WebSocket&amp;#10;/ws" style="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;" edge="1" parent="1" source="3" target="6"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="15" value="3. prompt +&amp;#10;permission&amp;#10;responses" style="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;" edge="1" parent="1" source="4" target="6"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="16" value="4. session_update&amp;#10;events (replay)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;" edge="1" parent="1" source="6" target="4"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="310" y="230"/><mxPoint x="280" y="230"/></Array></mxGeometry></mxCell><mxCell id="17" value="5. ACP&amp;#10;lifecycle" style="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;" edge="1" parent="1" source="6" target="7"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="18" value="6. stdio&amp;#10;JSON-RPC" style="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;" edge="1" parent="1" source="7" target="11"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="19" value="7. updates +&amp;#10;permissions" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeWidth=2;strokeColor=#0062FF;fontSize=14;fontColor=#0062FF;dashed=1;" edge="1" parent="1" source="11" target="6"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="720" y="200"/><mxPoint x="650" y="200"/></Array></mxGeometry></mxCell><mxCell id="20" value="file ops" style="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;" edge="1" parent="1" source="4" target="8"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell><mxCell id="21" value="shell I/O" style="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;" edge="1" parent="1" source="4" target="9"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell></root></mxGraphModel></diagram></mxfile>"> | ||
| 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 (chat panel + workspace: file tree, Monaco preview/editor, 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) (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (chat panel + workspace: file tree, Monaco preview/editor, 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) (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 | |
| 2 | 2 | |
| 3 | 3 | go 1.26.5 |
| 4 | 4 | |
| 5 | 5 | require github.com/coder/acp-go-sdk v0.13.5 |
| 6 | 6 | |
| 7 | 7 | 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/ori | 1 | +module rickub.com/bots-garden/ori |
| 2 | 2 | ||
| 3 | go 1.26.5 | 3 | go 1.26.5 |
| 4 | 4 | ||
| 5 | require github.com/coder/acp-go-sdk v0.13.5 | 5 | require github.com/coder/acp-go-sdk v0.13.5 |
| 6 | 6 | ||
| 7 | require github.com/coder/websocket v1.8.15 | 7 | 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 | ||
| 2 | 2 | github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko= |
| 3 | 3 | github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= |
| 4 | 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= | |
| @@ -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 ( | ||
| 12 | 12 | |
| 13 | 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 | 18 | // 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.Agent | 18 | // mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent |
modified
internal/bridge/bridge.go +44 -5 | @@ -13,6 +13,7 @@ import ( | ||
| 13 | 13 | "encoding/json" |
| 14 | 14 | "fmt" |
| 15 | 15 | "log/slog" |
| 16 | + "path/filepath" | |
| 16 | 17 | "strconv" |
| 17 | 18 | "sync" |
| 18 | 19 | |
| @@ -23,7 +24,7 @@ import ( | ||
| 23 | 24 | // satisfied by *agent.Session and by test fakes. |
| 24 | 25 | type Prompter interface { |
| 25 | 26 | ID() string |
| 26 | - PromptText(ctx context.Context, text string) (acp.StopReason, error) | |
| 27 | + Prompt(ctx context.Context, blocks []acp.ContentBlock) (acp.StopReason, error) | |
| 27 | 28 | Cancel(ctx context.Context) error |
| 28 | 29 | } |
| 29 | 30 | |
| @@ -54,6 +55,7 @@ type Bridge struct { | ||
| 54 | 55 | |
| 55 | 56 | mu sync.Mutex |
| 56 | 57 | session Prompter |
| 58 | + root string | |
| 57 | 59 | subscribers map[chan []byte]struct{} |
| 58 | 60 | history [][]byte |
| 59 | 61 | turnActive bool |
| @@ -85,6 +87,14 @@ func (b *Bridge) SetSession(session Prompter) { | ||
| 85 | 87 | 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 | 98 | // Subscribe registers a new client. It returns the client's event channel, |
| 89 | 99 | // the events to replay so the client catches up (history, then any pending |
| 90 | 100 | // permission requests), and an unsubscribe function. |
| @@ -127,7 +137,7 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) { | ||
| 127 | 137 | |
| 128 | 138 | switch msg.Type { |
| 129 | 139 | case IncomingPrompt: |
| 130 | - b.startTurn(msg.Text) | |
| 140 | + b.startTurn(msg.Text, msg.Attachments) | |
| 131 | 141 | case IncomingCancel: |
| 132 | 142 | b.cancelTurn(ctx) |
| 133 | 143 | case IncomingPermissionResponse: |
| @@ -139,9 +149,10 @@ func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) { | ||
| 139 | 149 | |
| 140 | 150 | // startTurn launches a prompt turn in the background; updates stream through |
| 141 | 151 | // HandleSessionUpdate while it runs. |
| 142 | -func (b *Bridge) startTurn(text string) { | |
| 152 | +func (b *Bridge) startTurn(text string, attachments []Attachment) { | |
| 143 | 153 | b.mu.Lock() |
| 144 | 154 | session := b.session |
| 155 | + root := b.root | |
| 145 | 156 | if session == nil { |
| 146 | 157 | b.mu.Unlock() |
| 147 | 158 | b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false) |
| @@ -155,13 +166,14 @@ func (b *Bridge) startTurn(text string) { | ||
| 155 | 166 | b.turnActive = true |
| 156 | 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 | 170 | b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true) |
| 160 | 171 | |
| 172 | + blocks := promptBlocks(text, attachments, root) | |
| 161 | 173 | go func() { |
| 162 | 174 | // The turn outlives the WebSocket message that started it, so it |
| 163 | 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 | 178 | b.mu.Lock() |
| 167 | 179 | 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 | 219 | func (b *Bridge) cancelTurn(ctx context.Context) { |
| 181 | 220 | b.mu.Lock() |
| 182 | 221 | 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() string | 26 | 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) error | 28 | Cancel(ctx context.Context) error |
| 28 | } | 29 | } |
| 29 | 30 | ||
| @@ -54,6 +55,7 @@ type Bridge struct { | |||
| 54 | 55 | ||
| 55 | mu sync.Mutex | 56 | mu sync.Mutex |
| 56 | session Prompter | 57 | session Prompter |
| 58 | + root string | ||
| 57 | subscribers map[chan []byte]struct{} | 59 | subscribers map[chan []byte]struct{} |
| 58 | history [][]byte | 60 | history [][]byte |
| 59 | turnActive bool | 61 | turnActive bool |
| @@ -85,6 +87,14 @@ func (b *Bridge) SetSession(session Prompter) { | |||
| 85 | b.session = session | 87 | 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 pending | 99 | // 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 through | 150 | // 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.session | 154 | 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 = true | 166 | 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 it | 174 | // 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 = false | 179 | 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.session | 221 | session := b.session |
modified
internal/bridge/bridge_test.go +48 -4 | @@ -10,25 +10,31 @@ import ( | ||
| 10 | 10 | |
| 11 | 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 | 16 | // fakeSession implements bridge.Prompter without any agent behind it. |
| 17 | 17 | type fakeSession struct { |
| 18 | 18 | mu sync.Mutex |
| 19 | 19 | prompts []string |
| 20 | + blocks [][]acp.ContentBlock | |
| 20 | 21 | cancelled bool |
| 21 | - // respond controls what PromptText returns; nil means end_turn. | |
| 22 | + // respond controls what Prompt returns; nil means end_turn. | |
| 22 | 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 | 25 | block chan struct{} |
| 25 | 26 | } |
| 26 | 27 | |
| 27 | 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 | 35 | f.mu.Lock() |
| 31 | 36 | f.prompts = append(f.prompts, text) |
| 37 | + f.blocks = append(f.blocks, blocks) | |
| 32 | 38 | respond := f.respond |
| 33 | 39 | block := f.block |
| 34 | 40 | f.mu.Unlock() |
| @@ -340,3 +346,41 @@ func TestPendingPermissionIsReplayedToLateJoiner(t *testing.T) { | ||
| 340 | 346 | b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`)) |
| 341 | 347 | <-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 | +} | |
| @@ -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.Mutex | 18 | mu sync.Mutex |
| 19 | prompts []string | 19 | prompts []string |
| 20 | + blocks [][]acp.ContentBlock | ||
| 20 | cancelled bool | 21 | 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.respond | 38 | respond := f.respond |
| 33 | block := f.block | 39 | 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 | <-answered | 347 | <-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" | ||
| 10 | 10 | |
| 11 | 11 | // Incoming message types (browser → server). |
| 12 | 12 | const ( |
| 13 | - // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}. | |
| 13 | + // IncomingPrompt starts a turn: {"type":"prompt","text":"...","attachments":[...]}. | |
| 14 | 14 | IncomingPrompt = "prompt" |
| 15 | 15 | // IncomingCancel interrupts the current turn: {"type":"cancel"}. |
| 16 | 16 | IncomingCancel = "cancel" |
| @@ -45,11 +45,25 @@ const ( | ||
| 45 | 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 | 60 | // Incoming is a message sent by the browser. |
| 49 | 61 | type Incoming struct { |
| 50 | 62 | Type string `json:"type"` |
| 51 | 63 | // Text is the prompt text (IncomingPrompt). |
| 52 | 64 | Text string `json:"text,omitempty"` |
| 65 | + // Attachments are the files mentioned in the prompt (IncomingPrompt). | |
| 66 | + Attachments []Attachment `json:"attachments,omitempty"` | |
| 53 | 67 | // RequestId identifies the permission request being answered. |
| 54 | 68 | RequestId string `json:"requestId,omitempty"` |
| 55 | 69 | // OptionId is the chosen ACP permission option. |
| @@ -73,6 +87,8 @@ type Outgoing struct { | ||
| 73 | 87 | Request json.RawMessage `json:"request,omitempty"` |
| 74 | 88 | // Text is the user's prompt text (OutgoingUserMessage). |
| 75 | 89 | Text string `json:"text,omitempty"` |
| 90 | + // Attachments are the files the prompt mentioned (OutgoingUserMessage). | |
| 91 | + Attachments []Attachment `json:"attachments,omitempty"` | |
| 76 | 92 | // StopReason is the ACP stop reason (OutgoingTurnEnded). |
| 77 | 93 | StopReason string `json:"stopReason,omitempty"` |
| 78 | 94 | // 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 ( | ||
| 11 | 11 | "github.com/coder/websocket" |
| 12 | 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 | 17 | // 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 WebSocket | 17 | // dialTestServer starts an HTTP test server exposing the bridge's WebSocket |
modified
internal/config/config_test.go +1 -1 | @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | "strings" |
| 8 | 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 | 13 | 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 ( | ||
| 7 | 7 | "testing" |
| 8 | 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 | 13 | // 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 | ||
| 61 | 61 | 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 | 68 | a.mu.Lock() |
| 66 | - defer a.mu.Unlock() | |
| 67 | 69 | 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 | + }} | |
| 69 | 88 | } |
| 70 | 89 | |
| 71 | 90 | // 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 | 104 | 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 | 109 | func promptText(params acp.PromptRequest) string { |
| 90 | 110 | text := "" |
| 91 | 111 | for _, block := range params.Prompt { |
| 92 | - if block.Text != nil { | |
| 112 | + switch { | |
| 113 | + case block.Text != nil: | |
| 93 | 114 | text += block.Text.Text |
| 115 | + case block.ResourceLink != nil: | |
| 116 | + text += " [attached: " + block.ResourceLink.Name + "]" | |
| 94 | 117 | } |
| 95 | 118 | } |
| 96 | 119 | return text |
| @@ -61,11 +61,30 @@ func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.Auth | |||
| 61 | return acp.AuthenticateResponse{}, nil | 61 | 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))}, 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 | + }} | ||
| 69 | } | 88 | } |
| 70 | 89 | ||
| 71 | // Cancel is a no-op: the SDK already cancels the context of the running | 90 | // 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}, nil | 104 | 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.Text | 114 | text += block.Text.Text |
| 115 | + case block.ResourceLink != nil: | ||
| 116 | + text += " [attached: " + block.ResourceLink.Name + "]" | ||
| 94 | } | 117 | } |
| 95 | } | 118 | } |
| 96 | return text | 119 | return text |
modified
internal/mockagent/mockagent_test.go +43 -2 | @@ -10,8 +10,8 @@ import ( | ||
| 10 | 10 | |
| 11 | 11 | acp "github.com/coder/acp-go-sdk" |
| 12 | 12 | |
| 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" | |
| 15 | 15 | ) |
| 16 | 16 | |
| 17 | 17 | // panelHandler records everything the mock streams, and answers the |
| @@ -21,6 +21,7 @@ type panelHandler struct { | ||
| 21 | 21 | optionId string |
| 22 | 22 | texts []string |
| 23 | 23 | updateKinds []string |
| 24 | + commands []string | |
| 24 | 25 | permissions int |
| 25 | 26 | } |
| 26 | 27 | |
| @@ -41,6 +42,11 @@ func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotif | ||
| 41 | 42 | h.updateKinds = append(h.updateKinds, "tool_call_update") |
| 42 | 43 | case n.Update.Plan != nil: |
| 43 | 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 | 61 | func (h *panelHandler) kinds() map[string]int { |
| 56 | 62 | h.mu.Lock() |
| 57 | 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 | 69 | counts := map[string]int{} |
| 59 | 70 | for _, k := range h.updateKinds { |
| 60 | 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 | 153 | func TestScenarioStopsOnRejectedPermission(t *testing.T) { |
| 113 | 154 | handler := &panelHandler{optionId: "reject-once"} |
| 114 | 155 | 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 the | 17 | // panelHandler records everything the mock streams, and answers the |
| @@ -21,6 +21,7 @@ type panelHandler struct { | |||
| 21 | optionId string | 21 | optionId string |
| 22 | texts []string | 22 | texts []string |
| 23 | updateKinds []string | 23 | updateKinds []string |
| 24 | + commands []string | ||
| 24 | permissions int | 25 | 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 differ | Binary 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 differ | Binary 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 @@ | ||
| 1 | 1 | { |
| 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 | + } | |
| 4480 | 4583 | } |
| @@ -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": true | 1406 | + "dev": true, |
| 1407 | - }, | 1407 | + "license": "MIT", |
| 1408 | - "@types/react-dom": { | 1408 | + "peer": true, |
| 1409 | - "optional": true | 1409 | + "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": true | 1419 | + "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": true | 1621 | + }, |
| 1622 | - }, | 1622 | + "engines": { |
| 1623 | - "vite": { | 1623 | + "node": "^20.19.0 || >=22.12.0" |
| 1624 | - "optional": true | 1624 | + }, |
| 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": true | 1984 | + "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": true | 2046 | + } |
| 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": true | 2189 | + "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": true | 2434 | + "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": true | 3564 | + "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": true | 4186 | + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", |
| 4187 | - }, | 4187 | + "dev": true, |
| 4188 | - "jiti": { | 4188 | + "funding": [ |
| 4189 | - "optional": true | 4189 | + { |
| 4190 | - }, | 4190 | + "type": "opencollective", |
| 4191 | - "less": { | 4191 | + "url": "https://opencollective.com/browserslist" |
| 4192 | - "optional": true | 4192 | + }, |
| 4193 | - }, | 4193 | + { |
| 4194 | - "lightningcss": { | 4194 | + "type": "tidelift", |
| 4195 | - "optional": true | 4195 | + "url": "https://tidelift.com/funding/github/npm/browserslist" |
| 4196 | - }, | 4196 | + }, |
| 4197 | - "sass": { | 4197 | + { |
| 4198 | - "optional": true | 4198 | + "type": "github", |
| 4199 | - }, | 4199 | + "url": "https://github.com/sponsors/ai" |
| 4200 | - "sass-embedded": { | 4200 | + } |
| 4201 | - "optional": true | 4201 | + ], |
| 4202 | - }, | 4202 | + "license": "MIT", |
| 4203 | - "stylus": { | 4203 | + "dependencies": { |
| 4204 | - "optional": true | 4204 | + "escalade": "^3.2.0", |
| 4205 | - }, | 4205 | + "picocolors": "^1.1.1" |
| 4206 | - "sugarss": { | 4206 | + }, |
| 4207 | - "optional": true | 4207 | + "bin": { |
| 4208 | - }, | 4208 | + "update-browserslist-db": "cli.js" |
| 4209 | - "terser": { | 4209 | + }, |
| 4210 | - "optional": true | 4210 | + "peerDependencies": { |
| 4211 | - }, | 4211 | + "browserslist": ">= 4.21.0" |
| 4212 | - "tsx": { | 4212 | + } |
| 4213 | - "optional": true | 4213 | + }, |
| 4214 | - }, | 4214 | + "node_modules/vfile": { |
| 4215 | - "yaml": { | 4215 | + "version": "6.0.3", |
| 4216 | - "optional": true | 4216 | + "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": true | 4294 | + "sass": { |
| 4295 | - }, | 4295 | + "optional": true |
| 4296 | - "@types/debug": { | 4296 | + }, |
| 4297 | - "optional": true | 4297 | + "sass-embedded": { |
| 4298 | - }, | 4298 | + "optional": true |
| 4299 | - "@types/node": { | 4299 | + }, |
| 4300 | - "optional": true | 4300 | + "stylus": { |
| 4301 | - }, | 4301 | + "optional": true |
| 4302 | - "@vitest/browser": { | 4302 | + }, |
| 4303 | - "optional": true | 4303 | + "sugarss": { |
| 4304 | - }, | 4304 | + "optional": true |
| 4305 | - "@vitest/ui": { | 4305 | + }, |
| 4306 | - "optional": true | 4306 | + "terser": { |
| 4307 | - }, | 4307 | + "optional": true |
| 4308 | - "happy-dom": { | 4308 | + }, |
| 4309 | - "optional": true | 4309 | + "tsx": { |
| 4310 | - }, | 4310 | + "optional": true |
| 4311 | - "jsdom": { | 4311 | + }, |
| 4312 | - "optional": true | 4312 | + "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": true | 4409 | + "optional": true |
| 4410 | - }, | 4410 | + } |
| 4411 | - "utf-8-validate": { | 4411 | + } |
| 4412 | - "optional": true | 4412 | + }, |
| 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": true | 4456 | + "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": true | 4459 | + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", |
| 4460 | - }, | 4460 | + "dev": true, |
| 4461 | - "react": { | 4461 | + "license": "MIT", |
| 4462 | - "optional": true | 4462 | + "engines": { |
| 4463 | - }, | 4463 | + "node": ">=18" |
| 4464 | - "use-sync-external-store": { | 4464 | + } |
| 4465 | - "optional": true | 4465 | + }, |
| 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 @@ | ||
| 7 | 7 | "dev": "vite", |
| 8 | 8 | "build": "tsc -b && vite build", |
| 9 | 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 | 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 | 20 | "react": "^19.0.0", |
| 14 | 21 | "react-dom": "^19.0.0", |
| 15 | 22 | "react-markdown": "^10.1.0", |
| 16 | 23 | "remark-gfm": "^4.0.1", |
| 24 | + "vscode-material-icons": "^0.1.1", | |
| 17 | 25 | "zustand": "^5.0.15" |
| 18 | 26 | }, |
| 19 | 27 | "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"; | |
| 3 | 9 | import { App } from "./App"; |
| 10 | +import type { ClientMessage } from "./protocol"; | |
| 4 | 11 | import { dispatch, resetStore } from "./store"; |
| 12 | +import { resetTheme } from "./theme"; | |
| 13 | +import { resetWorkspace } from "./workspace"; | |
| 5 | 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 | 30 | function fakeSocket() { |
| 9 | 31 | const sent: ClientMessage[] = []; |
| @@ -17,6 +39,44 @@ function fakeSocket() { | ||
| 17 | 39 | describe("App", () => { |
| 18 | 40 | beforeEach(() => { |
| 19 | 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 | 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 | 290 | it("does not send empty prompts", () => { |
| 176 | 291 | const { socket, sent } = fakeSocket(); |
| 177 | 292 | 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 @@ | ||
| 1 | 1 | /** |
| 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 | |
| 4 | 5 | * embeddings) can supply a fake. |
| 5 | 6 | * |
| 6 | 7 | * @example |
| 7 | 8 | * <App socket={connectSocket({ url: wsUrl(), dispatch })} /> |
| 8 | 9 | */ |
| 9 | 10 | |
| 10 | -import type { OriSocket } from "./ws"; | |
| 11 | -import { useChatState } from "./store"; | |
| 12 | 11 | import { ChatThread } from "./components/ChatThread"; |
| 13 | 12 | import { PermissionPrompt } from "./components/PermissionPrompt"; |
| 14 | 13 | import { PlanView } from "./components/PlanView"; |
| 15 | 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 | 21 | export function App({ socket }: { socket: OriSocket }) { |
| 18 | 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 | 27 | return ( |
| 21 | 28 | <div className="app"> |
| @@ -25,23 +32,55 @@ export function App({ socket }: { socket: OriSocket }) { | ||
| 25 | 32 | {state.connection} |
| 26 | 33 | {state.sessionId ? ` · ${state.sessionId}` : ""} |
| 27 | 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 | 51 | </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> | |
| 45 | 84 | </div> |
| 46 | 85 | ); |
| 47 | 86 | } |
| @@ -1,21 +1,28 @@ | |||
| 1 | /** | 1 | /** |
| 2 | - * Root component: header with connection/session status, the conversation | 2 | + * Root component: header with connection/session status and the workspace |
| 3 | - * thread, and the composer. The WebSocket is injected so tests (and future | 3 | + * 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 | * @example | 7 | * @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 | - <PermissionPrompt | 55 | + <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 | - <PromptInput | 63 | + 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 @@ | ||
| 1 | 1 | /* Ori — agent panel styling, in the spirit of Zed's agent panel: |
| 2 | 2 | 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 | 6 | :root { |
| 6 | - color-scheme: light dark; | |
| 7 | + color-scheme: light; | |
| 7 | 8 | font-family: system-ui, -apple-system, "Segoe UI", sans-serif; |
| 8 | 9 | |
| 9 | 10 | --bg: #fafafa; |
| @@ -18,19 +19,18 @@ | ||
| 18 | 19 | --code-bg: #f2f2f2; |
| 19 | 20 | } |
| 20 | 21 | |
| 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; | |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | 36 | body { |
| @@ -43,8 +43,27 @@ body { | ||
| 43 | 43 | display: flex; |
| 44 | 44 | flex-direction: column; |
| 45 | 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 | 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 | 69 | /* ---- header ---- */ |
| @@ -86,6 +105,419 @@ body { | ||
| 86 | 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 | 521 | /* ---- thread ---- */ |
| 90 | 522 | |
| 91 | 523 | .thread { |
| @@ -97,13 +529,38 @@ body { | ||
| 97 | 529 | gap: 0.75rem; |
| 98 | 530 | } |
| 99 | 531 | |
| 100 | -.thread-empty, | |
| 101 | -.thread-working { | |
| 532 | +.thread-empty { | |
| 102 | 533 | color: var(--muted); |
| 103 | 534 | font-size: 0.85rem; |
| 104 | 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 | 564 | .message { |
| 108 | 565 | border-radius: 8px; |
| 109 | 566 | padding: 0.55rem 0.8rem; |
| @@ -184,6 +641,11 @@ body { | ||
| 184 | 641 | user-select: none; |
| 185 | 642 | } |
| 186 | 643 | |
| 644 | +.thought-live .thought-summary { | |
| 645 | + cursor: default; | |
| 646 | + margin: 0; | |
| 647 | +} | |
| 648 | + | |
| 187 | 649 | .thought-body { |
| 188 | 650 | padding-top: 0.4rem; |
| 189 | 651 | } |
| @@ -280,6 +742,29 @@ body { | ||
| 280 | 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 | 768 | /* ---- plan ---- */ |
| 284 | 769 | |
| 285 | 770 | .plan { |
| @@ -370,6 +855,12 @@ body { | ||
| 370 | 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 | 864 | .composer-input { |
| 374 | 865 | flex: 1; |
| 375 | 866 | resize: none; |
| @@ -409,3 +900,66 @@ body { | ||
| 409 | 900 | .composer-stop { |
| 410 | 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 | +} | |
| @@ -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 at | 2 | 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"; | ||
| 10 | 10 | import type { ThreadItem } from "../reducer"; |
| 11 | 11 | import { Markdown } from "./Markdown"; |
| 12 | 12 | import { ToolCallCard } from "./ToolCallCard"; |
| 13 | +import { WorkingIndicator } from "./WorkingIndicator"; | |
| 13 | 14 | |
| 14 | 15 | interface ChatThreadProps { |
| 15 | 16 | thread: ThreadItem[]; |
| @@ -36,9 +37,7 @@ export function ChatThread({ thread, turnActive }: ChatThreadProps) { | ||
| 36 | 37 | // biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity. |
| 37 | 38 | <ThreadEntry key={index} item={item} /> |
| 38 | 39 | ))} |
| 39 | - {turnActive && ( | |
| 40 | - <output className="thread-working">the agent is working…</output> | |
| 41 | - )} | |
| 40 | + {turnActive && <WorkingIndicator />} | |
| 42 | 41 | <div ref={endRef} /> |
| 43 | 42 | </section> |
| 44 | 43 | ); |
| @@ -55,14 +54,7 @@ function ThreadEntry({ item }: { item: ThreadItem }) { | ||
| 55 | 54 | </div> |
| 56 | 55 | ); |
| 57 | 56 | 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} />; | |
| 66 | 58 | case "tool": |
| 67 | 59 | return <ToolCallCard call={item.call} />; |
| 68 | 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 | +} | |
| @@ -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 @@ | ||
| 12 | 12 | */ |
| 13 | 13 | |
| 14 | 14 | import type { PermissionRequestState } from "../reducer"; |
| 15 | +import { InlineText } from "./InlineText"; | |
| 15 | 16 | |
| 16 | 17 | interface PermissionPromptProps { |
| 17 | 18 | request: PermissionRequestState; |
| @@ -28,7 +29,9 @@ export function PermissionPrompt({ | ||
| 28 | 29 | role="alertdialog" |
| 29 | 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 | 35 | <div className="permission-options"> |
| 33 | 36 | {request.options.map((option) => ( |
| 34 | 37 | <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 | <button | 37 | <button |
modified
ui/src/components/PlanView.tsx +2 -1 | @@ -7,6 +7,7 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | import type { AcpPlanEntry } from "../protocol"; |
| 10 | +import { InlineText } from "./InlineText"; | |
| 10 | 11 | |
| 11 | 12 | const statusMarks: Record<string, string> = { |
| 12 | 13 | pending: "○", |
| @@ -29,7 +30,7 @@ export function PlanView({ entries }: { entries: AcpPlanEntry[] }) { | ||
| 29 | 30 | // biome-ignore lint/suspicious/noArrayIndexKey: ACP plan entries carry no id and the list is replaced wholesale on every update. |
| 30 | 31 | <li key={index} className={`plan-entry plan-entry-${entry.status}`}> |
| 31 | 32 | <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "} |
| 32 | - {entry.content} | |
| 33 | + <InlineText text={entry.content} /> | |
| 33 | 34 | </li> |
| 34 | 35 | ))} |
| 35 | 36 | </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 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * PromptInput is the message composer: Enter sends (Shift+Enter for a new |
| 3 | 3 | * 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 | 8 | * @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 | 22 | interface PromptInputProps { |
| 13 | 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 | 27 | onCancel: () => void; |
| 16 | 28 | } |
| 17 | 29 | |
| 18 | 30 | export function PromptInput({ |
| 19 | 31 | turnActive, |
| 32 | + commands, | |
| 20 | 33 | onSend, |
| 21 | 34 | onCancel, |
| 22 | 35 | }: PromptInputProps) { |
| 23 | 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 | 76 | const submit = () => { |
| 26 | 77 | const trimmed = text.trim(); |
| 27 | 78 | if (trimmed === "" || turnActive) { |
| 28 | 79 | return; |
| 29 | 80 | } |
| 30 | - onSend(trimmed); | |
| 81 | + onSend(trimmed, activeAttachments(trimmed, attachments)); | |
| 31 | 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 | 98 | return ( |
| @@ -39,20 +103,36 @@ export function PromptInput({ | ||
| 39 | 103 | submit(); |
| 40 | 104 | }} |
| 41 | 105 | > |
| 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 | |
| 53 | 124 | } |
| 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> | |
| 56 | 136 | {turnActive ? ( |
| 57 | 137 | <button type="button" className="composer-stop" onClick={onCancel}> |
| 58 | 138 | Stop |
| @@ -69,3 +149,35 @@ export function PromptInput({ | ||
| 69 | 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 | +} | |
| @@ -1,34 +1,98 @@ | |||
| 1 | /** | 1 | /** |
| 2 | * PromptInput is the message composer: Enter sends (Shift+Enter for a new | 2 | * 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, matching | 3 | * 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 | * @example | 8 | * @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 | - <textarea | 106 | + <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 | Stop | 138 | 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 @@ | ||
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | import type { ToolCall } from "../reducer"; |
| 12 | +import { InlineText } from "./InlineText"; | |
| 12 | 13 | import type { AcpToolCallContent } from "../protocol"; |
| 13 | 14 | import { Markdown } from "./Markdown"; |
| 14 | 15 | |
| @@ -39,7 +40,9 @@ export function ToolCallCard({ call }: { call: ToolCall }) { | ||
| 39 | 40 | <span className="tool-icon" aria-hidden> |
| 40 | 41 | {kindIcons[call.toolKind] ?? kindIcons.other} |
| 41 | 42 | </span> |
| 42 | - <span className="tool-title">{call.title}</span> | |
| 43 | + <span className="tool-title"> | |
| 44 | + <InlineText text={call.title} /> | |
| 45 | + </span> | |
| 43 | 46 | <span className={`tool-status tool-status-${call.status}`}> |
| 44 | 47 | {statusLabels[call.status] ?? call.status} |
| 45 | 48 | </span> |
| @@ -60,19 +63,22 @@ export function ToolCallCard({ call }: { call: ToolCall }) { | ||
| 60 | 63 | )} |
| 61 | 64 | {call.contents.map((content, index) => ( |
| 62 | 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 | 68 | </div> |
| 66 | 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 | 77 | switch (content.type) { |
| 72 | 78 | 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 | + ); | |
| 76 | 82 | case "diff": |
| 77 | 83 | return ( |
| 78 | 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 | 121 | function prefixLines(text: string, prefix: string): string { |
| 98 | 122 | return text |
| 99 | 123 | .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 text | 122 | 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"; | ||
| 2 | 2 | import { createRoot } from "react-dom/client"; |
| 3 | 3 | import { App } from "./App"; |
| 4 | 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 | 8 | import { connectSocket, wsUrl } from "./ws"; |
| 6 | 9 | import "./app.css"; |
| 7 | 10 | |
| @@ -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 { | ||
| 42 | 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 | 52 | /** The ACP payload of a session/request_permission call. */ |
| 46 | 53 | export interface AcpPermissionRequest { |
| 47 | 54 | sessionId?: string; |
| @@ -68,9 +75,20 @@ export interface AcpSessionUpdate { | ||
| 68 | 75 | status?: string; |
| 69 | 76 | locations?: AcpToolCallLocation[]; |
| 70 | 77 | entries?: AcpPlanEntry[]; |
| 78 | + availableCommands?: AcpAvailableCommand[]; | |
| 71 | 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 | 92 | /** A message sent by the server over the WebSocket. */ |
| 75 | 93 | export interface ServerMessage { |
| 76 | 94 | type: |
| @@ -88,13 +106,14 @@ export interface ServerMessage { | ||
| 88 | 106 | requestId?: string; |
| 89 | 107 | request?: AcpPermissionRequest; |
| 90 | 108 | text?: string; |
| 109 | + attachments?: PromptAttachment[]; | |
| 91 | 110 | stopReason?: string; |
| 92 | 111 | message?: string; |
| 93 | 112 | } |
| 94 | 113 | |
| 95 | 114 | /** A message the browser sends to the server. */ |
| 96 | 115 | export type ClientMessage = |
| 97 | - | { type: "prompt"; text: string } | |
| 116 | + | { type: "prompt"; text: string; attachments?: PromptAttachment[] } | |
| 98 | 117 | | { type: "cancel" } |
| 99 | 118 | | { |
| 100 | 119 | 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", () => { | ||
| 275 | 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 | 301 | it("tracks connection status changes", () => { |
| 279 | 302 | const state = reduce(initialState, { |
| 280 | 303 | 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 @@ | ||
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | 7 | import type { |
| 8 | + AcpAvailableCommand, | |
| 8 | 9 | AcpContentBlock, |
| 9 | 10 | AcpPlanEntry, |
| 10 | 11 | AcpPermissionOption, |
| @@ -48,6 +49,8 @@ export interface ChatState { | ||
| 48 | 49 | thread: ThreadItem[]; |
| 49 | 50 | plan: AcpPlanEntry[]; |
| 50 | 51 | permissions: PermissionRequestState[]; |
| 52 | + /** Slash commands the agent announced (available_commands_update). */ | |
| 53 | + commands: AcpAvailableCommand[]; | |
| 51 | 54 | } |
| 52 | 55 | |
| 53 | 56 | export const initialState: ChatState = { |
| @@ -57,6 +60,7 @@ export const initialState: ChatState = { | ||
| 57 | 60 | thread: [], |
| 58 | 61 | plan: [], |
| 59 | 62 | permissions: [], |
| 63 | + commands: [], | |
| 60 | 64 | }; |
| 61 | 65 | |
| 62 | 66 | /** Everything that can change the state. */ |
| @@ -95,6 +99,7 @@ const serverHandlers: Record<string, MessageHandler> = { | ||
| 95 | 99 | thread: [], |
| 96 | 100 | plan: [], |
| 97 | 101 | permissions: [], |
| 102 | + commands: [], | |
| 98 | 103 | }), |
| 99 | 104 | user_message: (state, msg) => |
| 100 | 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, | |
| 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. | |
| 121 | 125 | */ |
| 122 | 126 | const updateHandlers: Record<string, MessageHandler> = { |
| 123 | 127 | agent_message_chunk: (state, msg) => |
| @@ -127,6 +131,10 @@ const updateHandlers: Record<string, MessageHandler> = { | ||
| 127 | 131 | tool_call: upsertToolCall, |
| 128 | 132 | tool_call_update: upsertToolCall, |
| 129 | 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 | 140 | 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 the | 124 | + * 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 @@ | ||
| 12 | 12 | "skipLibCheck": true, |
| 13 | 13 | "isolatedModules": true, |
| 14 | 14 | "noEmit": true, |
| 15 | - "types": ["vitest/globals"] | |
| 15 | + "types": ["vitest/globals", "vite/client"] | |
| 16 | 16 | }, |
| 17 | 17 | "include": ["src", "vite.config.ts"] |
| 18 | 18 | } |
| @@ -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({ | ||
| 10 | 10 | server: { |
| 11 | 11 | proxy: { |
| 12 | 12 | "/ws": { target: "ws://localhost:8888", ws: true }, |
| 13 | + "/api": { target: "http://localhost:8888" }, | |
| 13 | 14 | "/healthz": { target: "http://localhost:8888" }, |
| 14 | 15 | }, |
| 15 | 16 | }, |
| @@ -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 | }, |