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

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

forked from bots-garden/ori

✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
k33g committed 2026-09-17T21:15:42+02:00 Browse files
2434cc7 parent: 4edda86
added .gitignore +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+# Go
2+/bin/
3+*.test
4+
5+# Node / Vite
6+node_modules/
7+ui/dist/*
8+!ui/dist/.gitkeep
9+
10+# Editors & OS
11+.DS_Store
12+*.log
new file mode 100644
@@ -0,0 +1,12 @@
1+# Go
2+/bin/
3+*.test
4+
5+# Node / Vite
6+node_modules/
7+ui/dist/*
8+!ui/dist/.gitkeep
9+
10+# Editors & OS
11+.DS_Store
12+*.log
added .qlty/.gitignore +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+*
2+!configs
3+!configs/**
4+!hooks
5+!hooks/**
6+!qlty.toml
7+!.gitignore
new file mode 100644
@@ -0,0 +1,7 @@
1+*
2+!configs
3+!configs/**
4+!hooks
5+!hooks/**
6+!qlty.toml
7+!.gitignore
added .qlty/configs/.shellcheckrc +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+source-path=SCRIPTDIR
\ No newline at end of file
new file mode 100644
@@ -0,0 +1 @@
1+source-path=SCRIPTDIR
\ No newline at end of file\ No newline at end of file
added .qlty/qlty.toml +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+# This file was automatically generated by `qlty init`.
2+# You can modify it to suit your needs.
3+# We recommend you to commit this file to your repository.
4+#
5+# This configuration is used by both Qlty CLI and Qlty Cloud.
6+#
7+# Qlty CLI -- Code quality toolkit for developers
8+# Qlty Cloud -- Fully automated Code Health Platform
9+#
10+# Try Qlty Cloud: https://qlty.sh
11+#
12+# For a guide to configuration, visit https://qlty.sh/d/config
13+# Or for a full reference, visit https://qlty.sh/d/qlty-toml
14+config_version = "0"
15+
16+exclude_patterns = [
17+ "*_min.*",
18+ "*-min.*",
19+ "*.min.*",
20+ "**/.yarn/**",
21+ "**/*.d.ts",
22+ "**/assets/**",
23+ "**/bower_components/**",
24+ "**/build/**",
25+ "**/cache/**",
26+ "**/config/**",
27+ "**/db/**",
28+ "**/deps/**",
29+ "**/dist/**",
30+ "**/extern/**",
31+ "**/external/**",
32+ "**/generated/**",
33+ "**/Godeps/**",
34+ "**/gradlew/**",
35+ "**/mvnw/**",
36+ "**/node_modules/**",
37+ "**/protos/**",
38+ "**/seed/**",
39+ "**/target/**",
40+ "**/templates/**",
41+ "**/testdata/**",
42+ "**/vendor/**",
43+ # Vendored dev-toolkit kit sources (SKILL.md files and the measurement
44+ # script quality_report.py itself). Excluded with the user's agreement so
45+ # the instrument is not measured with itself. What this hides: bandit/ruff
46+ # findings on quality_report.py (subprocess usage, which the script needs
47+ # to run qlty) — defects there belong to the kit's source, not this repo.
48+ ".claude/**",
49+]
50+
51+test_patterns = [
52+ "**/test/**",
53+ "**/spec/**",
54+ "**/*.test.*",
55+ "**/*.spec.*",
56+ "**/*_test.*",
57+ "**/*_spec.*",
58+ "**/test_*.*",
59+ "**/spec_*.*",
60+]
61+
62+[smells]
63+mode = "comment"
64+
65+[[source]]
66+name = "default"
67+default = true
68+
69+
70+[[plugin]]
71+name = "bandit"
72+
73+# radarlint-python removed with the user's agreement: it requires a ~300MB
74+# JDK that cannot fit on the dedicated ~/.qlty volume (488MB, already full of
75+# the other tool runtimes) — `qlty init` failed twice with "No space left on
76+# device" while extracting jdk-22.0.1+8. Its only target was the vendored kit
77+# script excluded above; bandit and ruff still cover any Python that appears.
78+
79+[[plugin]]
80+name = "golangci-lint"
81+
82+[[plugin]]
83+name = "biome"
84+
85+[[plugin]]
86+name = "ripgrep"
87+mode = "comment"
88+
89+[[plugin]]
90+name = "ruff"
91+drivers = [
92+ "lint",
93+]
94+
95+[[plugin]]
96+name = "shellcheck"
97+
98+[[plugin]]
99+name = "trufflehog"
new file mode 100644
@@ -0,0 +1,99 @@
1+# This file was automatically generated by `qlty init`.
2+# You can modify it to suit your needs.
3+# We recommend you to commit this file to your repository.
4+#
5+# This configuration is used by both Qlty CLI and Qlty Cloud.
6+#
7+# Qlty CLI -- Code quality toolkit for developers
8+# Qlty Cloud -- Fully automated Code Health Platform
9+#
10+# Try Qlty Cloud: https://qlty.sh
11+#
12+# For a guide to configuration, visit https://qlty.sh/d/config
13+# Or for a full reference, visit https://qlty.sh/d/qlty-toml
14+config_version = "0"
15+
16+exclude_patterns = [
17+ "*_min.*",
18+ "*-min.*",
19+ "*.min.*",
20+ "**/.yarn/**",
21+ "**/*.d.ts",
22+ "**/assets/**",
23+ "**/bower_components/**",
24+ "**/build/**",
25+ "**/cache/**",
26+ "**/config/**",
27+ "**/db/**",
28+ "**/deps/**",
29+ "**/dist/**",
30+ "**/extern/**",
31+ "**/external/**",
32+ "**/generated/**",
33+ "**/Godeps/**",
34+ "**/gradlew/**",
35+ "**/mvnw/**",
36+ "**/node_modules/**",
37+ "**/protos/**",
38+ "**/seed/**",
39+ "**/target/**",
40+ "**/templates/**",
41+ "**/testdata/**",
42+ "**/vendor/**",
43+ # Vendored dev-toolkit kit sources (SKILL.md files and the measurement
44+ # script quality_report.py itself). Excluded with the user's agreement so
45+ # the instrument is not measured with itself. What this hides: bandit/ruff
46+ # findings on quality_report.py (subprocess usage, which the script needs
47+ # to run qlty) — defects there belong to the kit's source, not this repo.
48+ ".claude/**",
49+]
50+
51+test_patterns = [
52+ "**/test/**",
53+ "**/spec/**",
54+ "**/*.test.*",
55+ "**/*.spec.*",
56+ "**/*_test.*",
57+ "**/*_spec.*",
58+ "**/test_*.*",
59+ "**/spec_*.*",
60+]
61+
62+[smells]
63+mode = "comment"
64+
65+[[source]]
66+name = "default"
67+default = true
68+
69+
70+[[plugin]]
71+name = "bandit"
72+
73+# radarlint-python removed with the user's agreement: it requires a ~300MB
74+# JDK that cannot fit on the dedicated ~/.qlty volume (488MB, already full of
75+# the other tool runtimes) — `qlty init` failed twice with "No space left on
76+# device" while extracting jdk-22.0.1+8. Its only target was the vendored kit
77+# script excluded above; bandit and ruff still cover any Python that appears.
78+
79+[[plugin]]
80+name = "golangci-lint"
81+
82+[[plugin]]
83+name = "biome"
84+
85+[[plugin]]
86+name = "ripgrep"
87+mode = "comment"
88+
89+[[plugin]]
90+name = "ruff"
91+drivers = [
92+ "lint",
93+]
94+
95+[[plugin]]
96+name = "shellcheck"
97+
98+[[plugin]]
99+name = "trufflehog"
added .quality/history.jsonl +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+{"branch": "feat/acp-webapp", "breaches": ["code smells: 5 (max 0)"], "commit": "4edda86", "counts": {}, "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": 1, "smells": 5, "timestamp": "2026-09-17T19:09:37Z"}
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"}
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"}
new file mode 100644
@@ -0,0 +1,4 @@
1+{"branch": "feat/acp-webapp", "breaches": ["code smells: 5 (max 0)"], "commit": "4edda86", "counts": {}, "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": 1, "smells": 5, "timestamp": "2026-09-17T19:09:37Z"}
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"}
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"}
added .quality/report-20260917T190937Z.md +85 -0
new file mode 100644
@@ -0,0 +1,85 @@
1+# Quality report — 2026-09-17T19:09:37Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #1 (first recorded run)
7+
8+## Gate violations
9+
10+- code smells: 5 (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: **5** (vs previous: —)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 8): Start |
31+| qlty:function-complexity | internal/bridge/ws.go | 16 | Function with high complexity (count = 22): WebSocketHandler |
32+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 10): play |
33+| qlty:return-statements | ui/src/reducer.ts | 82 | Function with many returns (count = 9): reduceServer |
34+| qlty:return-statements | ui/src/reducer.ts | 119 | Function with many returns (count = 6): reduceUpdate |
35+
36+## Metrics (`qlty metrics`)
37+
38+| metric | total | vs previous |
39+|---|---|---|
40+| funcs | 92 | — |
41+| classes | 27 | — |
42+| fields | 39 | — |
43+| cyclo | 307 | — |
44+| complex | 158 | — |
45+| lcom | 0 | — |
46+| lines | 1989 | — |
47+| loc | 1398 | — |
48+
49+### Most complex files
50+
51+| file | complex | cyclo | loc |
52+|---|---|---|---|
53+| internal/bridge/bridge.go | 26 | 40 | 201 |
54+| internal/mockagent/mockagent.go | 24 | 35 | 178 |
55+| internal/bridge/ws.go | 22 | 14 | 47 |
56+| ui/src/reducer.ts | 18 | 66 | 184 |
57+| internal/agent/agent.go | 15 | 29 | 114 |
58+| internal/agent/client.go | 11 | 28 | 74 |
59+| ui/src/ws.ts | 10 | 13 | 59 |
60+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
61+| ui/src/components/PromptInput.tsx | 7 | 9 | 48 |
62+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 78 |
63+| cmd/ori/main.go | 3 | 7 | 45 |
64+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
65+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
66+| internal/bridge/protocol.go | 1 | 3 | 42 |
67+| ui/embed.go | 1 | 3 | 13 |
68+
69+## Tooling notes
70+
71+- `qlty check` exit 99: ERROR
72+
73+ > Error installing go@1.22.0.
74+ >
75+ >
76+ > Caused by:
77+ > 0: Error extracting tar.gz archive from https://go.dev/dl/go1.22.0.linux-arm64.tar.gz
78+ > 1: failed to unpack `go/CONTRIBUTING.md` into `/home/agent/.qlty/cache/tools/go/1.22.0-c5a26d064299/CONTRIBUTING.md`
79+ > 2: No space left on device (os error 28)
80+
81+## Trend
82+
83+| run | timestamp | error | warning | smells | complex | gate |
84+|---|---|---|---|---|---|---|
85+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
new file mode 100644
@@ -0,0 +1,85 @@
1+# Quality report — 2026-09-17T19:09:37Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #1 (first recorded run)
7+
8+## Gate violations
9+
10+- code smells: 5 (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: **5** (vs previous: —)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 8): Start |
31+| qlty:function-complexity | internal/bridge/ws.go | 16 | Function with high complexity (count = 22): WebSocketHandler |
32+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 10): play |
33+| qlty:return-statements | ui/src/reducer.ts | 82 | Function with many returns (count = 9): reduceServer |
34+| qlty:return-statements | ui/src/reducer.ts | 119 | Function with many returns (count = 6): reduceUpdate |
35+
36+## Metrics (`qlty metrics`)
37+
38+| metric | total | vs previous |
39+|---|---|---|
40+| funcs | 92 | — |
41+| classes | 27 | — |
42+| fields | 39 | — |
43+| cyclo | 307 | — |
44+| complex | 158 | — |
45+| lcom | 0 | — |
46+| lines | 1989 | — |
47+| loc | 1398 | — |
48+
49+### Most complex files
50+
51+| file | complex | cyclo | loc |
52+|---|---|---|---|
53+| internal/bridge/bridge.go | 26 | 40 | 201 |
54+| internal/mockagent/mockagent.go | 24 | 35 | 178 |
55+| internal/bridge/ws.go | 22 | 14 | 47 |
56+| ui/src/reducer.ts | 18 | 66 | 184 |
57+| internal/agent/agent.go | 15 | 29 | 114 |
58+| internal/agent/client.go | 11 | 28 | 74 |
59+| ui/src/ws.ts | 10 | 13 | 59 |
60+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
61+| ui/src/components/PromptInput.tsx | 7 | 9 | 48 |
62+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 78 |
63+| cmd/ori/main.go | 3 | 7 | 45 |
64+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
65+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
66+| internal/bridge/protocol.go | 1 | 3 | 42 |
67+| ui/embed.go | 1 | 3 | 13 |
68+
69+## Tooling notes
70+
71+- `qlty check` exit 99: ERROR
72+
73+ > Error installing go@1.22.0.
74+ >
75+ >
76+ > Caused by:
77+ > 0: Error extracting tar.gz archive from https://go.dev/dl/go1.22.0.linux-arm64.tar.gz
78+ > 1: failed to unpack `go/CONTRIBUTING.md` into `/home/agent/.qlty/cache/tools/go/1.22.0-c5a26d064299/CONTRIBUTING.md`
79+ > 2: No space left on device (os error 28)
80+
81+## Trend
82+
83+| run | timestamp | error | warning | smells | complex | gate |
84+|---|---|---|---|---|---|---|
85+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
added .quality/report-20260917T191018Z.md +105 -0
new file mode 100644
@@ -0,0 +1,105 @@
1+# Quality report — 2026-09-17T19:10:18Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #2 (previous: 2026-09-17T19:09:37Z)
7+
8+## Gate violations
9+
10+- error-level issues: 13 (max 0)
11+- warning-level issues: 4 (max 0)
12+- code smells: 5 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 13 | — |
19+| warning | 4 | — |
20+| note | 21 | — |
21+
22+### Top rules
23+
24+| rule | count |
25+|---|---|
26+| biome:fmt | 19 |
27+| biome:parse | 7 |
28+| biome:lint/suspicious/noArrayIndexKey | 4 |
29+| golangci-lint:errcheck | 3 |
30+| golangci-lint:fmt | 2 |
31+| golangci-lint:staticcheck | 1 |
32+| biome:lint/correctness/useExhaustiveDependencies | 1 |
33+| biome:lint/a11y/useSemanticElements | 1 |
34+
35+### Most affected files
36+
37+| file | issues |
38+|---|---|
39+| .vscode/settings.json | 6 |
40+| ui/src/components/ChatThread.tsx | 4 |
41+| cmd/ori/e2e_test.go | 3 |
42+| ui/src/components/ToolCallCard.tsx | 3 |
43+| ui/src/components/PlanView.tsx | 2 |
44+| .vscode/extensions.json | 1 |
45+| internal/bridge/ws.go | 1 |
46+| internal/httpserver/httpserver_test.go | 1 |
47+| internal/mockagent/mockagent.go | 1 |
48+| ui/package.json | 1 |
49+| ui/src/App.test.tsx | 1 |
50+| ui/src/App.tsx | 1 |
51+| ui/src/app.css | 1 |
52+| ui/src/components/Markdown.tsx | 1 |
53+| ui/src/components/PermissionPrompt.tsx | 1 |
54+
55+## Code smells (`qlty smells`)
56+
57+Total: **5** (vs previous: ±0)
58+
59+| smell | file | line | detail |
60+|---|---|---|---|
61+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 8): Start |
62+| qlty:function-complexity | internal/bridge/ws.go | 16 | Function with high complexity (count = 22): WebSocketHandler |
63+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 10): play |
64+| qlty:return-statements | ui/src/reducer.ts | 82 | Function with many returns (count = 9): reduceServer |
65+| qlty:return-statements | ui/src/reducer.ts | 119 | Function with many returns (count = 6): reduceUpdate |
66+
67+## Metrics (`qlty metrics`)
68+
69+| metric | total | vs previous |
70+|---|---|---|
71+| funcs | 92 | ±0 |
72+| classes | 27 | ±0 |
73+| fields | 39 | ±0 |
74+| cyclo | 307 | ±0 |
75+| complex | 158 | ±0 |
76+| lcom | 0 | ±0 |
77+| lines | 1989 | ±0 |
78+| loc | 1398 | ±0 |
79+
80+### Most complex files
81+
82+| file | complex | cyclo | loc |
83+|---|---|---|---|
84+| internal/bridge/bridge.go | 26 | 40 | 201 |
85+| internal/mockagent/mockagent.go | 24 | 35 | 178 |
86+| internal/bridge/ws.go | 22 | 14 | 47 |
87+| ui/src/reducer.ts | 18 | 66 | 184 |
88+| internal/agent/agent.go | 15 | 29 | 114 |
89+| internal/agent/client.go | 11 | 28 | 74 |
90+| ui/src/ws.ts | 10 | 13 | 59 |
91+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
92+| ui/src/components/PromptInput.tsx | 7 | 9 | 48 |
93+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 78 |
94+| cmd/ori/main.go | 3 | 7 | 45 |
95+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
96+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
97+| internal/bridge/protocol.go | 1 | 3 | 42 |
98+| ui/embed.go | 1 | 3 | 13 |
99+
100+## Trend
101+
102+| run | timestamp | error | warning | smells | complex | gate |
103+|---|---|---|---|---|---|---|
104+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
105+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
new file mode 100644
@@ -0,0 +1,105 @@
1+# Quality report — 2026-09-17T19:10:18Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #2 (previous: 2026-09-17T19:09:37Z)
7+
8+## Gate violations
9+
10+- error-level issues: 13 (max 0)
11+- warning-level issues: 4 (max 0)
12+- code smells: 5 (max 0)
13+
14+## Lint issues (`qlty check`)
15+
16+| level | count | vs previous |
17+|---|---|---|
18+| error | 13 | — |
19+| warning | 4 | — |
20+| note | 21 | — |
21+
22+### Top rules
23+
24+| rule | count |
25+|---|---|
26+| biome:fmt | 19 |
27+| biome:parse | 7 |
28+| biome:lint/suspicious/noArrayIndexKey | 4 |
29+| golangci-lint:errcheck | 3 |
30+| golangci-lint:fmt | 2 |
31+| golangci-lint:staticcheck | 1 |
32+| biome:lint/correctness/useExhaustiveDependencies | 1 |
33+| biome:lint/a11y/useSemanticElements | 1 |
34+
35+### Most affected files
36+
37+| file | issues |
38+|---|---|
39+| .vscode/settings.json | 6 |
40+| ui/src/components/ChatThread.tsx | 4 |
41+| cmd/ori/e2e_test.go | 3 |
42+| ui/src/components/ToolCallCard.tsx | 3 |
43+| ui/src/components/PlanView.tsx | 2 |
44+| .vscode/extensions.json | 1 |
45+| internal/bridge/ws.go | 1 |
46+| internal/httpserver/httpserver_test.go | 1 |
47+| internal/mockagent/mockagent.go | 1 |
48+| ui/package.json | 1 |
49+| ui/src/App.test.tsx | 1 |
50+| ui/src/App.tsx | 1 |
51+| ui/src/app.css | 1 |
52+| ui/src/components/Markdown.tsx | 1 |
53+| ui/src/components/PermissionPrompt.tsx | 1 |
54+
55+## Code smells (`qlty smells`)
56+
57+Total: **5** (vs previous: ±0)
58+
59+| smell | file | line | detail |
60+|---|---|---|---|
61+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 8): Start |
62+| qlty:function-complexity | internal/bridge/ws.go | 16 | Function with high complexity (count = 22): WebSocketHandler |
63+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 10): play |
64+| qlty:return-statements | ui/src/reducer.ts | 82 | Function with many returns (count = 9): reduceServer |
65+| qlty:return-statements | ui/src/reducer.ts | 119 | Function with many returns (count = 6): reduceUpdate |
66+
67+## Metrics (`qlty metrics`)
68+
69+| metric | total | vs previous |
70+|---|---|---|
71+| funcs | 92 | ±0 |
72+| classes | 27 | ±0 |
73+| fields | 39 | ±0 |
74+| cyclo | 307 | ±0 |
75+| complex | 158 | ±0 |
76+| lcom | 0 | ±0 |
77+| lines | 1989 | ±0 |
78+| loc | 1398 | ±0 |
79+
80+### Most complex files
81+
82+| file | complex | cyclo | loc |
83+|---|---|---|---|
84+| internal/bridge/bridge.go | 26 | 40 | 201 |
85+| internal/mockagent/mockagent.go | 24 | 35 | 178 |
86+| internal/bridge/ws.go | 22 | 14 | 47 |
87+| ui/src/reducer.ts | 18 | 66 | 184 |
88+| internal/agent/agent.go | 15 | 29 | 114 |
89+| internal/agent/client.go | 11 | 28 | 74 |
90+| ui/src/ws.ts | 10 | 13 | 59 |
91+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
92+| ui/src/components/PromptInput.tsx | 7 | 9 | 48 |
93+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 78 |
94+| cmd/ori/main.go | 3 | 7 | 45 |
95+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
96+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
97+| internal/bridge/protocol.go | 1 | 3 | 42 |
98+| ui/embed.go | 1 | 3 | 13 |
99+
100+## Trend
101+
102+| run | timestamp | error | warning | smells | complex | gate |
103+|---|---|---|---|---|---|---|
104+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
105+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
added .quality/report-20260917T191434Z.md +76 -0
new file mode 100644
@@ -0,0 +1,76 @@
1+# Quality report — 2026-09-17T19:14:34Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #3 (previous: 2026-09-17T19:10:18Z)
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+| error | 0 | -13 |
17+| warning | 0 | -4 |
18+| note | 0 | -21 |
19+
20+### Top rules
21+
22+_none_
23+
24+### Most affected files
25+
26+_none_
27+
28+## Code smells (`qlty smells`)
29+
30+Total: **2** (vs previous: -3)
31+
32+| smell | file | line | detail |
33+|---|---|---|---|
34+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 6): Start |
35+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 6): play |
36+
37+## Metrics (`qlty metrics`)
38+
39+| metric | total | vs previous |
40+|---|---|---|
41+| funcs | 106 | +14 |
42+| classes | 27 | ±0 |
43+| fields | 39 | ±0 |
44+| cyclo | 294 | -13 |
45+| complex | 146 | -12 |
46+| lcom | 0 | ±0 |
47+| lines | 2064 | +75 |
48+| loc | 1453 | +55 |
49+
50+### Most complex files
51+
52+| file | complex | cyclo | loc |
53+|---|---|---|---|
54+| internal/bridge/bridge.go | 26 | 40 | 201 |
55+| internal/mockagent/mockagent.go | 20 | 32 | 176 |
56+| ui/src/reducer.ts | 17 | 53 | 187 |
57+| internal/agent/agent.go | 16 | 31 | 121 |
58+| internal/bridge/ws.go | 14 | 15 | 61 |
59+| internal/agent/client.go | 11 | 28 | 74 |
60+| ui/src/ws.ts | 10 | 13 | 59 |
61+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
62+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
63+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
64+| cmd/ori/main.go | 3 | 7 | 45 |
65+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
66+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
67+| internal/bridge/protocol.go | 1 | 3 | 42 |
68+| ui/embed.go | 1 | 3 | 13 |
69+
70+## Trend
71+
72+| run | timestamp | error | warning | smells | complex | gate |
73+|---|---|---|---|---|---|---|
74+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
75+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
76+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
new file mode 100644
@@ -0,0 +1,76 @@
1+# Quality report — 2026-09-17T19:14:34Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #3 (previous: 2026-09-17T19:10:18Z)
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+| error | 0 | -13 |
17+| warning | 0 | -4 |
18+| note | 0 | -21 |
19+
20+### Top rules
21+
22+_none_
23+
24+### Most affected files
25+
26+_none_
27+
28+## Code smells (`qlty smells`)
29+
30+Total: **2** (vs previous: -3)
31+
32+| smell | file | line | detail |
33+|---|---|---|---|
34+| qlty:return-statements | internal/agent/agent.go | 152 | Function with many returns (count = 6): Start |
35+| qlty:return-statements | internal/mockagent/mockagent.go | 106 | Function with many returns (count = 6): play |
36+
37+## Metrics (`qlty metrics`)
38+
39+| metric | total | vs previous |
40+|---|---|---|
41+| funcs | 106 | +14 |
42+| classes | 27 | ±0 |
43+| fields | 39 | ±0 |
44+| cyclo | 294 | -13 |
45+| complex | 146 | -12 |
46+| lcom | 0 | ±0 |
47+| lines | 2064 | +75 |
48+| loc | 1453 | +55 |
49+
50+### Most complex files
51+
52+| file | complex | cyclo | loc |
53+|---|---|---|---|
54+| internal/bridge/bridge.go | 26 | 40 | 201 |
55+| internal/mockagent/mockagent.go | 20 | 32 | 176 |
56+| ui/src/reducer.ts | 17 | 53 | 187 |
57+| internal/agent/agent.go | 16 | 31 | 121 |
58+| internal/bridge/ws.go | 14 | 15 | 61 |
59+| internal/agent/client.go | 11 | 28 | 74 |
60+| ui/src/ws.ts | 10 | 13 | 59 |
61+| internal/httpserver/httpserver.go | 8 | 8 | 33 |
62+| ui/src/components/PromptInput.tsx | 7 | 9 | 56 |
63+| ui/src/components/ToolCallCard.tsx | 6 | 19 | 84 |
64+| cmd/ori/main.go | 3 | 7 | 45 |
65+| ui/src/components/ChatThread.tsx | 2 | 10 | 58 |
66+| cmd/ori-mock-agent/main.go | 1 | 4 | 18 |
67+| internal/bridge/protocol.go | 1 | 3 | 42 |
68+| ui/embed.go | 1 | 3 | 13 |
69+
70+## Trend
71+
72+| run | timestamp | error | warning | smells | complex | gate |
73+|---|---|---|---|---|---|---|
74+| 1 | 2026-09-17T19:09:37Z | 0 | 0 | 5 | 158 | FAIL |
75+| 2 | 2026-09-17T19:10:18Z | 13 | 4 | 5 | 158 | FAIL |
76+| 3 | 2026-09-17T19:14:34Z | 0 | 0 | 2 | 146 | FAIL |
added .quality/report-20260917T191521Z.md +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-09-17T19:15:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-09-17T19:14:34Z)
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: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 111 | +5 |
31+| classes | 27 | ±0 |
32+| fields | 40 | +1 |
33+| cyclo | 294 | ±0 |
34+| complex | 145 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2068 | +4 |
37+| loc | 1451 | -2 |
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 | 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+
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 |
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-09-17T19:15:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-09-17T19:14:34Z)
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: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 111 | +5 |
31+| classes | 27 | ±0 |
32+| fields | 40 | +1 |
33+| cyclo | 294 | ±0 |
34+| complex | 145 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2068 | +4 |
37+| loc | 1451 | -2 |
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 | 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+
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 |
added .quality/report-latest.md +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-09-17T19:15:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-09-17T19:14:34Z)
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: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 111 | +5 |
31+| classes | 27 | ±0 |
32+| fields | 40 | +1 |
33+| cyclo | 294 | ±0 |
34+| complex | 145 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2068 | +4 |
37+| loc | 1451 | -2 |
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 | 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+
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 |
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-09-17T19:15:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4edda86` on `feat/acp-webapp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-09-17T19:14:34Z)
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: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 111 | +5 |
31+| classes | 27 | ±0 |
32+| fields | 40 | +1 |
33+| cyclo | 294 | ±0 |
34+| complex | 145 | -1 |
35+| lcom | 0 | ±0 |
36+| lines | 2068 | +4 |
37+| loc | 1451 | -2 |
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 | 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+
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 |
modified .vscode/extensions.json +11 -11
@@ -1,12 +1,12 @@
11 {
2- "recommendations": [
3- "ms-azuretools.vscode-docker",
4- "pkief.material-icon-theme",
5- "pkief.material-product-icons",
6- "aaron-bond.better-comments",
7- "hediet.vscode-drawio",
8- "anseki.vscode-color",
9- "jeronimoekerdt.color-picker-universal",
10- "wayou.vscode-todo-highlight",
11- ]
12-}
\ No newline at end of file
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "hediet.vscode-drawio",
8+ "anseki.vscode-color",
9+ "jeronimoekerdt.color-picker-universal",
10+ "wayou.vscode-todo-highlight"
11+ ]
12+}
@@ -1,12 +1,12 @@
1 {1 {
2- "recommendations": [
3- "ms-azuretools.vscode-docker",
4- "pkief.material-icon-theme",
5- "pkief.material-product-icons",
6- "aaron-bond.better-comments",
7- "hediet.vscode-drawio",
8- "anseki.vscode-color",
9- "jeronimoekerdt.color-picker-universal",
10- "wayou.vscode-todo-highlight",
11- ]
12-}
\ No newline at end of file\ No newline at end of file
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "hediet.vscode-drawio",
8+ "anseki.vscode-color",
9+ "jeronimoekerdt.color-picker-universal",
10+ "wayou.vscode-todo-highlight"
11+ ]
12+}
modified .vscode/settings.json +44 -46
@@ -1,48 +1,46 @@
11 {
2- "workbench.iconTheme": "material-icon-theme",
3- "workbench.colorTheme": "Light 2026",
4- "editor.fontSize": 14,
5- "terminal.integrated.fontSize": 14,
6- "editor.insertSpaces": true,
7- "editor.tabSize": 4,
8- "editor.detectIndentation": true,
9- "files.autoSave": "afterDelay",
10- "files.autoSaveDelay": 1000,
11- "workbench.tree.indent": 20,
12- "window.zoomLevel": 1.0,
13- "[markdown]": {
14- "editor.unicodeHighlight.ambiguousCharacters": false,
15- "editor.unicodeHighlight.invisibleCharacters": false,
16- "diffEditor.ignoreTrimWhitespace": false,
17- "editor.fontWeight": "normal",
18- "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
19- "editor.fontSize": 14,
20- "editor.wordWrap": "on",
21- "editor.quickSuggestions": {
22- "comments": "off",
23- "strings": "off",
24- "other": "off"
25- }
26- },
27- "markdown.preview.fontSize": 14,
28- // "workbench.editorAssociations": {
29- // "*.md": "vscode.markdown.preview.editor"
30- // },
31- "[dockerfile]": {
32- "editor.fontSize": 14,
33- },
34- "[dockercompose]": {
35- "editor.fontSize": 14,
36- },
37- "[json]": {
38- "editor.fontSize": 14,
39- },
40- "[yaml]": {
41- "editor.fontSize": 14,
42-
43- },
44- "[go]": {
45- "editor.fontSize": 14,
46- },
47-
2+ "workbench.iconTheme": "material-icon-theme",
3+ "workbench.colorTheme": "Light 2026",
4+ "editor.fontSize": 14,
5+ "terminal.integrated.fontSize": 14,
6+ "editor.insertSpaces": true,
7+ "editor.tabSize": 4,
8+ "editor.detectIndentation": true,
9+ "files.autoSave": "afterDelay",
10+ "files.autoSaveDelay": 1000,
11+ "workbench.tree.indent": 20,
12+ "window.zoomLevel": 1.0,
13+ "[markdown]": {
14+ "editor.unicodeHighlight.ambiguousCharacters": false,
15+ "editor.unicodeHighlight.invisibleCharacters": false,
16+ "diffEditor.ignoreTrimWhitespace": false,
17+ "editor.fontWeight": "normal",
18+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
19+ "editor.fontSize": 14,
20+ "editor.wordWrap": "on",
21+ "editor.quickSuggestions": {
22+ "comments": "off",
23+ "strings": "off",
24+ "other": "off"
25+ }
26+ },
27+ "markdown.preview.fontSize": 14,
28+ // "workbench.editorAssociations": {
29+ // "*.md": "vscode.markdown.preview.editor"
30+ // },
31+ "[dockerfile]": {
32+ "editor.fontSize": 14
33+ },
34+ "[dockercompose]": {
35+ "editor.fontSize": 14
36+ },
37+ "[json]": {
38+ "editor.fontSize": 14
39+ },
40+ "[yaml]": {
41+ "editor.fontSize": 14
42+ },
43+ "[go]": {
44+ "editor.fontSize": 14
45+ }
4846 }
@@ -1,48 +1,46 @@
1 {1 {
2- "workbench.iconTheme": "material-icon-theme",2+ "workbench.iconTheme": "material-icon-theme",
3- "workbench.colorTheme": "Light 2026",3+ "workbench.colorTheme": "Light 2026",
4- "editor.fontSize": 14,4+ "editor.fontSize": 14,
5- "terminal.integrated.fontSize": 14,5+ "terminal.integrated.fontSize": 14,
6- "editor.insertSpaces": true,6+ "editor.insertSpaces": true,
7- "editor.tabSize": 4,7+ "editor.tabSize": 4,
8- "editor.detectIndentation": true,8+ "editor.detectIndentation": true,
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": 1.0,
13- "[markdown]": {13+ "[markdown]": {
14- "editor.unicodeHighlight.ambiguousCharacters": false,14+ "editor.unicodeHighlight.ambiguousCharacters": false,
15- "editor.unicodeHighlight.invisibleCharacters": false,15+ "editor.unicodeHighlight.invisibleCharacters": false,
16- "diffEditor.ignoreTrimWhitespace": false,16+ "diffEditor.ignoreTrimWhitespace": false,
17- "editor.fontWeight": "normal",17+ "editor.fontWeight": "normal",
18- "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",18+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
19- "editor.fontSize": 14,19+ "editor.fontSize": 14,
20- "editor.wordWrap": "on",20+ "editor.wordWrap": "on",
21- "editor.quickSuggestions": {21+ "editor.quickSuggestions": {
22- "comments": "off",22+ "comments": "off",
23- "strings": "off",23+ "strings": "off",
24- "other": "off"24+ "other": "off"
25- }25+ }
26- },26+ },
27- "markdown.preview.fontSize": 14,27+ "markdown.preview.fontSize": 14,
28- // "workbench.editorAssociations": {28+ // "workbench.editorAssociations": {
29- // "*.md": "vscode.markdown.preview.editor"29+ // "*.md": "vscode.markdown.preview.editor"
30- // },30+ // },
31- "[dockerfile]": {31+ "[dockerfile]": {
32- "editor.fontSize": 14,32+ "editor.fontSize": 14
33- },33+ },
34- "[dockercompose]": {34+ "[dockercompose]": {
35- "editor.fontSize": 14,35+ "editor.fontSize": 14
36- },36+ },
37- "[json]": {37+ "[json]": {
38- "editor.fontSize": 14,38+ "editor.fontSize": 14
39- },39+ },
40- "[yaml]": {40+ "[yaml]": {
41- "editor.fontSize": 14,41+ "editor.fontSize": 14
42-42+ },
43- },43+ "[go]": {
44- "[go]": {44+ "editor.fontSize": 14
45- "editor.fontSize": 14,45+ }
46- },
47-
48 }46 }
added Makefile +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+# Ori — ACP web client. `make help` lists the targets.
2+
3+.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev clean
4+
5+help: ## Show this help
6+ @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-12s %s\n", $$1, $$2}'
7+
8+deps: ## Install frontend dependencies
9+ cd ui && npm install
10+
11+build-ui: ## Build the SPA into ui/dist
12+ cd ui && npm run build
13+
14+build-go: ## Build the server and mock-agent binaries into bin/
15+ go build -o bin/ori ./cmd/ori
16+ go build -o bin/ori-mock-agent ./cmd/ori-mock-agent
17+
18+build: build-ui build-go ## Build everything (SPA first, then the Go binary that embeds it)
19+
20+test-go: ## Run the Go test suite
21+ go test ./...
22+
23+test-ui: ## Run the frontend test suite
24+ cd ui && npm run test
25+
26+test: test-go test-ui ## Run all tests
27+
28+run: build ## Build then start the server on :8888 (Claude Code via ACP adapter)
29+ ./bin/ori
30+
31+run-mock: build ## Build then start the server with the demo mock agent (no Claude needed)
32+ ./bin/ori --agent-cmd "bin/ori-mock-agent"
33+
34+dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal)
35+ cd ui && npm run dev
36+
37+clean: ## Remove build artifacts
38+ rm -rf bin ui/dist/*
39+ @touch ui/dist/.gitkeep
new file mode 100644
@@ -0,0 +1,39 @@
1+# Ori — ACP web client. `make help` lists the targets.
2+
3+.PHONY: help deps build build-ui build-go test test-go test-ui run run-mock dev clean
4+
5+help: ## Show this help
6+ @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-12s %s\n", $$1, $$2}'
7+
8+deps: ## Install frontend dependencies
9+ cd ui && npm install
10+
11+build-ui: ## Build the SPA into ui/dist
12+ cd ui && npm run build
13+
14+build-go: ## Build the server and mock-agent binaries into bin/
15+ go build -o bin/ori ./cmd/ori
16+ go build -o bin/ori-mock-agent ./cmd/ori-mock-agent
17+
18+build: build-ui build-go ## Build everything (SPA first, then the Go binary that embeds it)
19+
20+test-go: ## Run the Go test suite
21+ go test ./...
22+
23+test-ui: ## Run the frontend test suite
24+ cd ui && npm run test
25+
26+test: test-go test-ui ## Run all tests
27+
28+run: build ## Build then start the server on :8888 (Claude Code via ACP adapter)
29+ ./bin/ori
30+
31+run-mock: build ## Build then start the server with the demo mock agent (no Claude needed)
32+ ./bin/ori --agent-cmd "bin/ori-mock-agent"
33+
34+dev: ## Start the Vite dev server (expects `go run ./cmd/ori` in another terminal)
35+ cd ui && npm run dev
36+
37+clean: ## Remove build artifacts
38+ rm -rf bin ui/dist/*
39+ @touch ui/dist/.gitkeep
added biome.json +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+{
2+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+ "overrides": [
4+ {
5+ "include": [".vscode/**"],
6+ "json": {
7+ "parser": {
8+ "allowComments": true,
9+ "allowTrailingCommas": true
10+ }
11+ }
12+ }
13+ ]
14+}
new file mode 100644
@@ -0,0 +1,14 @@
1+{
2+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+ "overrides": [
4+ {
5+ "include": [".vscode/**"],
6+ "json": {
7+ "parser": {
8+ "allowComments": true,
9+ "allowTrailingCommas": true
10+ }
11+ }
12+ }
13+ ]
14+}
added cmd/ori-mock-agent/main.go +27 -0
new file mode 100644
@@ -0,0 +1,27 @@
1+// Command ori-mock-agent is a deterministic ACP agent speaking on stdio.
2+// It lets you try the full ori experience — streaming, thoughts, plan, tool
3+// calls, permissions — without Claude Code or any network access:
4+//
5+// ori --agent-cmd "bin/ori-mock-agent"
6+package main
7+
8+import (
9+ "flag"
10+ "fmt"
11+ "os"
12+ "time"
13+
14+ "github.com/bots-garden/ori/internal/mockagent"
15+)
16+
17+func main() {
18+ delay := flag.Duration("delay", 300*time.Millisecond, "pause between streamed updates")
19+ flag.Parse()
20+
21+ agent := mockagent.New()
22+ agent.Delay = *delay
23+ if err := agent.Run(os.Stdin, os.Stdout); err != nil {
24+ fmt.Fprintln(os.Stderr, "ori-mock-agent:", err)
25+ os.Exit(1)
26+ }
27+}
new file mode 100644
@@ -0,0 +1,27 @@
1+// Command ori-mock-agent is a deterministic ACP agent speaking on stdio.
2+// It lets you try the full ori experience — streaming, thoughts, plan, tool
3+// calls, permissions — without Claude Code or any network access:
4+//
5+// ori --agent-cmd "bin/ori-mock-agent"
6+package main
7+
8+import (
9+ "flag"
10+ "fmt"
11+ "os"
12+ "time"
13+
14+ "github.com/bots-garden/ori/internal/mockagent"
15+)
16+
17+func main() {
18+ delay := flag.Duration("delay", 300*time.Millisecond, "pause between streamed updates")
19+ flag.Parse()
20+
21+ agent := mockagent.New()
22+ agent.Delay = *delay
23+ if err := agent.Run(os.Stdin, os.Stdout); err != nil {
24+ fmt.Fprintln(os.Stderr, "ori-mock-agent:", err)
25+ os.Exit(1)
26+ }
27+}
added cmd/ori/e2e_test.go +140 -0
new file mode 100644
@@ -0,0 +1,140 @@
1+package main_test
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "os/exec"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+ "testing/fstest"
12+ "time"
13+
14+ "github.com/coder/websocket"
15+ "github.com/coder/websocket/wsjson"
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"
20+)
21+
22+// TestEndToEnd exercises the full backend chain exactly as production wires
23+// it: a compiled mock ACP agent as a real subprocess, the agent client, the
24+// bridge, the HTTP server, and a real WebSocket client playing the browser.
25+func TestEndToEnd(t *testing.T) {
26+ if testing.Short() {
27+ t.Skip("skipping the end-to-end test in -short mode")
28+ }
29+
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")
32+ if output, err := build.CombinedOutput(); err != nil {
33+ t.Fatalf("building the mock agent failed: %v\n%s", err, output)
34+ }
35+
36+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
37+ defer cancel()
38+
39+ // Production wiring: agent subprocess → bridge → HTTP server.
40+ relay := bridge.New(nil)
41+ session, err := agent.Start(ctx, agent.Options{
42+ Command: []string{mockBinary, "--delay", "0s"},
43+ Cwd: t.TempDir(),
44+ Handler: relay,
45+ })
46+ if err != nil {
47+ t.Fatalf("starting the mock agent failed: %v", err)
48+ }
49+ defer func() { _ = session.Close() }()
50+ relay.SetSession(session)
51+
52+ ui := fstest.MapFS{"index.html": {Data: []byte("<html>ori</html>")}}
53+ server := httptest.NewServer(httpserver.Handler(ui, map[string]http.Handler{
54+ "GET /ws": relay.WebSocketHandler(),
55+ }))
56+ defer server.Close()
57+
58+ // The SPA is served.
59+ page, err := http.Get(server.URL + "/")
60+ if err != nil || page.StatusCode != http.StatusOK {
61+ t.Fatalf("GET / = %v status %v, want 200", err, page.StatusCode)
62+ }
63+ _ = page.Body.Close()
64+
65+ // The browser side: a real WebSocket connection.
66+ conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws", nil)
67+ if err != nil {
68+ t.Fatalf("websocket dial failed: %v", err)
69+ }
70+ defer func() { _ = conn.CloseNow() }()
71+
72+ read := func(wantType string) bridge.Outgoing {
73+ t.Helper()
74+ for {
75+ var msg bridge.Outgoing
76+ if err := wsjson.Read(ctx, conn, &msg); err != nil {
77+ t.Fatalf("websocket read failed while waiting for %q: %v", wantType, err)
78+ }
79+ if msg.Type == wantType {
80+ return msg
81+ }
82+ }
83+ }
84+
85+ hello := read(bridge.OutgoingHello)
86+ if !strings.HasPrefix(hello.SessionId, "mock-") {
87+ t.Errorf("hello sessionId = %q, want a mock-* session", hello.SessionId)
88+ }
89+
90+ if err := wsjson.Write(ctx, conn, bridge.Incoming{Type: bridge.IncomingPrompt, Text: "full tour"}); err != nil {
91+ t.Fatalf("sending the prompt failed: %v", err)
92+ }
93+
94+ if echo := read(bridge.OutgoingUserMessage); echo.Text != "full tour" {
95+ t.Errorf("user_message echo = %q, want \"full tour\"", echo.Text)
96+ }
97+ read(bridge.OutgoingTurnStarted)
98+
99+ // The scenario streams updates, then asks for permission.
100+ permission := read(bridge.OutgoingPermissionRequest)
101+ if permission.RequestId == "" || !strings.Contains(string(permission.Request), "allow-once") {
102+ t.Fatalf("permission_request incomplete: %+v", permission)
103+ }
104+ if err := wsjson.Write(ctx, conn, bridge.Incoming{
105+ Type: bridge.IncomingPermissionResponse,
106+ RequestId: permission.RequestId,
107+ OptionId: "allow-once",
108+ }); err != nil {
109+ t.Fatalf("answering the permission failed: %v", err)
110+ }
111+
112+ ended := read(bridge.OutgoingTurnEnded)
113+ if ended.StopReason != "end_turn" {
114+ t.Errorf("turn_ended stopReason = %q, want end_turn", ended.StopReason)
115+ }
116+
117+ // A second browser connecting now must receive the whole story replayed:
118+ // the user message, tool activity, and the final agent answer.
119+ late, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws", nil)
120+ if err != nil {
121+ t.Fatalf("late websocket dial failed: %v", err)
122+ }
123+ defer func() { _ = late.CloseNow() }()
124+
125+ sawUser, sawTool, sawAnswer := false, false, false
126+ for !sawUser || !sawTool || !sawAnswer {
127+ var msg bridge.Outgoing
128+ if err := wsjson.Read(ctx, late, &msg); err != nil {
129+ t.Fatalf("late client read failed (saw user=%v tool=%v answer=%v): %v", sawUser, sawTool, sawAnswer, err)
130+ }
131+ switch {
132+ case msg.Type == bridge.OutgoingUserMessage && msg.Text == "full tour":
133+ sawUser = true
134+ case msg.Type == bridge.OutgoingSessionUpdate && strings.Contains(string(msg.Update), "tool_call"):
135+ sawTool = true
136+ case msg.Type == bridge.OutgoingSessionUpdate && strings.Contains(string(msg.Update), "Done!"):
137+ sawAnswer = true
138+ }
139+ }
140+}
new file mode 100644
@@ -0,0 +1,140 @@
1+package main_test
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "os/exec"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+ "testing/fstest"
12+ "time"
13+
14+ "github.com/coder/websocket"
15+ "github.com/coder/websocket/wsjson"
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"
20+)
21+
22+// TestEndToEnd exercises the full backend chain exactly as production wires
23+// it: a compiled mock ACP agent as a real subprocess, the agent client, the
24+// bridge, the HTTP server, and a real WebSocket client playing the browser.
25+func TestEndToEnd(t *testing.T) {
26+ if testing.Short() {
27+ t.Skip("skipping the end-to-end test in -short mode")
28+ }
29+
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")
32+ if output, err := build.CombinedOutput(); err != nil {
33+ t.Fatalf("building the mock agent failed: %v\n%s", err, output)
34+ }
35+
36+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
37+ defer cancel()
38+
39+ // Production wiring: agent subprocess → bridge → HTTP server.
40+ relay := bridge.New(nil)
41+ session, err := agent.Start(ctx, agent.Options{
42+ Command: []string{mockBinary, "--delay", "0s"},
43+ Cwd: t.TempDir(),
44+ Handler: relay,
45+ })
46+ if err != nil {
47+ t.Fatalf("starting the mock agent failed: %v", err)
48+ }
49+ defer func() { _ = session.Close() }()
50+ relay.SetSession(session)
51+
52+ ui := fstest.MapFS{"index.html": {Data: []byte("<html>ori</html>")}}
53+ server := httptest.NewServer(httpserver.Handler(ui, map[string]http.Handler{
54+ "GET /ws": relay.WebSocketHandler(),
55+ }))
56+ defer server.Close()
57+
58+ // The SPA is served.
59+ page, err := http.Get(server.URL + "/")
60+ if err != nil || page.StatusCode != http.StatusOK {
61+ t.Fatalf("GET / = %v status %v, want 200", err, page.StatusCode)
62+ }
63+ _ = page.Body.Close()
64+
65+ // The browser side: a real WebSocket connection.
66+ conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws", nil)
67+ if err != nil {
68+ t.Fatalf("websocket dial failed: %v", err)
69+ }
70+ defer func() { _ = conn.CloseNow() }()
71+
72+ read := func(wantType string) bridge.Outgoing {
73+ t.Helper()
74+ for {
75+ var msg bridge.Outgoing
76+ if err := wsjson.Read(ctx, conn, &msg); err != nil {
77+ t.Fatalf("websocket read failed while waiting for %q: %v", wantType, err)
78+ }
79+ if msg.Type == wantType {
80+ return msg
81+ }
82+ }
83+ }
84+
85+ hello := read(bridge.OutgoingHello)
86+ if !strings.HasPrefix(hello.SessionId, "mock-") {
87+ t.Errorf("hello sessionId = %q, want a mock-* session", hello.SessionId)
88+ }
89+
90+ if err := wsjson.Write(ctx, conn, bridge.Incoming{Type: bridge.IncomingPrompt, Text: "full tour"}); err != nil {
91+ t.Fatalf("sending the prompt failed: %v", err)
92+ }
93+
94+ if echo := read(bridge.OutgoingUserMessage); echo.Text != "full tour" {
95+ t.Errorf("user_message echo = %q, want \"full tour\"", echo.Text)
96+ }
97+ read(bridge.OutgoingTurnStarted)
98+
99+ // The scenario streams updates, then asks for permission.
100+ permission := read(bridge.OutgoingPermissionRequest)
101+ if permission.RequestId == "" || !strings.Contains(string(permission.Request), "allow-once") {
102+ t.Fatalf("permission_request incomplete: %+v", permission)
103+ }
104+ if err := wsjson.Write(ctx, conn, bridge.Incoming{
105+ Type: bridge.IncomingPermissionResponse,
106+ RequestId: permission.RequestId,
107+ OptionId: "allow-once",
108+ }); err != nil {
109+ t.Fatalf("answering the permission failed: %v", err)
110+ }
111+
112+ ended := read(bridge.OutgoingTurnEnded)
113+ if ended.StopReason != "end_turn" {
114+ t.Errorf("turn_ended stopReason = %q, want end_turn", ended.StopReason)
115+ }
116+
117+ // A second browser connecting now must receive the whole story replayed:
118+ // the user message, tool activity, and the final agent answer.
119+ late, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws", nil)
120+ if err != nil {
121+ t.Fatalf("late websocket dial failed: %v", err)
122+ }
123+ defer func() { _ = late.CloseNow() }()
124+
125+ sawUser, sawTool, sawAnswer := false, false, false
126+ for !sawUser || !sawTool || !sawAnswer {
127+ var msg bridge.Outgoing
128+ if err := wsjson.Read(ctx, late, &msg); err != nil {
129+ t.Fatalf("late client read failed (saw user=%v tool=%v answer=%v): %v", sawUser, sawTool, sawAnswer, err)
130+ }
131+ switch {
132+ case msg.Type == bridge.OutgoingUserMessage && msg.Text == "full tour":
133+ sawUser = true
134+ case msg.Type == bridge.OutgoingSessionUpdate && strings.Contains(string(msg.Update), "tool_call"):
135+ sawTool = true
136+ case msg.Type == bridge.OutgoingSessionUpdate && strings.Contains(string(msg.Update), "Done!"):
137+ sawAnswer = true
138+ }
139+ }
140+}
added cmd/ori/main.go +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+// Command ori serves the ori web application: a browser client for ACP
2+// (Agent Client Protocol) code agents such as Claude Code.
3+//
4+// Usage:
5+//
6+// ori [--addr :8888] [--cwd DIR] [--agent-cmd "npx -y @zed-industries/claude-code-acp"]
7+package main
8+
9+import (
10+ "context"
11+ "fmt"
12+ "log/slog"
13+ "net/http"
14+ "os"
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"
21+)
22+
23+func main() {
24+ if err := run(os.Args[1:]); err != nil {
25+ fmt.Fprintln(os.Stderr, "ori:", err)
26+ os.Exit(1)
27+ }
28+}
29+
30+func run(args []string) error {
31+ cfg, err := config.FromArgs(args)
32+ if err != nil {
33+ return err
34+ }
35+
36+ logger := slog.Default()
37+ relay := bridge.New(logger)
38+
39+ logger.Info("starting agent", "command", cfg.AgentCommand, "cwd", cfg.Cwd)
40+ session, err := agent.Start(context.Background(), agent.Options{
41+ Command: cfg.AgentCommand,
42+ Cwd: cfg.Cwd,
43+ Handler: relay,
44+ Logger: logger,
45+ })
46+ if err != nil {
47+ return fmt.Errorf("start agent: %w", err)
48+ }
49+ defer func() { _ = session.Close() }()
50+ relay.SetSession(session)
51+ logger.Info("agent session ready", "sessionId", session.ID())
52+
53+ handler := httpserver.Handler(ui.Dist(), map[string]http.Handler{
54+ "GET /ws": relay.WebSocketHandler(),
55+ })
56+
57+ logger.Info("ori listening", "addr", cfg.Addr)
58+ return http.ListenAndServe(cfg.Addr, handler)
59+}
new file mode 100644
@@ -0,0 +1,59 @@
1+// Command ori serves the ori web application: a browser client for ACP
2+// (Agent Client Protocol) code agents such as Claude Code.
3+//
4+// Usage:
5+//
6+// ori [--addr :8888] [--cwd DIR] [--agent-cmd "npx -y @zed-industries/claude-code-acp"]
7+package main
8+
9+import (
10+ "context"
11+ "fmt"
12+ "log/slog"
13+ "net/http"
14+ "os"
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"
21+)
22+
23+func main() {
24+ if err := run(os.Args[1:]); err != nil {
25+ fmt.Fprintln(os.Stderr, "ori:", err)
26+ os.Exit(1)
27+ }
28+}
29+
30+func run(args []string) error {
31+ cfg, err := config.FromArgs(args)
32+ if err != nil {
33+ return err
34+ }
35+
36+ logger := slog.Default()
37+ relay := bridge.New(logger)
38+
39+ logger.Info("starting agent", "command", cfg.AgentCommand, "cwd", cfg.Cwd)
40+ session, err := agent.Start(context.Background(), agent.Options{
41+ Command: cfg.AgentCommand,
42+ Cwd: cfg.Cwd,
43+ Handler: relay,
44+ Logger: logger,
45+ })
46+ if err != nil {
47+ return fmt.Errorf("start agent: %w", err)
48+ }
49+ defer func() { _ = session.Close() }()
50+ relay.SetSession(session)
51+ logger.Info("agent session ready", "sessionId", session.ID())
52+
53+ handler := httpserver.Handler(ui.Dist(), map[string]http.Handler{
54+ "GET /ws": relay.WebSocketHandler(),
55+ })
56+
57+ logger.Info("ori listening", "addr", cfg.Addr)
58+ return http.ListenAndServe(cfg.Addr, handler)
59+}
added go.mod +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+module github.com/bots-garden/ori
2+
3+go 1.26.5
4+
5+require github.com/coder/acp-go-sdk v0.13.5
6+
7+require github.com/coder/websocket v1.8.15
new file mode 100644
@@ -0,0 +1,7 @@
1+module github.com/bots-garden/ori
2+
3+go 1.26.5
4+
5+require github.com/coder/acp-go-sdk v0.13.5
6+
7+require github.com/coder/websocket v1.8.15
added go.sum +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
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=
4+github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
new file mode 100644
@@ -0,0 +1,4 @@
1+github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
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=
4+github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
added internal/agent/agent.go +201 -0
new file mode 100644
@@ -0,0 +1,201 @@
1+// Package agent runs an ACP (Agent Client Protocol) code agent as a
2+// subprocess and exposes its session lifecycle to the rest of ori.
3+//
4+// It is agent-agnostic: any executable speaking ACP on stdio works (the
5+// Claude Code adapter, Gemini CLI, a test mock, ...). Events flowing from the
6+// agent (streamed updates, permission requests) are delivered to a Handler
7+// supplied by the caller.
8+package agent
9+
10+import (
11+ "context"
12+ "errors"
13+ "fmt"
14+ "io"
15+ "log/slog"
16+ "os"
17+ "os/exec"
18+
19+ acp "github.com/coder/acp-go-sdk"
20+)
21+
22+// Handler receives the events an ACP agent sends during a session.
23+//
24+// HandleSessionUpdate is called for every streamed update (message chunks,
25+// tool calls, plan changes, ...). HandlePermissionRequest must block until a
26+// decision is available and return it; returning an error rejects the
27+// request.
28+type Handler interface {
29+ HandleSessionUpdate(ctx context.Context, notification acp.SessionNotification)
30+ HandlePermissionRequest(ctx context.Context, request acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error)
31+}
32+
33+// Options configure Start and Connect.
34+type Options struct {
35+ // Command is the agent command line: executable first, then arguments.
36+ // Only used by Start.
37+ Command []string
38+ // Cwd is the absolute working directory given to the ACP session.
39+ Cwd string
40+ // Handler receives session updates and permission requests. Required.
41+ Handler Handler
42+ // Logger receives connection diagnostics. Optional.
43+ Logger *slog.Logger
44+ // Stderr receives the agent process's stderr. Defaults to os.Stderr.
45+ // Only used by Start.
46+ Stderr io.Writer
47+}
48+
49+// Session is a live conversation with a running ACP agent.
50+type Session struct {
51+ id acp.SessionId
52+ conn *acp.ClientSideConnection
53+ initResp acp.InitializeResponse
54+ shutdown func() error
55+}
56+
57+// ID returns the ACP session identifier.
58+func (s *Session) ID() string { return string(s.id) }
59+
60+// InitializeResult returns the capabilities the agent advertised during the
61+// ACP initialize handshake.
62+func (s *Session) InitializeResult() acp.InitializeResponse { return s.initResp }
63+
64+// PromptText sends a plain-text user prompt and blocks until the turn ends,
65+// while updates stream to the Handler. It returns the reason the turn
66+// stopped.
67+//
68+// Example:
69+//
70+// stop, err := session.PromptText(ctx, "Explain this repository")
71+// if err == nil && stop == acp.StopReasonEndTurn {
72+// fmt.Println("turn finished")
73+// }
74+func (s *Session) PromptText(ctx context.Context, text string) (acp.StopReason, error) {
75+ return s.Prompt(ctx, []acp.ContentBlock{acp.TextBlock(text)})
76+}
77+
78+// Prompt sends a user prompt made of arbitrary content blocks and blocks
79+// until the turn ends. Updates stream to the Handler while it runs.
80+func (s *Session) Prompt(ctx context.Context, blocks []acp.ContentBlock) (acp.StopReason, error) {
81+ resp, err := s.conn.Prompt(ctx, acp.PromptRequest{SessionId: s.id, Prompt: blocks})
82+ if err != nil {
83+ return "", fmt.Errorf("prompt: %w", err)
84+ }
85+ return resp.StopReason, nil
86+}
87+
88+// Cancel asks the agent to stop the current turn. The pending Prompt call
89+// then returns with acp.StopReasonCancelled.
90+func (s *Session) Cancel(ctx context.Context) error {
91+ if err := s.conn.Cancel(ctx, acp.CancelNotification{SessionId: s.id}); err != nil {
92+ return fmt.Errorf("cancel: %w", err)
93+ }
94+ return nil
95+}
96+
97+// Done closes when the agent disconnects, whatever the cause.
98+func (s *Session) Done() <-chan struct{} { return s.conn.Done() }
99+
100+// Close terminates the agent. It is safe to call more than once.
101+func (s *Session) Close() error {
102+ if s.shutdown == nil {
103+ return nil
104+ }
105+ shutdown := s.shutdown
106+ s.shutdown = nil
107+ return shutdown()
108+}
109+
110+// Connect performs the ACP handshake (initialize + session/new) over an
111+// existing transport: in writes to the agent, out reads from it. It is the
112+// transport-independent core of Start and what tests use with in-memory
113+// pipes.
114+func Connect(ctx context.Context, in io.Writer, out io.Reader, opts Options) (*Session, error) {
115+ if opts.Handler == nil {
116+ return nil, errors.New("agent: Options.Handler is required")
117+ }
118+
119+ conn := acp.NewClientSideConnection(&client{handler: opts.Handler}, in, out)
120+ if opts.Logger != nil {
121+ conn.SetLogger(opts.Logger)
122+ }
123+
124+ initResp, err := conn.Initialize(ctx, acp.InitializeRequest{
125+ ProtocolVersion: acp.ProtocolVersionNumber,
126+ ClientCapabilities: acp.ClientCapabilities{
127+ Fs: acp.FileSystemCapabilities{ReadTextFile: true, WriteTextFile: true},
128+ },
129+ })
130+ if err != nil {
131+ return nil, fmt.Errorf("initialize: %w", err)
132+ }
133+
134+ newResp, err := conn.NewSession(ctx, acp.NewSessionRequest{Cwd: opts.Cwd, McpServers: []acp.McpServer{}})
135+ if err != nil {
136+ return nil, fmt.Errorf("new session: %w", err)
137+ }
138+
139+ return &Session{id: newResp.SessionId, conn: conn, initResp: initResp}, nil
140+}
141+
142+// Start spawns opts.Command as a subprocess speaking ACP on its stdio, then
143+// performs the handshake. Closing the returned Session kills the process.
144+//
145+// Example:
146+//
147+// session, err := agent.Start(ctx, agent.Options{
148+// Command: []string{"npx", "-y", "@zed-industries/claude-code-acp"},
149+// Cwd: "/work/my-project",
150+// Handler: myHandler,
151+// })
152+func Start(ctx context.Context, opts Options) (*Session, error) {
153+ if len(opts.Command) == 0 {
154+ return nil, errors.New("agent: Options.Command is required")
155+ }
156+
157+ cmd, stdin, stdout, err := spawnProcess(opts)
158+ if err != nil {
159+ return nil, err
160+ }
161+
162+ session, err := Connect(ctx, stdin, stdout, opts)
163+ if err != nil {
164+ _ = killProcess(cmd)
165+ return nil, err
166+ }
167+ session.shutdown = func() error { return killProcess(cmd) }
168+ return session, nil
169+}
170+
171+// killProcess terminates the agent process and reaps it; the "killed" error
172+// from Wait is the expected outcome, not a failure.
173+func killProcess(cmd *exec.Cmd) error {
174+ if err := cmd.Process.Kill(); err != nil {
175+ return err
176+ }
177+ _ = cmd.Wait()
178+ return nil
179+}
180+
181+// spawnProcess launches the agent command with its stdio wired for ACP.
182+func spawnProcess(opts Options) (*exec.Cmd, io.WriteCloser, io.ReadCloser, error) {
183+ cmd := exec.Command(opts.Command[0], opts.Command[1:]...)
184+ cmd.Dir = opts.Cwd
185+ cmd.Stderr = opts.Stderr
186+ if cmd.Stderr == nil {
187+ cmd.Stderr = os.Stderr
188+ }
189+ stdin, err := cmd.StdinPipe()
190+ if err != nil {
191+ return nil, nil, nil, fmt.Errorf("agent stdin: %w", err)
192+ }
193+ stdout, err := cmd.StdoutPipe()
194+ if err != nil {
195+ return nil, nil, nil, fmt.Errorf("agent stdout: %w", err)
196+ }
197+ if err := cmd.Start(); err != nil {
198+ return nil, nil, nil, fmt.Errorf("start agent %q: %w", opts.Command[0], err)
199+ }
200+ return cmd, stdin, stdout, nil
201+}
new file mode 100644
@@ -0,0 +1,201 @@
1+// Package agent runs an ACP (Agent Client Protocol) code agent as a
2+// subprocess and exposes its session lifecycle to the rest of ori.
3+//
4+// It is agent-agnostic: any executable speaking ACP on stdio works (the
5+// Claude Code adapter, Gemini CLI, a test mock, ...). Events flowing from the
6+// agent (streamed updates, permission requests) are delivered to a Handler
7+// supplied by the caller.
8+package agent
9+
10+import (
11+ "context"
12+ "errors"
13+ "fmt"
14+ "io"
15+ "log/slog"
16+ "os"
17+ "os/exec"
18+
19+ acp "github.com/coder/acp-go-sdk"
20+)
21+
22+// Handler receives the events an ACP agent sends during a session.
23+//
24+// HandleSessionUpdate is called for every streamed update (message chunks,
25+// tool calls, plan changes, ...). HandlePermissionRequest must block until a
26+// decision is available and return it; returning an error rejects the
27+// request.
28+type Handler interface {
29+ HandleSessionUpdate(ctx context.Context, notification acp.SessionNotification)
30+ HandlePermissionRequest(ctx context.Context, request acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error)
31+}
32+
33+// Options configure Start and Connect.
34+type Options struct {
35+ // Command is the agent command line: executable first, then arguments.
36+ // Only used by Start.
37+ Command []string
38+ // Cwd is the absolute working directory given to the ACP session.
39+ Cwd string
40+ // Handler receives session updates and permission requests. Required.
41+ Handler Handler
42+ // Logger receives connection diagnostics. Optional.
43+ Logger *slog.Logger
44+ // Stderr receives the agent process's stderr. Defaults to os.Stderr.
45+ // Only used by Start.
46+ Stderr io.Writer
47+}
48+
49+// Session is a live conversation with a running ACP agent.
50+type Session struct {
51+ id acp.SessionId
52+ conn *acp.ClientSideConnection
53+ initResp acp.InitializeResponse
54+ shutdown func() error
55+}
56+
57+// ID returns the ACP session identifier.
58+func (s *Session) ID() string { return string(s.id) }
59+
60+// InitializeResult returns the capabilities the agent advertised during the
61+// ACP initialize handshake.
62+func (s *Session) InitializeResult() acp.InitializeResponse { return s.initResp }
63+
64+// PromptText sends a plain-text user prompt and blocks until the turn ends,
65+// while updates stream to the Handler. It returns the reason the turn
66+// stopped.
67+//
68+// Example:
69+//
70+// stop, err := session.PromptText(ctx, "Explain this repository")
71+// if err == nil && stop == acp.StopReasonEndTurn {
72+// fmt.Println("turn finished")
73+// }
74+func (s *Session) PromptText(ctx context.Context, text string) (acp.StopReason, error) {
75+ return s.Prompt(ctx, []acp.ContentBlock{acp.TextBlock(text)})
76+}
77+
78+// Prompt sends a user prompt made of arbitrary content blocks and blocks
79+// until the turn ends. Updates stream to the Handler while it runs.
80+func (s *Session) Prompt(ctx context.Context, blocks []acp.ContentBlock) (acp.StopReason, error) {
81+ resp, err := s.conn.Prompt(ctx, acp.PromptRequest{SessionId: s.id, Prompt: blocks})
82+ if err != nil {
83+ return "", fmt.Errorf("prompt: %w", err)
84+ }
85+ return resp.StopReason, nil
86+}
87+
88+// Cancel asks the agent to stop the current turn. The pending Prompt call
89+// then returns with acp.StopReasonCancelled.
90+func (s *Session) Cancel(ctx context.Context) error {
91+ if err := s.conn.Cancel(ctx, acp.CancelNotification{SessionId: s.id}); err != nil {
92+ return fmt.Errorf("cancel: %w", err)
93+ }
94+ return nil
95+}
96+
97+// Done closes when the agent disconnects, whatever the cause.
98+func (s *Session) Done() <-chan struct{} { return s.conn.Done() }
99+
100+// Close terminates the agent. It is safe to call more than once.
101+func (s *Session) Close() error {
102+ if s.shutdown == nil {
103+ return nil
104+ }
105+ shutdown := s.shutdown
106+ s.shutdown = nil
107+ return shutdown()
108+}
109+
110+// Connect performs the ACP handshake (initialize + session/new) over an
111+// existing transport: in writes to the agent, out reads from it. It is the
112+// transport-independent core of Start and what tests use with in-memory
113+// pipes.
114+func Connect(ctx context.Context, in io.Writer, out io.Reader, opts Options) (*Session, error) {
115+ if opts.Handler == nil {
116+ return nil, errors.New("agent: Options.Handler is required")
117+ }
118+
119+ conn := acp.NewClientSideConnection(&client{handler: opts.Handler}, in, out)
120+ if opts.Logger != nil {
121+ conn.SetLogger(opts.Logger)
122+ }
123+
124+ initResp, err := conn.Initialize(ctx, acp.InitializeRequest{
125+ ProtocolVersion: acp.ProtocolVersionNumber,
126+ ClientCapabilities: acp.ClientCapabilities{
127+ Fs: acp.FileSystemCapabilities{ReadTextFile: true, WriteTextFile: true},
128+ },
129+ })
130+ if err != nil {
131+ return nil, fmt.Errorf("initialize: %w", err)
132+ }
133+
134+ newResp, err := conn.NewSession(ctx, acp.NewSessionRequest{Cwd: opts.Cwd, McpServers: []acp.McpServer{}})
135+ if err != nil {
136+ return nil, fmt.Errorf("new session: %w", err)
137+ }
138+
139+ return &Session{id: newResp.SessionId, conn: conn, initResp: initResp}, nil
140+}
141+
142+// Start spawns opts.Command as a subprocess speaking ACP on its stdio, then
143+// performs the handshake. Closing the returned Session kills the process.
144+//
145+// Example:
146+//
147+// session, err := agent.Start(ctx, agent.Options{
148+// Command: []string{"npx", "-y", "@zed-industries/claude-code-acp"},
149+// Cwd: "/work/my-project",
150+// Handler: myHandler,
151+// })
152+func Start(ctx context.Context, opts Options) (*Session, error) {
153+ if len(opts.Command) == 0 {
154+ return nil, errors.New("agent: Options.Command is required")
155+ }
156+
157+ cmd, stdin, stdout, err := spawnProcess(opts)
158+ if err != nil {
159+ return nil, err
160+ }
161+
162+ session, err := Connect(ctx, stdin, stdout, opts)
163+ if err != nil {
164+ _ = killProcess(cmd)
165+ return nil, err
166+ }
167+ session.shutdown = func() error { return killProcess(cmd) }
168+ return session, nil
169+}
170+
171+// killProcess terminates the agent process and reaps it; the "killed" error
172+// from Wait is the expected outcome, not a failure.
173+func killProcess(cmd *exec.Cmd) error {
174+ if err := cmd.Process.Kill(); err != nil {
175+ return err
176+ }
177+ _ = cmd.Wait()
178+ return nil
179+}
180+
181+// spawnProcess launches the agent command with its stdio wired for ACP.
182+func spawnProcess(opts Options) (*exec.Cmd, io.WriteCloser, io.ReadCloser, error) {
183+ cmd := exec.Command(opts.Command[0], opts.Command[1:]...)
184+ cmd.Dir = opts.Cwd
185+ cmd.Stderr = opts.Stderr
186+ if cmd.Stderr == nil {
187+ cmd.Stderr = os.Stderr
188+ }
189+ stdin, err := cmd.StdinPipe()
190+ if err != nil {
191+ return nil, nil, nil, fmt.Errorf("agent stdin: %w", err)
192+ }
193+ stdout, err := cmd.StdoutPipe()
194+ if err != nil {
195+ return nil, nil, nil, fmt.Errorf("agent stdout: %w", err)
196+ }
197+ if err := cmd.Start(); err != nil {
198+ return nil, nil, nil, fmt.Errorf("start agent %q: %w", opts.Command[0], err)
199+ }
200+ return cmd, stdin, stdout, nil
201+}
added internal/agent/agent_test.go +278 -0
new file mode 100644
@@ -0,0 +1,278 @@
1+package agent_test
2+
3+import (
4+ "context"
5+ "io"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "sync"
10+ "testing"
11+ "time"
12+
13+ acp "github.com/coder/acp-go-sdk"
14+
15+ "github.com/bots-garden/ori/internal/agent"
16+)
17+
18+// mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent
19+// interface satisfies the methods this test never exercises; calling one of
20+// them would panic, which is exactly what we want a test to do.
21+type mockAgent struct {
22+ acp.Agent
23+ conn *acp.AgentSideConnection
24+
25+ mu sync.Mutex
26+ cancelled bool
27+ // askPermission makes Prompt request a permission before answering.
28+ askPermission bool
29+ // readFile makes Prompt read this file through the client before answering.
30+ readFile string
31+ // writeFile makes Prompt write "written by agent" to this file.
32+ writeFile string
33+}
34+
35+func (a *mockAgent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
36+
37+func (a *mockAgent) Initialize(_ context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
38+ return acp.InitializeResponse{ProtocolVersion: params.ProtocolVersion}, nil
39+}
40+
41+func (a *mockAgent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
42+ return acp.NewSessionResponse{SessionId: "sess-mock"}, nil
43+}
44+
45+func (a *mockAgent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
46+ return acp.AuthenticateResponse{}, nil
47+}
48+
49+func (a *mockAgent) Cancel(context.Context, acp.CancelNotification) error {
50+ a.mu.Lock()
51+ defer a.mu.Unlock()
52+ a.cancelled = true
53+ return nil
54+}
55+
56+func (a *mockAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
57+ send := func(text string) error {
58+ return a.conn.SessionUpdate(ctx, acp.SessionNotification{
59+ SessionId: params.SessionId,
60+ Update: acp.UpdateAgentMessageText(text),
61+ })
62+ }
63+ if err := send("Hello "); err != nil {
64+ return acp.PromptResponse{}, err
65+ }
66+ if a.askPermission {
67+ resp, err := a.conn.RequestPermission(ctx, acp.RequestPermissionRequest{
68+ SessionId: params.SessionId,
69+ Options: []acp.PermissionOption{
70+ {OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce},
71+ {OptionId: "reject", Name: "Reject", Kind: acp.PermissionOptionKindRejectOnce},
72+ },
73+ })
74+ if err != nil {
75+ return acp.PromptResponse{}, err
76+ }
77+ if resp.Outcome.Selected == nil || resp.Outcome.Selected.OptionId != "allow" {
78+ return acp.PromptResponse{StopReason: acp.StopReasonRefusal}, nil
79+ }
80+ }
81+ if a.readFile != "" {
82+ if _, err := a.conn.ReadTextFile(ctx, acp.ReadTextFileRequest{SessionId: params.SessionId, Path: a.readFile}); err != nil {
83+ return acp.PromptResponse{}, err
84+ }
85+ }
86+ if a.writeFile != "" {
87+ if _, err := a.conn.WriteTextFile(ctx, acp.WriteTextFileRequest{SessionId: params.SessionId, Path: a.writeFile, Content: "written by agent"}); err != nil {
88+ return acp.PromptResponse{}, err
89+ }
90+ }
91+ if err := send("world"); err != nil {
92+ return acp.PromptResponse{}, err
93+ }
94+ return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
95+}
96+
97+// recordingHandler collects session updates and answers permission requests.
98+type recordingHandler struct {
99+ mu sync.Mutex
100+ texts []string
101+ granted bool
102+ asked int
103+}
104+
105+func (h *recordingHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotification) {
106+ h.mu.Lock()
107+ defer h.mu.Unlock()
108+ if chunk := n.Update.AgentMessageChunk; chunk != nil && chunk.Content.Text != nil {
109+ h.texts = append(h.texts, chunk.Content.Text.Text)
110+ }
111+}
112+
113+func (h *recordingHandler) HandlePermissionRequest(_ context.Context, req acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
114+ h.mu.Lock()
115+ defer h.mu.Unlock()
116+ h.asked++
117+ choice := acp.PermissionOptionId("reject")
118+ if h.granted {
119+ choice = "allow"
120+ }
121+ _ = req
122+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(choice)}, nil
123+}
124+
125+func (h *recordingHandler) joined() string {
126+ h.mu.Lock()
127+ defer h.mu.Unlock()
128+ return strings.Join(h.texts, "")
129+}
130+
131+// connectMock wires a mock agent and an agent.Session together with in-memory
132+// pipes, no subprocess involved.
133+func connectMock(t *testing.T, mock *mockAgent, handler agent.Handler) *agent.Session {
134+ t.Helper()
135+ clientToAgentR, clientToAgentW := io.Pipe()
136+ agentToClientR, agentToClientW := io.Pipe()
137+
138+ mock.SetAgentConnection(acp.NewAgentSideConnection(mock, agentToClientW, clientToAgentR))
139+
140+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
141+ t.Cleanup(cancel)
142+ session, err := agent.Connect(ctx, clientToAgentW, agentToClientR, agent.Options{
143+ Cwd: t.TempDir(),
144+ Handler: handler,
145+ })
146+ if err != nil {
147+ t.Fatalf("Connect returned an error: %v", err)
148+ }
149+ return session
150+}
151+
152+func TestConnectPerformsHandshake(t *testing.T) {
153+ session := connectMock(t, &mockAgent{}, &recordingHandler{})
154+ if session.ID() != "sess-mock" {
155+ t.Errorf("session ID = %q, want %q", session.ID(), "sess-mock")
156+ }
157+}
158+
159+func TestConnectRequiresHandler(t *testing.T) {
160+ _, err := agent.Connect(context.Background(), io.Discard, strings.NewReader(""), agent.Options{})
161+ if err == nil {
162+ t.Fatal("Connect accepted a nil Handler, want an error")
163+ }
164+}
165+
166+func TestPromptStreamsUpdates(t *testing.T) {
167+ handler := &recordingHandler{}
168+ session := connectMock(t, &mockAgent{}, handler)
169+
170+ stop, err := session.PromptText(context.Background(), "hi")
171+ if err != nil {
172+ t.Fatalf("PromptText returned an error: %v", err)
173+ }
174+ if stop != acp.StopReasonEndTurn {
175+ t.Errorf("stop reason = %q, want %q", stop, acp.StopReasonEndTurn)
176+ }
177+ if got := handler.joined(); got != "Hello world" {
178+ t.Errorf("streamed text = %q, want %q", got, "Hello world")
179+ }
180+}
181+
182+func TestPermissionGrantedFlowsBackToAgent(t *testing.T) {
183+ handler := &recordingHandler{granted: true}
184+ session := connectMock(t, &mockAgent{askPermission: true}, handler)
185+
186+ stop, err := session.PromptText(context.Background(), "do something sensitive")
187+ if err != nil {
188+ t.Fatalf("PromptText returned an error: %v", err)
189+ }
190+ if stop != acp.StopReasonEndTurn {
191+ t.Errorf("stop reason = %q, want %q (permission was granted)", stop, acp.StopReasonEndTurn)
192+ }
193+ if handler.asked != 1 {
194+ t.Errorf("permission requests seen by handler = %d, want 1", handler.asked)
195+ }
196+}
197+
198+func TestPermissionRejectedStopsTheTurn(t *testing.T) {
199+ handler := &recordingHandler{granted: false}
200+ session := connectMock(t, &mockAgent{askPermission: true}, handler)
201+
202+ stop, err := session.PromptText(context.Background(), "do something sensitive")
203+ if err != nil {
204+ t.Fatalf("PromptText returned an error: %v", err)
205+ }
206+ if stop != acp.StopReasonRefusal {
207+ t.Errorf("stop reason = %q, want %q (permission was rejected)", stop, acp.StopReasonRefusal)
208+ }
209+}
210+
211+func TestCancelReachesTheAgent(t *testing.T) {
212+ mock := &mockAgent{}
213+ session := connectMock(t, mock, &recordingHandler{})
214+
215+ if err := session.Cancel(context.Background()); err != nil {
216+ t.Fatalf("Cancel returned an error: %v", err)
217+ }
218+ // Cancel is a notification: poll briefly for its arrival.
219+ deadline := time.Now().Add(2 * time.Second)
220+ for {
221+ mock.mu.Lock()
222+ cancelled := mock.cancelled
223+ mock.mu.Unlock()
224+ if cancelled {
225+ return
226+ }
227+ if time.Now().After(deadline) {
228+ t.Fatal("agent never received the cancel notification")
229+ }
230+ time.Sleep(10 * time.Millisecond)
231+ }
232+}
233+
234+func TestAgentCanReadClientFiles(t *testing.T) {
235+ file := filepath.Join(t.TempDir(), "note.txt")
236+ if err := os.WriteFile(file, []byte("line 1\nline 2"), 0o644); err != nil {
237+ t.Fatal(err)
238+ }
239+ session := connectMock(t, &mockAgent{readFile: file}, &recordingHandler{})
240+
241+ if _, err := session.PromptText(context.Background(), "read it"); err != nil {
242+ t.Fatalf("PromptText returned an error: %v", err)
243+ }
244+}
245+
246+func TestAgentCanWriteClientFiles(t *testing.T) {
247+ file := filepath.Join(t.TempDir(), "sub", "out.txt")
248+ session := connectMock(t, &mockAgent{writeFile: file}, &recordingHandler{})
249+
250+ if _, err := session.PromptText(context.Background(), "write it"); err != nil {
251+ t.Fatalf("PromptText returned an error: %v", err)
252+ }
253+ content, err := os.ReadFile(file)
254+ if err != nil {
255+ t.Fatalf("the agent's write never landed: %v", err)
256+ }
257+ if string(content) != "written by agent" {
258+ t.Errorf("written content = %q, want %q", content, "written by agent")
259+ }
260+}
261+
262+func TestStartRejectsMissingCommand(t *testing.T) {
263+ _, err := agent.Start(context.Background(), agent.Options{Handler: &recordingHandler{}})
264+ if err == nil {
265+ t.Fatal("Start accepted an empty Command, want an error")
266+ }
267+}
268+
269+func TestStartReportsUnknownExecutable(t *testing.T) {
270+ _, err := agent.Start(context.Background(), agent.Options{
271+ Command: []string{"/does/not/exist-ori-agent"},
272+ Cwd: t.TempDir(),
273+ Handler: &recordingHandler{},
274+ })
275+ if err == nil {
276+ t.Fatal("Start accepted a non-existent executable, want an error")
277+ }
278+}
new file mode 100644
@@ -0,0 +1,278 @@
1+package agent_test
2+
3+import (
4+ "context"
5+ "io"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "sync"
10+ "testing"
11+ "time"
12+
13+ acp "github.com/coder/acp-go-sdk"
14+
15+ "github.com/bots-garden/ori/internal/agent"
16+)
17+
18+// mockAgent is a deterministic in-process ACP agent. The embedded acp.Agent
19+// interface satisfies the methods this test never exercises; calling one of
20+// them would panic, which is exactly what we want a test to do.
21+type mockAgent struct {
22+ acp.Agent
23+ conn *acp.AgentSideConnection
24+
25+ mu sync.Mutex
26+ cancelled bool
27+ // askPermission makes Prompt request a permission before answering.
28+ askPermission bool
29+ // readFile makes Prompt read this file through the client before answering.
30+ readFile string
31+ // writeFile makes Prompt write "written by agent" to this file.
32+ writeFile string
33+}
34+
35+func (a *mockAgent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
36+
37+func (a *mockAgent) Initialize(_ context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
38+ return acp.InitializeResponse{ProtocolVersion: params.ProtocolVersion}, nil
39+}
40+
41+func (a *mockAgent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
42+ return acp.NewSessionResponse{SessionId: "sess-mock"}, nil
43+}
44+
45+func (a *mockAgent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
46+ return acp.AuthenticateResponse{}, nil
47+}
48+
49+func (a *mockAgent) Cancel(context.Context, acp.CancelNotification) error {
50+ a.mu.Lock()
51+ defer a.mu.Unlock()
52+ a.cancelled = true
53+ return nil
54+}
55+
56+func (a *mockAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
57+ send := func(text string) error {
58+ return a.conn.SessionUpdate(ctx, acp.SessionNotification{
59+ SessionId: params.SessionId,
60+ Update: acp.UpdateAgentMessageText(text),
61+ })
62+ }
63+ if err := send("Hello "); err != nil {
64+ return acp.PromptResponse{}, err
65+ }
66+ if a.askPermission {
67+ resp, err := a.conn.RequestPermission(ctx, acp.RequestPermissionRequest{
68+ SessionId: params.SessionId,
69+ Options: []acp.PermissionOption{
70+ {OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce},
71+ {OptionId: "reject", Name: "Reject", Kind: acp.PermissionOptionKindRejectOnce},
72+ },
73+ })
74+ if err != nil {
75+ return acp.PromptResponse{}, err
76+ }
77+ if resp.Outcome.Selected == nil || resp.Outcome.Selected.OptionId != "allow" {
78+ return acp.PromptResponse{StopReason: acp.StopReasonRefusal}, nil
79+ }
80+ }
81+ if a.readFile != "" {
82+ if _, err := a.conn.ReadTextFile(ctx, acp.ReadTextFileRequest{SessionId: params.SessionId, Path: a.readFile}); err != nil {
83+ return acp.PromptResponse{}, err
84+ }
85+ }
86+ if a.writeFile != "" {
87+ if _, err := a.conn.WriteTextFile(ctx, acp.WriteTextFileRequest{SessionId: params.SessionId, Path: a.writeFile, Content: "written by agent"}); err != nil {
88+ return acp.PromptResponse{}, err
89+ }
90+ }
91+ if err := send("world"); err != nil {
92+ return acp.PromptResponse{}, err
93+ }
94+ return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
95+}
96+
97+// recordingHandler collects session updates and answers permission requests.
98+type recordingHandler struct {
99+ mu sync.Mutex
100+ texts []string
101+ granted bool
102+ asked int
103+}
104+
105+func (h *recordingHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotification) {
106+ h.mu.Lock()
107+ defer h.mu.Unlock()
108+ if chunk := n.Update.AgentMessageChunk; chunk != nil && chunk.Content.Text != nil {
109+ h.texts = append(h.texts, chunk.Content.Text.Text)
110+ }
111+}
112+
113+func (h *recordingHandler) HandlePermissionRequest(_ context.Context, req acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
114+ h.mu.Lock()
115+ defer h.mu.Unlock()
116+ h.asked++
117+ choice := acp.PermissionOptionId("reject")
118+ if h.granted {
119+ choice = "allow"
120+ }
121+ _ = req
122+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(choice)}, nil
123+}
124+
125+func (h *recordingHandler) joined() string {
126+ h.mu.Lock()
127+ defer h.mu.Unlock()
128+ return strings.Join(h.texts, "")
129+}
130+
131+// connectMock wires a mock agent and an agent.Session together with in-memory
132+// pipes, no subprocess involved.
133+func connectMock(t *testing.T, mock *mockAgent, handler agent.Handler) *agent.Session {
134+ t.Helper()
135+ clientToAgentR, clientToAgentW := io.Pipe()
136+ agentToClientR, agentToClientW := io.Pipe()
137+
138+ mock.SetAgentConnection(acp.NewAgentSideConnection(mock, agentToClientW, clientToAgentR))
139+
140+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
141+ t.Cleanup(cancel)
142+ session, err := agent.Connect(ctx, clientToAgentW, agentToClientR, agent.Options{
143+ Cwd: t.TempDir(),
144+ Handler: handler,
145+ })
146+ if err != nil {
147+ t.Fatalf("Connect returned an error: %v", err)
148+ }
149+ return session
150+}
151+
152+func TestConnectPerformsHandshake(t *testing.T) {
153+ session := connectMock(t, &mockAgent{}, &recordingHandler{})
154+ if session.ID() != "sess-mock" {
155+ t.Errorf("session ID = %q, want %q", session.ID(), "sess-mock")
156+ }
157+}
158+
159+func TestConnectRequiresHandler(t *testing.T) {
160+ _, err := agent.Connect(context.Background(), io.Discard, strings.NewReader(""), agent.Options{})
161+ if err == nil {
162+ t.Fatal("Connect accepted a nil Handler, want an error")
163+ }
164+}
165+
166+func TestPromptStreamsUpdates(t *testing.T) {
167+ handler := &recordingHandler{}
168+ session := connectMock(t, &mockAgent{}, handler)
169+
170+ stop, err := session.PromptText(context.Background(), "hi")
171+ if err != nil {
172+ t.Fatalf("PromptText returned an error: %v", err)
173+ }
174+ if stop != acp.StopReasonEndTurn {
175+ t.Errorf("stop reason = %q, want %q", stop, acp.StopReasonEndTurn)
176+ }
177+ if got := handler.joined(); got != "Hello world" {
178+ t.Errorf("streamed text = %q, want %q", got, "Hello world")
179+ }
180+}
181+
182+func TestPermissionGrantedFlowsBackToAgent(t *testing.T) {
183+ handler := &recordingHandler{granted: true}
184+ session := connectMock(t, &mockAgent{askPermission: true}, handler)
185+
186+ stop, err := session.PromptText(context.Background(), "do something sensitive")
187+ if err != nil {
188+ t.Fatalf("PromptText returned an error: %v", err)
189+ }
190+ if stop != acp.StopReasonEndTurn {
191+ t.Errorf("stop reason = %q, want %q (permission was granted)", stop, acp.StopReasonEndTurn)
192+ }
193+ if handler.asked != 1 {
194+ t.Errorf("permission requests seen by handler = %d, want 1", handler.asked)
195+ }
196+}
197+
198+func TestPermissionRejectedStopsTheTurn(t *testing.T) {
199+ handler := &recordingHandler{granted: false}
200+ session := connectMock(t, &mockAgent{askPermission: true}, handler)
201+
202+ stop, err := session.PromptText(context.Background(), "do something sensitive")
203+ if err != nil {
204+ t.Fatalf("PromptText returned an error: %v", err)
205+ }
206+ if stop != acp.StopReasonRefusal {
207+ t.Errorf("stop reason = %q, want %q (permission was rejected)", stop, acp.StopReasonRefusal)
208+ }
209+}
210+
211+func TestCancelReachesTheAgent(t *testing.T) {
212+ mock := &mockAgent{}
213+ session := connectMock(t, mock, &recordingHandler{})
214+
215+ if err := session.Cancel(context.Background()); err != nil {
216+ t.Fatalf("Cancel returned an error: %v", err)
217+ }
218+ // Cancel is a notification: poll briefly for its arrival.
219+ deadline := time.Now().Add(2 * time.Second)
220+ for {
221+ mock.mu.Lock()
222+ cancelled := mock.cancelled
223+ mock.mu.Unlock()
224+ if cancelled {
225+ return
226+ }
227+ if time.Now().After(deadline) {
228+ t.Fatal("agent never received the cancel notification")
229+ }
230+ time.Sleep(10 * time.Millisecond)
231+ }
232+}
233+
234+func TestAgentCanReadClientFiles(t *testing.T) {
235+ file := filepath.Join(t.TempDir(), "note.txt")
236+ if err := os.WriteFile(file, []byte("line 1\nline 2"), 0o644); err != nil {
237+ t.Fatal(err)
238+ }
239+ session := connectMock(t, &mockAgent{readFile: file}, &recordingHandler{})
240+
241+ if _, err := session.PromptText(context.Background(), "read it"); err != nil {
242+ t.Fatalf("PromptText returned an error: %v", err)
243+ }
244+}
245+
246+func TestAgentCanWriteClientFiles(t *testing.T) {
247+ file := filepath.Join(t.TempDir(), "sub", "out.txt")
248+ session := connectMock(t, &mockAgent{writeFile: file}, &recordingHandler{})
249+
250+ if _, err := session.PromptText(context.Background(), "write it"); err != nil {
251+ t.Fatalf("PromptText returned an error: %v", err)
252+ }
253+ content, err := os.ReadFile(file)
254+ if err != nil {
255+ t.Fatalf("the agent's write never landed: %v", err)
256+ }
257+ if string(content) != "written by agent" {
258+ t.Errorf("written content = %q, want %q", content, "written by agent")
259+ }
260+}
261+
262+func TestStartRejectsMissingCommand(t *testing.T) {
263+ _, err := agent.Start(context.Background(), agent.Options{Handler: &recordingHandler{}})
264+ if err == nil {
265+ t.Fatal("Start accepted an empty Command, want an error")
266+ }
267+}
268+
269+func TestStartReportsUnknownExecutable(t *testing.T) {
270+ _, err := agent.Start(context.Background(), agent.Options{
271+ Command: []string{"/does/not/exist-ori-agent"},
272+ Cwd: t.TempDir(),
273+ Handler: &recordingHandler{},
274+ })
275+ if err == nil {
276+ t.Fatal("Start accepted a non-existent executable, want an error")
277+ }
278+}
added internal/agent/client.go +98 -0
new file mode 100644
@@ -0,0 +1,98 @@
1+package agent
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+
10+ acp "github.com/coder/acp-go-sdk"
11+)
12+
13+// client implements acp.Client: the callbacks the agent invokes on us.
14+// Session updates and permission requests are delegated to the Handler; file
15+// system access is served directly; terminals are not supported in this
16+// version, matching the capabilities announced during initialize.
17+type client struct {
18+ handler Handler
19+}
20+
21+var _ acp.Client = (*client)(nil)
22+
23+func (c *client) SessionUpdate(ctx context.Context, params acp.SessionNotification) error {
24+ c.handler.HandleSessionUpdate(ctx, params)
25+ return nil
26+}
27+
28+func (c *client) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
29+ return c.handler.HandlePermissionRequest(ctx, params)
30+}
31+
32+func (c *client) ReadTextFile(_ context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) {
33+ if !filepath.IsAbs(params.Path) {
34+ return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
35+ }
36+ raw, err := os.ReadFile(params.Path)
37+ if err != nil {
38+ return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err)
39+ }
40+ return acp.ReadTextFileResponse{Content: sliceLines(string(raw), params.Line, params.Limit)}, nil
41+}
42+
43+func (c *client) WriteTextFile(_ context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) {
44+ if !filepath.IsAbs(params.Path) {
45+ return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
46+ }
47+ if dir := filepath.Dir(params.Path); dir != "" {
48+ if err := os.MkdirAll(dir, 0o755); err != nil {
49+ return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err)
50+ }
51+ }
52+ if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil {
53+ return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err)
54+ }
55+ return acp.WriteTextFileResponse{}, nil
56+}
57+
58+// sliceLines applies ACP's optional 1-based line offset and line count to a
59+// file's content.
60+func sliceLines(content string, line, limit *int) string {
61+ if line == nil && limit == nil {
62+ return content
63+ }
64+ lines := strings.Split(content, "\n")
65+ start := 0
66+ if line != nil && *line > 0 {
67+ start = min(*line-1, len(lines))
68+ }
69+ end := len(lines)
70+ if limit != nil && *limit > 0 && start+*limit < end {
71+ end = start + *limit
72+ }
73+ return strings.Join(lines[start:end], "\n")
74+}
75+
76+// Terminal support is announced as absent during initialize; a conforming
77+// agent never calls these. They still answer with a proper JSON-RPC error
78+// instead of panicking on a non-conforming one.
79+
80+func (c *client) CreateTerminal(context.Context, acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) {
81+ return acp.CreateTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalCreate)
82+}
83+
84+func (c *client) KillTerminal(context.Context, acp.KillTerminalRequest) (acp.KillTerminalResponse, error) {
85+ return acp.KillTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalKill)
86+}
87+
88+func (c *client) TerminalOutput(context.Context, acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) {
89+ return acp.TerminalOutputResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalOutput)
90+}
91+
92+func (c *client) ReleaseTerminal(context.Context, acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) {
93+ return acp.ReleaseTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalRelease)
94+}
95+
96+func (c *client) WaitForTerminalExit(context.Context, acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) {
97+ return acp.WaitForTerminalExitResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalWaitForExit)
98+}
new file mode 100644
@@ -0,0 +1,98 @@
1+package agent
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+
10+ acp "github.com/coder/acp-go-sdk"
11+)
12+
13+// client implements acp.Client: the callbacks the agent invokes on us.
14+// Session updates and permission requests are delegated to the Handler; file
15+// system access is served directly; terminals are not supported in this
16+// version, matching the capabilities announced during initialize.
17+type client struct {
18+ handler Handler
19+}
20+
21+var _ acp.Client = (*client)(nil)
22+
23+func (c *client) SessionUpdate(ctx context.Context, params acp.SessionNotification) error {
24+ c.handler.HandleSessionUpdate(ctx, params)
25+ return nil
26+}
27+
28+func (c *client) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
29+ return c.handler.HandlePermissionRequest(ctx, params)
30+}
31+
32+func (c *client) ReadTextFile(_ context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) {
33+ if !filepath.IsAbs(params.Path) {
34+ return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
35+ }
36+ raw, err := os.ReadFile(params.Path)
37+ if err != nil {
38+ return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err)
39+ }
40+ return acp.ReadTextFileResponse{Content: sliceLines(string(raw), params.Line, params.Limit)}, nil
41+}
42+
43+func (c *client) WriteTextFile(_ context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) {
44+ if !filepath.IsAbs(params.Path) {
45+ return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
46+ }
47+ if dir := filepath.Dir(params.Path); dir != "" {
48+ if err := os.MkdirAll(dir, 0o755); err != nil {
49+ return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err)
50+ }
51+ }
52+ if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil {
53+ return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err)
54+ }
55+ return acp.WriteTextFileResponse{}, nil
56+}
57+
58+// sliceLines applies ACP's optional 1-based line offset and line count to a
59+// file's content.
60+func sliceLines(content string, line, limit *int) string {
61+ if line == nil && limit == nil {
62+ return content
63+ }
64+ lines := strings.Split(content, "\n")
65+ start := 0
66+ if line != nil && *line > 0 {
67+ start = min(*line-1, len(lines))
68+ }
69+ end := len(lines)
70+ if limit != nil && *limit > 0 && start+*limit < end {
71+ end = start + *limit
72+ }
73+ return strings.Join(lines[start:end], "\n")
74+}
75+
76+// Terminal support is announced as absent during initialize; a conforming
77+// agent never calls these. They still answer with a proper JSON-RPC error
78+// instead of panicking on a non-conforming one.
79+
80+func (c *client) CreateTerminal(context.Context, acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) {
81+ return acp.CreateTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalCreate)
82+}
83+
84+func (c *client) KillTerminal(context.Context, acp.KillTerminalRequest) (acp.KillTerminalResponse, error) {
85+ return acp.KillTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalKill)
86+}
87+
88+func (c *client) TerminalOutput(context.Context, acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) {
89+ return acp.TerminalOutputResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalOutput)
90+}
91+
92+func (c *client) ReleaseTerminal(context.Context, acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) {
93+ return acp.ReleaseTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalRelease)
94+}
95+
96+func (c *client) WaitForTerminalExit(context.Context, acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) {
97+ return acp.WaitForTerminalExitResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalWaitForExit)
98+}
added internal/bridge/bridge.go +282 -0
new file mode 100644
@@ -0,0 +1,282 @@
1+// Package bridge relays events between the ACP agent session and the
2+// browsers connected over WebSocket.
3+//
4+// It is the meeting point of two asynchronous worlds: the agent streams
5+// updates and blocks on permission requests; browsers connect, disconnect and
6+// answer at human speed. The bridge broadcasts agent events to every client,
7+// replays the session history to late joiners, and routes each permission
8+// decision back to the exact request waiting for it.
9+package bridge
10+
11+import (
12+ "context"
13+ "encoding/json"
14+ "fmt"
15+ "log/slog"
16+ "strconv"
17+ "sync"
18+
19+ acp "github.com/coder/acp-go-sdk"
20+)
21+
22+// Prompter is the slice of the agent session the bridge drives. It is
23+// satisfied by *agent.Session and by test fakes.
24+type Prompter interface {
25+ ID() string
26+ PromptText(ctx context.Context, text string) (acp.StopReason, error)
27+ Cancel(ctx context.Context) error
28+}
29+
30+// maxHistory bounds the number of events replayed to late-joining clients;
31+// beyond it the oldest events are dropped (the live broadcast is unaffected).
32+const maxHistory = 4096
33+
34+// clientBuffer is each subscriber's outgoing queue; a client too slow to
35+// drain it is disconnected rather than allowed to stall the whole bridge.
36+const clientBuffer = 256
37+
38+type permissionDecision struct {
39+ optionId string
40+ cancelled bool
41+}
42+
43+// Bridge broadcasts agent events to WebSocket clients and routes their
44+// responses back to the agent. The zero value is not usable; call New.
45+//
46+// Example:
47+//
48+// b := bridge.New(slog.Default())
49+// session, _ := agent.Start(ctx, agent.Options{Handler: b, ...})
50+// b.SetSession(session)
51+// http.Handle("GET /ws", b.WebSocketHandler())
52+type Bridge struct {
53+ logger *slog.Logger
54+
55+ mu sync.Mutex
56+ session Prompter
57+ subscribers map[chan []byte]struct{}
58+ history [][]byte
59+ turnActive bool
60+ nextRequest int
61+ pendingPerms map[string]chan permissionDecision
62+ // pendingRequests keeps the encoded permission_request events so they
63+ // can be replayed to clients that connect while a request is open.
64+ pendingRequests map[string][]byte
65+}
66+
67+// New creates an empty bridge; attach the agent session with SetSession.
68+func New(logger *slog.Logger) *Bridge {
69+ if logger == nil {
70+ logger = slog.Default()
71+ }
72+ return &Bridge{
73+ logger: logger,
74+ subscribers: make(map[chan []byte]struct{}),
75+ pendingPerms: make(map[string]chan permissionDecision),
76+ pendingRequests: make(map[string][]byte),
77+ }
78+}
79+
80+// SetSession attaches the agent session the bridge drives. It must be called
81+// before the first client message arrives.
82+func (b *Bridge) SetSession(session Prompter) {
83+ b.mu.Lock()
84+ defer b.mu.Unlock()
85+ b.session = session
86+}
87+
88+// 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
90+// permission requests), and an unsubscribe function.
91+func (b *Bridge) Subscribe() (events <-chan []byte, replay [][]byte, unsubscribe func()) {
92+ ch := make(chan []byte, clientBuffer)
93+ b.mu.Lock()
94+ defer b.mu.Unlock()
95+
96+ b.subscribers[ch] = struct{}{}
97+
98+ sessionId := ""
99+ if b.session != nil {
100+ sessionId = b.session.ID()
101+ }
102+ replay = append(replay, encode(Outgoing{Type: OutgoingHello, SessionId: sessionId, TurnActive: b.turnActive}))
103+ replay = append(replay, b.history...)
104+ for _, raw := range b.pendingRequests {
105+ replay = append(replay, raw)
106+ }
107+
108+ return ch, replay, func() {
109+ b.mu.Lock()
110+ defer b.mu.Unlock()
111+ if _, ok := b.subscribers[ch]; ok {
112+ delete(b.subscribers, ch)
113+ close(ch)
114+ }
115+ }
116+}
117+
118+// HandleIncoming processes one message from a client. Errors are reported to
119+// the clients as OutgoingError events, never returned: a malformed message
120+// from one browser must not tear down the connection handling.
121+func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
122+ var msg Incoming
123+ if err := json.Unmarshal(raw, &msg); err != nil {
124+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "malformed message: " + err.Error()}), false)
125+ return
126+ }
127+
128+ switch msg.Type {
129+ case IncomingPrompt:
130+ b.startTurn(msg.Text)
131+ case IncomingCancel:
132+ b.cancelTurn(ctx)
133+ case IncomingPermissionResponse:
134+ b.resolvePermission(msg)
135+ default:
136+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: fmt.Sprintf("unknown message type %q", msg.Type)}), false)
137+ }
138+}
139+
140+// startTurn launches a prompt turn in the background; updates stream through
141+// HandleSessionUpdate while it runs.
142+func (b *Bridge) startTurn(text string) {
143+ b.mu.Lock()
144+ session := b.session
145+ if session == nil {
146+ b.mu.Unlock()
147+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false)
148+ return
149+ }
150+ if b.turnActive {
151+ b.mu.Unlock()
152+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "a turn is already running"}), false)
153+ return
154+ }
155+ b.turnActive = true
156+ b.mu.Unlock()
157+
158+ b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text}), true)
159+ b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true)
160+
161+ go func() {
162+ // The turn outlives the WebSocket message that started it, so it
163+ // runs under its own context, ended by session/cancel only.
164+ stop, err := session.PromptText(context.Background(), text)
165+
166+ b.mu.Lock()
167+ b.turnActive = false
168+ b.mu.Unlock()
169+
170+ if err != nil {
171+ b.logger.Error("prompt failed", "error", err)
172+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "prompt failed: " + err.Error()}), true)
173+ b.broadcast(encode(Outgoing{Type: OutgoingTurnEnded, StopReason: string(acp.StopReasonRefusal)}), true)
174+ return
175+ }
176+ b.broadcast(encode(Outgoing{Type: OutgoingTurnEnded, StopReason: string(stop)}), true)
177+ }()
178+}
179+
180+func (b *Bridge) cancelTurn(ctx context.Context) {
181+ b.mu.Lock()
182+ session := b.session
183+ b.mu.Unlock()
184+ if session == nil {
185+ return
186+ }
187+ if err := session.Cancel(ctx); err != nil {
188+ b.logger.Error("cancel failed", "error", err)
189+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "cancel failed: " + err.Error()}), false)
190+ }
191+}
192+
193+func (b *Bridge) resolvePermission(msg Incoming) {
194+ b.mu.Lock()
195+ waiting, ok := b.pendingPerms[msg.RequestId]
196+ if ok {
197+ delete(b.pendingPerms, msg.RequestId)
198+ delete(b.pendingRequests, msg.RequestId)
199+ }
200+ b.mu.Unlock()
201+ if !ok {
202+ // Either already answered by another client or unknown: ignore.
203+ return
204+ }
205+ waiting <- permissionDecision{optionId: msg.OptionId, cancelled: msg.Cancelled}
206+ b.broadcast(encode(Outgoing{Type: OutgoingPermissionResolved, RequestId: msg.RequestId}), true)
207+}
208+
209+// HandleSessionUpdate implements agent.Handler: every ACP update is relayed
210+// verbatim to the clients and recorded for replay.
211+func (b *Bridge) HandleSessionUpdate(_ context.Context, notification acp.SessionNotification) {
212+ update, err := json.Marshal(notification.Update)
213+ if err != nil {
214+ b.logger.Error("marshal session update", "error", err)
215+ return
216+ }
217+ b.broadcast(encode(Outgoing{Type: OutgoingSessionUpdate, Update: update}), true)
218+}
219+
220+// HandlePermissionRequest implements agent.Handler: the request is broadcast
221+// to the clients and the call blocks until one of them answers or the agent
222+// cancels the turn.
223+func (b *Bridge) HandlePermissionRequest(ctx context.Context, request acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
224+ rawRequest, err := json.Marshal(request)
225+ if err != nil {
226+ return acp.RequestPermissionResponse{}, fmt.Errorf("marshal permission request: %w", err)
227+ }
228+
229+ decisionCh := make(chan permissionDecision, 1)
230+ b.mu.Lock()
231+ b.nextRequest++
232+ requestId := "perm-" + strconv.Itoa(b.nextRequest)
233+ event := encode(Outgoing{Type: OutgoingPermissionRequest, RequestId: requestId, Request: rawRequest})
234+ b.pendingPerms[requestId] = decisionCh
235+ b.pendingRequests[requestId] = event
236+ b.mu.Unlock()
237+
238+ b.broadcast(event, false)
239+
240+ select {
241+ case decision := <-decisionCh:
242+ if decision.cancelled {
243+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeCancelled()}, nil
244+ }
245+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(acp.PermissionOptionId(decision.optionId))}, nil
246+ case <-ctx.Done():
247+ // The agent cancelled the turn (or disconnected) while waiting.
248+ b.mu.Lock()
249+ delete(b.pendingPerms, requestId)
250+ delete(b.pendingRequests, requestId)
251+ b.mu.Unlock()
252+ b.broadcast(encode(Outgoing{Type: OutgoingPermissionResolved, RequestId: requestId}), true)
253+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeCancelled()}, nil
254+ }
255+}
256+
257+// broadcast sends an event to every connected client, disconnecting any
258+// client whose buffer is full. With record=true the event also joins the
259+// replay history.
260+func (b *Bridge) broadcast(event []byte, record bool) {
261+ b.mu.Lock()
262+ defer b.mu.Unlock()
263+
264+ if record {
265+ b.history = append(b.history, event)
266+ if len(b.history) > maxHistory {
267+ b.history = b.history[len(b.history)-maxHistory:]
268+ }
269+ }
270+
271+ for ch := range b.subscribers {
272+ select {
273+ case ch <- event:
274+ default:
275+ // The client stopped draining; drop it instead of blocking
276+ // the agent's event stream.
277+ delete(b.subscribers, ch)
278+ close(ch)
279+ b.logger.Warn("dropped a slow websocket client")
280+ }
281+ }
282+}
new file mode 100644
@@ -0,0 +1,282 @@
1+// Package bridge relays events between the ACP agent session and the
2+// browsers connected over WebSocket.
3+//
4+// It is the meeting point of two asynchronous worlds: the agent streams
5+// updates and blocks on permission requests; browsers connect, disconnect and
6+// answer at human speed. The bridge broadcasts agent events to every client,
7+// replays the session history to late joiners, and routes each permission
8+// decision back to the exact request waiting for it.
9+package bridge
10+
11+import (
12+ "context"
13+ "encoding/json"
14+ "fmt"
15+ "log/slog"
16+ "strconv"
17+ "sync"
18+
19+ acp "github.com/coder/acp-go-sdk"
20+)
21+
22+// Prompter is the slice of the agent session the bridge drives. It is
23+// satisfied by *agent.Session and by test fakes.
24+type Prompter interface {
25+ ID() string
26+ PromptText(ctx context.Context, text string) (acp.StopReason, error)
27+ Cancel(ctx context.Context) error
28+}
29+
30+// maxHistory bounds the number of events replayed to late-joining clients;
31+// beyond it the oldest events are dropped (the live broadcast is unaffected).
32+const maxHistory = 4096
33+
34+// clientBuffer is each subscriber's outgoing queue; a client too slow to
35+// drain it is disconnected rather than allowed to stall the whole bridge.
36+const clientBuffer = 256
37+
38+type permissionDecision struct {
39+ optionId string
40+ cancelled bool
41+}
42+
43+// Bridge broadcasts agent events to WebSocket clients and routes their
44+// responses back to the agent. The zero value is not usable; call New.
45+//
46+// Example:
47+//
48+// b := bridge.New(slog.Default())
49+// session, _ := agent.Start(ctx, agent.Options{Handler: b, ...})
50+// b.SetSession(session)
51+// http.Handle("GET /ws", b.WebSocketHandler())
52+type Bridge struct {
53+ logger *slog.Logger
54+
55+ mu sync.Mutex
56+ session Prompter
57+ subscribers map[chan []byte]struct{}
58+ history [][]byte
59+ turnActive bool
60+ nextRequest int
61+ pendingPerms map[string]chan permissionDecision
62+ // pendingRequests keeps the encoded permission_request events so they
63+ // can be replayed to clients that connect while a request is open.
64+ pendingRequests map[string][]byte
65+}
66+
67+// New creates an empty bridge; attach the agent session with SetSession.
68+func New(logger *slog.Logger) *Bridge {
69+ if logger == nil {
70+ logger = slog.Default()
71+ }
72+ return &Bridge{
73+ logger: logger,
74+ subscribers: make(map[chan []byte]struct{}),
75+ pendingPerms: make(map[string]chan permissionDecision),
76+ pendingRequests: make(map[string][]byte),
77+ }
78+}
79+
80+// SetSession attaches the agent session the bridge drives. It must be called
81+// before the first client message arrives.
82+func (b *Bridge) SetSession(session Prompter) {
83+ b.mu.Lock()
84+ defer b.mu.Unlock()
85+ b.session = session
86+}
87+
88+// 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
90+// permission requests), and an unsubscribe function.
91+func (b *Bridge) Subscribe() (events <-chan []byte, replay [][]byte, unsubscribe func()) {
92+ ch := make(chan []byte, clientBuffer)
93+ b.mu.Lock()
94+ defer b.mu.Unlock()
95+
96+ b.subscribers[ch] = struct{}{}
97+
98+ sessionId := ""
99+ if b.session != nil {
100+ sessionId = b.session.ID()
101+ }
102+ replay = append(replay, encode(Outgoing{Type: OutgoingHello, SessionId: sessionId, TurnActive: b.turnActive}))
103+ replay = append(replay, b.history...)
104+ for _, raw := range b.pendingRequests {
105+ replay = append(replay, raw)
106+ }
107+
108+ return ch, replay, func() {
109+ b.mu.Lock()
110+ defer b.mu.Unlock()
111+ if _, ok := b.subscribers[ch]; ok {
112+ delete(b.subscribers, ch)
113+ close(ch)
114+ }
115+ }
116+}
117+
118+// HandleIncoming processes one message from a client. Errors are reported to
119+// the clients as OutgoingError events, never returned: a malformed message
120+// from one browser must not tear down the connection handling.
121+func (b *Bridge) HandleIncoming(ctx context.Context, raw []byte) {
122+ var msg Incoming
123+ if err := json.Unmarshal(raw, &msg); err != nil {
124+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "malformed message: " + err.Error()}), false)
125+ return
126+ }
127+
128+ switch msg.Type {
129+ case IncomingPrompt:
130+ b.startTurn(msg.Text)
131+ case IncomingCancel:
132+ b.cancelTurn(ctx)
133+ case IncomingPermissionResponse:
134+ b.resolvePermission(msg)
135+ default:
136+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: fmt.Sprintf("unknown message type %q", msg.Type)}), false)
137+ }
138+}
139+
140+// startTurn launches a prompt turn in the background; updates stream through
141+// HandleSessionUpdate while it runs.
142+func (b *Bridge) startTurn(text string) {
143+ b.mu.Lock()
144+ session := b.session
145+ if session == nil {
146+ b.mu.Unlock()
147+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "no agent session"}), false)
148+ return
149+ }
150+ if b.turnActive {
151+ b.mu.Unlock()
152+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "a turn is already running"}), false)
153+ return
154+ }
155+ b.turnActive = true
156+ b.mu.Unlock()
157+
158+ b.broadcast(encode(Outgoing{Type: OutgoingUserMessage, Text: text}), true)
159+ b.broadcast(encode(Outgoing{Type: OutgoingTurnStarted}), true)
160+
161+ go func() {
162+ // The turn outlives the WebSocket message that started it, so it
163+ // runs under its own context, ended by session/cancel only.
164+ stop, err := session.PromptText(context.Background(), text)
165+
166+ b.mu.Lock()
167+ b.turnActive = false
168+ b.mu.Unlock()
169+
170+ if err != nil {
171+ b.logger.Error("prompt failed", "error", err)
172+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "prompt failed: " + err.Error()}), true)
173+ b.broadcast(encode(Outgoing{Type: OutgoingTurnEnded, StopReason: string(acp.StopReasonRefusal)}), true)
174+ return
175+ }
176+ b.broadcast(encode(Outgoing{Type: OutgoingTurnEnded, StopReason: string(stop)}), true)
177+ }()
178+}
179+
180+func (b *Bridge) cancelTurn(ctx context.Context) {
181+ b.mu.Lock()
182+ session := b.session
183+ b.mu.Unlock()
184+ if session == nil {
185+ return
186+ }
187+ if err := session.Cancel(ctx); err != nil {
188+ b.logger.Error("cancel failed", "error", err)
189+ b.broadcast(encode(Outgoing{Type: OutgoingError, Message: "cancel failed: " + err.Error()}), false)
190+ }
191+}
192+
193+func (b *Bridge) resolvePermission(msg Incoming) {
194+ b.mu.Lock()
195+ waiting, ok := b.pendingPerms[msg.RequestId]
196+ if ok {
197+ delete(b.pendingPerms, msg.RequestId)
198+ delete(b.pendingRequests, msg.RequestId)
199+ }
200+ b.mu.Unlock()
201+ if !ok {
202+ // Either already answered by another client or unknown: ignore.
203+ return
204+ }
205+ waiting <- permissionDecision{optionId: msg.OptionId, cancelled: msg.Cancelled}
206+ b.broadcast(encode(Outgoing{Type: OutgoingPermissionResolved, RequestId: msg.RequestId}), true)
207+}
208+
209+// HandleSessionUpdate implements agent.Handler: every ACP update is relayed
210+// verbatim to the clients and recorded for replay.
211+func (b *Bridge) HandleSessionUpdate(_ context.Context, notification acp.SessionNotification) {
212+ update, err := json.Marshal(notification.Update)
213+ if err != nil {
214+ b.logger.Error("marshal session update", "error", err)
215+ return
216+ }
217+ b.broadcast(encode(Outgoing{Type: OutgoingSessionUpdate, Update: update}), true)
218+}
219+
220+// HandlePermissionRequest implements agent.Handler: the request is broadcast
221+// to the clients and the call blocks until one of them answers or the agent
222+// cancels the turn.
223+func (b *Bridge) HandlePermissionRequest(ctx context.Context, request acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
224+ rawRequest, err := json.Marshal(request)
225+ if err != nil {
226+ return acp.RequestPermissionResponse{}, fmt.Errorf("marshal permission request: %w", err)
227+ }
228+
229+ decisionCh := make(chan permissionDecision, 1)
230+ b.mu.Lock()
231+ b.nextRequest++
232+ requestId := "perm-" + strconv.Itoa(b.nextRequest)
233+ event := encode(Outgoing{Type: OutgoingPermissionRequest, RequestId: requestId, Request: rawRequest})
234+ b.pendingPerms[requestId] = decisionCh
235+ b.pendingRequests[requestId] = event
236+ b.mu.Unlock()
237+
238+ b.broadcast(event, false)
239+
240+ select {
241+ case decision := <-decisionCh:
242+ if decision.cancelled {
243+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeCancelled()}, nil
244+ }
245+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(acp.PermissionOptionId(decision.optionId))}, nil
246+ case <-ctx.Done():
247+ // The agent cancelled the turn (or disconnected) while waiting.
248+ b.mu.Lock()
249+ delete(b.pendingPerms, requestId)
250+ delete(b.pendingRequests, requestId)
251+ b.mu.Unlock()
252+ b.broadcast(encode(Outgoing{Type: OutgoingPermissionResolved, RequestId: requestId}), true)
253+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeCancelled()}, nil
254+ }
255+}
256+
257+// broadcast sends an event to every connected client, disconnecting any
258+// client whose buffer is full. With record=true the event also joins the
259+// replay history.
260+func (b *Bridge) broadcast(event []byte, record bool) {
261+ b.mu.Lock()
262+ defer b.mu.Unlock()
263+
264+ if record {
265+ b.history = append(b.history, event)
266+ if len(b.history) > maxHistory {
267+ b.history = b.history[len(b.history)-maxHistory:]
268+ }
269+ }
270+
271+ for ch := range b.subscribers {
272+ select {
273+ case ch <- event:
274+ default:
275+ // The client stopped draining; drop it instead of blocking
276+ // the agent's event stream.
277+ delete(b.subscribers, ch)
278+ close(ch)
279+ b.logger.Warn("dropped a slow websocket client")
280+ }
281+ }
282+}
added internal/bridge/bridge_test.go +342 -0
new file mode 100644
@@ -0,0 +1,342 @@
1+package bridge_test
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "errors"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ acp "github.com/coder/acp-go-sdk"
12+
13+ "github.com/bots-garden/ori/internal/bridge"
14+)
15+
16+// fakeSession implements bridge.Prompter without any agent behind it.
17+type fakeSession struct {
18+ mu sync.Mutex
19+ prompts []string
20+ cancelled bool
21+ // respond controls what PromptText returns; nil means end_turn.
22+ respond func(text string) (acp.StopReason, error)
23+ // block, when non-nil, is closed by the test to let PromptText return.
24+ block chan struct{}
25+}
26+
27+func (f *fakeSession) ID() string { return "sess-fake" }
28+
29+func (f *fakeSession) PromptText(_ context.Context, text string) (acp.StopReason, error) {
30+ f.mu.Lock()
31+ f.prompts = append(f.prompts, text)
32+ respond := f.respond
33+ block := f.block
34+ f.mu.Unlock()
35+ if block != nil {
36+ <-block
37+ }
38+ if respond != nil {
39+ return respond(text)
40+ }
41+ return acp.StopReasonEndTurn, nil
42+}
43+
44+func (f *fakeSession) Cancel(context.Context) error {
45+ f.mu.Lock()
46+ defer f.mu.Unlock()
47+ f.cancelled = true
48+ return nil
49+}
50+
51+// collect drains a subscriber channel until the wanted message type shows up
52+// or the timeout hits; it returns every message seen, decoded.
53+func collect(t *testing.T, ch <-chan []byte, wantType string) []bridge.Outgoing {
54+ t.Helper()
55+ var seen []bridge.Outgoing
56+ deadline := time.After(5 * time.Second)
57+ for {
58+ select {
59+ case raw, ok := <-ch:
60+ if !ok {
61+ t.Fatalf("channel closed while waiting for %q; saw %+v", wantType, seen)
62+ }
63+ var msg bridge.Outgoing
64+ if err := json.Unmarshal(raw, &msg); err != nil {
65+ t.Fatalf("undecodable outgoing message %q: %v", raw, err)
66+ }
67+ seen = append(seen, msg)
68+ if msg.Type == wantType {
69+ return seen
70+ }
71+ case <-deadline:
72+ t.Fatalf("timed out waiting for %q; saw %+v", wantType, seen)
73+ }
74+ }
75+}
76+
77+func decode(t *testing.T, raw []byte) bridge.Outgoing {
78+ t.Helper()
79+ var msg bridge.Outgoing
80+ if err := json.Unmarshal(raw, &msg); err != nil {
81+ t.Fatalf("undecodable message %q: %v", raw, err)
82+ }
83+ return msg
84+}
85+
86+func TestSubscribeSendsHello(t *testing.T) {
87+ b := bridge.New(nil)
88+ b.SetSession(&fakeSession{})
89+
90+ _, replay, unsubscribe := b.Subscribe()
91+ defer unsubscribe()
92+
93+ if len(replay) == 0 {
94+ t.Fatal("Subscribe returned no replay events, want at least the hello")
95+ }
96+ hello := decode(t, replay[0])
97+ if hello.Type != bridge.OutgoingHello || hello.SessionId != "sess-fake" {
98+ t.Errorf("first replay event = %+v, want hello for sess-fake", hello)
99+ }
100+}
101+
102+func TestPromptRunsTurnAndBroadcastsMarkers(t *testing.T) {
103+ b := bridge.New(nil)
104+ session := &fakeSession{}
105+ b.SetSession(session)
106+
107+ events, _, unsubscribe := b.Subscribe()
108+ defer unsubscribe()
109+
110+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"hello agent"}`))
111+
112+ seen := collect(t, events, bridge.OutgoingTurnEnded)
113+ if seen[0].Type != bridge.OutgoingUserMessage || seen[0].Text != "hello agent" {
114+ t.Errorf("first event = %+v, want the echoed user message", seen[0])
115+ }
116+ if seen[1].Type != bridge.OutgoingTurnStarted {
117+ t.Errorf("second event = %q, want turn_started", seen[1].Type)
118+ }
119+ last := seen[len(seen)-1]
120+ if last.StopReason != string(acp.StopReasonEndTurn) {
121+ t.Errorf("turn_ended stopReason = %q, want end_turn", last.StopReason)
122+ }
123+ session.mu.Lock()
124+ defer session.mu.Unlock()
125+ if len(session.prompts) != 1 || session.prompts[0] != "hello agent" {
126+ t.Errorf("session received prompts %v, want [hello agent]", session.prompts)
127+ }
128+}
129+
130+func TestSecondPromptDuringTurnIsRejected(t *testing.T) {
131+ b := bridge.New(nil)
132+ session := &fakeSession{block: make(chan struct{})}
133+ b.SetSession(session)
134+
135+ events, _, unsubscribe := b.Subscribe()
136+ defer unsubscribe()
137+
138+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"first"}`))
139+ collect(t, events, bridge.OutgoingTurnStarted)
140+
141+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"second"}`))
142+ seen := collect(t, events, bridge.OutgoingError)
143+ if last := seen[len(seen)-1]; last.Message != "a turn is already running" {
144+ t.Errorf("error message = %q", last.Message)
145+ }
146+
147+ close(session.block)
148+ collect(t, events, bridge.OutgoingTurnEnded)
149+}
150+
151+func TestPromptWithoutSessionReportsError(t *testing.T) {
152+ b := bridge.New(nil)
153+ events, _, unsubscribe := b.Subscribe()
154+ defer unsubscribe()
155+
156+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"x"}`))
157+ seen := collect(t, events, bridge.OutgoingError)
158+ if last := seen[len(seen)-1]; last.Message != "no agent session" {
159+ t.Errorf("error message = %q, want \"no agent session\"", last.Message)
160+ }
161+}
162+
163+func TestPromptFailureEndsTurnWithError(t *testing.T) {
164+ b := bridge.New(nil)
165+ b.SetSession(&fakeSession{respond: func(string) (acp.StopReason, error) {
166+ return "", errors.New("agent exploded")
167+ }})
168+ events, _, unsubscribe := b.Subscribe()
169+ defer unsubscribe()
170+
171+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"x"}`))
172+ seen := collect(t, events, bridge.OutgoingTurnEnded)
173+
174+ foundError := false
175+ for _, msg := range seen {
176+ if msg.Type == bridge.OutgoingError {
177+ foundError = true
178+ }
179+ }
180+ if !foundError {
181+ t.Error("no error event before turn_ended, want one mentioning the failure")
182+ }
183+}
184+
185+func TestCancelReachesSession(t *testing.T) {
186+ b := bridge.New(nil)
187+ session := &fakeSession{}
188+ b.SetSession(session)
189+
190+ b.HandleIncoming(context.Background(), []byte(`{"type":"cancel"}`))
191+
192+ session.mu.Lock()
193+ defer session.mu.Unlock()
194+ if !session.cancelled {
195+ t.Error("cancel never reached the session")
196+ }
197+}
198+
199+func TestMalformedAndUnknownMessagesReportErrors(t *testing.T) {
200+ b := bridge.New(nil)
201+ events, _, unsubscribe := b.Subscribe()
202+ defer unsubscribe()
203+
204+ b.HandleIncoming(context.Background(), []byte(`{not json`))
205+ collect(t, events, bridge.OutgoingError)
206+
207+ b.HandleIncoming(context.Background(), []byte(`{"type":"nope"}`))
208+ seen := collect(t, events, bridge.OutgoingError)
209+ if last := seen[len(seen)-1]; last.Message != `unknown message type "nope"` {
210+ t.Errorf("error message = %q", last.Message)
211+ }
212+}
213+
214+func TestSessionUpdateIsRelayedAndReplayed(t *testing.T) {
215+ b := bridge.New(nil)
216+ b.SetSession(&fakeSession{})
217+ events, _, unsubscribe := b.Subscribe()
218+ defer unsubscribe()
219+
220+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
221+ SessionId: "sess-fake",
222+ Update: acp.UpdateAgentMessageText("chunk!"),
223+ })
224+
225+ seen := collect(t, events, bridge.OutgoingSessionUpdate)
226+ update := seen[len(seen)-1].Update
227+ if update == nil {
228+ t.Fatal("session_update event has no update payload")
229+ }
230+ var acpUpdate struct {
231+ SessionUpdate string `json:"sessionUpdate"`
232+ Content struct {
233+ Text string `json:"text"`
234+ } `json:"content"`
235+ }
236+ if err := json.Unmarshal(update, &acpUpdate); err != nil {
237+ t.Fatalf("update payload is not the raw ACP shape: %v", err)
238+ }
239+ if acpUpdate.SessionUpdate != "agent_message_chunk" || acpUpdate.Content.Text != "chunk!" {
240+ t.Errorf("relayed update = %+v, want agent_message_chunk with text \"chunk!\"", acpUpdate)
241+ }
242+
243+ // A client connecting after the fact receives the update in its replay.
244+ _, replay, unsubscribeLate := b.Subscribe()
245+ defer unsubscribeLate()
246+ found := false
247+ for _, raw := range replay {
248+ if decode(t, raw).Type == bridge.OutgoingSessionUpdate {
249+ found = true
250+ }
251+ }
252+ if !found {
253+ t.Error("late subscriber's replay lacks the session update")
254+ }
255+}
256+
257+func TestPermissionRoundTrip(t *testing.T) {
258+ b := bridge.New(nil)
259+ b.SetSession(&fakeSession{})
260+ events, _, unsubscribe := b.Subscribe()
261+ defer unsubscribe()
262+
263+ type result struct {
264+ resp acp.RequestPermissionResponse
265+ err error
266+ }
267+ got := make(chan result, 1)
268+ go func() {
269+ resp, err := b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{
270+ SessionId: "sess-fake",
271+ Options: []acp.PermissionOption{{OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce}},
272+ })
273+ got <- result{resp, err}
274+ }()
275+
276+ seen := collect(t, events, bridge.OutgoingPermissionRequest)
277+ request := seen[len(seen)-1]
278+ if request.RequestId == "" || request.Request == nil {
279+ t.Fatalf("permission_request event incomplete: %+v", request)
280+ }
281+
282+ b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+request.RequestId+`","optionId":"allow"}`))
283+
284+ r := <-got
285+ if r.err != nil {
286+ t.Fatalf("HandlePermissionRequest returned an error: %v", r.err)
287+ }
288+ if r.resp.Outcome.Selected == nil || r.resp.Outcome.Selected.OptionId != "allow" {
289+ t.Errorf("outcome = %+v, want selected \"allow\"", r.resp.Outcome)
290+ }
291+
292+ collect(t, events, bridge.OutgoingPermissionResolved)
293+}
294+
295+func TestPermissionCancelledByAgentContext(t *testing.T) {
296+ b := bridge.New(nil)
297+ b.SetSession(&fakeSession{})
298+
299+ ctx, cancel := context.WithCancel(context.Background())
300+ go cancel()
301+
302+ resp, err := b.HandlePermissionRequest(ctx, acp.RequestPermissionRequest{SessionId: "sess-fake"})
303+ if err != nil {
304+ t.Fatalf("HandlePermissionRequest returned an error: %v", err)
305+ }
306+ if resp.Outcome.Cancelled == nil {
307+ t.Errorf("outcome = %+v, want cancelled", resp.Outcome)
308+ }
309+}
310+
311+func TestPendingPermissionIsReplayedToLateJoiner(t *testing.T) {
312+ b := bridge.New(nil)
313+ b.SetSession(&fakeSession{})
314+
315+ answered := make(chan struct{})
316+ go func() {
317+ _, _ = b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{SessionId: "sess-fake"})
318+ close(answered)
319+ }()
320+
321+ // Wait for the request to be registered by watching a live subscriber.
322+ events, _, unsubscribe := b.Subscribe()
323+ seen := collect(t, events, bridge.OutgoingPermissionRequest)
324+ requestId := seen[len(seen)-1].RequestId
325+ unsubscribe()
326+
327+ _, replay, unsubscribeLate := b.Subscribe()
328+ defer unsubscribeLate()
329+ found := false
330+ for _, raw := range replay {
331+ msg := decode(t, raw)
332+ if msg.Type == bridge.OutgoingPermissionRequest && msg.RequestId == requestId {
333+ found = true
334+ }
335+ }
336+ if !found {
337+ t.Error("late subscriber's replay lacks the pending permission request")
338+ }
339+
340+ b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`))
341+ <-answered
342+}
new file mode 100644
@@ -0,0 +1,342 @@
1+package bridge_test
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "errors"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ acp "github.com/coder/acp-go-sdk"
12+
13+ "github.com/bots-garden/ori/internal/bridge"
14+)
15+
16+// fakeSession implements bridge.Prompter without any agent behind it.
17+type fakeSession struct {
18+ mu sync.Mutex
19+ prompts []string
20+ cancelled bool
21+ // respond controls what PromptText returns; nil means end_turn.
22+ respond func(text string) (acp.StopReason, error)
23+ // block, when non-nil, is closed by the test to let PromptText return.
24+ block chan struct{}
25+}
26+
27+func (f *fakeSession) ID() string { return "sess-fake" }
28+
29+func (f *fakeSession) PromptText(_ context.Context, text string) (acp.StopReason, error) {
30+ f.mu.Lock()
31+ f.prompts = append(f.prompts, text)
32+ respond := f.respond
33+ block := f.block
34+ f.mu.Unlock()
35+ if block != nil {
36+ <-block
37+ }
38+ if respond != nil {
39+ return respond(text)
40+ }
41+ return acp.StopReasonEndTurn, nil
42+}
43+
44+func (f *fakeSession) Cancel(context.Context) error {
45+ f.mu.Lock()
46+ defer f.mu.Unlock()
47+ f.cancelled = true
48+ return nil
49+}
50+
51+// collect drains a subscriber channel until the wanted message type shows up
52+// or the timeout hits; it returns every message seen, decoded.
53+func collect(t *testing.T, ch <-chan []byte, wantType string) []bridge.Outgoing {
54+ t.Helper()
55+ var seen []bridge.Outgoing
56+ deadline := time.After(5 * time.Second)
57+ for {
58+ select {
59+ case raw, ok := <-ch:
60+ if !ok {
61+ t.Fatalf("channel closed while waiting for %q; saw %+v", wantType, seen)
62+ }
63+ var msg bridge.Outgoing
64+ if err := json.Unmarshal(raw, &msg); err != nil {
65+ t.Fatalf("undecodable outgoing message %q: %v", raw, err)
66+ }
67+ seen = append(seen, msg)
68+ if msg.Type == wantType {
69+ return seen
70+ }
71+ case <-deadline:
72+ t.Fatalf("timed out waiting for %q; saw %+v", wantType, seen)
73+ }
74+ }
75+}
76+
77+func decode(t *testing.T, raw []byte) bridge.Outgoing {
78+ t.Helper()
79+ var msg bridge.Outgoing
80+ if err := json.Unmarshal(raw, &msg); err != nil {
81+ t.Fatalf("undecodable message %q: %v", raw, err)
82+ }
83+ return msg
84+}
85+
86+func TestSubscribeSendsHello(t *testing.T) {
87+ b := bridge.New(nil)
88+ b.SetSession(&fakeSession{})
89+
90+ _, replay, unsubscribe := b.Subscribe()
91+ defer unsubscribe()
92+
93+ if len(replay) == 0 {
94+ t.Fatal("Subscribe returned no replay events, want at least the hello")
95+ }
96+ hello := decode(t, replay[0])
97+ if hello.Type != bridge.OutgoingHello || hello.SessionId != "sess-fake" {
98+ t.Errorf("first replay event = %+v, want hello for sess-fake", hello)
99+ }
100+}
101+
102+func TestPromptRunsTurnAndBroadcastsMarkers(t *testing.T) {
103+ b := bridge.New(nil)
104+ session := &fakeSession{}
105+ b.SetSession(session)
106+
107+ events, _, unsubscribe := b.Subscribe()
108+ defer unsubscribe()
109+
110+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"hello agent"}`))
111+
112+ seen := collect(t, events, bridge.OutgoingTurnEnded)
113+ if seen[0].Type != bridge.OutgoingUserMessage || seen[0].Text != "hello agent" {
114+ t.Errorf("first event = %+v, want the echoed user message", seen[0])
115+ }
116+ if seen[1].Type != bridge.OutgoingTurnStarted {
117+ t.Errorf("second event = %q, want turn_started", seen[1].Type)
118+ }
119+ last := seen[len(seen)-1]
120+ if last.StopReason != string(acp.StopReasonEndTurn) {
121+ t.Errorf("turn_ended stopReason = %q, want end_turn", last.StopReason)
122+ }
123+ session.mu.Lock()
124+ defer session.mu.Unlock()
125+ if len(session.prompts) != 1 || session.prompts[0] != "hello agent" {
126+ t.Errorf("session received prompts %v, want [hello agent]", session.prompts)
127+ }
128+}
129+
130+func TestSecondPromptDuringTurnIsRejected(t *testing.T) {
131+ b := bridge.New(nil)
132+ session := &fakeSession{block: make(chan struct{})}
133+ b.SetSession(session)
134+
135+ events, _, unsubscribe := b.Subscribe()
136+ defer unsubscribe()
137+
138+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"first"}`))
139+ collect(t, events, bridge.OutgoingTurnStarted)
140+
141+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"second"}`))
142+ seen := collect(t, events, bridge.OutgoingError)
143+ if last := seen[len(seen)-1]; last.Message != "a turn is already running" {
144+ t.Errorf("error message = %q", last.Message)
145+ }
146+
147+ close(session.block)
148+ collect(t, events, bridge.OutgoingTurnEnded)
149+}
150+
151+func TestPromptWithoutSessionReportsError(t *testing.T) {
152+ b := bridge.New(nil)
153+ events, _, unsubscribe := b.Subscribe()
154+ defer unsubscribe()
155+
156+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"x"}`))
157+ seen := collect(t, events, bridge.OutgoingError)
158+ if last := seen[len(seen)-1]; last.Message != "no agent session" {
159+ t.Errorf("error message = %q, want \"no agent session\"", last.Message)
160+ }
161+}
162+
163+func TestPromptFailureEndsTurnWithError(t *testing.T) {
164+ b := bridge.New(nil)
165+ b.SetSession(&fakeSession{respond: func(string) (acp.StopReason, error) {
166+ return "", errors.New("agent exploded")
167+ }})
168+ events, _, unsubscribe := b.Subscribe()
169+ defer unsubscribe()
170+
171+ b.HandleIncoming(context.Background(), []byte(`{"type":"prompt","text":"x"}`))
172+ seen := collect(t, events, bridge.OutgoingTurnEnded)
173+
174+ foundError := false
175+ for _, msg := range seen {
176+ if msg.Type == bridge.OutgoingError {
177+ foundError = true
178+ }
179+ }
180+ if !foundError {
181+ t.Error("no error event before turn_ended, want one mentioning the failure")
182+ }
183+}
184+
185+func TestCancelReachesSession(t *testing.T) {
186+ b := bridge.New(nil)
187+ session := &fakeSession{}
188+ b.SetSession(session)
189+
190+ b.HandleIncoming(context.Background(), []byte(`{"type":"cancel"}`))
191+
192+ session.mu.Lock()
193+ defer session.mu.Unlock()
194+ if !session.cancelled {
195+ t.Error("cancel never reached the session")
196+ }
197+}
198+
199+func TestMalformedAndUnknownMessagesReportErrors(t *testing.T) {
200+ b := bridge.New(nil)
201+ events, _, unsubscribe := b.Subscribe()
202+ defer unsubscribe()
203+
204+ b.HandleIncoming(context.Background(), []byte(`{not json`))
205+ collect(t, events, bridge.OutgoingError)
206+
207+ b.HandleIncoming(context.Background(), []byte(`{"type":"nope"}`))
208+ seen := collect(t, events, bridge.OutgoingError)
209+ if last := seen[len(seen)-1]; last.Message != `unknown message type "nope"` {
210+ t.Errorf("error message = %q", last.Message)
211+ }
212+}
213+
214+func TestSessionUpdateIsRelayedAndReplayed(t *testing.T) {
215+ b := bridge.New(nil)
216+ b.SetSession(&fakeSession{})
217+ events, _, unsubscribe := b.Subscribe()
218+ defer unsubscribe()
219+
220+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
221+ SessionId: "sess-fake",
222+ Update: acp.UpdateAgentMessageText("chunk!"),
223+ })
224+
225+ seen := collect(t, events, bridge.OutgoingSessionUpdate)
226+ update := seen[len(seen)-1].Update
227+ if update == nil {
228+ t.Fatal("session_update event has no update payload")
229+ }
230+ var acpUpdate struct {
231+ SessionUpdate string `json:"sessionUpdate"`
232+ Content struct {
233+ Text string `json:"text"`
234+ } `json:"content"`
235+ }
236+ if err := json.Unmarshal(update, &acpUpdate); err != nil {
237+ t.Fatalf("update payload is not the raw ACP shape: %v", err)
238+ }
239+ if acpUpdate.SessionUpdate != "agent_message_chunk" || acpUpdate.Content.Text != "chunk!" {
240+ t.Errorf("relayed update = %+v, want agent_message_chunk with text \"chunk!\"", acpUpdate)
241+ }
242+
243+ // A client connecting after the fact receives the update in its replay.
244+ _, replay, unsubscribeLate := b.Subscribe()
245+ defer unsubscribeLate()
246+ found := false
247+ for _, raw := range replay {
248+ if decode(t, raw).Type == bridge.OutgoingSessionUpdate {
249+ found = true
250+ }
251+ }
252+ if !found {
253+ t.Error("late subscriber's replay lacks the session update")
254+ }
255+}
256+
257+func TestPermissionRoundTrip(t *testing.T) {
258+ b := bridge.New(nil)
259+ b.SetSession(&fakeSession{})
260+ events, _, unsubscribe := b.Subscribe()
261+ defer unsubscribe()
262+
263+ type result struct {
264+ resp acp.RequestPermissionResponse
265+ err error
266+ }
267+ got := make(chan result, 1)
268+ go func() {
269+ resp, err := b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{
270+ SessionId: "sess-fake",
271+ Options: []acp.PermissionOption{{OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce}},
272+ })
273+ got <- result{resp, err}
274+ }()
275+
276+ seen := collect(t, events, bridge.OutgoingPermissionRequest)
277+ request := seen[len(seen)-1]
278+ if request.RequestId == "" || request.Request == nil {
279+ t.Fatalf("permission_request event incomplete: %+v", request)
280+ }
281+
282+ b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+request.RequestId+`","optionId":"allow"}`))
283+
284+ r := <-got
285+ if r.err != nil {
286+ t.Fatalf("HandlePermissionRequest returned an error: %v", r.err)
287+ }
288+ if r.resp.Outcome.Selected == nil || r.resp.Outcome.Selected.OptionId != "allow" {
289+ t.Errorf("outcome = %+v, want selected \"allow\"", r.resp.Outcome)
290+ }
291+
292+ collect(t, events, bridge.OutgoingPermissionResolved)
293+}
294+
295+func TestPermissionCancelledByAgentContext(t *testing.T) {
296+ b := bridge.New(nil)
297+ b.SetSession(&fakeSession{})
298+
299+ ctx, cancel := context.WithCancel(context.Background())
300+ go cancel()
301+
302+ resp, err := b.HandlePermissionRequest(ctx, acp.RequestPermissionRequest{SessionId: "sess-fake"})
303+ if err != nil {
304+ t.Fatalf("HandlePermissionRequest returned an error: %v", err)
305+ }
306+ if resp.Outcome.Cancelled == nil {
307+ t.Errorf("outcome = %+v, want cancelled", resp.Outcome)
308+ }
309+}
310+
311+func TestPendingPermissionIsReplayedToLateJoiner(t *testing.T) {
312+ b := bridge.New(nil)
313+ b.SetSession(&fakeSession{})
314+
315+ answered := make(chan struct{})
316+ go func() {
317+ _, _ = b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{SessionId: "sess-fake"})
318+ close(answered)
319+ }()
320+
321+ // Wait for the request to be registered by watching a live subscriber.
322+ events, _, unsubscribe := b.Subscribe()
323+ seen := collect(t, events, bridge.OutgoingPermissionRequest)
324+ requestId := seen[len(seen)-1].RequestId
325+ unsubscribe()
326+
327+ _, replay, unsubscribeLate := b.Subscribe()
328+ defer unsubscribeLate()
329+ found := false
330+ for _, raw := range replay {
331+ msg := decode(t, raw)
332+ if msg.Type == bridge.OutgoingPermissionRequest && msg.RequestId == requestId {
333+ found = true
334+ }
335+ }
336+ if !found {
337+ t.Error("late subscriber's replay lacks the pending permission request")
338+ }
339+
340+ b.HandleIncoming(context.Background(), []byte(`{"type":"permission_response","requestId":"`+requestId+`","cancelled":true}`))
341+ <-answered
342+}
added internal/bridge/protocol.go +91 -0
new file mode 100644
@@ -0,0 +1,91 @@
1+package bridge
2+
3+import "encoding/json"
4+
5+// This file is the wire contract between the SPA and the backend, over a
6+// WebSocket carrying one JSON message per text frame. ACP payloads (session
7+// updates, permission requests) are relayed verbatim so the frontend works
8+// with the protocol's own shapes, documented at
9+// https://agentclientprotocol.com.
10+
11+// Incoming message types (browser → server).
12+const (
13+ // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}.
14+ IncomingPrompt = "prompt"
15+ // IncomingCancel interrupts the current turn: {"type":"cancel"}.
16+ IncomingCancel = "cancel"
17+ // IncomingPermissionResponse answers a permission request:
18+ // {"type":"permission_response","requestId":"...","optionId":"..."} or
19+ // {"type":"permission_response","requestId":"...","cancelled":true}.
20+ IncomingPermissionResponse = "permission_response"
21+)
22+
23+// Outgoing message types (server → browser).
24+const (
25+ // OutgoingHello is sent once per connection with the session state.
26+ OutgoingHello = "hello"
27+ // OutgoingUserMessage echoes the prompt that started a turn, so every
28+ // client (and every reconnection replay) renders the user's side of the
29+ // conversation from the same source of truth as the agent's side.
30+ OutgoingUserMessage = "user_message"
31+ // OutgoingSessionUpdate relays one ACP session/update notification;
32+ // its "update" field is the raw ACP SessionUpdate object.
33+ OutgoingSessionUpdate = "session_update"
34+ // OutgoingPermissionRequest relays an ACP permission request; its
35+ // "request" field is the raw ACP RequestPermissionRequest object.
36+ OutgoingPermissionRequest = "permission_request"
37+ // OutgoingPermissionResolved tells every client a permission request
38+ // was answered (so open dialogs close everywhere).
39+ OutgoingPermissionResolved = "permission_resolved"
40+ // OutgoingTurnStarted marks the start of a prompt turn.
41+ OutgoingTurnStarted = "turn_started"
42+ // OutgoingTurnEnded marks the end of a turn, with its stop reason.
43+ OutgoingTurnEnded = "turn_ended"
44+ // OutgoingError carries a human-readable error message.
45+ OutgoingError = "error"
46+)
47+
48+// Incoming is a message sent by the browser.
49+type Incoming struct {
50+ Type string `json:"type"`
51+ // Text is the prompt text (IncomingPrompt).
52+ Text string `json:"text,omitempty"`
53+ // RequestId identifies the permission request being answered.
54+ RequestId string `json:"requestId,omitempty"`
55+ // OptionId is the chosen ACP permission option.
56+ OptionId string `json:"optionId,omitempty"`
57+ // Cancelled dismisses the permission request instead of choosing.
58+ Cancelled bool `json:"cancelled,omitempty"`
59+}
60+
61+// Outgoing is a message sent to the browser.
62+type Outgoing struct {
63+ Type string `json:"type"`
64+ // SessionId is set on OutgoingHello.
65+ SessionId string `json:"sessionId,omitempty"`
66+ // TurnActive is set on OutgoingHello: true while a prompt turn runs.
67+ TurnActive bool `json:"turnActive,omitempty"`
68+ // Update is the raw ACP SessionUpdate (OutgoingSessionUpdate).
69+ Update json.RawMessage `json:"update,omitempty"`
70+ // RequestId identifies a permission request across messages.
71+ RequestId string `json:"requestId,omitempty"`
72+ // Request is the raw ACP RequestPermissionRequest (OutgoingPermissionRequest).
73+ Request json.RawMessage `json:"request,omitempty"`
74+ // Text is the user's prompt text (OutgoingUserMessage).
75+ Text string `json:"text,omitempty"`
76+ // StopReason is the ACP stop reason (OutgoingTurnEnded).
77+ StopReason string `json:"stopReason,omitempty"`
78+ // Message is a human-readable error (OutgoingError).
79+ Message string `json:"message,omitempty"`
80+}
81+
82+// encode marshals an Outgoing message; the bridge treats a marshalling
83+// failure as a programming error and panics, because every field is
84+// marshal-safe by construction.
85+func encode(msg Outgoing) []byte {
86+ raw, err := json.Marshal(msg)
87+ if err != nil {
88+ panic(err)
89+ }
90+ return raw
91+}
new file mode 100644
@@ -0,0 +1,91 @@
1+package bridge
2+
3+import "encoding/json"
4+
5+// This file is the wire contract between the SPA and the backend, over a
6+// WebSocket carrying one JSON message per text frame. ACP payloads (session
7+// updates, permission requests) are relayed verbatim so the frontend works
8+// with the protocol's own shapes, documented at
9+// https://agentclientprotocol.com.
10+
11+// Incoming message types (browser → server).
12+const (
13+ // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}.
14+ IncomingPrompt = "prompt"
15+ // IncomingCancel interrupts the current turn: {"type":"cancel"}.
16+ IncomingCancel = "cancel"
17+ // IncomingPermissionResponse answers a permission request:
18+ // {"type":"permission_response","requestId":"...","optionId":"..."} or
19+ // {"type":"permission_response","requestId":"...","cancelled":true}.
20+ IncomingPermissionResponse = "permission_response"
21+)
22+
23+// Outgoing message types (server → browser).
24+const (
25+ // OutgoingHello is sent once per connection with the session state.
26+ OutgoingHello = "hello"
27+ // OutgoingUserMessage echoes the prompt that started a turn, so every
28+ // client (and every reconnection replay) renders the user's side of the
29+ // conversation from the same source of truth as the agent's side.
30+ OutgoingUserMessage = "user_message"
31+ // OutgoingSessionUpdate relays one ACP session/update notification;
32+ // its "update" field is the raw ACP SessionUpdate object.
33+ OutgoingSessionUpdate = "session_update"
34+ // OutgoingPermissionRequest relays an ACP permission request; its
35+ // "request" field is the raw ACP RequestPermissionRequest object.
36+ OutgoingPermissionRequest = "permission_request"
37+ // OutgoingPermissionResolved tells every client a permission request
38+ // was answered (so open dialogs close everywhere).
39+ OutgoingPermissionResolved = "permission_resolved"
40+ // OutgoingTurnStarted marks the start of a prompt turn.
41+ OutgoingTurnStarted = "turn_started"
42+ // OutgoingTurnEnded marks the end of a turn, with its stop reason.
43+ OutgoingTurnEnded = "turn_ended"
44+ // OutgoingError carries a human-readable error message.
45+ OutgoingError = "error"
46+)
47+
48+// Incoming is a message sent by the browser.
49+type Incoming struct {
50+ Type string `json:"type"`
51+ // Text is the prompt text (IncomingPrompt).
52+ Text string `json:"text,omitempty"`
53+ // RequestId identifies the permission request being answered.
54+ RequestId string `json:"requestId,omitempty"`
55+ // OptionId is the chosen ACP permission option.
56+ OptionId string `json:"optionId,omitempty"`
57+ // Cancelled dismisses the permission request instead of choosing.
58+ Cancelled bool `json:"cancelled,omitempty"`
59+}
60+
61+// Outgoing is a message sent to the browser.
62+type Outgoing struct {
63+ Type string `json:"type"`
64+ // SessionId is set on OutgoingHello.
65+ SessionId string `json:"sessionId,omitempty"`
66+ // TurnActive is set on OutgoingHello: true while a prompt turn runs.
67+ TurnActive bool `json:"turnActive,omitempty"`
68+ // Update is the raw ACP SessionUpdate (OutgoingSessionUpdate).
69+ Update json.RawMessage `json:"update,omitempty"`
70+ // RequestId identifies a permission request across messages.
71+ RequestId string `json:"requestId,omitempty"`
72+ // Request is the raw ACP RequestPermissionRequest (OutgoingPermissionRequest).
73+ Request json.RawMessage `json:"request,omitempty"`
74+ // Text is the user's prompt text (OutgoingUserMessage).
75+ Text string `json:"text,omitempty"`
76+ // StopReason is the ACP stop reason (OutgoingTurnEnded).
77+ StopReason string `json:"stopReason,omitempty"`
78+ // Message is a human-readable error (OutgoingError).
79+ Message string `json:"message,omitempty"`
80+}
81+
82+// encode marshals an Outgoing message; the bridge treats a marshalling
83+// failure as a programming error and panics, because every field is
84+// marshal-safe by construction.
85+func encode(msg Outgoing) []byte {
86+ raw, err := json.Marshal(msg)
87+ if err != nil {
88+ panic(err)
89+ }
90+ return raw
91+}
added internal/bridge/ws.go +89 -0
new file mode 100644
@@ -0,0 +1,89 @@
1+package bridge
2+
3+import (
4+ "context"
5+ "net/http"
6+
7+ "github.com/coder/websocket"
8+)
9+
10+// WebSocketHandler returns the HTTP handler that upgrades a request to a
11+// WebSocket and attaches it to the bridge: replayed history first, then live
12+// events, while incoming messages feed HandleIncoming.
13+//
14+// Example:
15+//
16+// mux.Handle("GET /ws", b.WebSocketHandler())
17+func (b *Bridge) WebSocketHandler() http.Handler {
18+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
20+ // ori serves localhost setups (possibly port-forwarded); the
21+ // SPA and this endpoint share an origin in production, and the
22+ // Vite dev server proxies /ws, so localhost patterns suffice.
23+ OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
24+ })
25+ if err != nil {
26+ b.logger.Warn("websocket accept failed", "error", err)
27+ return
28+ }
29+ defer func() { _ = conn.CloseNow() }()
30+ b.serveClient(r.Context(), conn)
31+ })
32+}
33+
34+// serveClient runs one client's session: replay, then a write pump for live
35+// events alongside a read loop for incoming messages, until either side ends.
36+func (b *Bridge) serveClient(ctx context.Context, conn *websocket.Conn) {
37+ events, replay, unsubscribe := b.Subscribe()
38+ defer unsubscribe()
39+
40+ if !writeEvents(ctx, conn, replay) {
41+ return
42+ }
43+
44+ writeDone := make(chan struct{})
45+ go func() {
46+ defer close(writeDone)
47+ pumpEvents(ctx, conn, events)
48+ }()
49+
50+ b.readLoop(ctx, conn)
51+
52+ // Stop the write pump by closing its channel, then close cleanly.
53+ unsubscribe()
54+ <-writeDone
55+ _ = conn.Close(websocket.StatusNormalClosure, "")
56+}
57+
58+// writeEvents sends a batch of events; false means the connection is gone.
59+func writeEvents(ctx context.Context, conn *websocket.Conn, events [][]byte) bool {
60+ for _, event := range events {
61+ if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
62+ return false
63+ }
64+ }
65+ return true
66+}
67+
68+// pumpEvents forwards live bridge events to the socket until the
69+// subscription channel closes or a write fails.
70+func pumpEvents(ctx context.Context, conn *websocket.Conn, events <-chan []byte) {
71+ for event := range events {
72+ if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
73+ return
74+ }
75+ }
76+}
77+
78+// readLoop feeds client messages to the bridge until the socket closes.
79+func (b *Bridge) readLoop(ctx context.Context, conn *websocket.Conn) {
80+ for {
81+ kind, data, err := conn.Read(ctx)
82+ if err != nil {
83+ return
84+ }
85+ if kind == websocket.MessageText {
86+ b.HandleIncoming(ctx, data)
87+ }
88+ }
89+}
new file mode 100644
@@ -0,0 +1,89 @@
1+package bridge
2+
3+import (
4+ "context"
5+ "net/http"
6+
7+ "github.com/coder/websocket"
8+)
9+
10+// WebSocketHandler returns the HTTP handler that upgrades a request to a
11+// WebSocket and attaches it to the bridge: replayed history first, then live
12+// events, while incoming messages feed HandleIncoming.
13+//
14+// Example:
15+//
16+// mux.Handle("GET /ws", b.WebSocketHandler())
17+func (b *Bridge) WebSocketHandler() http.Handler {
18+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
20+ // ori serves localhost setups (possibly port-forwarded); the
21+ // SPA and this endpoint share an origin in production, and the
22+ // Vite dev server proxies /ws, so localhost patterns suffice.
23+ OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
24+ })
25+ if err != nil {
26+ b.logger.Warn("websocket accept failed", "error", err)
27+ return
28+ }
29+ defer func() { _ = conn.CloseNow() }()
30+ b.serveClient(r.Context(), conn)
31+ })
32+}
33+
34+// serveClient runs one client's session: replay, then a write pump for live
35+// events alongside a read loop for incoming messages, until either side ends.
36+func (b *Bridge) serveClient(ctx context.Context, conn *websocket.Conn) {
37+ events, replay, unsubscribe := b.Subscribe()
38+ defer unsubscribe()
39+
40+ if !writeEvents(ctx, conn, replay) {
41+ return
42+ }
43+
44+ writeDone := make(chan struct{})
45+ go func() {
46+ defer close(writeDone)
47+ pumpEvents(ctx, conn, events)
48+ }()
49+
50+ b.readLoop(ctx, conn)
51+
52+ // Stop the write pump by closing its channel, then close cleanly.
53+ unsubscribe()
54+ <-writeDone
55+ _ = conn.Close(websocket.StatusNormalClosure, "")
56+}
57+
58+// writeEvents sends a batch of events; false means the connection is gone.
59+func writeEvents(ctx context.Context, conn *websocket.Conn, events [][]byte) bool {
60+ for _, event := range events {
61+ if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
62+ return false
63+ }
64+ }
65+ return true
66+}
67+
68+// pumpEvents forwards live bridge events to the socket until the
69+// subscription channel closes or a write fails.
70+func pumpEvents(ctx context.Context, conn *websocket.Conn, events <-chan []byte) {
71+ for event := range events {
72+ if err := conn.Write(ctx, websocket.MessageText, event); err != nil {
73+ return
74+ }
75+ }
76+}
77+
78+// readLoop feeds client messages to the bridge until the socket closes.
79+func (b *Bridge) readLoop(ctx context.Context, conn *websocket.Conn) {
80+ for {
81+ kind, data, err := conn.Read(ctx)
82+ if err != nil {
83+ return
84+ }
85+ if kind == websocket.MessageText {
86+ b.HandleIncoming(ctx, data)
87+ }
88+ }
89+}
added internal/bridge/ws_test.go +133 -0
new file mode 100644
@@ -0,0 +1,133 @@
1+package bridge_test
2+
3+import (
4+ "context"
5+ "net/http/httptest"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ acp "github.com/coder/acp-go-sdk"
11+ "github.com/coder/websocket"
12+ "github.com/coder/websocket/wsjson"
13+
14+ "github.com/bots-garden/ori/internal/bridge"
15+)
16+
17+// dialTestServer starts an HTTP test server exposing the bridge's WebSocket
18+// handler and connects a client to it.
19+func dialTestServer(t *testing.T, b *bridge.Bridge) (*websocket.Conn, context.Context) {
20+ t.Helper()
21+ server := httptest.NewServer(b.WebSocketHandler())
22+ t.Cleanup(server.Close)
23+
24+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
25+ t.Cleanup(cancel)
26+
27+ url := "ws" + strings.TrimPrefix(server.URL, "http")
28+ conn, _, err := websocket.Dial(ctx, url, nil)
29+ if err != nil {
30+ t.Fatalf("websocket dial failed: %v", err)
31+ }
32+ t.Cleanup(func() { _ = conn.CloseNow() })
33+ return conn, ctx
34+}
35+
36+// readUntil reads messages until the wanted type arrives.
37+func readUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, wantType string) bridge.Outgoing {
38+ t.Helper()
39+ for {
40+ var msg bridge.Outgoing
41+ if err := wsjson.Read(ctx, conn, &msg); err != nil {
42+ t.Fatalf("websocket read failed while waiting for %q: %v", wantType, err)
43+ }
44+ if msg.Type == wantType {
45+ return msg
46+ }
47+ }
48+}
49+
50+func TestWebSocketHelloThenPromptTurn(t *testing.T) {
51+ b := bridge.New(nil)
52+ b.SetSession(&fakeSession{})
53+ conn, ctx := dialTestServer(t, b)
54+
55+ hello := readUntil(t, ctx, conn, bridge.OutgoingHello)
56+ if hello.SessionId != "sess-fake" {
57+ t.Errorf("hello sessionId = %q, want sess-fake", hello.SessionId)
58+ }
59+
60+ if err := wsjson.Write(ctx, conn, bridge.Incoming{Type: bridge.IncomingPrompt, Text: "hi"}); err != nil {
61+ t.Fatalf("websocket write failed: %v", err)
62+ }
63+ readUntil(t, ctx, conn, bridge.OutgoingTurnStarted)
64+ ended := readUntil(t, ctx, conn, bridge.OutgoingTurnEnded)
65+ if ended.StopReason != string(acp.StopReasonEndTurn) {
66+ t.Errorf("turn_ended stopReason = %q, want end_turn", ended.StopReason)
67+ }
68+}
69+
70+func TestWebSocketReceivesSessionUpdates(t *testing.T) {
71+ b := bridge.New(nil)
72+ b.SetSession(&fakeSession{})
73+ conn, ctx := dialTestServer(t, b)
74+ readUntil(t, ctx, conn, bridge.OutgoingHello)
75+
76+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
77+ SessionId: "sess-fake",
78+ Update: acp.UpdateAgentMessageText("streamed"),
79+ })
80+
81+ update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate)
82+ if update.Update == nil || !strings.Contains(string(update.Update), "streamed") {
83+ t.Errorf("session_update payload = %s, want it to carry the streamed text", update.Update)
84+ }
85+}
86+
87+func TestWebSocketPermissionRoundTrip(t *testing.T) {
88+ b := bridge.New(nil)
89+ b.SetSession(&fakeSession{})
90+ conn, ctx := dialTestServer(t, b)
91+ readUntil(t, ctx, conn, bridge.OutgoingHello)
92+
93+ done := make(chan acp.RequestPermissionResponse, 1)
94+ go func() {
95+ resp, _ := b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{
96+ SessionId: "sess-fake",
97+ Options: []acp.PermissionOption{{OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce}},
98+ })
99+ done <- resp
100+ }()
101+
102+ request := readUntil(t, ctx, conn, bridge.OutgoingPermissionRequest)
103+ if err := wsjson.Write(ctx, conn, bridge.Incoming{
104+ Type: bridge.IncomingPermissionResponse,
105+ RequestId: request.RequestId,
106+ OptionId: "allow",
107+ }); err != nil {
108+ t.Fatalf("websocket write failed: %v", err)
109+ }
110+
111+ resp := <-done
112+ if resp.Outcome.Selected == nil || resp.Outcome.Selected.OptionId != "allow" {
113+ t.Errorf("outcome = %+v, want selected \"allow\"", resp.Outcome)
114+ }
115+ readUntil(t, ctx, conn, bridge.OutgoingPermissionResolved)
116+}
117+
118+func TestWebSocketLateJoinerGetsReplay(t *testing.T) {
119+ b := bridge.New(nil)
120+ b.SetSession(&fakeSession{})
121+
122+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
123+ SessionId: "sess-fake",
124+ Update: acp.UpdateAgentMessageText("before you arrived"),
125+ })
126+
127+ conn, ctx := dialTestServer(t, b)
128+ readUntil(t, ctx, conn, bridge.OutgoingHello)
129+ update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate)
130+ if !strings.Contains(string(update.Update), "before you arrived") {
131+ t.Errorf("replayed update = %s, want the pre-connection chunk", update.Update)
132+ }
133+}
new file mode 100644
@@ -0,0 +1,133 @@
1+package bridge_test
2+
3+import (
4+ "context"
5+ "net/http/httptest"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ acp "github.com/coder/acp-go-sdk"
11+ "github.com/coder/websocket"
12+ "github.com/coder/websocket/wsjson"
13+
14+ "github.com/bots-garden/ori/internal/bridge"
15+)
16+
17+// dialTestServer starts an HTTP test server exposing the bridge's WebSocket
18+// handler and connects a client to it.
19+func dialTestServer(t *testing.T, b *bridge.Bridge) (*websocket.Conn, context.Context) {
20+ t.Helper()
21+ server := httptest.NewServer(b.WebSocketHandler())
22+ t.Cleanup(server.Close)
23+
24+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
25+ t.Cleanup(cancel)
26+
27+ url := "ws" + strings.TrimPrefix(server.URL, "http")
28+ conn, _, err := websocket.Dial(ctx, url, nil)
29+ if err != nil {
30+ t.Fatalf("websocket dial failed: %v", err)
31+ }
32+ t.Cleanup(func() { _ = conn.CloseNow() })
33+ return conn, ctx
34+}
35+
36+// readUntil reads messages until the wanted type arrives.
37+func readUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, wantType string) bridge.Outgoing {
38+ t.Helper()
39+ for {
40+ var msg bridge.Outgoing
41+ if err := wsjson.Read(ctx, conn, &msg); err != nil {
42+ t.Fatalf("websocket read failed while waiting for %q: %v", wantType, err)
43+ }
44+ if msg.Type == wantType {
45+ return msg
46+ }
47+ }
48+}
49+
50+func TestWebSocketHelloThenPromptTurn(t *testing.T) {
51+ b := bridge.New(nil)
52+ b.SetSession(&fakeSession{})
53+ conn, ctx := dialTestServer(t, b)
54+
55+ hello := readUntil(t, ctx, conn, bridge.OutgoingHello)
56+ if hello.SessionId != "sess-fake" {
57+ t.Errorf("hello sessionId = %q, want sess-fake", hello.SessionId)
58+ }
59+
60+ if err := wsjson.Write(ctx, conn, bridge.Incoming{Type: bridge.IncomingPrompt, Text: "hi"}); err != nil {
61+ t.Fatalf("websocket write failed: %v", err)
62+ }
63+ readUntil(t, ctx, conn, bridge.OutgoingTurnStarted)
64+ ended := readUntil(t, ctx, conn, bridge.OutgoingTurnEnded)
65+ if ended.StopReason != string(acp.StopReasonEndTurn) {
66+ t.Errorf("turn_ended stopReason = %q, want end_turn", ended.StopReason)
67+ }
68+}
69+
70+func TestWebSocketReceivesSessionUpdates(t *testing.T) {
71+ b := bridge.New(nil)
72+ b.SetSession(&fakeSession{})
73+ conn, ctx := dialTestServer(t, b)
74+ readUntil(t, ctx, conn, bridge.OutgoingHello)
75+
76+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
77+ SessionId: "sess-fake",
78+ Update: acp.UpdateAgentMessageText("streamed"),
79+ })
80+
81+ update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate)
82+ if update.Update == nil || !strings.Contains(string(update.Update), "streamed") {
83+ t.Errorf("session_update payload = %s, want it to carry the streamed text", update.Update)
84+ }
85+}
86+
87+func TestWebSocketPermissionRoundTrip(t *testing.T) {
88+ b := bridge.New(nil)
89+ b.SetSession(&fakeSession{})
90+ conn, ctx := dialTestServer(t, b)
91+ readUntil(t, ctx, conn, bridge.OutgoingHello)
92+
93+ done := make(chan acp.RequestPermissionResponse, 1)
94+ go func() {
95+ resp, _ := b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{
96+ SessionId: "sess-fake",
97+ Options: []acp.PermissionOption{{OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce}},
98+ })
99+ done <- resp
100+ }()
101+
102+ request := readUntil(t, ctx, conn, bridge.OutgoingPermissionRequest)
103+ if err := wsjson.Write(ctx, conn, bridge.Incoming{
104+ Type: bridge.IncomingPermissionResponse,
105+ RequestId: request.RequestId,
106+ OptionId: "allow",
107+ }); err != nil {
108+ t.Fatalf("websocket write failed: %v", err)
109+ }
110+
111+ resp := <-done
112+ if resp.Outcome.Selected == nil || resp.Outcome.Selected.OptionId != "allow" {
113+ t.Errorf("outcome = %+v, want selected \"allow\"", resp.Outcome)
114+ }
115+ readUntil(t, ctx, conn, bridge.OutgoingPermissionResolved)
116+}
117+
118+func TestWebSocketLateJoinerGetsReplay(t *testing.T) {
119+ b := bridge.New(nil)
120+ b.SetSession(&fakeSession{})
121+
122+ b.HandleSessionUpdate(context.Background(), acp.SessionNotification{
123+ SessionId: "sess-fake",
124+ Update: acp.UpdateAgentMessageText("before you arrived"),
125+ })
126+
127+ conn, ctx := dialTestServer(t, b)
128+ readUntil(t, ctx, conn, bridge.OutgoingHello)
129+ update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate)
130+ if !strings.Contains(string(update.Update), "before you arrived") {
131+ t.Errorf("replayed update = %s, want the pre-connection chunk", update.Update)
132+ }
133+}
added internal/config/config.go +77 -0
new file mode 100644
@@ -0,0 +1,77 @@
1+// Package config parses the command-line configuration of the ori server.
2+//
3+// It is deliberately tiny: three flags, validated once at startup, then passed
4+// around as an immutable value.
5+package config
6+
7+import (
8+ "flag"
9+ "fmt"
10+ "os"
11+ "path/filepath"
12+ "strings"
13+)
14+
15+// DefaultAgentCommand is the ACP agent spawned when --agent-cmd is not given.
16+// It is the official Claude Code ACP adapter, the same one Zed uses.
17+const DefaultAgentCommand = "npx -y @zed-industries/claude-code-acp"
18+
19+// Config holds the runtime configuration of the ori server.
20+type Config struct {
21+ // Addr is the TCP address the HTTP server listens on, e.g. ":8888".
22+ Addr string
23+ // Cwd is the absolute working directory handed to the agent session.
24+ Cwd string
25+ // AgentCommand is the command line used to spawn the ACP agent,
26+ // split on whitespace (the first field is the executable).
27+ AgentCommand []string
28+}
29+
30+// FromArgs parses command-line arguments (without the program name) into a
31+// Config. It returns an error for unknown flags, an empty agent command, or a
32+// working directory that does not exist.
33+//
34+// Example:
35+//
36+// cfg, err := config.FromArgs([]string{"--addr", ":9000", "--cwd", "/tmp"})
37+// if err != nil {
38+// log.Fatal(err)
39+// }
40+// fmt.Println(cfg.Addr) // ":9000"
41+func FromArgs(args []string) (Config, error) {
42+ fs := flag.NewFlagSet("ori", flag.ContinueOnError)
43+ addr := fs.String("addr", ":8888", "TCP address the HTTP server listens on")
44+ cwd := fs.String("cwd", "", "working directory for the agent session (default: current directory)")
45+ agentCmd := fs.String("agent-cmd", DefaultAgentCommand, "command spawning the ACP agent, split on whitespace")
46+
47+ if err := fs.Parse(args); err != nil {
48+ return Config{}, err
49+ }
50+
51+ command := strings.Fields(*agentCmd)
52+ if len(command) == 0 {
53+ return Config{}, fmt.Errorf("--agent-cmd must not be empty")
54+ }
55+
56+ dir := *cwd
57+ if dir == "" {
58+ wd, err := os.Getwd()
59+ if err != nil {
60+ return Config{}, fmt.Errorf("resolve current directory: %w", err)
61+ }
62+ dir = wd
63+ }
64+ absDir, err := filepath.Abs(dir)
65+ if err != nil {
66+ return Config{}, fmt.Errorf("resolve --cwd: %w", err)
67+ }
68+ info, err := os.Stat(absDir)
69+ if err != nil {
70+ return Config{}, fmt.Errorf("--cwd: %w", err)
71+ }
72+ if !info.IsDir() {
73+ return Config{}, fmt.Errorf("--cwd: %s is not a directory", absDir)
74+ }
75+
76+ return Config{Addr: *addr, Cwd: absDir, AgentCommand: command}, nil
77+}
new file mode 100644
@@ -0,0 +1,77 @@
1+// Package config parses the command-line configuration of the ori server.
2+//
3+// It is deliberately tiny: three flags, validated once at startup, then passed
4+// around as an immutable value.
5+package config
6+
7+import (
8+ "flag"
9+ "fmt"
10+ "os"
11+ "path/filepath"
12+ "strings"
13+)
14+
15+// DefaultAgentCommand is the ACP agent spawned when --agent-cmd is not given.
16+// It is the official Claude Code ACP adapter, the same one Zed uses.
17+const DefaultAgentCommand = "npx -y @zed-industries/claude-code-acp"
18+
19+// Config holds the runtime configuration of the ori server.
20+type Config struct {
21+ // Addr is the TCP address the HTTP server listens on, e.g. ":8888".
22+ Addr string
23+ // Cwd is the absolute working directory handed to the agent session.
24+ Cwd string
25+ // AgentCommand is the command line used to spawn the ACP agent,
26+ // split on whitespace (the first field is the executable).
27+ AgentCommand []string
28+}
29+
30+// FromArgs parses command-line arguments (without the program name) into a
31+// Config. It returns an error for unknown flags, an empty agent command, or a
32+// working directory that does not exist.
33+//
34+// Example:
35+//
36+// cfg, err := config.FromArgs([]string{"--addr", ":9000", "--cwd", "/tmp"})
37+// if err != nil {
38+// log.Fatal(err)
39+// }
40+// fmt.Println(cfg.Addr) // ":9000"
41+func FromArgs(args []string) (Config, error) {
42+ fs := flag.NewFlagSet("ori", flag.ContinueOnError)
43+ addr := fs.String("addr", ":8888", "TCP address the HTTP server listens on")
44+ cwd := fs.String("cwd", "", "working directory for the agent session (default: current directory)")
45+ agentCmd := fs.String("agent-cmd", DefaultAgentCommand, "command spawning the ACP agent, split on whitespace")
46+
47+ if err := fs.Parse(args); err != nil {
48+ return Config{}, err
49+ }
50+
51+ command := strings.Fields(*agentCmd)
52+ if len(command) == 0 {
53+ return Config{}, fmt.Errorf("--agent-cmd must not be empty")
54+ }
55+
56+ dir := *cwd
57+ if dir == "" {
58+ wd, err := os.Getwd()
59+ if err != nil {
60+ return Config{}, fmt.Errorf("resolve current directory: %w", err)
61+ }
62+ dir = wd
63+ }
64+ absDir, err := filepath.Abs(dir)
65+ if err != nil {
66+ return Config{}, fmt.Errorf("resolve --cwd: %w", err)
67+ }
68+ info, err := os.Stat(absDir)
69+ if err != nil {
70+ return Config{}, fmt.Errorf("--cwd: %w", err)
71+ }
72+ if !info.IsDir() {
73+ return Config{}, fmt.Errorf("--cwd: %s is not a directory", absDir)
74+ }
75+
76+ return Config{Addr: *addr, Cwd: absDir, AgentCommand: command}, nil
77+}
added internal/config/config_test.go +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+package config_test
2+
3+import (
4+ "fmt"
5+ "log"
6+ "os"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bots-garden/ori/internal/config"
11+)
12+
13+func TestFromArgsDefaults(t *testing.T) {
14+ cfg, err := config.FromArgs(nil)
15+ if err != nil {
16+ t.Fatalf("FromArgs(nil) returned an error: %v", err)
17+ }
18+ if cfg.Addr != ":8888" {
19+ t.Errorf("default Addr = %q, want %q", cfg.Addr, ":8888")
20+ }
21+ wd, _ := os.Getwd()
22+ if cfg.Cwd != wd {
23+ t.Errorf("default Cwd = %q, want current directory %q", cfg.Cwd, wd)
24+ }
25+ want := strings.Fields(config.DefaultAgentCommand)
26+ if strings.Join(cfg.AgentCommand, " ") != strings.Join(want, " ") {
27+ t.Errorf("default AgentCommand = %v, want %v", cfg.AgentCommand, want)
28+ }
29+}
30+
31+func TestFromArgsOverrides(t *testing.T) {
32+ dir := t.TempDir()
33+ cfg, err := config.FromArgs([]string{"--addr", ":9000", "--cwd", dir, "--agent-cmd", "gemini --experimental-acp"})
34+ if err != nil {
35+ t.Fatalf("FromArgs returned an error: %v", err)
36+ }
37+ if cfg.Addr != ":9000" {
38+ t.Errorf("Addr = %q, want %q", cfg.Addr, ":9000")
39+ }
40+ if cfg.Cwd != dir {
41+ t.Errorf("Cwd = %q, want %q", cfg.Cwd, dir)
42+ }
43+ if len(cfg.AgentCommand) != 2 || cfg.AgentCommand[0] != "gemini" || cfg.AgentCommand[1] != "--experimental-acp" {
44+ t.Errorf("AgentCommand = %v, want [gemini --experimental-acp]", cfg.AgentCommand)
45+ }
46+}
47+
48+func TestFromArgsRejectsEmptyAgentCommand(t *testing.T) {
49+ if _, err := config.FromArgs([]string{"--agent-cmd", " "}); err == nil {
50+ t.Fatal("FromArgs accepted an empty --agent-cmd, want an error")
51+ }
52+}
53+
54+func TestFromArgsRejectsMissingCwd(t *testing.T) {
55+ if _, err := config.FromArgs([]string{"--cwd", "/does/not/exist"}); err == nil {
56+ t.Fatal("FromArgs accepted a non-existent --cwd, want an error")
57+ }
58+}
59+
60+func TestFromArgsRejectsFileAsCwd(t *testing.T) {
61+ file := t.TempDir() + "/plain-file"
62+ if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
63+ t.Fatal(err)
64+ }
65+ if _, err := config.FromArgs([]string{"--cwd", file}); err == nil {
66+ t.Fatal("FromArgs accepted a plain file as --cwd, want an error")
67+ }
68+}
69+
70+func TestFromArgsRejectsUnknownFlag(t *testing.T) {
71+ if _, err := config.FromArgs([]string{"--nope"}); err == nil {
72+ t.Fatal("FromArgs accepted an unknown flag, want an error")
73+ }
74+}
75+
76+func ExampleFromArgs() {
77+ cfg, err := config.FromArgs([]string{"--addr", ":9000"})
78+ if err != nil {
79+ log.Fatal(err)
80+ }
81+ fmt.Println(cfg.Addr)
82+ // Output: :9000
83+}
new file mode 100644
@@ -0,0 +1,83 @@
1+package config_test
2+
3+import (
4+ "fmt"
5+ "log"
6+ "os"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bots-garden/ori/internal/config"
11+)
12+
13+func TestFromArgsDefaults(t *testing.T) {
14+ cfg, err := config.FromArgs(nil)
15+ if err != nil {
16+ t.Fatalf("FromArgs(nil) returned an error: %v", err)
17+ }
18+ if cfg.Addr != ":8888" {
19+ t.Errorf("default Addr = %q, want %q", cfg.Addr, ":8888")
20+ }
21+ wd, _ := os.Getwd()
22+ if cfg.Cwd != wd {
23+ t.Errorf("default Cwd = %q, want current directory %q", cfg.Cwd, wd)
24+ }
25+ want := strings.Fields(config.DefaultAgentCommand)
26+ if strings.Join(cfg.AgentCommand, " ") != strings.Join(want, " ") {
27+ t.Errorf("default AgentCommand = %v, want %v", cfg.AgentCommand, want)
28+ }
29+}
30+
31+func TestFromArgsOverrides(t *testing.T) {
32+ dir := t.TempDir()
33+ cfg, err := config.FromArgs([]string{"--addr", ":9000", "--cwd", dir, "--agent-cmd", "gemini --experimental-acp"})
34+ if err != nil {
35+ t.Fatalf("FromArgs returned an error: %v", err)
36+ }
37+ if cfg.Addr != ":9000" {
38+ t.Errorf("Addr = %q, want %q", cfg.Addr, ":9000")
39+ }
40+ if cfg.Cwd != dir {
41+ t.Errorf("Cwd = %q, want %q", cfg.Cwd, dir)
42+ }
43+ if len(cfg.AgentCommand) != 2 || cfg.AgentCommand[0] != "gemini" || cfg.AgentCommand[1] != "--experimental-acp" {
44+ t.Errorf("AgentCommand = %v, want [gemini --experimental-acp]", cfg.AgentCommand)
45+ }
46+}
47+
48+func TestFromArgsRejectsEmptyAgentCommand(t *testing.T) {
49+ if _, err := config.FromArgs([]string{"--agent-cmd", " "}); err == nil {
50+ t.Fatal("FromArgs accepted an empty --agent-cmd, want an error")
51+ }
52+}
53+
54+func TestFromArgsRejectsMissingCwd(t *testing.T) {
55+ if _, err := config.FromArgs([]string{"--cwd", "/does/not/exist"}); err == nil {
56+ t.Fatal("FromArgs accepted a non-existent --cwd, want an error")
57+ }
58+}
59+
60+func TestFromArgsRejectsFileAsCwd(t *testing.T) {
61+ file := t.TempDir() + "/plain-file"
62+ if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
63+ t.Fatal(err)
64+ }
65+ if _, err := config.FromArgs([]string{"--cwd", file}); err == nil {
66+ t.Fatal("FromArgs accepted a plain file as --cwd, want an error")
67+ }
68+}
69+
70+func TestFromArgsRejectsUnknownFlag(t *testing.T) {
71+ if _, err := config.FromArgs([]string{"--nope"}); err == nil {
72+ t.Fatal("FromArgs accepted an unknown flag, want an error")
73+ }
74+}
75+
76+func ExampleFromArgs() {
77+ cfg, err := config.FromArgs([]string{"--addr", ":9000"})
78+ if err != nil {
79+ log.Fatal(err)
80+ }
81+ fmt.Println(cfg.Addr)
82+ // Output: :9000
83+}
added internal/httpserver/httpserver.go +54 -0
new file mode 100644
@@ -0,0 +1,54 @@
1+// Package httpserver assembles ori's HTTP surface: the embedded single-page
2+// application, a health check, and (from later steps) the WebSocket bridge to
3+// the ACP agent.
4+package httpserver
5+
6+import (
7+ "io/fs"
8+ "net/http"
9+)
10+
11+// Handler builds the root http.Handler of the ori server.
12+//
13+// uiFS is the root of the built SPA (index.html at its root, see ui.Dist).
14+// extra maps additional route patterns (http.ServeMux syntax) to handlers and
15+// is how the WebSocket endpoint plugs in without this package importing it.
16+//
17+// Example:
18+//
19+// h := httpserver.Handler(ui.Dist(), map[string]http.Handler{
20+// "GET /ws": wsHandler,
21+// })
22+// log.Fatal(http.ListenAndServe(":8888", h))
23+func Handler(uiFS fs.FS, extra map[string]http.Handler) http.Handler {
24+ mux := http.NewServeMux()
25+ mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
26+ w.Header().Set("Content-Type", "application/json")
27+ _, _ = w.Write([]byte(`{"status":"ok"}`))
28+ })
29+ for pattern, handler := range extra {
30+ mux.Handle(pattern, handler)
31+ }
32+ mux.Handle("/", spaHandler(uiFS))
33+ return mux
34+}
35+
36+// spaHandler serves the static files of the SPA and falls back to index.html
37+// for any unknown path, so client-side routes deep-link correctly.
38+func spaHandler(uiFS fs.FS) http.Handler {
39+ fileServer := http.FileServerFS(uiFS)
40+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
41+ if _, err := fs.Stat(uiFS, "index.html"); err != nil {
42+ http.Error(w, "UI not built: run `make build-ui` and rebuild the server", http.StatusServiceUnavailable)
43+ return
44+ }
45+ path := r.URL.Path
46+ if path != "/" {
47+ if _, err := fs.Stat(uiFS, path[1:]); err != nil {
48+ // Unknown path: let the SPA router handle it.
49+ r.URL.Path = "/"
50+ }
51+ }
52+ fileServer.ServeHTTP(w, r)
53+ })
54+}
new file mode 100644
@@ -0,0 +1,54 @@
1+// Package httpserver assembles ori's HTTP surface: the embedded single-page
2+// application, a health check, and (from later steps) the WebSocket bridge to
3+// the ACP agent.
4+package httpserver
5+
6+import (
7+ "io/fs"
8+ "net/http"
9+)
10+
11+// Handler builds the root http.Handler of the ori server.
12+//
13+// uiFS is the root of the built SPA (index.html at its root, see ui.Dist).
14+// extra maps additional route patterns (http.ServeMux syntax) to handlers and
15+// is how the WebSocket endpoint plugs in without this package importing it.
16+//
17+// Example:
18+//
19+// h := httpserver.Handler(ui.Dist(), map[string]http.Handler{
20+// "GET /ws": wsHandler,
21+// })
22+// log.Fatal(http.ListenAndServe(":8888", h))
23+func Handler(uiFS fs.FS, extra map[string]http.Handler) http.Handler {
24+ mux := http.NewServeMux()
25+ mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
26+ w.Header().Set("Content-Type", "application/json")
27+ _, _ = w.Write([]byte(`{"status":"ok"}`))
28+ })
29+ for pattern, handler := range extra {
30+ mux.Handle(pattern, handler)
31+ }
32+ mux.Handle("/", spaHandler(uiFS))
33+ return mux
34+}
35+
36+// spaHandler serves the static files of the SPA and falls back to index.html
37+// for any unknown path, so client-side routes deep-link correctly.
38+func spaHandler(uiFS fs.FS) http.Handler {
39+ fileServer := http.FileServerFS(uiFS)
40+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
41+ if _, err := fs.Stat(uiFS, "index.html"); err != nil {
42+ http.Error(w, "UI not built: run `make build-ui` and rebuild the server", http.StatusServiceUnavailable)
43+ return
44+ }
45+ path := r.URL.Path
46+ if path != "/" {
47+ if _, err := fs.Stat(uiFS, path[1:]); err != nil {
48+ // Unknown path: let the SPA router handle it.
49+ r.URL.Path = "/"
50+ }
51+ }
52+ fileServer.ServeHTTP(w, r)
53+ })
54+}
added internal/httpserver/httpserver_test.go +86 -0
new file mode 100644
@@ -0,0 +1,86 @@
1+package httpserver_test
2+
3+import (
4+ "io"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+ "testing/fstest"
9+
10+ "github.com/bots-garden/ori/internal/httpserver"
11+)
12+
13+// builtUI mimics a Vite production build.
14+var builtUI = fstest.MapFS{
15+ "index.html": {Data: []byte("<html>ori</html>")},
16+ "assets/app.js": {Data: []byte("console.log('ori')")},
17+ "assets/app.css": {Data: []byte("body{}")},
18+ "vite.svg": {Data: []byte("<svg/>")},
19+}
20+
21+func get(t *testing.T, handler http.Handler, path string) *httptest.ResponseRecorder {
22+ t.Helper()
23+ rec := httptest.NewRecorder()
24+ handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
25+ return rec
26+}
27+
28+func TestHealthz(t *testing.T) {
29+ rec := get(t, httpserver.Handler(builtUI, nil), "/healthz")
30+ if rec.Code != http.StatusOK {
31+ t.Fatalf("GET /healthz status = %d, want 200", rec.Code)
32+ }
33+ if body := rec.Body.String(); body != `{"status":"ok"}` {
34+ t.Errorf("GET /healthz body = %q", body)
35+ }
36+}
37+
38+func TestServesIndex(t *testing.T) {
39+ rec := get(t, httpserver.Handler(builtUI, nil), "/")
40+ if rec.Code != http.StatusOK {
41+ t.Fatalf("GET / status = %d, want 200", rec.Code)
42+ }
43+ if body := rec.Body.String(); body != "<html>ori</html>" {
44+ t.Errorf("GET / body = %q, want index.html content", body)
45+ }
46+}
47+
48+func TestServesStaticAsset(t *testing.T) {
49+ rec := get(t, httpserver.Handler(builtUI, nil), "/assets/app.js")
50+ if rec.Code != http.StatusOK {
51+ t.Fatalf("GET /assets/app.js status = %d, want 200", rec.Code)
52+ }
53+ if body := rec.Body.String(); body != "console.log('ori')" {
54+ t.Errorf("GET /assets/app.js body = %q", body)
55+ }
56+}
57+
58+func TestUnknownPathFallsBackToIndex(t *testing.T) {
59+ rec := get(t, httpserver.Handler(builtUI, nil), "/some/client/route")
60+ if rec.Code != http.StatusOK {
61+ t.Fatalf("GET /some/client/route status = %d, want 200 (SPA fallback)", rec.Code)
62+ }
63+ if body := rec.Body.String(); body != "<html>ori</html>" {
64+ t.Errorf("SPA fallback body = %q, want index.html content", body)
65+ }
66+}
67+
68+func TestMissingBuildAnswers503(t *testing.T) {
69+ empty := fstest.MapFS{".gitkeep": {Data: nil}}
70+ rec := get(t, httpserver.Handler(empty, nil), "/")
71+ if rec.Code != http.StatusServiceUnavailable {
72+ t.Fatalf("GET / without a UI build status = %d, want 503", rec.Code)
73+ }
74+}
75+
76+func TestExtraRouteIsMounted(t *testing.T) {
77+ extra := map[string]http.Handler{
78+ "GET /ws": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
79+ _, _ = io.WriteString(w, "ws-here")
80+ }),
81+ }
82+ rec := get(t, httpserver.Handler(builtUI, extra), "/ws")
83+ if rec.Code != http.StatusOK || rec.Body.String() != "ws-here" {
84+ t.Fatalf("GET /ws = %d %q, want 200 \"ws-here\"", rec.Code, rec.Body.String())
85+ }
86+}
new file mode 100644
@@ -0,0 +1,86 @@
1+package httpserver_test
2+
3+import (
4+ "io"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+ "testing/fstest"
9+
10+ "github.com/bots-garden/ori/internal/httpserver"
11+)
12+
13+// builtUI mimics a Vite production build.
14+var builtUI = fstest.MapFS{
15+ "index.html": {Data: []byte("<html>ori</html>")},
16+ "assets/app.js": {Data: []byte("console.log('ori')")},
17+ "assets/app.css": {Data: []byte("body{}")},
18+ "vite.svg": {Data: []byte("<svg/>")},
19+}
20+
21+func get(t *testing.T, handler http.Handler, path string) *httptest.ResponseRecorder {
22+ t.Helper()
23+ rec := httptest.NewRecorder()
24+ handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
25+ return rec
26+}
27+
28+func TestHealthz(t *testing.T) {
29+ rec := get(t, httpserver.Handler(builtUI, nil), "/healthz")
30+ if rec.Code != http.StatusOK {
31+ t.Fatalf("GET /healthz status = %d, want 200", rec.Code)
32+ }
33+ if body := rec.Body.String(); body != `{"status":"ok"}` {
34+ t.Errorf("GET /healthz body = %q", body)
35+ }
36+}
37+
38+func TestServesIndex(t *testing.T) {
39+ rec := get(t, httpserver.Handler(builtUI, nil), "/")
40+ if rec.Code != http.StatusOK {
41+ t.Fatalf("GET / status = %d, want 200", rec.Code)
42+ }
43+ if body := rec.Body.String(); body != "<html>ori</html>" {
44+ t.Errorf("GET / body = %q, want index.html content", body)
45+ }
46+}
47+
48+func TestServesStaticAsset(t *testing.T) {
49+ rec := get(t, httpserver.Handler(builtUI, nil), "/assets/app.js")
50+ if rec.Code != http.StatusOK {
51+ t.Fatalf("GET /assets/app.js status = %d, want 200", rec.Code)
52+ }
53+ if body := rec.Body.String(); body != "console.log('ori')" {
54+ t.Errorf("GET /assets/app.js body = %q", body)
55+ }
56+}
57+
58+func TestUnknownPathFallsBackToIndex(t *testing.T) {
59+ rec := get(t, httpserver.Handler(builtUI, nil), "/some/client/route")
60+ if rec.Code != http.StatusOK {
61+ t.Fatalf("GET /some/client/route status = %d, want 200 (SPA fallback)", rec.Code)
62+ }
63+ if body := rec.Body.String(); body != "<html>ori</html>" {
64+ t.Errorf("SPA fallback body = %q, want index.html content", body)
65+ }
66+}
67+
68+func TestMissingBuildAnswers503(t *testing.T) {
69+ empty := fstest.MapFS{".gitkeep": {Data: nil}}
70+ rec := get(t, httpserver.Handler(empty, nil), "/")
71+ if rec.Code != http.StatusServiceUnavailable {
72+ t.Fatalf("GET / without a UI build status = %d, want 503", rec.Code)
73+ }
74+}
75+
76+func TestExtraRouteIsMounted(t *testing.T) {
77+ extra := map[string]http.Handler{
78+ "GET /ws": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
79+ _, _ = io.WriteString(w, "ws-here")
80+ }),
81+ }
82+ rec := get(t, httpserver.Handler(builtUI, extra), "/ws")
83+ if rec.Code != http.StatusOK || rec.Body.String() != "ws-here" {
84+ t.Fatalf("GET /ws = %d %q, want 200 \"ws-here\"", rec.Code, rec.Body.String())
85+ }
86+}
added internal/mockagent/mockagent.go +235 -0
new file mode 100644
@@ -0,0 +1,235 @@
1+// Package mockagent implements a small, deterministic ACP agent used to
2+// exercise and demo ori without any real AI agent behind it: it streams a
3+// thought, a plan, tool calls (including a file diff), asks one permission,
4+// then answers. The ori-mock-agent command exposes it on stdio.
5+package mockagent
6+
7+import (
8+ "context"
9+ "fmt"
10+ "io"
11+ "strconv"
12+ "sync"
13+ "time"
14+
15+ acp "github.com/coder/acp-go-sdk"
16+)
17+
18+// Agent is the mock ACP agent. The embedded acp.Agent interface covers the
19+// protocol methods ori's client never calls (session/load, logout, ...);
20+// invoking one of them would panic, which is acceptable for a test double.
21+type Agent struct {
22+ acp.Agent
23+
24+ // Delay spaces the streamed updates out so a human watching the UI sees
25+ // the streaming happen. Tests keep it at zero.
26+ Delay time.Duration
27+
28+ conn *acp.AgentSideConnection
29+ mu sync.Mutex
30+ sessions int
31+}
32+
33+// New returns a mock agent; wire it with SetAgentConnection before use.
34+func New() *Agent { return &Agent{} }
35+
36+// SetAgentConnection injects the connection used to push updates and ask
37+// permissions. The SDK does not do this automatically.
38+func (a *Agent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
39+
40+// Run serves the mock agent over the given transport (stdio in the command)
41+// and blocks until the peer disconnects.
42+//
43+// Example:
44+//
45+// agent := mockagent.New()
46+// if err := agent.Run(os.Stdin, os.Stdout); err != nil {
47+// log.Fatal(err)
48+// }
49+func (a *Agent) Run(in io.Reader, out io.Writer) error {
50+ conn := acp.NewAgentSideConnection(a, out, in)
51+ a.SetAgentConnection(conn)
52+ <-conn.Done()
53+ return nil
54+}
55+
56+func (a *Agent) Initialize(_ context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
57+ return acp.InitializeResponse{ProtocolVersion: params.ProtocolVersion}, nil
58+}
59+
60+func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
61+ return acp.AuthenticateResponse{}, nil
62+}
63+
64+func (a *Agent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
65+ a.mu.Lock()
66+ defer a.mu.Unlock()
67+ a.sessions++
68+ return acp.NewSessionResponse{SessionId: acp.SessionId("mock-" + strconv.Itoa(a.sessions))}, nil
69+}
70+
71+// Cancel is a no-op: the SDK already cancels the context of the running
72+// Prompt when session/cancel arrives, which the scenario checks at each step.
73+func (a *Agent) Cancel(context.Context, acp.CancelNotification) error { return nil }
74+
75+// Prompt plays the demo scenario and streams it to the client.
76+func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
77+ scenario := &scenario{agent: a, ctx: ctx, sessionId: params.SessionId, prompt: promptText(params)}
78+ stop, err := scenario.play()
79+ if err != nil {
80+ if ctx.Err() != nil {
81+ return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
82+ }
83+ return acp.PromptResponse{}, err
84+ }
85+ return acp.PromptResponse{StopReason: stop}, nil
86+}
87+
88+// promptText joins the text blocks of the user's prompt.
89+func promptText(params acp.PromptRequest) string {
90+ text := ""
91+ for _, block := range params.Prompt {
92+ if block.Text != nil {
93+ text += block.Text.Text
94+ }
95+ }
96+ return text
97+}
98+
99+// scenario streams one full demo turn step by step.
100+type scenario struct {
101+ agent *Agent
102+ ctx context.Context
103+ sessionId acp.SessionId
104+ prompt string
105+}
106+
107+func (s *scenario) play() (acp.StopReason, error) {
108+ if err := s.runSteps(s.sayThought, s.sendPlan, s.sendReadToolCall, s.sayIntro); err != nil {
109+ return "", err
110+ }
111+
112+ allowed, err := s.askPermission()
113+ if err != nil {
114+ return "", err
115+ }
116+ if !allowed {
117+ return acp.StopReasonRefusal, s.runSteps(s.sayRefused)
118+ }
119+
120+ return acp.StopReasonEndTurn, s.runSteps(s.sendEditToolCall, s.saySummary, s.completePlan)
121+}
122+
123+func (s *scenario) sayThought() error {
124+ return s.update(acp.UpdateAgentThoughtText("The user wants a demo. Let me show every panel feature."))
125+}
126+
127+func (s *scenario) sayIntro() error {
128+ return s.update(acp.UpdateAgentMessageText("I looked at the project. "))
129+}
130+
131+func (s *scenario) sayRefused() error {
132+ return s.update(acp.UpdateAgentMessageText("Understood, I will not write the file."))
133+}
134+
135+func (s *scenario) saySummary() error {
136+ return s.update(acp.UpdateAgentMessageText(fmt.Sprintf("Done! You said: **%s**\n\n- streamed thought ✅\n- plan ✅\n- tool calls ✅\n- permission ✅", s.prompt)))
137+}
138+
139+// runSteps executes scenario actions in order, stopping at the first error.
140+func (s *scenario) runSteps(steps ...func() error) error {
141+ for _, step := range steps {
142+ if err := s.step(step); err != nil {
143+ return err
144+ }
145+ }
146+ return nil
147+}
148+
149+// step runs one scenario action, respecting cancellation and pacing.
150+func (s *scenario) step(action func() error) error {
151+ if err := s.ctx.Err(); err != nil {
152+ return err
153+ }
154+ if s.agent.Delay > 0 {
155+ select {
156+ case <-time.After(s.agent.Delay):
157+ case <-s.ctx.Done():
158+ return s.ctx.Err()
159+ }
160+ }
161+ return action()
162+}
163+
164+func (s *scenario) update(u acp.SessionUpdate) error {
165+ return s.agent.conn.SessionUpdate(s.ctx, acp.SessionNotification{SessionId: s.sessionId, Update: u})
166+}
167+
168+func (s *scenario) sendPlan() error {
169+ return s.update(acp.UpdatePlan(
170+ acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusInProgress},
171+ acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusPending},
172+ ))
173+}
174+
175+func (s *scenario) completePlan() error {
176+ return s.update(acp.UpdatePlan(
177+ acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusCompleted},
178+ acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusCompleted},
179+ ))
180+}
181+
182+func (s *scenario) sendReadToolCall() error {
183+ if err := s.update(acp.StartToolCall(
184+ "read-1", "Reading README.md",
185+ acp.WithStartKind(acp.ToolKindRead),
186+ acp.WithStartStatus(acp.ToolCallStatusInProgress),
187+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "README.md"}}),
188+ )); err != nil {
189+ return err
190+ }
191+ return s.step(func() error {
192+ return s.update(acp.UpdateToolCall(
193+ "read-1",
194+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
195+ acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolContent(acp.TextBlock("# Ori\n> demo project"))}),
196+ ))
197+ })
198+}
199+
200+func (s *scenario) sendEditToolCall() error {
201+ if err := s.update(acp.StartToolCall(
202+ "edit-1", "Writing hello.txt",
203+ acp.WithStartKind(acp.ToolKindEdit),
204+ acp.WithStartStatus(acp.ToolCallStatusInProgress),
205+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "hello.txt"}}),
206+ )); err != nil {
207+ return err
208+ }
209+ return s.step(func() error {
210+ return s.update(acp.UpdateToolCall(
211+ "edit-1",
212+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
213+ acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolDiffContent("hello.txt", "hello from the ori mock agent\n")}),
214+ ))
215+ })
216+}
217+
218+// askPermission asks the client whether the mock may "write" a file and
219+// interprets the outcome.
220+func (s *scenario) askPermission() (bool, error) {
221+ resp, err := s.agent.conn.RequestPermission(s.ctx, acp.RequestPermissionRequest{
222+ SessionId: s.sessionId,
223+ ToolCall: acp.ToolCallUpdate{ToolCallId: "edit-1", Title: strPtr("Write hello.txt")},
224+ Options: []acp.PermissionOption{
225+ {OptionId: "allow-once", Name: "Allow once", Kind: acp.PermissionOptionKindAllowOnce},
226+ {OptionId: "reject-once", Name: "Reject", Kind: acp.PermissionOptionKindRejectOnce},
227+ },
228+ })
229+ if err != nil {
230+ return false, err
231+ }
232+ return resp.Outcome.Selected != nil && resp.Outcome.Selected.OptionId == "allow-once", nil
233+}
234+
235+func strPtr(s string) *string { return &s }
new file mode 100644
@@ -0,0 +1,235 @@
1+// Package mockagent implements a small, deterministic ACP agent used to
2+// exercise and demo ori without any real AI agent behind it: it streams a
3+// thought, a plan, tool calls (including a file diff), asks one permission,
4+// then answers. The ori-mock-agent command exposes it on stdio.
5+package mockagent
6+
7+import (
8+ "context"
9+ "fmt"
10+ "io"
11+ "strconv"
12+ "sync"
13+ "time"
14+
15+ acp "github.com/coder/acp-go-sdk"
16+)
17+
18+// Agent is the mock ACP agent. The embedded acp.Agent interface covers the
19+// protocol methods ori's client never calls (session/load, logout, ...);
20+// invoking one of them would panic, which is acceptable for a test double.
21+type Agent struct {
22+ acp.Agent
23+
24+ // Delay spaces the streamed updates out so a human watching the UI sees
25+ // the streaming happen. Tests keep it at zero.
26+ Delay time.Duration
27+
28+ conn *acp.AgentSideConnection
29+ mu sync.Mutex
30+ sessions int
31+}
32+
33+// New returns a mock agent; wire it with SetAgentConnection before use.
34+func New() *Agent { return &Agent{} }
35+
36+// SetAgentConnection injects the connection used to push updates and ask
37+// permissions. The SDK does not do this automatically.
38+func (a *Agent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
39+
40+// Run serves the mock agent over the given transport (stdio in the command)
41+// and blocks until the peer disconnects.
42+//
43+// Example:
44+//
45+// agent := mockagent.New()
46+// if err := agent.Run(os.Stdin, os.Stdout); err != nil {
47+// log.Fatal(err)
48+// }
49+func (a *Agent) Run(in io.Reader, out io.Writer) error {
50+ conn := acp.NewAgentSideConnection(a, out, in)
51+ a.SetAgentConnection(conn)
52+ <-conn.Done()
53+ return nil
54+}
55+
56+func (a *Agent) Initialize(_ context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
57+ return acp.InitializeResponse{ProtocolVersion: params.ProtocolVersion}, nil
58+}
59+
60+func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
61+ return acp.AuthenticateResponse{}, nil
62+}
63+
64+func (a *Agent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
65+ a.mu.Lock()
66+ defer a.mu.Unlock()
67+ a.sessions++
68+ return acp.NewSessionResponse{SessionId: acp.SessionId("mock-" + strconv.Itoa(a.sessions))}, nil
69+}
70+
71+// Cancel is a no-op: the SDK already cancels the context of the running
72+// Prompt when session/cancel arrives, which the scenario checks at each step.
73+func (a *Agent) Cancel(context.Context, acp.CancelNotification) error { return nil }
74+
75+// Prompt plays the demo scenario and streams it to the client.
76+func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
77+ scenario := &scenario{agent: a, ctx: ctx, sessionId: params.SessionId, prompt: promptText(params)}
78+ stop, err := scenario.play()
79+ if err != nil {
80+ if ctx.Err() != nil {
81+ return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
82+ }
83+ return acp.PromptResponse{}, err
84+ }
85+ return acp.PromptResponse{StopReason: stop}, nil
86+}
87+
88+// promptText joins the text blocks of the user's prompt.
89+func promptText(params acp.PromptRequest) string {
90+ text := ""
91+ for _, block := range params.Prompt {
92+ if block.Text != nil {
93+ text += block.Text.Text
94+ }
95+ }
96+ return text
97+}
98+
99+// scenario streams one full demo turn step by step.
100+type scenario struct {
101+ agent *Agent
102+ ctx context.Context
103+ sessionId acp.SessionId
104+ prompt string
105+}
106+
107+func (s *scenario) play() (acp.StopReason, error) {
108+ if err := s.runSteps(s.sayThought, s.sendPlan, s.sendReadToolCall, s.sayIntro); err != nil {
109+ return "", err
110+ }
111+
112+ allowed, err := s.askPermission()
113+ if err != nil {
114+ return "", err
115+ }
116+ if !allowed {
117+ return acp.StopReasonRefusal, s.runSteps(s.sayRefused)
118+ }
119+
120+ return acp.StopReasonEndTurn, s.runSteps(s.sendEditToolCall, s.saySummary, s.completePlan)
121+}
122+
123+func (s *scenario) sayThought() error {
124+ return s.update(acp.UpdateAgentThoughtText("The user wants a demo. Let me show every panel feature."))
125+}
126+
127+func (s *scenario) sayIntro() error {
128+ return s.update(acp.UpdateAgentMessageText("I looked at the project. "))
129+}
130+
131+func (s *scenario) sayRefused() error {
132+ return s.update(acp.UpdateAgentMessageText("Understood, I will not write the file."))
133+}
134+
135+func (s *scenario) saySummary() error {
136+ return s.update(acp.UpdateAgentMessageText(fmt.Sprintf("Done! You said: **%s**\n\n- streamed thought ✅\n- plan ✅\n- tool calls ✅\n- permission ✅", s.prompt)))
137+}
138+
139+// runSteps executes scenario actions in order, stopping at the first error.
140+func (s *scenario) runSteps(steps ...func() error) error {
141+ for _, step := range steps {
142+ if err := s.step(step); err != nil {
143+ return err
144+ }
145+ }
146+ return nil
147+}
148+
149+// step runs one scenario action, respecting cancellation and pacing.
150+func (s *scenario) step(action func() error) error {
151+ if err := s.ctx.Err(); err != nil {
152+ return err
153+ }
154+ if s.agent.Delay > 0 {
155+ select {
156+ case <-time.After(s.agent.Delay):
157+ case <-s.ctx.Done():
158+ return s.ctx.Err()
159+ }
160+ }
161+ return action()
162+}
163+
164+func (s *scenario) update(u acp.SessionUpdate) error {
165+ return s.agent.conn.SessionUpdate(s.ctx, acp.SessionNotification{SessionId: s.sessionId, Update: u})
166+}
167+
168+func (s *scenario) sendPlan() error {
169+ return s.update(acp.UpdatePlan(
170+ acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusInProgress},
171+ acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusPending},
172+ ))
173+}
174+
175+func (s *scenario) completePlan() error {
176+ return s.update(acp.UpdatePlan(
177+ acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusCompleted},
178+ acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusCompleted},
179+ ))
180+}
181+
182+func (s *scenario) sendReadToolCall() error {
183+ if err := s.update(acp.StartToolCall(
184+ "read-1", "Reading README.md",
185+ acp.WithStartKind(acp.ToolKindRead),
186+ acp.WithStartStatus(acp.ToolCallStatusInProgress),
187+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "README.md"}}),
188+ )); err != nil {
189+ return err
190+ }
191+ return s.step(func() error {
192+ return s.update(acp.UpdateToolCall(
193+ "read-1",
194+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
195+ acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolContent(acp.TextBlock("# Ori\n> demo project"))}),
196+ ))
197+ })
198+}
199+
200+func (s *scenario) sendEditToolCall() error {
201+ if err := s.update(acp.StartToolCall(
202+ "edit-1", "Writing hello.txt",
203+ acp.WithStartKind(acp.ToolKindEdit),
204+ acp.WithStartStatus(acp.ToolCallStatusInProgress),
205+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "hello.txt"}}),
206+ )); err != nil {
207+ return err
208+ }
209+ return s.step(func() error {
210+ return s.update(acp.UpdateToolCall(
211+ "edit-1",
212+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
213+ acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolDiffContent("hello.txt", "hello from the ori mock agent\n")}),
214+ ))
215+ })
216+}
217+
218+// askPermission asks the client whether the mock may "write" a file and
219+// interprets the outcome.
220+func (s *scenario) askPermission() (bool, error) {
221+ resp, err := s.agent.conn.RequestPermission(s.ctx, acp.RequestPermissionRequest{
222+ SessionId: s.sessionId,
223+ ToolCall: acp.ToolCallUpdate{ToolCallId: "edit-1", Title: strPtr("Write hello.txt")},
224+ Options: []acp.PermissionOption{
225+ {OptionId: "allow-once", Name: "Allow once", Kind: acp.PermissionOptionKindAllowOnce},
226+ {OptionId: "reject-once", Name: "Reject", Kind: acp.PermissionOptionKindRejectOnce},
227+ },
228+ })
229+ if err != nil {
230+ return false, err
231+ }
232+ return resp.Outcome.Selected != nil && resp.Outcome.Selected.OptionId == "allow-once", nil
233+}
234+
235+func strPtr(s string) *string { return &s }
added internal/mockagent/mockagent_test.go +126 -0
new file mode 100644
@@ -0,0 +1,126 @@
1+package mockagent_test
2+
3+import (
4+ "context"
5+ "io"
6+ "strings"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ acp "github.com/coder/acp-go-sdk"
12+
13+ "github.com/bots-garden/ori/internal/agent"
14+ "github.com/bots-garden/ori/internal/mockagent"
15+)
16+
17+// panelHandler records everything the mock streams, and answers the
18+// permission request with a fixed choice.
19+type panelHandler struct {
20+ mu sync.Mutex
21+ optionId string
22+ texts []string
23+ updateKinds []string
24+ permissions int
25+}
26+
27+func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotification) {
28+ h.mu.Lock()
29+ defer h.mu.Unlock()
30+ switch {
31+ case n.Update.AgentMessageChunk != nil:
32+ h.updateKinds = append(h.updateKinds, "message")
33+ if t := n.Update.AgentMessageChunk.Content.Text; t != nil {
34+ h.texts = append(h.texts, t.Text)
35+ }
36+ case n.Update.AgentThoughtChunk != nil:
37+ h.updateKinds = append(h.updateKinds, "thought")
38+ case n.Update.ToolCall != nil:
39+ h.updateKinds = append(h.updateKinds, "tool_call")
40+ case n.Update.ToolCallUpdate != nil:
41+ h.updateKinds = append(h.updateKinds, "tool_call_update")
42+ case n.Update.Plan != nil:
43+ h.updateKinds = append(h.updateKinds, "plan")
44+ }
45+}
46+
47+func (h *panelHandler) HandlePermissionRequest(_ context.Context, req acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
48+ h.mu.Lock()
49+ defer h.mu.Unlock()
50+ h.permissions++
51+ _ = req
52+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(acp.PermissionOptionId(h.optionId))}, nil
53+}
54+
55+func (h *panelHandler) kinds() map[string]int {
56+ h.mu.Lock()
57+ defer h.mu.Unlock()
58+ counts := map[string]int{}
59+ for _, k := range h.updateKinds {
60+ counts[k]++
61+ }
62+ return counts
63+}
64+
65+// connectPanel wires the mock agent to ori's real client code over pipes.
66+func connectPanel(t *testing.T, handler agent.Handler) *agent.Session {
67+ t.Helper()
68+ clientToAgentR, clientToAgentW := io.Pipe()
69+ agentToClientR, agentToClientW := io.Pipe()
70+
71+ mock := mockagent.New()
72+ go func() { _ = mock.Run(clientToAgentR, agentToClientW) }()
73+
74+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
75+ t.Cleanup(cancel)
76+ session, err := agent.Connect(ctx, clientToAgentW, agentToClientR, agent.Options{
77+ Cwd: t.TempDir(),
78+ Handler: handler,
79+ })
80+ if err != nil {
81+ t.Fatalf("Connect to mock agent failed: %v", err)
82+ }
83+ return session
84+}
85+
86+func TestScenarioStreamsEveryPanelFeature(t *testing.T) {
87+ handler := &panelHandler{optionId: "allow-once"}
88+ session := connectPanel(t, handler)
89+
90+ stop, err := session.PromptText(context.Background(), "show me everything")
91+ if err != nil {
92+ t.Fatalf("PromptText returned an error: %v", err)
93+ }
94+ if stop != acp.StopReasonEndTurn {
95+ t.Errorf("stop reason = %q, want end_turn", stop)
96+ }
97+
98+ counts := handler.kinds()
99+ for kind, want := range map[string]int{"thought": 1, "plan": 2, "tool_call": 2, "tool_call_update": 2} {
100+ if counts[kind] < want {
101+ t.Errorf("streamed %d %q updates, want at least %d (all: %v)", counts[kind], kind, want, counts)
102+ }
103+ }
104+ if handler.permissions != 1 {
105+ t.Errorf("permission requests = %d, want 1", handler.permissions)
106+ }
107+ if all := strings.Join(handler.texts, ""); !strings.Contains(all, "show me everything") {
108+ t.Errorf("final message %q does not echo the prompt", all)
109+ }
110+}
111+
112+func TestScenarioStopsOnRejectedPermission(t *testing.T) {
113+ handler := &panelHandler{optionId: "reject-once"}
114+ session := connectPanel(t, handler)
115+
116+ stop, err := session.PromptText(context.Background(), "try to write")
117+ if err != nil {
118+ t.Fatalf("PromptText returned an error: %v", err)
119+ }
120+ if stop != acp.StopReasonRefusal {
121+ t.Errorf("stop reason = %q, want refusal after a rejected permission", stop)
122+ }
123+ if counts := handler.kinds(); counts["tool_call"] != 1 {
124+ t.Errorf("tool calls = %d, want only the read call (no edit after rejection)", counts["tool_call"])
125+ }
126+}
new file mode 100644
@@ -0,0 +1,126 @@
1+package mockagent_test
2+
3+import (
4+ "context"
5+ "io"
6+ "strings"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ acp "github.com/coder/acp-go-sdk"
12+
13+ "github.com/bots-garden/ori/internal/agent"
14+ "github.com/bots-garden/ori/internal/mockagent"
15+)
16+
17+// panelHandler records everything the mock streams, and answers the
18+// permission request with a fixed choice.
19+type panelHandler struct {
20+ mu sync.Mutex
21+ optionId string
22+ texts []string
23+ updateKinds []string
24+ permissions int
25+}
26+
27+func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotification) {
28+ h.mu.Lock()
29+ defer h.mu.Unlock()
30+ switch {
31+ case n.Update.AgentMessageChunk != nil:
32+ h.updateKinds = append(h.updateKinds, "message")
33+ if t := n.Update.AgentMessageChunk.Content.Text; t != nil {
34+ h.texts = append(h.texts, t.Text)
35+ }
36+ case n.Update.AgentThoughtChunk != nil:
37+ h.updateKinds = append(h.updateKinds, "thought")
38+ case n.Update.ToolCall != nil:
39+ h.updateKinds = append(h.updateKinds, "tool_call")
40+ case n.Update.ToolCallUpdate != nil:
41+ h.updateKinds = append(h.updateKinds, "tool_call_update")
42+ case n.Update.Plan != nil:
43+ h.updateKinds = append(h.updateKinds, "plan")
44+ }
45+}
46+
47+func (h *panelHandler) HandlePermissionRequest(_ context.Context, req acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
48+ h.mu.Lock()
49+ defer h.mu.Unlock()
50+ h.permissions++
51+ _ = req
52+ return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(acp.PermissionOptionId(h.optionId))}, nil
53+}
54+
55+func (h *panelHandler) kinds() map[string]int {
56+ h.mu.Lock()
57+ defer h.mu.Unlock()
58+ counts := map[string]int{}
59+ for _, k := range h.updateKinds {
60+ counts[k]++
61+ }
62+ return counts
63+}
64+
65+// connectPanel wires the mock agent to ori's real client code over pipes.
66+func connectPanel(t *testing.T, handler agent.Handler) *agent.Session {
67+ t.Helper()
68+ clientToAgentR, clientToAgentW := io.Pipe()
69+ agentToClientR, agentToClientW := io.Pipe()
70+
71+ mock := mockagent.New()
72+ go func() { _ = mock.Run(clientToAgentR, agentToClientW) }()
73+
74+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
75+ t.Cleanup(cancel)
76+ session, err := agent.Connect(ctx, clientToAgentW, agentToClientR, agent.Options{
77+ Cwd: t.TempDir(),
78+ Handler: handler,
79+ })
80+ if err != nil {
81+ t.Fatalf("Connect to mock agent failed: %v", err)
82+ }
83+ return session
84+}
85+
86+func TestScenarioStreamsEveryPanelFeature(t *testing.T) {
87+ handler := &panelHandler{optionId: "allow-once"}
88+ session := connectPanel(t, handler)
89+
90+ stop, err := session.PromptText(context.Background(), "show me everything")
91+ if err != nil {
92+ t.Fatalf("PromptText returned an error: %v", err)
93+ }
94+ if stop != acp.StopReasonEndTurn {
95+ t.Errorf("stop reason = %q, want end_turn", stop)
96+ }
97+
98+ counts := handler.kinds()
99+ for kind, want := range map[string]int{"thought": 1, "plan": 2, "tool_call": 2, "tool_call_update": 2} {
100+ if counts[kind] < want {
101+ t.Errorf("streamed %d %q updates, want at least %d (all: %v)", counts[kind], kind, want, counts)
102+ }
103+ }
104+ if handler.permissions != 1 {
105+ t.Errorf("permission requests = %d, want 1", handler.permissions)
106+ }
107+ if all := strings.Join(handler.texts, ""); !strings.Contains(all, "show me everything") {
108+ t.Errorf("final message %q does not echo the prompt", all)
109+ }
110+}
111+
112+func TestScenarioStopsOnRejectedPermission(t *testing.T) {
113+ handler := &panelHandler{optionId: "reject-once"}
114+ session := connectPanel(t, handler)
115+
116+ stop, err := session.PromptText(context.Background(), "try to write")
117+ if err != nil {
118+ t.Fatalf("PromptText returned an error: %v", err)
119+ }
120+ if stop != acp.StopReasonRefusal {
121+ t.Errorf("stop reason = %q, want refusal after a rejected permission", stop)
122+ }
123+ if counts := handler.kinds(); counts["tool_call"] != 1 {
124+ t.Errorf("tool calls = %d, want only the read call (no edit after rejection)", counts["tool_call"])
125+ }
126+}
added ui/embed.go +31 -0
new file mode 100644
@@ -0,0 +1,31 @@
1+// Package ui embeds the production build of the single-page application so
2+// the ori server ships as a single binary.
3+//
4+// The Vite build writes to ui/dist (run `make build-ui`). When dist only
5+// contains the committed .gitkeep placeholder, the server still compiles and
6+// answers with an explanatory error page instead of the SPA.
7+package ui
8+
9+import (
10+ "embed"
11+ "io/fs"
12+)
13+
14+//go:embed all:dist
15+var dist embed.FS
16+
17+// Dist returns the embedded SPA build as a filesystem rooted at the dist
18+// directory, i.e. index.html lives at the root of the returned fs.FS.
19+//
20+// Example:
21+//
22+// http.Handle("/", http.FileServerFS(ui.Dist()))
23+func Dist() fs.FS {
24+ sub, err := fs.Sub(dist, "dist")
25+ if err != nil {
26+ // The dist directory is embedded at compile time; its absence is a
27+ // build-system bug, not a runtime condition a caller could handle.
28+ panic(err)
29+ }
30+ return sub
31+}
new file mode 100644
@@ -0,0 +1,31 @@
1+// Package ui embeds the production build of the single-page application so
2+// the ori server ships as a single binary.
3+//
4+// The Vite build writes to ui/dist (run `make build-ui`). When dist only
5+// contains the committed .gitkeep placeholder, the server still compiles and
6+// answers with an explanatory error page instead of the SPA.
7+package ui
8+
9+import (
10+ "embed"
11+ "io/fs"
12+)
13+
14+//go:embed all:dist
15+var dist embed.FS
16+
17+// Dist returns the embedded SPA build as a filesystem rooted at the dist
18+// directory, i.e. index.html lives at the root of the returned fs.FS.
19+//
20+// Example:
21+//
22+// http.Handle("/", http.FileServerFS(ui.Dist()))
23+func Dist() fs.FS {
24+ sub, err := fs.Sub(dist, "dist")
25+ if err != nil {
26+ // The dist directory is embedded at compile time; its absence is a
27+ // build-system bug, not a runtime condition a caller could handle.
28+ panic(err)
29+ }
30+ return sub
31+}
added ui/index.html +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ <title>Ori</title>
7+ </head>
8+ <body>
9+ <div id="root"></div>
10+ <script type="module" src="/src/main.tsx"></script>
11+ </body>
12+</html>
new file mode 100644
@@ -0,0 +1,12 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ <title>Ori</title>
7+ </head>
8+ <body>
9+ <div id="root"></div>
10+ <script type="module" src="/src/main.tsx"></script>
11+ </body>
12+</html>
added ui/package-lock.json +4480 -0
new file mode 100644
@@ -0,0 +1,4480 @@
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+ }
4480+}
new file mode 100644
@@ -0,0 +1,4480 @@
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+ }
4480+}
added ui/package.json +32 -0
new file mode 100644
@@ -0,0 +1,32 @@
1+{
2+ "name": "ori-ui",
3+ "private": true,
4+ "version": "0.1.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "typecheck": "tsc -b",
10+ "test": "vitest run"
11+ },
12+ "dependencies": {
13+ "react": "^19.0.0",
14+ "react-dom": "^19.0.0",
15+ "react-markdown": "^10.1.0",
16+ "remark-gfm": "^4.0.1",
17+ "zustand": "^5.0.15"
18+ },
19+ "devDependencies": {
20+ "@testing-library/react": "^16.0.0",
21+ "@types/react": "^19.0.0",
22+ "@types/react-dom": "^19.0.0",
23+ "@vitejs/plugin-react": "^5.0.0",
24+ "jsdom": "^26.0.0",
25+ "typescript": "^5.6.0",
26+ "vite": "^7.0.0",
27+ "vitest": "^3.0.0"
28+ },
29+ "allowScripts": {
30+ "esbuild@0.28.2": true
31+ }
32+}
new file mode 100644
@@ -0,0 +1,32 @@
1+{
2+ "name": "ori-ui",
3+ "private": true,
4+ "version": "0.1.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "typecheck": "tsc -b",
10+ "test": "vitest run"
11+ },
12+ "dependencies": {
13+ "react": "^19.0.0",
14+ "react-dom": "^19.0.0",
15+ "react-markdown": "^10.1.0",
16+ "remark-gfm": "^4.0.1",
17+ "zustand": "^5.0.15"
18+ },
19+ "devDependencies": {
20+ "@testing-library/react": "^16.0.0",
21+ "@types/react": "^19.0.0",
22+ "@types/react-dom": "^19.0.0",
23+ "@vitejs/plugin-react": "^5.0.0",
24+ "jsdom": "^26.0.0",
25+ "typescript": "^5.6.0",
26+ "vite": "^7.0.0",
27+ "vitest": "^3.0.0"
28+ },
29+ "allowScripts": {
30+ "esbuild@0.28.2": true
31+ }
32+}
added ui/src/App.test.tsx +186 -0
new file mode 100644
@@ -0,0 +1,186 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import { act, fireEvent, render, screen } from "@testing-library/react";
3+import { App } from "./App";
4+import { dispatch, resetStore } from "./store";
5+import type { OriSocket } from "./ws";
6+import type { ClientMessage } from "./protocol";
7+
8+function fakeSocket() {
9+ const sent: ClientMessage[] = [];
10+ const socket: OriSocket = {
11+ send: (message) => sent.push(message),
12+ close: () => {},
13+ };
14+ return { socket, sent };
15+}
16+
17+describe("App", () => {
18+ beforeEach(() => {
19+ resetStore();
20+ });
21+
22+ it("sends a prompt typed in the composer", () => {
23+ const { socket, sent } = fakeSocket();
24+ render(<App socket={socket} />);
25+
26+ fireEvent.change(screen.getByLabelText("prompt"), {
27+ target: { value: "hello agent" },
28+ });
29+ fireEvent.click(screen.getByRole("button", { name: "Send" }));
30+
31+ expect(sent).toEqual([{ type: "prompt", text: "hello agent" }]);
32+ });
33+
34+ it("shows the streamed conversation from the store", () => {
35+ const { socket } = fakeSocket();
36+ render(<App socket={socket} />);
37+
38+ act(() => {
39+ dispatch({
40+ type: "server",
41+ message: { type: "user_message", text: "my question" },
42+ });
43+ dispatch({
44+ type: "server",
45+ message: {
46+ type: "session_update",
47+ update: {
48+ sessionUpdate: "agent_message_chunk",
49+ content: { type: "text", text: "my **answer**" },
50+ },
51+ },
52+ });
53+ });
54+
55+ expect(screen.getByText("my question")).toBeDefined();
56+ expect(screen.getByText("answer")).toBeDefined(); // bold inner text
57+ });
58+
59+ it("offers Stop instead of Send during a turn, and Stop cancels", () => {
60+ const { socket, sent } = fakeSocket();
61+ render(<App socket={socket} />);
62+
63+ act(() => {
64+ dispatch({ type: "server", message: { type: "turn_started" } });
65+ });
66+
67+ expect(screen.queryByRole("button", { name: "Send" })).toBeNull();
68+ fireEvent.click(screen.getByRole("button", { name: "Stop" }));
69+ expect(sent).toEqual([{ type: "cancel" }]);
70+ });
71+
72+ it("shows the connection status and session id from hello", () => {
73+ const { socket } = fakeSocket();
74+ render(<App socket={socket} />);
75+
76+ act(() => {
77+ dispatch({
78+ type: "server",
79+ message: { type: "hello", sessionId: "sess-42" },
80+ });
81+ });
82+
83+ expect(screen.getByText(/online/)).toBeDefined();
84+ expect(screen.getByText(/sess-42/)).toBeDefined();
85+ });
86+
87+ it("renders tool calls, plan and thoughts from updates", () => {
88+ const { socket } = fakeSocket();
89+ render(<App socket={socket} />);
90+
91+ act(() => {
92+ dispatch({
93+ type: "server",
94+ message: {
95+ type: "session_update",
96+ update: {
97+ sessionUpdate: "tool_call",
98+ toolCallId: "c1",
99+ title: "Running tests",
100+ kind: "execute",
101+ status: "in_progress",
102+ },
103+ },
104+ });
105+ dispatch({
106+ type: "server",
107+ message: {
108+ type: "session_update",
109+ update: {
110+ sessionUpdate: "plan",
111+ entries: [
112+ {
113+ content: "write the fix",
114+ priority: "high",
115+ status: "in_progress",
116+ },
117+ ],
118+ },
119+ },
120+ });
121+ dispatch({
122+ type: "server",
123+ message: {
124+ type: "session_update",
125+ update: {
126+ sessionUpdate: "agent_thought_chunk",
127+ content: { type: "text", text: "let me think" },
128+ },
129+ },
130+ });
131+ });
132+
133+ expect(screen.getByText("Running tests")).toBeDefined();
134+ expect(screen.getByText(/write the fix/)).toBeDefined();
135+ expect(screen.getByText("let me think")).toBeDefined();
136+ });
137+
138+ it("answers a permission request through the socket", () => {
139+ const { socket, sent } = fakeSocket();
140+ render(<App socket={socket} />);
141+
142+ act(() => {
143+ dispatch({
144+ type: "server",
145+ message: {
146+ type: "permission_request",
147+ requestId: "perm-9",
148+ request: {
149+ toolCall: { title: "Write main.go" },
150+ options: [
151+ {
152+ optionId: "allow-once",
153+ name: "Allow once",
154+ kind: "allow_once",
155+ },
156+ { optionId: "reject-once", name: "Reject", kind: "reject_once" },
157+ ],
158+ },
159+ },
160+ });
161+ });
162+
163+ expect(screen.getByText(/Write main\.go/)).toBeDefined();
164+ fireEvent.click(screen.getByRole("button", { name: "Allow once" }));
165+
166+ expect(sent).toEqual([
167+ {
168+ type: "permission_response",
169+ requestId: "perm-9",
170+ optionId: "allow-once",
171+ },
172+ ]);
173+ });
174+
175+ it("does not send empty prompts", () => {
176+ const { socket, sent } = fakeSocket();
177+ render(<App socket={socket} />);
178+
179+ fireEvent.change(screen.getByLabelText("prompt"), {
180+ target: { value: " " },
181+ });
182+ fireEvent.keyDown(screen.getByLabelText("prompt"), { key: "Enter" });
183+
184+ expect(sent).toEqual([]);
185+ });
186+});
new file mode 100644
@@ -0,0 +1,186 @@
1+import { beforeEach, describe, expect, it } from "vitest";
2+import { act, fireEvent, render, screen } from "@testing-library/react";
3+import { App } from "./App";
4+import { dispatch, resetStore } from "./store";
5+import type { OriSocket } from "./ws";
6+import type { ClientMessage } from "./protocol";
7+
8+function fakeSocket() {
9+ const sent: ClientMessage[] = [];
10+ const socket: OriSocket = {
11+ send: (message) => sent.push(message),
12+ close: () => {},
13+ };
14+ return { socket, sent };
15+}
16+
17+describe("App", () => {
18+ beforeEach(() => {
19+ resetStore();
20+ });
21+
22+ it("sends a prompt typed in the composer", () => {
23+ const { socket, sent } = fakeSocket();
24+ render(<App socket={socket} />);
25+
26+ fireEvent.change(screen.getByLabelText("prompt"), {
27+ target: { value: "hello agent" },
28+ });
29+ fireEvent.click(screen.getByRole("button", { name: "Send" }));
30+
31+ expect(sent).toEqual([{ type: "prompt", text: "hello agent" }]);
32+ });
33+
34+ it("shows the streamed conversation from the store", () => {
35+ const { socket } = fakeSocket();
36+ render(<App socket={socket} />);
37+
38+ act(() => {
39+ dispatch({
40+ type: "server",
41+ message: { type: "user_message", text: "my question" },
42+ });
43+ dispatch({
44+ type: "server",
45+ message: {
46+ type: "session_update",
47+ update: {
48+ sessionUpdate: "agent_message_chunk",
49+ content: { type: "text", text: "my **answer**" },
50+ },
51+ },
52+ });
53+ });
54+
55+ expect(screen.getByText("my question")).toBeDefined();
56+ expect(screen.getByText("answer")).toBeDefined(); // bold inner text
57+ });
58+
59+ it("offers Stop instead of Send during a turn, and Stop cancels", () => {
60+ const { socket, sent } = fakeSocket();
61+ render(<App socket={socket} />);
62+
63+ act(() => {
64+ dispatch({ type: "server", message: { type: "turn_started" } });
65+ });
66+
67+ expect(screen.queryByRole("button", { name: "Send" })).toBeNull();
68+ fireEvent.click(screen.getByRole("button", { name: "Stop" }));
69+ expect(sent).toEqual([{ type: "cancel" }]);
70+ });
71+
72+ it("shows the connection status and session id from hello", () => {
73+ const { socket } = fakeSocket();
74+ render(<App socket={socket} />);
75+
76+ act(() => {
77+ dispatch({
78+ type: "server",
79+ message: { type: "hello", sessionId: "sess-42" },
80+ });
81+ });
82+
83+ expect(screen.getByText(/online/)).toBeDefined();
84+ expect(screen.getByText(/sess-42/)).toBeDefined();
85+ });
86+
87+ it("renders tool calls, plan and thoughts from updates", () => {
88+ const { socket } = fakeSocket();
89+ render(<App socket={socket} />);
90+
91+ act(() => {
92+ dispatch({
93+ type: "server",
94+ message: {
95+ type: "session_update",
96+ update: {
97+ sessionUpdate: "tool_call",
98+ toolCallId: "c1",
99+ title: "Running tests",
100+ kind: "execute",
101+ status: "in_progress",
102+ },
103+ },
104+ });
105+ dispatch({
106+ type: "server",
107+ message: {
108+ type: "session_update",
109+ update: {
110+ sessionUpdate: "plan",
111+ entries: [
112+ {
113+ content: "write the fix",
114+ priority: "high",
115+ status: "in_progress",
116+ },
117+ ],
118+ },
119+ },
120+ });
121+ dispatch({
122+ type: "server",
123+ message: {
124+ type: "session_update",
125+ update: {
126+ sessionUpdate: "agent_thought_chunk",
127+ content: { type: "text", text: "let me think" },
128+ },
129+ },
130+ });
131+ });
132+
133+ expect(screen.getByText("Running tests")).toBeDefined();
134+ expect(screen.getByText(/write the fix/)).toBeDefined();
135+ expect(screen.getByText("let me think")).toBeDefined();
136+ });
137+
138+ it("answers a permission request through the socket", () => {
139+ const { socket, sent } = fakeSocket();
140+ render(<App socket={socket} />);
141+
142+ act(() => {
143+ dispatch({
144+ type: "server",
145+ message: {
146+ type: "permission_request",
147+ requestId: "perm-9",
148+ request: {
149+ toolCall: { title: "Write main.go" },
150+ options: [
151+ {
152+ optionId: "allow-once",
153+ name: "Allow once",
154+ kind: "allow_once",
155+ },
156+ { optionId: "reject-once", name: "Reject", kind: "reject_once" },
157+ ],
158+ },
159+ },
160+ });
161+ });
162+
163+ expect(screen.getByText(/Write main\.go/)).toBeDefined();
164+ fireEvent.click(screen.getByRole("button", { name: "Allow once" }));
165+
166+ expect(sent).toEqual([
167+ {
168+ type: "permission_response",
169+ requestId: "perm-9",
170+ optionId: "allow-once",
171+ },
172+ ]);
173+ });
174+
175+ it("does not send empty prompts", () => {
176+ const { socket, sent } = fakeSocket();
177+ render(<App socket={socket} />);
178+
179+ fireEvent.change(screen.getByLabelText("prompt"), {
180+ target: { value: " " },
181+ });
182+ fireEvent.keyDown(screen.getByLabelText("prompt"), { key: "Enter" });
183+
184+ expect(sent).toEqual([]);
185+ });
186+});
added ui/src/App.tsx +47 -0
new file mode 100644
@@ -0,0 +1,47 @@
1+/**
2+ * Root component: header with connection/session status, the conversation
3+ * thread, and the composer. The WebSocket is injected so tests (and future
4+ * embeddings) can supply a fake.
5+ *
6+ * @example
7+ * <App socket={connectSocket({ url: wsUrl(), dispatch })} />
8+ */
9+
10+import type { OriSocket } from "./ws";
11+import { useChatState } from "./store";
12+import { ChatThread } from "./components/ChatThread";
13+import { PermissionPrompt } from "./components/PermissionPrompt";
14+import { PlanView } from "./components/PlanView";
15+import { PromptInput } from "./components/PromptInput";
16+
17+export function App({ socket }: { socket: OriSocket }) {
18+ const state = useChatState();
19+
20+ return (
21+ <div className="app">
22+ <header className="topbar">
23+ <h1 className="topbar-title">Ori</h1>
24+ <span className={`status status-${state.connection}`}>
25+ {state.connection}
26+ {state.sessionId ? ` · ${state.sessionId}` : ""}
27+ </span>
28+ </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+ />
45+ </div>
46+ );
47+}
new file mode 100644
@@ -0,0 +1,47 @@
1+/**
2+ * Root component: header with connection/session status, the conversation
3+ * thread, and the composer. The WebSocket is injected so tests (and future
4+ * embeddings) can supply a fake.
5+ *
6+ * @example
7+ * <App socket={connectSocket({ url: wsUrl(), dispatch })} />
8+ */
9+
10+import type { OriSocket } from "./ws";
11+import { useChatState } from "./store";
12+import { ChatThread } from "./components/ChatThread";
13+import { PermissionPrompt } from "./components/PermissionPrompt";
14+import { PlanView } from "./components/PlanView";
15+import { PromptInput } from "./components/PromptInput";
16+
17+export function App({ socket }: { socket: OriSocket }) {
18+ const state = useChatState();
19+
20+ return (
21+ <div className="app">
22+ <header className="topbar">
23+ <h1 className="topbar-title">Ori</h1>
24+ <span className={`status status-${state.connection}`}>
25+ {state.connection}
26+ {state.sessionId ? ` · ${state.sessionId}` : ""}
27+ </span>
28+ </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+ />
45+ </div>
46+ );
47+}
added ui/src/app.css +411 -0
new file mode 100644
@@ -0,0 +1,411 @@
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
3+ the bottom. Light and dark follow the system preference. */
4+
5+:root {
6+ color-scheme: light dark;
7+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
8+
9+ --bg: #fafafa;
10+ --panel: #ffffff;
11+ --text: #1c1c1c;
12+ --muted: #6b6b6b;
13+ --border: #e2e2e2;
14+ --accent: #3b82f6;
15+ --user-bubble: #e8f0fe;
16+ --error: #b91c1c;
17+ --error-bg: #fee2e2;
18+ --code-bg: #f2f2f2;
19+}
20+
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+ }
34+}
35+
36+body {
37+ margin: 0;
38+ background: var(--bg);
39+ color: var(--text);
40+}
41+
42+.app {
43+ display: flex;
44+ flex-direction: column;
45+ height: 100vh;
46+ max-width: 52rem;
47+ margin: 0 auto;
48+}
49+
50+/* ---- header ---- */
51+
52+.topbar {
53+ display: flex;
54+ align-items: baseline;
55+ gap: 0.75rem;
56+ padding: 0.6rem 1rem;
57+ border-bottom: 1px solid var(--border);
58+}
59+
60+.topbar-title {
61+ font-size: 1.05rem;
62+ margin: 0;
63+}
64+
65+.status {
66+ font-size: 0.75rem;
67+ color: var(--muted);
68+}
69+
70+.status-online::before,
71+.status-offline::before,
72+.status-connecting::before {
73+ content: "●";
74+ margin-right: 0.3rem;
75+}
76+
77+.status-online::before {
78+ color: #22c55e;
79+}
80+
81+.status-offline::before {
82+ color: #ef4444;
83+}
84+
85+.status-connecting::before {
86+ color: #eab308;
87+}
88+
89+/* ---- thread ---- */
90+
91+.thread {
92+ flex: 1;
93+ overflow-y: auto;
94+ padding: 1rem;
95+ display: flex;
96+ flex-direction: column;
97+ gap: 0.75rem;
98+}
99+
100+.thread-empty,
101+.thread-working {
102+ color: var(--muted);
103+ font-size: 0.85rem;
104+ font-style: italic;
105+}
106+
107+.message {
108+ border-radius: 8px;
109+ padding: 0.55rem 0.8rem;
110+ line-height: 1.5;
111+ overflow-wrap: anywhere;
112+}
113+
114+.message-user {
115+ align-self: flex-end;
116+ background: var(--user-bubble);
117+ white-space: pre-wrap;
118+ max-width: 85%;
119+}
120+
121+.message-agent {
122+ background: var(--panel);
123+ border: 1px solid var(--border);
124+}
125+
126+.message-error {
127+ background: var(--error-bg);
128+ color: var(--error);
129+ font-size: 0.85rem;
130+}
131+
132+/* ---- markdown ---- */
133+
134+.markdown > :first-child {
135+ margin-top: 0;
136+}
137+
138+.markdown > :last-child {
139+ margin-bottom: 0;
140+}
141+
142+.markdown pre {
143+ background: var(--code-bg);
144+ border: 1px solid var(--border);
145+ border-radius: 6px;
146+ padding: 0.6rem;
147+ overflow-x: auto;
148+}
149+
150+.markdown code {
151+ background: var(--code-bg);
152+ border-radius: 4px;
153+ padding: 0.1rem 0.3rem;
154+ font-size: 0.85em;
155+}
156+
157+.markdown pre code {
158+ background: none;
159+ padding: 0;
160+}
161+
162+.markdown table {
163+ border-collapse: collapse;
164+}
165+
166+.markdown th,
167+.markdown td {
168+ border: 1px solid var(--border);
169+ padding: 0.3rem 0.6rem;
170+}
171+
172+/* ---- thoughts ---- */
173+
174+.thought {
175+ border: 1px dashed var(--border);
176+ border-radius: 8px;
177+ padding: 0.35rem 0.8rem;
178+ color: var(--muted);
179+ font-size: 0.9rem;
180+}
181+
182+.thought-summary {
183+ cursor: pointer;
184+ user-select: none;
185+}
186+
187+.thought-body {
188+ padding-top: 0.4rem;
189+}
190+
191+/* ---- tool calls ---- */
192+
193+.tool {
194+ border: 1px solid var(--border);
195+ border-left: 3px solid var(--muted);
196+ border-radius: 8px;
197+ padding: 0.35rem 0.8rem;
198+ background: var(--panel);
199+ font-size: 0.9rem;
200+}
201+
202+.tool-in_progress {
203+ border-left-color: var(--accent);
204+}
205+
206+.tool-completed {
207+ border-left-color: #22c55e;
208+}
209+
210+.tool-failed {
211+ border-left-color: #ef4444;
212+}
213+
214+.tool-summary {
215+ display: flex;
216+ align-items: center;
217+ gap: 0.5rem;
218+ cursor: pointer;
219+ user-select: none;
220+}
221+
222+.tool-title {
223+ flex: 1;
224+ overflow: hidden;
225+ text-overflow: ellipsis;
226+ white-space: nowrap;
227+}
228+
229+.tool-status {
230+ font-size: 0.72rem;
231+ color: var(--muted);
232+ border: 1px solid var(--border);
233+ border-radius: 999px;
234+ padding: 0.05rem 0.5rem;
235+}
236+
237+.tool-status-failed {
238+ color: var(--error);
239+}
240+
241+.tool-body {
242+ padding: 0.4rem 0 0.2rem;
243+}
244+
245+.tool-locations {
246+ margin: 0 0 0.4rem;
247+ padding-left: 1.1rem;
248+ color: var(--muted);
249+}
250+
251+.tool-diff {
252+ background: var(--code-bg);
253+ border: 1px solid var(--border);
254+ border-radius: 6px;
255+ padding: 0.5rem;
256+ overflow-x: auto;
257+}
258+
259+.tool-diff-path {
260+ display: block;
261+ margin-bottom: 0.3rem;
262+ color: var(--muted);
263+}
264+
265+.tool-diff pre {
266+ margin: 0;
267+ font-size: 0.8rem;
268+}
269+
270+.tool-diff-old {
271+ color: #dc2626;
272+}
273+
274+.tool-diff-new {
275+ color: #16a34a;
276+}
277+
278+.tool-terminal {
279+ color: var(--muted);
280+ font-style: italic;
281+}
282+
283+/* ---- plan ---- */
284+
285+.plan {
286+ margin: 0 1rem;
287+ border: 1px solid var(--border);
288+ border-radius: 8px;
289+ padding: 0.4rem 0.8rem;
290+ background: var(--panel);
291+ font-size: 0.85rem;
292+}
293+
294+.plan-summary {
295+ cursor: pointer;
296+ user-select: none;
297+ color: var(--muted);
298+}
299+
300+.plan-entries {
301+ list-style: none;
302+ margin: 0.4rem 0 0.2rem;
303+ padding: 0;
304+}
305+
306+.plan-entry {
307+ padding: 0.1rem 0;
308+}
309+
310+.plan-entry-completed {
311+ color: var(--muted);
312+ text-decoration: line-through;
313+}
314+
315+.plan-entry-in_progress {
316+ color: var(--accent);
317+}
318+
319+/* ---- permissions ---- */
320+
321+.permission {
322+ margin: 0.5rem 1rem 0;
323+ border: 1px solid var(--accent);
324+ border-radius: 8px;
325+ padding: 0.6rem 0.8rem;
326+ background: var(--panel);
327+}
328+
329+.permission-title {
330+ margin: 0 0 0.5rem;
331+ font-size: 0.9rem;
332+}
333+
334+.permission-options {
335+ display: flex;
336+ gap: 0.5rem;
337+ flex-wrap: wrap;
338+}
339+
340+.permission-option {
341+ border: 1px solid var(--border);
342+ border-radius: 6px;
343+ padding: 0.35rem 0.8rem;
344+ font: inherit;
345+ font-size: 0.85rem;
346+ cursor: pointer;
347+ background: var(--panel);
348+ color: var(--text);
349+}
350+
351+.permission-allow_once,
352+.permission-allow_always {
353+ background: var(--accent);
354+ border-color: var(--accent);
355+ color: #ffffff;
356+}
357+
358+.permission-reject_once,
359+.permission-reject_always {
360+ border-color: #dc2626;
361+ color: #dc2626;
362+}
363+
364+/* ---- composer ---- */
365+
366+.composer {
367+ display: flex;
368+ gap: 0.5rem;
369+ padding: 0.75rem 1rem 1rem;
370+ border-top: 1px solid var(--border);
371+}
372+
373+.composer-input {
374+ flex: 1;
375+ resize: none;
376+ background: var(--panel);
377+ color: var(--text);
378+ border: 1px solid var(--border);
379+ border-radius: 8px;
380+ padding: 0.55rem 0.7rem;
381+ font: inherit;
382+}
383+
384+.composer-input:focus {
385+ outline: 2px solid var(--accent);
386+ outline-offset: -1px;
387+}
388+
389+.composer-send,
390+.composer-stop {
391+ align-self: flex-end;
392+ border: none;
393+ border-radius: 8px;
394+ padding: 0.55rem 1rem;
395+ font: inherit;
396+ cursor: pointer;
397+ color: #ffffff;
398+}
399+
400+.composer-send {
401+ background: var(--accent);
402+}
403+
404+.composer-send:disabled {
405+ opacity: 0.5;
406+ cursor: default;
407+}
408+
409+.composer-stop {
410+ background: #dc2626;
411+}
new file mode 100644
@@ -0,0 +1,411 @@
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
3+ the bottom. Light and dark follow the system preference. */
4+
5+:root {
6+ color-scheme: light dark;
7+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
8+
9+ --bg: #fafafa;
10+ --panel: #ffffff;
11+ --text: #1c1c1c;
12+ --muted: #6b6b6b;
13+ --border: #e2e2e2;
14+ --accent: #3b82f6;
15+ --user-bubble: #e8f0fe;
16+ --error: #b91c1c;
17+ --error-bg: #fee2e2;
18+ --code-bg: #f2f2f2;
19+}
20+
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+ }
34+}
35+
36+body {
37+ margin: 0;
38+ background: var(--bg);
39+ color: var(--text);
40+}
41+
42+.app {
43+ display: flex;
44+ flex-direction: column;
45+ height: 100vh;
46+ max-width: 52rem;
47+ margin: 0 auto;
48+}
49+
50+/* ---- header ---- */
51+
52+.topbar {
53+ display: flex;
54+ align-items: baseline;
55+ gap: 0.75rem;
56+ padding: 0.6rem 1rem;
57+ border-bottom: 1px solid var(--border);
58+}
59+
60+.topbar-title {
61+ font-size: 1.05rem;
62+ margin: 0;
63+}
64+
65+.status {
66+ font-size: 0.75rem;
67+ color: var(--muted);
68+}
69+
70+.status-online::before,
71+.status-offline::before,
72+.status-connecting::before {
73+ content: "●";
74+ margin-right: 0.3rem;
75+}
76+
77+.status-online::before {
78+ color: #22c55e;
79+}
80+
81+.status-offline::before {
82+ color: #ef4444;
83+}
84+
85+.status-connecting::before {
86+ color: #eab308;
87+}
88+
89+/* ---- thread ---- */
90+
91+.thread {
92+ flex: 1;
93+ overflow-y: auto;
94+ padding: 1rem;
95+ display: flex;
96+ flex-direction: column;
97+ gap: 0.75rem;
98+}
99+
100+.thread-empty,
101+.thread-working {
102+ color: var(--muted);
103+ font-size: 0.85rem;
104+ font-style: italic;
105+}
106+
107+.message {
108+ border-radius: 8px;
109+ padding: 0.55rem 0.8rem;
110+ line-height: 1.5;
111+ overflow-wrap: anywhere;
112+}
113+
114+.message-user {
115+ align-self: flex-end;
116+ background: var(--user-bubble);
117+ white-space: pre-wrap;
118+ max-width: 85%;
119+}
120+
121+.message-agent {
122+ background: var(--panel);
123+ border: 1px solid var(--border);
124+}
125+
126+.message-error {
127+ background: var(--error-bg);
128+ color: var(--error);
129+ font-size: 0.85rem;
130+}
131+
132+/* ---- markdown ---- */
133+
134+.markdown > :first-child {
135+ margin-top: 0;
136+}
137+
138+.markdown > :last-child {
139+ margin-bottom: 0;
140+}
141+
142+.markdown pre {
143+ background: var(--code-bg);
144+ border: 1px solid var(--border);
145+ border-radius: 6px;
146+ padding: 0.6rem;
147+ overflow-x: auto;
148+}
149+
150+.markdown code {
151+ background: var(--code-bg);
152+ border-radius: 4px;
153+ padding: 0.1rem 0.3rem;
154+ font-size: 0.85em;
155+}
156+
157+.markdown pre code {
158+ background: none;
159+ padding: 0;
160+}
161+
162+.markdown table {
163+ border-collapse: collapse;
164+}
165+
166+.markdown th,
167+.markdown td {
168+ border: 1px solid var(--border);
169+ padding: 0.3rem 0.6rem;
170+}
171+
172+/* ---- thoughts ---- */
173+
174+.thought {
175+ border: 1px dashed var(--border);
176+ border-radius: 8px;
177+ padding: 0.35rem 0.8rem;
178+ color: var(--muted);
179+ font-size: 0.9rem;
180+}
181+
182+.thought-summary {
183+ cursor: pointer;
184+ user-select: none;
185+}
186+
187+.thought-body {
188+ padding-top: 0.4rem;
189+}
190+
191+/* ---- tool calls ---- */
192+
193+.tool {
194+ border: 1px solid var(--border);
195+ border-left: 3px solid var(--muted);
196+ border-radius: 8px;
197+ padding: 0.35rem 0.8rem;
198+ background: var(--panel);
199+ font-size: 0.9rem;
200+}
201+
202+.tool-in_progress {
203+ border-left-color: var(--accent);
204+}
205+
206+.tool-completed {
207+ border-left-color: #22c55e;
208+}
209+
210+.tool-failed {
211+ border-left-color: #ef4444;
212+}
213+
214+.tool-summary {
215+ display: flex;
216+ align-items: center;
217+ gap: 0.5rem;
218+ cursor: pointer;
219+ user-select: none;
220+}
221+
222+.tool-title {
223+ flex: 1;
224+ overflow: hidden;
225+ text-overflow: ellipsis;
226+ white-space: nowrap;
227+}
228+
229+.tool-status {
230+ font-size: 0.72rem;
231+ color: var(--muted);
232+ border: 1px solid var(--border);
233+ border-radius: 999px;
234+ padding: 0.05rem 0.5rem;
235+}
236+
237+.tool-status-failed {
238+ color: var(--error);
239+}
240+
241+.tool-body {
242+ padding: 0.4rem 0 0.2rem;
243+}
244+
245+.tool-locations {
246+ margin: 0 0 0.4rem;
247+ padding-left: 1.1rem;
248+ color: var(--muted);
249+}
250+
251+.tool-diff {
252+ background: var(--code-bg);
253+ border: 1px solid var(--border);
254+ border-radius: 6px;
255+ padding: 0.5rem;
256+ overflow-x: auto;
257+}
258+
259+.tool-diff-path {
260+ display: block;
261+ margin-bottom: 0.3rem;
262+ color: var(--muted);
263+}
264+
265+.tool-diff pre {
266+ margin: 0;
267+ font-size: 0.8rem;
268+}
269+
270+.tool-diff-old {
271+ color: #dc2626;
272+}
273+
274+.tool-diff-new {
275+ color: #16a34a;
276+}
277+
278+.tool-terminal {
279+ color: var(--muted);
280+ font-style: italic;
281+}
282+
283+/* ---- plan ---- */
284+
285+.plan {
286+ margin: 0 1rem;
287+ border: 1px solid var(--border);
288+ border-radius: 8px;
289+ padding: 0.4rem 0.8rem;
290+ background: var(--panel);
291+ font-size: 0.85rem;
292+}
293+
294+.plan-summary {
295+ cursor: pointer;
296+ user-select: none;
297+ color: var(--muted);
298+}
299+
300+.plan-entries {
301+ list-style: none;
302+ margin: 0.4rem 0 0.2rem;
303+ padding: 0;
304+}
305+
306+.plan-entry {
307+ padding: 0.1rem 0;
308+}
309+
310+.plan-entry-completed {
311+ color: var(--muted);
312+ text-decoration: line-through;
313+}
314+
315+.plan-entry-in_progress {
316+ color: var(--accent);
317+}
318+
319+/* ---- permissions ---- */
320+
321+.permission {
322+ margin: 0.5rem 1rem 0;
323+ border: 1px solid var(--accent);
324+ border-radius: 8px;
325+ padding: 0.6rem 0.8rem;
326+ background: var(--panel);
327+}
328+
329+.permission-title {
330+ margin: 0 0 0.5rem;
331+ font-size: 0.9rem;
332+}
333+
334+.permission-options {
335+ display: flex;
336+ gap: 0.5rem;
337+ flex-wrap: wrap;
338+}
339+
340+.permission-option {
341+ border: 1px solid var(--border);
342+ border-radius: 6px;
343+ padding: 0.35rem 0.8rem;
344+ font: inherit;
345+ font-size: 0.85rem;
346+ cursor: pointer;
347+ background: var(--panel);
348+ color: var(--text);
349+}
350+
351+.permission-allow_once,
352+.permission-allow_always {
353+ background: var(--accent);
354+ border-color: var(--accent);
355+ color: #ffffff;
356+}
357+
358+.permission-reject_once,
359+.permission-reject_always {
360+ border-color: #dc2626;
361+ color: #dc2626;
362+}
363+
364+/* ---- composer ---- */
365+
366+.composer {
367+ display: flex;
368+ gap: 0.5rem;
369+ padding: 0.75rem 1rem 1rem;
370+ border-top: 1px solid var(--border);
371+}
372+
373+.composer-input {
374+ flex: 1;
375+ resize: none;
376+ background: var(--panel);
377+ color: var(--text);
378+ border: 1px solid var(--border);
379+ border-radius: 8px;
380+ padding: 0.55rem 0.7rem;
381+ font: inherit;
382+}
383+
384+.composer-input:focus {
385+ outline: 2px solid var(--accent);
386+ outline-offset: -1px;
387+}
388+
389+.composer-send,
390+.composer-stop {
391+ align-self: flex-end;
392+ border: none;
393+ border-radius: 8px;
394+ padding: 0.55rem 1rem;
395+ font: inherit;
396+ cursor: pointer;
397+ color: #ffffff;
398+}
399+
400+.composer-send {
401+ background: var(--accent);
402+}
403+
404+.composer-send:disabled {
405+ opacity: 0.5;
406+ cursor: default;
407+}
408+
409+.composer-stop {
410+ background: #dc2626;
411+}
added ui/src/components/ChatThread.tsx +75 -0
new file mode 100644
@@ -0,0 +1,75 @@
1+/**
2+ * ChatThread renders the conversation: user prompts, streamed agent answers
3+ * (markdown) and inline errors, auto-scrolled to the latest entry.
4+ *
5+ * @example
6+ * <ChatThread thread={[{ kind: "user", text: "hi" }]} turnActive={false} />
7+ */
8+
9+import { useEffect, useRef } from "react";
10+import type { ThreadItem } from "../reducer";
11+import { Markdown } from "./Markdown";
12+import { ToolCallCard } from "./ToolCallCard";
13+
14+interface ChatThreadProps {
15+ thread: ThreadItem[];
16+ turnActive: boolean;
17+}
18+
19+export function ChatThread({ thread, turnActive }: ChatThreadProps) {
20+ const endRef = useRef<HTMLDivElement>(null);
21+
22+ // biome-ignore lint/correctness/useExhaustiveDependencies: thread and turnActive intentionally retrigger the scroll-to-bottom on every conversation change.
23+ useEffect(() => {
24+ // Optional call: test DOMs (jsdom) do not implement scrollIntoView.
25+ endRef.current?.scrollIntoView?.({ behavior: "smooth" });
26+ }, [thread, turnActive]);
27+
28+ return (
29+ <section className="thread" aria-label="conversation">
30+ {thread.length === 0 && (
31+ <p className="thread-empty">
32+ Send a prompt to start working with the agent.
33+ </p>
34+ )}
35+ {thread.map((item, index) => (
36+ // 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+ ))}
39+ {turnActive && (
40+ <output className="thread-working">the agent is working</output>
41+ )}
42+ <div ref={endRef} />
43+ </section>
44+ );
45+}
46+
47+function ThreadEntry({ item }: { item: ThreadItem }) {
48+ switch (item.kind) {
49+ case "user":
50+ return <div className="message message-user">{item.text}</div>;
51+ case "agent":
52+ return (
53+ <div className="message message-agent">
54+ <Markdown text={item.text} />
55+ </div>
56+ );
57+ 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+ );
66+ case "tool":
67+ return <ToolCallCard call={item.call} />;
68+ case "error":
69+ return (
70+ <div className="message message-error" role="alert">
71+ {item.text}
72+ </div>
73+ );
74+ }
75+}
new file mode 100644
@@ -0,0 +1,75 @@
1+/**
2+ * ChatThread renders the conversation: user prompts, streamed agent answers
3+ * (markdown) and inline errors, auto-scrolled to the latest entry.
4+ *
5+ * @example
6+ * <ChatThread thread={[{ kind: "user", text: "hi" }]} turnActive={false} />
7+ */
8+
9+import { useEffect, useRef } from "react";
10+import type { ThreadItem } from "../reducer";
11+import { Markdown } from "./Markdown";
12+import { ToolCallCard } from "./ToolCallCard";
13+
14+interface ChatThreadProps {
15+ thread: ThreadItem[];
16+ turnActive: boolean;
17+}
18+
19+export function ChatThread({ thread, turnActive }: ChatThreadProps) {
20+ const endRef = useRef<HTMLDivElement>(null);
21+
22+ // biome-ignore lint/correctness/useExhaustiveDependencies: thread and turnActive intentionally retrigger the scroll-to-bottom on every conversation change.
23+ useEffect(() => {
24+ // Optional call: test DOMs (jsdom) do not implement scrollIntoView.
25+ endRef.current?.scrollIntoView?.({ behavior: "smooth" });
26+ }, [thread, turnActive]);
27+
28+ return (
29+ <section className="thread" aria-label="conversation">
30+ {thread.length === 0 && (
31+ <p className="thread-empty">
32+ Send a prompt to start working with the agent.
33+ </p>
34+ )}
35+ {thread.map((item, index) => (
36+ // 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+ ))}
39+ {turnActive && (
40+ <output className="thread-working">the agent is working</output>
41+ )}
42+ <div ref={endRef} />
43+ </section>
44+ );
45+}
46+
47+function ThreadEntry({ item }: { item: ThreadItem }) {
48+ switch (item.kind) {
49+ case "user":
50+ return <div className="message message-user">{item.text}</div>;
51+ case "agent":
52+ return (
53+ <div className="message message-agent">
54+ <Markdown text={item.text} />
55+ </div>
56+ );
57+ 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+ );
66+ case "tool":
67+ return <ToolCallCard call={item.call} />;
68+ case "error":
69+ return (
70+ <div className="message message-error" role="alert">
71+ {item.text}
72+ </div>
73+ );
74+ }
75+}
added ui/src/components/Markdown.tsx +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+/**
2+ * Markdown renders agent text (GitHub-flavoured: tables, task lists, ...).
3+ * Kept as the single markdown entry point so styling and plugins stay
4+ * consistent everywhere agent content appears.
5+ *
6+ * @example
7+ * <Markdown text="**bold** and `code`" />
8+ */
9+
10+import ReactMarkdown from "react-markdown";
11+import remarkGfm from "remark-gfm";
12+
13+export function Markdown({ text }: { text: string }) {
14+ return (
15+ <div className="markdown">
16+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>
17+ </div>
18+ );
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+/**
2+ * Markdown renders agent text (GitHub-flavoured: tables, task lists, ...).
3+ * Kept as the single markdown entry point so styling and plugins stay
4+ * consistent everywhere agent content appears.
5+ *
6+ * @example
7+ * <Markdown text="**bold** and `code`" />
8+ */
9+
10+import ReactMarkdown from "react-markdown";
11+import remarkGfm from "remark-gfm";
12+
13+export function Markdown({ text }: { text: string }) {
14+ return (
15+ <div className="markdown">
16+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>
17+ </div>
18+ );
19+}
added ui/src/components/PermissionPrompt.tsx +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+/**
2+ * PermissionPrompt asks the user to approve or reject a sensitive agent
3+ * action, offering exactly the options the agent proposed (allow once,
4+ * always, reject, ...), like Zed's permission bar.
5+ *
6+ * @example
7+ * <PermissionPrompt
8+ * request={{ requestId: "perm-1", title: "Run `make test`", options: [
9+ * { optionId: "allow", name: "Allow", kind: "allow_once" }] }}
10+ * onRespond={(requestId, optionId) => console.log(requestId, optionId)}
11+ * />
12+ */
13+
14+import type { PermissionRequestState } from "../reducer";
15+
16+interface PermissionPromptProps {
17+ request: PermissionRequestState;
18+ onRespond: (requestId: string, optionId: string) => void;
19+}
20+
21+export function PermissionPrompt({
22+ request,
23+ onRespond,
24+}: PermissionPromptProps) {
25+ return (
26+ <div
27+ className="permission"
28+ role="alertdialog"
29+ aria-label="permission request"
30+ >
31+ <p className="permission-title">🔐 {request.title}</p>
32+ <div className="permission-options">
33+ {request.options.map((option) => (
34+ <button
35+ key={option.optionId}
36+ type="button"
37+ className={`permission-option permission-${option.kind}`}
38+ onClick={() => onRespond(request.requestId, option.optionId)}
39+ >
40+ {option.name}
41+ </button>
42+ ))}
43+ </div>
44+ </div>
45+ );
46+}
new file mode 100644
@@ -0,0 +1,46 @@
1+/**
2+ * PermissionPrompt asks the user to approve or reject a sensitive agent
3+ * action, offering exactly the options the agent proposed (allow once,
4+ * always, reject, ...), like Zed's permission bar.
5+ *
6+ * @example
7+ * <PermissionPrompt
8+ * request={{ requestId: "perm-1", title: "Run `make test`", options: [
9+ * { optionId: "allow", name: "Allow", kind: "allow_once" }] }}
10+ * onRespond={(requestId, optionId) => console.log(requestId, optionId)}
11+ * />
12+ */
13+
14+import type { PermissionRequestState } from "../reducer";
15+
16+interface PermissionPromptProps {
17+ request: PermissionRequestState;
18+ onRespond: (requestId: string, optionId: string) => void;
19+}
20+
21+export function PermissionPrompt({
22+ request,
23+ onRespond,
24+}: PermissionPromptProps) {
25+ return (
26+ <div
27+ className="permission"
28+ role="alertdialog"
29+ aria-label="permission request"
30+ >
31+ <p className="permission-title">🔐 {request.title}</p>
32+ <div className="permission-options">
33+ {request.options.map((option) => (
34+ <button
35+ key={option.optionId}
36+ type="button"
37+ className={`permission-option permission-${option.kind}`}
38+ onClick={() => onRespond(request.requestId, option.optionId)}
39+ >
40+ {option.name}
41+ </button>
42+ ))}
43+ </div>
44+ </div>
45+ );
46+}
added ui/src/components/PlanView.tsx +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+/**
2+ * PlanView renders the agent's current execution plan (ACP "plan" updates)
3+ * as a compact checklist, like the plan block of Zed's agent panel.
4+ *
5+ * @example
6+ * <PlanView entries={[{ content: "Read the code", priority: "medium", status: "completed" }]} />
7+ */
8+
9+import type { AcpPlanEntry } from "../protocol";
10+
11+const statusMarks: Record<string, string> = {
12+ pending: "○",
13+ in_progress: "◐",
14+ completed: "●",
15+};
16+
17+export function PlanView({ entries }: { entries: AcpPlanEntry[] }) {
18+ if (entries.length === 0) {
19+ return null;
20+ }
21+ return (
22+ <details className="plan" open>
23+ <summary className="plan-summary">
24+ Plan ({entries.filter((e) => e.status === "completed").length}/
25+ {entries.length})
26+ </summary>
27+ <ul className="plan-entries">
28+ {entries.map((entry, index) => (
29+ // 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+ <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "}
32+ {entry.content}
33+ </li>
34+ ))}
35+ </ul>
36+ </details>
37+ );
38+}
new file mode 100644
@@ -0,0 +1,38 @@
1+/**
2+ * PlanView renders the agent's current execution plan (ACP "plan" updates)
3+ * as a compact checklist, like the plan block of Zed's agent panel.
4+ *
5+ * @example
6+ * <PlanView entries={[{ content: "Read the code", priority: "medium", status: "completed" }]} />
7+ */
8+
9+import type { AcpPlanEntry } from "../protocol";
10+
11+const statusMarks: Record<string, string> = {
12+ pending: "○",
13+ in_progress: "◐",
14+ completed: "●",
15+};
16+
17+export function PlanView({ entries }: { entries: AcpPlanEntry[] }) {
18+ if (entries.length === 0) {
19+ return null;
20+ }
21+ return (
22+ <details className="plan" open>
23+ <summary className="plan-summary">
24+ Plan ({entries.filter((e) => e.status === "completed").length}/
25+ {entries.length})
26+ </summary>
27+ <ul className="plan-entries">
28+ {entries.map((entry, index) => (
29+ // 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+ <span aria-hidden>{statusMarks[entry.status] ?? "○"}</span>{" "}
32+ {entry.content}
33+ </li>
34+ ))}
35+ </ul>
36+ </details>
37+ );
38+}
added ui/src/components/PromptInput.tsx +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+/**
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
4+ * the behaviour of Zed's agent panel.
5+ *
6+ * @example
7+ * <PromptInput turnActive={false} onSend={(t) => console.log(t)} onCancel={() => {}} />
8+ */
9+
10+import { useState } from "react";
11+
12+interface PromptInputProps {
13+ turnActive: boolean;
14+ onSend: (text: string) => void;
15+ onCancel: () => void;
16+}
17+
18+export function PromptInput({
19+ turnActive,
20+ onSend,
21+ onCancel,
22+}: PromptInputProps) {
23+ const [text, setText] = useState("");
24+
25+ const submit = () => {
26+ const trimmed = text.trim();
27+ if (trimmed === "" || turnActive) {
28+ return;
29+ }
30+ onSend(trimmed);
31+ setText("");
32+ };
33+
34+ return (
35+ <form
36+ className="composer"
37+ onSubmit={(event) => {
38+ event.preventDefault();
39+ submit();
40+ }}
41+ >
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();
53+ }
54+ }}
55+ />
56+ {turnActive ? (
57+ <button type="button" className="composer-stop" onClick={onCancel}>
58+ Stop
59+ </button>
60+ ) : (
61+ <button
62+ type="submit"
63+ className="composer-send"
64+ disabled={text.trim() === ""}
65+ >
66+ Send
67+ </button>
68+ )}
69+ </form>
70+ );
71+}
new file mode 100644
@@ -0,0 +1,71 @@
1+/**
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
4+ * the behaviour of Zed's agent panel.
5+ *
6+ * @example
7+ * <PromptInput turnActive={false} onSend={(t) => console.log(t)} onCancel={() => {}} />
8+ */
9+
10+import { useState } from "react";
11+
12+interface PromptInputProps {
13+ turnActive: boolean;
14+ onSend: (text: string) => void;
15+ onCancel: () => void;
16+}
17+
18+export function PromptInput({
19+ turnActive,
20+ onSend,
21+ onCancel,
22+}: PromptInputProps) {
23+ const [text, setText] = useState("");
24+
25+ const submit = () => {
26+ const trimmed = text.trim();
27+ if (trimmed === "" || turnActive) {
28+ return;
29+ }
30+ onSend(trimmed);
31+ setText("");
32+ };
33+
34+ return (
35+ <form
36+ className="composer"
37+ onSubmit={(event) => {
38+ event.preventDefault();
39+ submit();
40+ }}
41+ >
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();
53+ }
54+ }}
55+ />
56+ {turnActive ? (
57+ <button type="button" className="composer-stop" onClick={onCancel}>
58+ Stop
59+ </button>
60+ ) : (
61+ <button
62+ type="submit"
63+ className="composer-send"
64+ disabled={text.trim() === ""}
65+ >
66+ Send
67+ </button>
68+ )}
69+ </form>
70+ );
71+}
added ui/src/components/ToolCallCard.tsx +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+/**
2+ * ToolCallCard shows one tool call the agent is running, in the spirit of
3+ * Zed's agent panel: a collapsible card with the tool kind, a live status,
4+ * touched locations, and the call's output (text, or a file diff).
5+ *
6+ * @example
7+ * <ToolCallCard call={{ toolCallId: "c1", title: "Reading main.go",
8+ * toolKind: "read", status: "in_progress", contents: [], locations: [] }} />
9+ */
10+
11+import type { ToolCall } from "../reducer";
12+import type { AcpToolCallContent } from "../protocol";
13+import { Markdown } from "./Markdown";
14+
15+const kindIcons: Record<string, string> = {
16+ read: "📖",
17+ edit: "✏️",
18+ delete: "🗑️",
19+ move: "📦",
20+ search: "🔍",
21+ execute: "⚡",
22+ think: "💭",
23+ fetch: "🌐",
24+ switch_mode: "🔀",
25+ other: "🔧",
26+};
27+
28+const statusLabels: Record<string, string> = {
29+ pending: "pending",
30+ in_progress: "running",
31+ completed: "done",
32+ failed: "failed",
33+};
34+
35+export function ToolCallCard({ call }: { call: ToolCall }) {
36+ return (
37+ <details className={`tool tool-${call.status}`}>
38+ <summary className="tool-summary">
39+ <span className="tool-icon" aria-hidden>
40+ {kindIcons[call.toolKind] ?? kindIcons.other}
41+ </span>
42+ <span className="tool-title">{call.title}</span>
43+ <span className={`tool-status tool-status-${call.status}`}>
44+ {statusLabels[call.status] ?? call.status}
45+ </span>
46+ </summary>
47+ <div className="tool-body">
48+ {call.locations.length > 0 && (
49+ <ul className="tool-locations">
50+ {call.locations.map((location, index) => (
51+ // biome-ignore lint/suspicious/noArrayIndexKey: tool-call locations carry no id and the list is replaced wholesale on every update.
52+ <li key={index}>
53+ <code>
54+ {location.path}
55+ {location.line != null ? `:${location.line}` : ""}
56+ </code>
57+ </li>
58+ ))}
59+ </ul>
60+ )}
61+ {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.
63+ <ToolContent key={index} content={content} />
64+ ))}
65+ </div>
66+ </details>
67+ );
68+}
69+
70+function ToolContent({ content }: { content: AcpToolCallContent }) {
71+ switch (content.type) {
72+ case "content":
73+ return content.content?.text ? (
74+ <Markdown text={content.content.text} />
75+ ) : null;
76+ case "diff":
77+ return (
78+ <div className="tool-diff">
79+ <code className="tool-diff-path">{content.path}</code>
80+ {content.oldText != null && (
81+ <pre className="tool-diff-old">
82+ {prefixLines(content.oldText, "- ")}
83+ </pre>
84+ )}
85+ <pre className="tool-diff-new">
86+ {prefixLines(content.newText ?? "", "+ ")}
87+ </pre>
88+ </div>
89+ );
90+ case "terminal":
91+ return <p className="tool-terminal">terminal: {content.terminalId}</p>;
92+ default:
93+ return null;
94+ }
95+}
96+
97+function prefixLines(text: string, prefix: string): string {
98+ return text
99+ .split("\n")
100+ .map((line) => prefix + line)
101+ .join("\n");
102+}
new file mode 100644
@@ -0,0 +1,102 @@
1+/**
2+ * ToolCallCard shows one tool call the agent is running, in the spirit of
3+ * Zed's agent panel: a collapsible card with the tool kind, a live status,
4+ * touched locations, and the call's output (text, or a file diff).
5+ *
6+ * @example
7+ * <ToolCallCard call={{ toolCallId: "c1", title: "Reading main.go",
8+ * toolKind: "read", status: "in_progress", contents: [], locations: [] }} />
9+ */
10+
11+import type { ToolCall } from "../reducer";
12+import type { AcpToolCallContent } from "../protocol";
13+import { Markdown } from "./Markdown";
14+
15+const kindIcons: Record<string, string> = {
16+ read: "📖",
17+ edit: "✏️",
18+ delete: "🗑️",
19+ move: "📦",
20+ search: "🔍",
21+ execute: "⚡",
22+ think: "💭",
23+ fetch: "🌐",
24+ switch_mode: "🔀",
25+ other: "🔧",
26+};
27+
28+const statusLabels: Record<string, string> = {
29+ pending: "pending",
30+ in_progress: "running",
31+ completed: "done",
32+ failed: "failed",
33+};
34+
35+export function ToolCallCard({ call }: { call: ToolCall }) {
36+ return (
37+ <details className={`tool tool-${call.status}`}>
38+ <summary className="tool-summary">
39+ <span className="tool-icon" aria-hidden>
40+ {kindIcons[call.toolKind] ?? kindIcons.other}
41+ </span>
42+ <span className="tool-title">{call.title}</span>
43+ <span className={`tool-status tool-status-${call.status}`}>
44+ {statusLabels[call.status] ?? call.status}
45+ </span>
46+ </summary>
47+ <div className="tool-body">
48+ {call.locations.length > 0 && (
49+ <ul className="tool-locations">
50+ {call.locations.map((location, index) => (
51+ // biome-ignore lint/suspicious/noArrayIndexKey: tool-call locations carry no id and the list is replaced wholesale on every update.
52+ <li key={index}>
53+ <code>
54+ {location.path}
55+ {location.line != null ? `:${location.line}` : ""}
56+ </code>
57+ </li>
58+ ))}
59+ </ul>
60+ )}
61+ {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.
63+ <ToolContent key={index} content={content} />
64+ ))}
65+ </div>
66+ </details>
67+ );
68+}
69+
70+function ToolContent({ content }: { content: AcpToolCallContent }) {
71+ switch (content.type) {
72+ case "content":
73+ return content.content?.text ? (
74+ <Markdown text={content.content.text} />
75+ ) : null;
76+ case "diff":
77+ return (
78+ <div className="tool-diff">
79+ <code className="tool-diff-path">{content.path}</code>
80+ {content.oldText != null && (
81+ <pre className="tool-diff-old">
82+ {prefixLines(content.oldText, "- ")}
83+ </pre>
84+ )}
85+ <pre className="tool-diff-new">
86+ {prefixLines(content.newText ?? "", "+ ")}
87+ </pre>
88+ </div>
89+ );
90+ case "terminal":
91+ return <p className="tool-terminal">terminal: {content.terminalId}</p>;
92+ default:
93+ return null;
94+ }
95+}
96+
97+function prefixLines(text: string, prefix: string): string {
98+ return text
99+ .split("\n")
100+ .map((line) => prefix + line)
101+ .join("\n");
102+}
added ui/src/main.tsx +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+import { StrictMode } from "react";
2+import { createRoot } from "react-dom/client";
3+import { App } from "./App";
4+import { dispatch } from "./store";
5+import { connectSocket, wsUrl } from "./ws";
6+import "./app.css";
7+
8+const root = document.getElementById("root");
9+if (!root) {
10+ throw new Error("index.html must contain a #root element");
11+}
12+
13+const socket = connectSocket({ url: wsUrl(), dispatch });
14+
15+createRoot(root).render(
16+ <StrictMode>
17+ <App socket={socket} />
18+ </StrictMode>,
19+);
new file mode 100644
@@ -0,0 +1,19 @@
1+import { StrictMode } from "react";
2+import { createRoot } from "react-dom/client";
3+import { App } from "./App";
4+import { dispatch } from "./store";
5+import { connectSocket, wsUrl } from "./ws";
6+import "./app.css";
7+
8+const root = document.getElementById("root");
9+if (!root) {
10+ throw new Error("index.html must contain a #root element");
11+}
12+
13+const socket = connectSocket({ url: wsUrl(), dispatch });
14+
15+createRoot(root).render(
16+ <StrictMode>
17+ <App socket={socket} />
18+ </StrictMode>,
19+);
added ui/src/protocol.ts +104 -0
new file mode 100644
@@ -0,0 +1,104 @@
1+/**
2+ * Wire contract with the ori backend, mirroring internal/bridge/protocol.go.
3+ *
4+ * ACP payloads (session updates, permission requests) arrive verbatim from
5+ * the agent, so their shapes follow https://agentclientprotocol.com.
6+ */
7+
8+/** An ACP content block; only text blocks are rendered in this version. */
9+export interface AcpContentBlock {
10+ type: string;
11+ text?: string;
12+ [key: string]: unknown;
13+}
14+
15+/** One piece of tool-call output: nested content, a file diff, or a terminal. */
16+export interface AcpToolCallContent {
17+ type: string;
18+ content?: AcpContentBlock;
19+ path?: string;
20+ oldText?: string | null;
21+ newText?: string;
22+ terminalId?: string;
23+}
24+
25+/** A file location a tool call touches (Zed uses this to "follow the agent"). */
26+export interface AcpToolCallLocation {
27+ path: string;
28+ line?: number;
29+}
30+
31+/** One entry of the agent's execution plan. */
32+export interface AcpPlanEntry {
33+ content: string;
34+ priority: string;
35+ status: string;
36+}
37+
38+/** One choice offered by a permission request. */
39+export interface AcpPermissionOption {
40+ optionId: string;
41+ name: string;
42+ kind: string;
43+}
44+
45+/** The ACP payload of a session/request_permission call. */
46+export interface AcpPermissionRequest {
47+ sessionId?: string;
48+ toolCall?: {
49+ toolCallId?: string;
50+ title?: string | null;
51+ kind?: string;
52+ [key: string]: unknown;
53+ };
54+ options?: AcpPermissionOption[];
55+}
56+
57+/**
58+ * One ACP session/update notification payload, discriminated by
59+ * sessionUpdate. Message/thought chunks use `content` as a single block;
60+ * tool calls use it as an array of AcpToolCallContent.
61+ */
62+export interface AcpSessionUpdate {
63+ sessionUpdate: string;
64+ content?: AcpContentBlock | AcpToolCallContent[];
65+ toolCallId?: string;
66+ title?: string | null;
67+ kind?: string;
68+ status?: string;
69+ locations?: AcpToolCallLocation[];
70+ entries?: AcpPlanEntry[];
71+ [key: string]: unknown;
72+}
73+
74+/** A message sent by the server over the WebSocket. */
75+export interface ServerMessage {
76+ type:
77+ | "hello"
78+ | "user_message"
79+ | "session_update"
80+ | "permission_request"
81+ | "permission_resolved"
82+ | "turn_started"
83+ | "turn_ended"
84+ | "error";
85+ sessionId?: string;
86+ turnActive?: boolean;
87+ update?: AcpSessionUpdate;
88+ requestId?: string;
89+ request?: AcpPermissionRequest;
90+ text?: string;
91+ stopReason?: string;
92+ message?: string;
93+}
94+
95+/** A message the browser sends to the server. */
96+export type ClientMessage =
97+ | { type: "prompt"; text: string }
98+ | { type: "cancel" }
99+ | {
100+ type: "permission_response";
101+ requestId: string;
102+ optionId?: string;
103+ cancelled?: boolean;
104+ };
new file mode 100644
@@ -0,0 +1,104 @@
1+/**
2+ * Wire contract with the ori backend, mirroring internal/bridge/protocol.go.
3+ *
4+ * ACP payloads (session updates, permission requests) arrive verbatim from
5+ * the agent, so their shapes follow https://agentclientprotocol.com.
6+ */
7+
8+/** An ACP content block; only text blocks are rendered in this version. */
9+export interface AcpContentBlock {
10+ type: string;
11+ text?: string;
12+ [key: string]: unknown;
13+}
14+
15+/** One piece of tool-call output: nested content, a file diff, or a terminal. */
16+export interface AcpToolCallContent {
17+ type: string;
18+ content?: AcpContentBlock;
19+ path?: string;
20+ oldText?: string | null;
21+ newText?: string;
22+ terminalId?: string;
23+}
24+
25+/** A file location a tool call touches (Zed uses this to "follow the agent"). */
26+export interface AcpToolCallLocation {
27+ path: string;
28+ line?: number;
29+}
30+
31+/** One entry of the agent's execution plan. */
32+export interface AcpPlanEntry {
33+ content: string;
34+ priority: string;
35+ status: string;
36+}
37+
38+/** One choice offered by a permission request. */
39+export interface AcpPermissionOption {
40+ optionId: string;
41+ name: string;
42+ kind: string;
43+}
44+
45+/** The ACP payload of a session/request_permission call. */
46+export interface AcpPermissionRequest {
47+ sessionId?: string;
48+ toolCall?: {
49+ toolCallId?: string;
50+ title?: string | null;
51+ kind?: string;
52+ [key: string]: unknown;
53+ };
54+ options?: AcpPermissionOption[];
55+}
56+
57+/**
58+ * One ACP session/update notification payload, discriminated by
59+ * sessionUpdate. Message/thought chunks use `content` as a single block;
60+ * tool calls use it as an array of AcpToolCallContent.
61+ */
62+export interface AcpSessionUpdate {
63+ sessionUpdate: string;
64+ content?: AcpContentBlock | AcpToolCallContent[];
65+ toolCallId?: string;
66+ title?: string | null;
67+ kind?: string;
68+ status?: string;
69+ locations?: AcpToolCallLocation[];
70+ entries?: AcpPlanEntry[];
71+ [key: string]: unknown;
72+}
73+
74+/** A message sent by the server over the WebSocket. */
75+export interface ServerMessage {
76+ type:
77+ | "hello"
78+ | "user_message"
79+ | "session_update"
80+ | "permission_request"
81+ | "permission_resolved"
82+ | "turn_started"
83+ | "turn_ended"
84+ | "error";
85+ sessionId?: string;
86+ turnActive?: boolean;
87+ update?: AcpSessionUpdate;
88+ requestId?: string;
89+ request?: AcpPermissionRequest;
90+ text?: string;
91+ stopReason?: string;
92+ message?: string;
93+}
94+
95+/** A message the browser sends to the server. */
96+export type ClientMessage =
97+ | { type: "prompt"; text: string }
98+ | { type: "cancel" }
99+ | {
100+ type: "permission_response";
101+ requestId: string;
102+ optionId?: string;
103+ cancelled?: boolean;
104+ };
added ui/src/reducer.test.ts +299 -0
new file mode 100644
@@ -0,0 +1,299 @@
1+import { describe, expect, it } from "vitest";
2+import { initialState, reduce, type ChatState } from "./reducer";
3+import type { ServerMessage } from "./protocol";
4+
5+function afterServer(
6+ state: ChatState,
7+ ...messages: ServerMessage[]
8+): ChatState {
9+ return messages.reduce(
10+ (s, message) => reduce(s, { type: "server", message }),
11+ state,
12+ );
13+}
14+
15+describe("reduce", () => {
16+ it("stores the session and goes online on hello", () => {
17+ const state = afterServer(initialState, {
18+ type: "hello",
19+ sessionId: "s1",
20+ turnActive: true,
21+ });
22+ expect(state.sessionId).toBe("s1");
23+ expect(state.connection).toBe("online");
24+ expect(state.turnActive).toBe(true);
25+ });
26+
27+ it("resets the thread on hello so reconnect replays are idempotent", () => {
28+ let state = afterServer(
29+ initialState,
30+ { type: "hello", sessionId: "s1" },
31+ { type: "user_message", text: "hi" },
32+ );
33+ expect(state.thread).toHaveLength(1);
34+ state = afterServer(
35+ state,
36+ { type: "hello", sessionId: "s1" },
37+ { type: "user_message", text: "hi" },
38+ );
39+ expect(state.thread).toHaveLength(1);
40+ });
41+
42+ it("appends user messages to the thread", () => {
43+ const state = afterServer(initialState, {
44+ type: "user_message",
45+ text: "do the thing",
46+ });
47+ expect(state.thread).toEqual([{ kind: "user", text: "do the thing" }]);
48+ });
49+
50+ it("accumulates agent chunks into one open item", () => {
51+ const chunk = (text: string): ServerMessage => ({
52+ type: "session_update",
53+ update: {
54+ sessionUpdate: "agent_message_chunk",
55+ content: { type: "text", text },
56+ },
57+ });
58+ const state = afterServer(initialState, chunk("Hello "), chunk("world"));
59+ expect(state.thread).toEqual([
60+ { kind: "agent", text: "Hello world", closed: false },
61+ ]);
62+ });
63+
64+ it("starts a fresh agent item after the turn ends", () => {
65+ const chunk = (text: string): ServerMessage => ({
66+ type: "session_update",
67+ update: {
68+ sessionUpdate: "agent_message_chunk",
69+ content: { type: "text", text },
70+ },
71+ });
72+ const state = afterServer(
73+ initialState,
74+ chunk("first answer"),
75+ { type: "turn_ended", stopReason: "end_turn" },
76+ chunk("second answer"),
77+ );
78+ expect(state.thread).toHaveLength(2);
79+ expect(state.thread[0]).toMatchObject({
80+ kind: "agent",
81+ text: "first answer",
82+ closed: true,
83+ });
84+ expect(state.thread[1]).toMatchObject({
85+ kind: "agent",
86+ text: "second answer",
87+ closed: false,
88+ });
89+ });
90+
91+ it("tracks the turn flag through markers", () => {
92+ let state = afterServer(initialState, { type: "turn_started" });
93+ expect(state.turnActive).toBe(true);
94+ state = afterServer(state, { type: "turn_ended", stopReason: "end_turn" });
95+ expect(state.turnActive).toBe(false);
96+ });
97+
98+ it("renders server errors as thread items", () => {
99+ const state = afterServer(initialState, { type: "error", message: "boom" });
100+ expect(state.thread).toEqual([{ kind: "error", text: "boom" }]);
101+ });
102+
103+ it("ignores unknown update kinds without breaking", () => {
104+ const state = afterServer(initialState, {
105+ type: "session_update",
106+ update: { sessionUpdate: "something_new" },
107+ });
108+ expect(state.thread).toEqual([]);
109+ });
110+
111+ it("accumulates thought chunks separately from agent messages", () => {
112+ const state = afterServer(
113+ initialState,
114+ {
115+ type: "session_update",
116+ update: {
117+ sessionUpdate: "agent_thought_chunk",
118+ content: { type: "text", text: "hmm " },
119+ },
120+ },
121+ {
122+ type: "session_update",
123+ update: {
124+ sessionUpdate: "agent_thought_chunk",
125+ content: { type: "text", text: "ok" },
126+ },
127+ },
128+ {
129+ type: "session_update",
130+ update: {
131+ sessionUpdate: "agent_message_chunk",
132+ content: { type: "text", text: "Answer" },
133+ },
134+ },
135+ );
136+ expect(state.thread).toHaveLength(2);
137+ expect(state.thread[0]).toMatchObject({ kind: "thought", text: "hmm ok" });
138+ expect(state.thread[1]).toMatchObject({ kind: "agent", text: "Answer" });
139+ });
140+
141+ it("creates a tool call and merges its updates", () => {
142+ let state = afterServer(initialState, {
143+ type: "session_update",
144+ update: {
145+ sessionUpdate: "tool_call",
146+ toolCallId: "call-1",
147+ title: "Reading files",
148+ kind: "read",
149+ status: "pending",
150+ locations: [{ path: "/p/main.go" }],
151+ },
152+ });
153+ expect(state.thread).toEqual([
154+ {
155+ kind: "tool",
156+ call: {
157+ toolCallId: "call-1",
158+ title: "Reading files",
159+ toolKind: "read",
160+ status: "pending",
161+ contents: [],
162+ locations: [{ path: "/p/main.go" }],
163+ },
164+ },
165+ ]);
166+
167+ state = afterServer(state, {
168+ type: "session_update",
169+ update: {
170+ sessionUpdate: "tool_call_update",
171+ toolCallId: "call-1",
172+ status: "completed",
173+ content: [
174+ { type: "content", content: { type: "text", text: "42 lines" } },
175+ ],
176+ },
177+ });
178+ expect(state.thread).toHaveLength(1);
179+ const item = state.thread[0];
180+ if (item.kind !== "tool") throw new Error("expected a tool item");
181+ expect(item.call.status).toBe("completed");
182+ expect(item.call.title).toBe("Reading files");
183+ expect(item.call.contents).toHaveLength(1);
184+ });
185+
186+ it("creates a tool item when an update arrives for an unknown id", () => {
187+ const state = afterServer(initialState, {
188+ type: "session_update",
189+ update: {
190+ sessionUpdate: "tool_call_update",
191+ toolCallId: "ghost",
192+ status: "in_progress",
193+ },
194+ });
195+ expect(state.thread).toHaveLength(1);
196+ expect(state.thread[0]).toMatchObject({
197+ kind: "tool",
198+ call: { toolCallId: "ghost", status: "in_progress" },
199+ });
200+ });
201+
202+ it("replaces the plan on each plan update", () => {
203+ let state = afterServer(initialState, {
204+ type: "session_update",
205+ update: {
206+ sessionUpdate: "plan",
207+ entries: [{ content: "step 1", priority: "high", status: "pending" }],
208+ },
209+ });
210+ expect(state.plan).toHaveLength(1);
211+ state = afterServer(state, {
212+ type: "session_update",
213+ update: {
214+ sessionUpdate: "plan",
215+ entries: [
216+ { content: "step 1", priority: "high", status: "completed" },
217+ { content: "step 2", priority: "low", status: "pending" },
218+ ],
219+ },
220+ });
221+ expect(state.plan).toHaveLength(2);
222+ expect(state.plan[0].status).toBe("completed");
223+ });
224+
225+ it("tracks permission requests until they are resolved", () => {
226+ let state = afterServer(initialState, {
227+ type: "permission_request",
228+ requestId: "perm-1",
229+ request: {
230+ toolCall: { title: "Run make test" },
231+ options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }],
232+ },
233+ });
234+ expect(state.permissions).toEqual([
235+ {
236+ requestId: "perm-1",
237+ title: "Run make test",
238+ options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }],
239+ },
240+ ]);
241+
242+ // replayed duplicates are ignored
243+ state = afterServer(state, {
244+ type: "permission_request",
245+ requestId: "perm-1",
246+ request: { options: [] },
247+ });
248+ expect(state.permissions).toHaveLength(1);
249+
250+ state = afterServer(state, {
251+ type: "permission_resolved",
252+ requestId: "perm-1",
253+ });
254+ expect(state.permissions).toEqual([]);
255+ });
256+
257+ it("clears plan and permissions on hello", () => {
258+ let state = afterServer(
259+ initialState,
260+ {
261+ type: "permission_request",
262+ requestId: "perm-1",
263+ request: { options: [] },
264+ },
265+ {
266+ type: "session_update",
267+ update: {
268+ sessionUpdate: "plan",
269+ entries: [{ content: "x", priority: "low", status: "pending" }],
270+ },
271+ },
272+ );
273+ state = afterServer(state, { type: "hello", sessionId: "s1" });
274+ expect(state.permissions).toEqual([]);
275+ expect(state.plan).toEqual([]);
276+ });
277+
278+ it("tracks connection status changes", () => {
279+ const state = reduce(initialState, {
280+ type: "connection",
281+ status: "offline",
282+ });
283+ expect(state.connection).toBe("offline");
284+ });
285+
286+ it("never mutates the previous state", () => {
287+ const before = afterServer(initialState, {
288+ type: "user_message",
289+ text: "a",
290+ });
291+ const snapshot = JSON.parse(JSON.stringify(before));
292+ afterServer(
293+ before,
294+ { type: "user_message", text: "b" },
295+ { type: "turn_started" },
296+ );
297+ expect(before).toEqual(snapshot);
298+ });
299+});
new file mode 100644
@@ -0,0 +1,299 @@
1+import { describe, expect, it } from "vitest";
2+import { initialState, reduce, type ChatState } from "./reducer";
3+import type { ServerMessage } from "./protocol";
4+
5+function afterServer(
6+ state: ChatState,
7+ ...messages: ServerMessage[]
8+): ChatState {
9+ return messages.reduce(
10+ (s, message) => reduce(s, { type: "server", message }),
11+ state,
12+ );
13+}
14+
15+describe("reduce", () => {
16+ it("stores the session and goes online on hello", () => {
17+ const state = afterServer(initialState, {
18+ type: "hello",
19+ sessionId: "s1",
20+ turnActive: true,
21+ });
22+ expect(state.sessionId).toBe("s1");
23+ expect(state.connection).toBe("online");
24+ expect(state.turnActive).toBe(true);
25+ });
26+
27+ it("resets the thread on hello so reconnect replays are idempotent", () => {
28+ let state = afterServer(
29+ initialState,
30+ { type: "hello", sessionId: "s1" },
31+ { type: "user_message", text: "hi" },
32+ );
33+ expect(state.thread).toHaveLength(1);
34+ state = afterServer(
35+ state,
36+ { type: "hello", sessionId: "s1" },
37+ { type: "user_message", text: "hi" },
38+ );
39+ expect(state.thread).toHaveLength(1);
40+ });
41+
42+ it("appends user messages to the thread", () => {
43+ const state = afterServer(initialState, {
44+ type: "user_message",
45+ text: "do the thing",
46+ });
47+ expect(state.thread).toEqual([{ kind: "user", text: "do the thing" }]);
48+ });
49+
50+ it("accumulates agent chunks into one open item", () => {
51+ const chunk = (text: string): ServerMessage => ({
52+ type: "session_update",
53+ update: {
54+ sessionUpdate: "agent_message_chunk",
55+ content: { type: "text", text },
56+ },
57+ });
58+ const state = afterServer(initialState, chunk("Hello "), chunk("world"));
59+ expect(state.thread).toEqual([
60+ { kind: "agent", text: "Hello world", closed: false },
61+ ]);
62+ });
63+
64+ it("starts a fresh agent item after the turn ends", () => {
65+ const chunk = (text: string): ServerMessage => ({
66+ type: "session_update",
67+ update: {
68+ sessionUpdate: "agent_message_chunk",
69+ content: { type: "text", text },
70+ },
71+ });
72+ const state = afterServer(
73+ initialState,
74+ chunk("first answer"),
75+ { type: "turn_ended", stopReason: "end_turn" },
76+ chunk("second answer"),
77+ );
78+ expect(state.thread).toHaveLength(2);
79+ expect(state.thread[0]).toMatchObject({
80+ kind: "agent",
81+ text: "first answer",
82+ closed: true,
83+ });
84+ expect(state.thread[1]).toMatchObject({
85+ kind: "agent",
86+ text: "second answer",
87+ closed: false,
88+ });
89+ });
90+
91+ it("tracks the turn flag through markers", () => {
92+ let state = afterServer(initialState, { type: "turn_started" });
93+ expect(state.turnActive).toBe(true);
94+ state = afterServer(state, { type: "turn_ended", stopReason: "end_turn" });
95+ expect(state.turnActive).toBe(false);
96+ });
97+
98+ it("renders server errors as thread items", () => {
99+ const state = afterServer(initialState, { type: "error", message: "boom" });
100+ expect(state.thread).toEqual([{ kind: "error", text: "boom" }]);
101+ });
102+
103+ it("ignores unknown update kinds without breaking", () => {
104+ const state = afterServer(initialState, {
105+ type: "session_update",
106+ update: { sessionUpdate: "something_new" },
107+ });
108+ expect(state.thread).toEqual([]);
109+ });
110+
111+ it("accumulates thought chunks separately from agent messages", () => {
112+ const state = afterServer(
113+ initialState,
114+ {
115+ type: "session_update",
116+ update: {
117+ sessionUpdate: "agent_thought_chunk",
118+ content: { type: "text", text: "hmm " },
119+ },
120+ },
121+ {
122+ type: "session_update",
123+ update: {
124+ sessionUpdate: "agent_thought_chunk",
125+ content: { type: "text", text: "ok" },
126+ },
127+ },
128+ {
129+ type: "session_update",
130+ update: {
131+ sessionUpdate: "agent_message_chunk",
132+ content: { type: "text", text: "Answer" },
133+ },
134+ },
135+ );
136+ expect(state.thread).toHaveLength(2);
137+ expect(state.thread[0]).toMatchObject({ kind: "thought", text: "hmm ok" });
138+ expect(state.thread[1]).toMatchObject({ kind: "agent", text: "Answer" });
139+ });
140+
141+ it("creates a tool call and merges its updates", () => {
142+ let state = afterServer(initialState, {
143+ type: "session_update",
144+ update: {
145+ sessionUpdate: "tool_call",
146+ toolCallId: "call-1",
147+ title: "Reading files",
148+ kind: "read",
149+ status: "pending",
150+ locations: [{ path: "/p/main.go" }],
151+ },
152+ });
153+ expect(state.thread).toEqual([
154+ {
155+ kind: "tool",
156+ call: {
157+ toolCallId: "call-1",
158+ title: "Reading files",
159+ toolKind: "read",
160+ status: "pending",
161+ contents: [],
162+ locations: [{ path: "/p/main.go" }],
163+ },
164+ },
165+ ]);
166+
167+ state = afterServer(state, {
168+ type: "session_update",
169+ update: {
170+ sessionUpdate: "tool_call_update",
171+ toolCallId: "call-1",
172+ status: "completed",
173+ content: [
174+ { type: "content", content: { type: "text", text: "42 lines" } },
175+ ],
176+ },
177+ });
178+ expect(state.thread).toHaveLength(1);
179+ const item = state.thread[0];
180+ if (item.kind !== "tool") throw new Error("expected a tool item");
181+ expect(item.call.status).toBe("completed");
182+ expect(item.call.title).toBe("Reading files");
183+ expect(item.call.contents).toHaveLength(1);
184+ });
185+
186+ it("creates a tool item when an update arrives for an unknown id", () => {
187+ const state = afterServer(initialState, {
188+ type: "session_update",
189+ update: {
190+ sessionUpdate: "tool_call_update",
191+ toolCallId: "ghost",
192+ status: "in_progress",
193+ },
194+ });
195+ expect(state.thread).toHaveLength(1);
196+ expect(state.thread[0]).toMatchObject({
197+ kind: "tool",
198+ call: { toolCallId: "ghost", status: "in_progress" },
199+ });
200+ });
201+
202+ it("replaces the plan on each plan update", () => {
203+ let state = afterServer(initialState, {
204+ type: "session_update",
205+ update: {
206+ sessionUpdate: "plan",
207+ entries: [{ content: "step 1", priority: "high", status: "pending" }],
208+ },
209+ });
210+ expect(state.plan).toHaveLength(1);
211+ state = afterServer(state, {
212+ type: "session_update",
213+ update: {
214+ sessionUpdate: "plan",
215+ entries: [
216+ { content: "step 1", priority: "high", status: "completed" },
217+ { content: "step 2", priority: "low", status: "pending" },
218+ ],
219+ },
220+ });
221+ expect(state.plan).toHaveLength(2);
222+ expect(state.plan[0].status).toBe("completed");
223+ });
224+
225+ it("tracks permission requests until they are resolved", () => {
226+ let state = afterServer(initialState, {
227+ type: "permission_request",
228+ requestId: "perm-1",
229+ request: {
230+ toolCall: { title: "Run make test" },
231+ options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }],
232+ },
233+ });
234+ expect(state.permissions).toEqual([
235+ {
236+ requestId: "perm-1",
237+ title: "Run make test",
238+ options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }],
239+ },
240+ ]);
241+
242+ // replayed duplicates are ignored
243+ state = afterServer(state, {
244+ type: "permission_request",
245+ requestId: "perm-1",
246+ request: { options: [] },
247+ });
248+ expect(state.permissions).toHaveLength(1);
249+
250+ state = afterServer(state, {
251+ type: "permission_resolved",
252+ requestId: "perm-1",
253+ });
254+ expect(state.permissions).toEqual([]);
255+ });
256+
257+ it("clears plan and permissions on hello", () => {
258+ let state = afterServer(
259+ initialState,
260+ {
261+ type: "permission_request",
262+ requestId: "perm-1",
263+ request: { options: [] },
264+ },
265+ {
266+ type: "session_update",
267+ update: {
268+ sessionUpdate: "plan",
269+ entries: [{ content: "x", priority: "low", status: "pending" }],
270+ },
271+ },
272+ );
273+ state = afterServer(state, { type: "hello", sessionId: "s1" });
274+ expect(state.permissions).toEqual([]);
275+ expect(state.plan).toEqual([]);
276+ });
277+
278+ it("tracks connection status changes", () => {
279+ const state = reduce(initialState, {
280+ type: "connection",
281+ status: "offline",
282+ });
283+ expect(state.connection).toBe("offline");
284+ });
285+
286+ it("never mutates the previous state", () => {
287+ const before = afterServer(initialState, {
288+ type: "user_message",
289+ text: "a",
290+ });
291+ const snapshot = JSON.parse(JSON.stringify(before));
292+ afterServer(
293+ before,
294+ { type: "user_message", text: "b" },
295+ { type: "turn_started" },
296+ );
297+ expect(before).toEqual(snapshot);
298+ });
299+});
added ui/src/reducer.ts +244 -0
new file mode 100644
@@ -0,0 +1,244 @@
1+/**
2+ * Pure state logic of the chat: a reducer folding server messages and
3+ * connection changes into the UI state. Keeping it a plain function makes the
4+ * whole conversation flow testable without React or a WebSocket.
5+ */
6+
7+import type {
8+ AcpContentBlock,
9+ AcpPlanEntry,
10+ AcpPermissionOption,
11+ AcpToolCallContent,
12+ AcpToolCallLocation,
13+ ServerMessage,
14+} from "./protocol";
15+
16+/** The UI's view of one tool call, folded from tool_call / tool_call_update. */
17+export interface ToolCall {
18+ toolCallId: string;
19+ title: string;
20+ toolKind: string;
21+ status: string;
22+ contents: AcpToolCallContent[];
23+ locations: AcpToolCallLocation[];
24+}
25+
26+/** One entry of the conversation thread. */
27+export type ThreadItem =
28+ | { kind: "user"; text: string }
29+ | { kind: "agent"; text: string; closed: boolean }
30+ | { kind: "thought"; text: string; closed: boolean }
31+ | { kind: "tool"; call: ToolCall }
32+ | { kind: "error"; text: string };
33+
34+/** A permission request awaiting the user's decision. */
35+export interface PermissionRequestState {
36+ requestId: string;
37+ title: string;
38+ options: AcpPermissionOption[];
39+}
40+
41+export type ConnectionStatus = "connecting" | "online" | "offline";
42+
43+/** The whole state the UI renders from. */
44+export interface ChatState {
45+ connection: ConnectionStatus;
46+ sessionId: string | null;
47+ turnActive: boolean;
48+ thread: ThreadItem[];
49+ plan: AcpPlanEntry[];
50+ permissions: PermissionRequestState[];
51+}
52+
53+export const initialState: ChatState = {
54+ connection: "connecting",
55+ sessionId: null,
56+ turnActive: false,
57+ thread: [],
58+ plan: [],
59+ permissions: [],
60+};
61+
62+/** Everything that can change the state. */
63+export type ChatEvent =
64+ | { type: "server"; message: ServerMessage }
65+ | { type: "connection"; status: ConnectionStatus };
66+
67+/**
68+ * reduce returns the next state for an event. It never mutates its input.
69+ *
70+ * @example
71+ * let state = initialState;
72+ * state = reduce(state, { type: "server", message: { type: "hello", sessionId: "s1" } });
73+ * state.sessionId; // "s1"
74+ */
75+export function reduce(state: ChatState, event: ChatEvent): ChatState {
76+ if (event.type === "connection") {
77+ return { ...state, connection: event.status };
78+ }
79+ // Unknown or not-yet-handled message types must never break the UI.
80+ const handler = serverHandlers[event.message.type];
81+ return handler ? handler(state, event.message) : state;
82+}
83+
84+type MessageHandler = (state: ChatState, msg: ServerMessage) => ChatState;
85+
86+/** One handler per server message type; unknown types fall through in reduce. */
87+const serverHandlers: Record<string, MessageHandler> = {
88+ // A hello opens every (re)connection, right before the full history
89+ // replay: resetting everything here makes reconnects idempotent.
90+ hello: (state, msg) => ({
91+ ...state,
92+ connection: "online",
93+ sessionId: msg.sessionId ?? null,
94+ turnActive: msg.turnActive ?? false,
95+ thread: [],
96+ plan: [],
97+ permissions: [],
98+ }),
99+ user_message: (state, msg) =>
100+ withItem(state, { kind: "user", text: msg.text ?? "" }),
101+ turn_started: (state) => ({ ...state, turnActive: true }),
102+ turn_ended: (state) => ({
103+ ...state,
104+ turnActive: false,
105+ thread: closeStreamItems(state.thread),
106+ }),
107+ session_update: reduceUpdate,
108+ permission_request: addPermission,
109+ permission_resolved: (state, msg) => ({
110+ ...state,
111+ permissions: state.permissions.filter((p) => p.requestId !== msg.requestId),
112+ }),
113+ error: (state, msg) =>
114+ withItem(state, { kind: "error", text: msg.message ?? "unknown error" }),
115+};
116+
117+/**
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.
121+ */
122+const updateHandlers: Record<string, MessageHandler> = {
123+ agent_message_chunk: (state, msg) =>
124+ appendStreamText(state, "agent", blockText(msg.update?.content)),
125+ agent_thought_chunk: (state, msg) =>
126+ appendStreamText(state, "thought", blockText(msg.update?.content)),
127+ tool_call: upsertToolCall,
128+ tool_call_update: upsertToolCall,
129+ plan: (state, msg) => ({ ...state, plan: msg.update?.entries ?? [] }),
130+};
131+
132+function reduceUpdate(state: ChatState, msg: ServerMessage): ChatState {
133+ const handler = msg.update
134+ ? updateHandlers[msg.update.sessionUpdate]
135+ : undefined;
136+ return handler ? handler(state, msg) : state;
137+}
138+
139+/** blockText extracts the text of a single content block, if any. */
140+function blockText(
141+ content: AcpContentBlock | AcpToolCallContent[] | undefined,
142+): string {
143+ if (!content || Array.isArray(content)) {
144+ return "";
145+ }
146+ return content.text ?? "";
147+}
148+
149+function withItem(state: ChatState, item: ThreadItem): ChatState {
150+ return { ...state, thread: [...state.thread, item] };
151+}
152+
153+/** Streamed chunks accumulate into the last open item of the same kind. */
154+function appendStreamText(
155+ state: ChatState,
156+ kind: "agent" | "thought",
157+ text: string,
158+): ChatState {
159+ const thread = [...state.thread];
160+ const last = thread[thread.length - 1];
161+ if (last && last.kind === kind && !last.closed) {
162+ thread[thread.length - 1] = { ...last, text: last.text + text };
163+ return { ...state, thread };
164+ }
165+ thread.push({ kind, text, closed: false });
166+ return { ...state, thread };
167+}
168+
169+/**
170+ * upsertToolCall folds a tool_call or tool_call_update into the thread:
171+ * updates merge only the fields they carry, and an update for an unknown id
172+ * creates the entry (the adapter may resume a call after a reconnect).
173+ */
174+function upsertToolCall(state: ChatState, msg: ServerMessage): ChatState {
175+ const update = msg.update;
176+ if (!update || !update.toolCallId) {
177+ return state;
178+ }
179+
180+ const contents = Array.isArray(update.content) ? update.content : undefined;
181+ const thread = [...state.thread];
182+ const index = thread.findIndex(
183+ (item) =>
184+ item.kind === "tool" && item.call.toolCallId === update.toolCallId,
185+ );
186+
187+ if (index >= 0) {
188+ const existing = thread[index] as Extract<ThreadItem, { kind: "tool" }>;
189+ thread[index] = {
190+ kind: "tool",
191+ call: {
192+ ...existing.call,
193+ title: update.title ?? existing.call.title,
194+ toolKind: update.kind ?? existing.call.toolKind,
195+ status: update.status ?? existing.call.status,
196+ contents: contents ?? existing.call.contents,
197+ locations: update.locations ?? existing.call.locations,
198+ },
199+ };
200+ return { ...state, thread };
201+ }
202+
203+ thread.push({
204+ kind: "tool",
205+ call: {
206+ toolCallId: update.toolCallId,
207+ title: update.title ?? "Tool call",
208+ toolKind: update.kind ?? "other",
209+ status: update.status ?? "pending",
210+ contents: contents ?? [],
211+ locations: update.locations ?? [],
212+ },
213+ });
214+ return { ...state, thread };
215+}
216+
217+function addPermission(state: ChatState, msg: ServerMessage): ChatState {
218+ if (
219+ !msg.requestId ||
220+ state.permissions.some((p) => p.requestId === msg.requestId)
221+ ) {
222+ return state;
223+ }
224+ return {
225+ ...state,
226+ permissions: [
227+ ...state.permissions,
228+ {
229+ requestId: msg.requestId,
230+ title: msg.request?.toolCall?.title ?? "The agent asks for permission",
231+ options: msg.request?.options ?? [],
232+ },
233+ ],
234+ };
235+}
236+
237+/** turn_ended closes every streaming item so the next turn starts fresh. */
238+function closeStreamItems(thread: ThreadItem[]): ThreadItem[] {
239+ return thread.map((item) =>
240+ item.kind === "agent" || item.kind === "thought"
241+ ? { ...item, closed: true }
242+ : item,
243+ );
244+}
new file mode 100644
@@ -0,0 +1,244 @@
1+/**
2+ * Pure state logic of the chat: a reducer folding server messages and
3+ * connection changes into the UI state. Keeping it a plain function makes the
4+ * whole conversation flow testable without React or a WebSocket.
5+ */
6+
7+import type {
8+ AcpContentBlock,
9+ AcpPlanEntry,
10+ AcpPermissionOption,
11+ AcpToolCallContent,
12+ AcpToolCallLocation,
13+ ServerMessage,
14+} from "./protocol";
15+
16+/** The UI's view of one tool call, folded from tool_call / tool_call_update. */
17+export interface ToolCall {
18+ toolCallId: string;
19+ title: string;
20+ toolKind: string;
21+ status: string;
22+ contents: AcpToolCallContent[];
23+ locations: AcpToolCallLocation[];
24+}
25+
26+/** One entry of the conversation thread. */
27+export type ThreadItem =
28+ | { kind: "user"; text: string }
29+ | { kind: "agent"; text: string; closed: boolean }
30+ | { kind: "thought"; text: string; closed: boolean }
31+ | { kind: "tool"; call: ToolCall }
32+ | { kind: "error"; text: string };
33+
34+/** A permission request awaiting the user's decision. */
35+export interface PermissionRequestState {
36+ requestId: string;
37+ title: string;
38+ options: AcpPermissionOption[];
39+}
40+
41+export type ConnectionStatus = "connecting" | "online" | "offline";
42+
43+/** The whole state the UI renders from. */
44+export interface ChatState {
45+ connection: ConnectionStatus;
46+ sessionId: string | null;
47+ turnActive: boolean;
48+ thread: ThreadItem[];
49+ plan: AcpPlanEntry[];
50+ permissions: PermissionRequestState[];
51+}
52+
53+export const initialState: ChatState = {
54+ connection: "connecting",
55+ sessionId: null,
56+ turnActive: false,
57+ thread: [],
58+ plan: [],
59+ permissions: [],
60+};
61+
62+/** Everything that can change the state. */
63+export type ChatEvent =
64+ | { type: "server"; message: ServerMessage }
65+ | { type: "connection"; status: ConnectionStatus };
66+
67+/**
68+ * reduce returns the next state for an event. It never mutates its input.
69+ *
70+ * @example
71+ * let state = initialState;
72+ * state = reduce(state, { type: "server", message: { type: "hello", sessionId: "s1" } });
73+ * state.sessionId; // "s1"
74+ */
75+export function reduce(state: ChatState, event: ChatEvent): ChatState {
76+ if (event.type === "connection") {
77+ return { ...state, connection: event.status };
78+ }
79+ // Unknown or not-yet-handled message types must never break the UI.
80+ const handler = serverHandlers[event.message.type];
81+ return handler ? handler(state, event.message) : state;
82+}
83+
84+type MessageHandler = (state: ChatState, msg: ServerMessage) => ChatState;
85+
86+/** One handler per server message type; unknown types fall through in reduce. */
87+const serverHandlers: Record<string, MessageHandler> = {
88+ // A hello opens every (re)connection, right before the full history
89+ // replay: resetting everything here makes reconnects idempotent.
90+ hello: (state, msg) => ({
91+ ...state,
92+ connection: "online",
93+ sessionId: msg.sessionId ?? null,
94+ turnActive: msg.turnActive ?? false,
95+ thread: [],
96+ plan: [],
97+ permissions: [],
98+ }),
99+ user_message: (state, msg) =>
100+ withItem(state, { kind: "user", text: msg.text ?? "" }),
101+ turn_started: (state) => ({ ...state, turnActive: true }),
102+ turn_ended: (state) => ({
103+ ...state,
104+ turnActive: false,
105+ thread: closeStreamItems(state.thread),
106+ }),
107+ session_update: reduceUpdate,
108+ permission_request: addPermission,
109+ permission_resolved: (state, msg) => ({
110+ ...state,
111+ permissions: state.permissions.filter((p) => p.requestId !== msg.requestId),
112+ }),
113+ error: (state, msg) =>
114+ withItem(state, { kind: "error", text: msg.message ?? "unknown error" }),
115+};
116+
117+/**
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.
121+ */
122+const updateHandlers: Record<string, MessageHandler> = {
123+ agent_message_chunk: (state, msg) =>
124+ appendStreamText(state, "agent", blockText(msg.update?.content)),
125+ agent_thought_chunk: (state, msg) =>
126+ appendStreamText(state, "thought", blockText(msg.update?.content)),
127+ tool_call: upsertToolCall,
128+ tool_call_update: upsertToolCall,
129+ plan: (state, msg) => ({ ...state, plan: msg.update?.entries ?? [] }),
130+};
131+
132+function reduceUpdate(state: ChatState, msg: ServerMessage): ChatState {
133+ const handler = msg.update
134+ ? updateHandlers[msg.update.sessionUpdate]
135+ : undefined;
136+ return handler ? handler(state, msg) : state;
137+}
138+
139+/** blockText extracts the text of a single content block, if any. */
140+function blockText(
141+ content: AcpContentBlock | AcpToolCallContent[] | undefined,
142+): string {
143+ if (!content || Array.isArray(content)) {
144+ return "";
145+ }
146+ return content.text ?? "";
147+}
148+
149+function withItem(state: ChatState, item: ThreadItem): ChatState {
150+ return { ...state, thread: [...state.thread, item] };
151+}
152+
153+/** Streamed chunks accumulate into the last open item of the same kind. */
154+function appendStreamText(
155+ state: ChatState,
156+ kind: "agent" | "thought",
157+ text: string,
158+): ChatState {
159+ const thread = [...state.thread];
160+ const last = thread[thread.length - 1];
161+ if (last && last.kind === kind && !last.closed) {
162+ thread[thread.length - 1] = { ...last, text: last.text + text };
163+ return { ...state, thread };
164+ }
165+ thread.push({ kind, text, closed: false });
166+ return { ...state, thread };
167+}
168+
169+/**
170+ * upsertToolCall folds a tool_call or tool_call_update into the thread:
171+ * updates merge only the fields they carry, and an update for an unknown id
172+ * creates the entry (the adapter may resume a call after a reconnect).
173+ */
174+function upsertToolCall(state: ChatState, msg: ServerMessage): ChatState {
175+ const update = msg.update;
176+ if (!update || !update.toolCallId) {
177+ return state;
178+ }
179+
180+ const contents = Array.isArray(update.content) ? update.content : undefined;
181+ const thread = [...state.thread];
182+ const index = thread.findIndex(
183+ (item) =>
184+ item.kind === "tool" && item.call.toolCallId === update.toolCallId,
185+ );
186+
187+ if (index >= 0) {
188+ const existing = thread[index] as Extract<ThreadItem, { kind: "tool" }>;
189+ thread[index] = {
190+ kind: "tool",
191+ call: {
192+ ...existing.call,
193+ title: update.title ?? existing.call.title,
194+ toolKind: update.kind ?? existing.call.toolKind,
195+ status: update.status ?? existing.call.status,
196+ contents: contents ?? existing.call.contents,
197+ locations: update.locations ?? existing.call.locations,
198+ },
199+ };
200+ return { ...state, thread };
201+ }
202+
203+ thread.push({
204+ kind: "tool",
205+ call: {
206+ toolCallId: update.toolCallId,
207+ title: update.title ?? "Tool call",
208+ toolKind: update.kind ?? "other",
209+ status: update.status ?? "pending",
210+ contents: contents ?? [],
211+ locations: update.locations ?? [],
212+ },
213+ });
214+ return { ...state, thread };
215+}
216+
217+function addPermission(state: ChatState, msg: ServerMessage): ChatState {
218+ if (
219+ !msg.requestId ||
220+ state.permissions.some((p) => p.requestId === msg.requestId)
221+ ) {
222+ return state;
223+ }
224+ return {
225+ ...state,
226+ permissions: [
227+ ...state.permissions,
228+ {
229+ requestId: msg.requestId,
230+ title: msg.request?.toolCall?.title ?? "The agent asks for permission",
231+ options: msg.request?.options ?? [],
232+ },
233+ ],
234+ };
235+}
236+
237+/** turn_ended closes every streaming item so the next turn starts fresh. */
238+function closeStreamItems(thread: ThreadItem[]): ThreadItem[] {
239+ return thread.map((item) =>
240+ item.kind === "agent" || item.kind === "thought"
241+ ? { ...item, closed: true }
242+ : item,
243+ );
244+}
added ui/src/store.ts +31 -0
new file mode 100644
@@ -0,0 +1,31 @@
1+/**
2+ * The application store: a zustand wrapper around the pure reducer.
3+ *
4+ * @example
5+ * import { useChatState, dispatch } from "./store";
6+ * dispatch({ type: "connection", status: "online" });
7+ * const state = useChatState(); // inside a component
8+ */
9+
10+import { create } from "zustand";
11+import {
12+ initialState,
13+ reduce,
14+ type ChatEvent,
15+ type ChatState,
16+} from "./reducer";
17+
18+const store = create<ChatState>(() => initialState);
19+
20+/** React hook returning the current chat state (re-renders on change). */
21+export const useChatState = store;
22+
23+/** dispatch applies an event to the store through the reducer. */
24+export function dispatch(event: ChatEvent): void {
25+ store.setState((state) => reduce(state, event));
26+}
27+
28+/** resetStore restores the initial state; meant for tests. */
29+export function resetStore(): void {
30+ store.setState(initialState, true);
31+}
new file mode 100644
@@ -0,0 +1,31 @@
1+/**
2+ * The application store: a zustand wrapper around the pure reducer.
3+ *
4+ * @example
5+ * import { useChatState, dispatch } from "./store";
6+ * dispatch({ type: "connection", status: "online" });
7+ * const state = useChatState(); // inside a component
8+ */
9+
10+import { create } from "zustand";
11+import {
12+ initialState,
13+ reduce,
14+ type ChatEvent,
15+ type ChatState,
16+} from "./reducer";
17+
18+const store = create<ChatState>(() => initialState);
19+
20+/** React hook returning the current chat state (re-renders on change). */
21+export const useChatState = store;
22+
23+/** dispatch applies an event to the store through the reducer. */
24+export function dispatch(event: ChatEvent): void {
25+ store.setState((state) => reduce(state, event));
26+}
27+
28+/** resetStore restores the initial state; meant for tests. */
29+export function resetStore(): void {
30+ store.setState(initialState, true);
31+}
added ui/src/ws.test.ts +135 -0
new file mode 100644
@@ -0,0 +1,135 @@
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+import { connectSocket } from "./ws";
3+import type { ChatEvent } from "./reducer";
4+
5+/** A minimal scriptable WebSocket double. */
6+class FakeWebSocket {
7+ static OPEN = 1;
8+ static instances: FakeWebSocket[] = [];
9+
10+ url: string;
11+ readyState = 0;
12+ sent: string[] = [];
13+ onopen: (() => void) | null = null;
14+ onmessage: ((event: { data: string }) => void) | null = null;
15+ onclose: (() => void) | null = null;
16+
17+ constructor(url: string) {
18+ this.url = url;
19+ FakeWebSocket.instances.push(this);
20+ }
21+
22+ send(data: string) {
23+ this.sent.push(data);
24+ }
25+
26+ close() {
27+ this.readyState = 3;
28+ this.onclose?.();
29+ }
30+
31+ /* test helpers */
32+ serverOpens() {
33+ this.readyState = FakeWebSocket.OPEN;
34+ this.onopen?.();
35+ }
36+
37+ serverSends(payload: unknown) {
38+ this.onmessage?.({ data: JSON.stringify(payload) });
39+ }
40+
41+ serverDrops() {
42+ this.readyState = 3;
43+ this.onclose?.();
44+ }
45+}
46+
47+describe("connectSocket", () => {
48+ let events: ChatEvent[];
49+ const dispatch = (event: ChatEvent) => events.push(event);
50+
51+ beforeEach(() => {
52+ events = [];
53+ FakeWebSocket.instances = [];
54+ vi.useFakeTimers();
55+ });
56+
57+ afterEach(() => {
58+ vi.useRealTimers();
59+ });
60+
61+ const connect = () =>
62+ connectSocket({
63+ url: "ws://test/ws",
64+ dispatch,
65+ webSocketImpl: FakeWebSocket as unknown as typeof WebSocket,
66+ initialRetryMs: 100,
67+ });
68+
69+ it("dispatches parsed server messages", () => {
70+ connect();
71+ const ws = FakeWebSocket.instances[0];
72+ ws.serverOpens();
73+ ws.serverSends({ type: "hello", sessionId: "s1" });
74+
75+ expect(events).toContainEqual({
76+ type: "server",
77+ message: { type: "hello", sessionId: "s1" },
78+ });
79+ });
80+
81+ it("sends only while the socket is open", () => {
82+ const socket = connect();
83+ const ws = FakeWebSocket.instances[0];
84+
85+ socket.send({ type: "prompt", text: "too early" });
86+ expect(ws.sent).toHaveLength(0);
87+
88+ ws.serverOpens();
89+ socket.send({ type: "prompt", text: "hello" });
90+ expect(ws.sent).toEqual([
91+ JSON.stringify({ type: "prompt", text: "hello" }),
92+ ]);
93+ });
94+
95+ it("reconnects with backoff after a drop", () => {
96+ connect();
97+ FakeWebSocket.instances[0].serverOpens();
98+ FakeWebSocket.instances[0].serverDrops();
99+
100+ expect(events).toContainEqual({ type: "connection", status: "offline" });
101+ expect(FakeWebSocket.instances).toHaveLength(1);
102+
103+ vi.advanceTimersByTime(100);
104+ expect(FakeWebSocket.instances).toHaveLength(2);
105+
106+ // second drop: the delay doubles
107+ FakeWebSocket.instances[1].serverDrops();
108+ vi.advanceTimersByTime(100);
109+ expect(FakeWebSocket.instances).toHaveLength(2);
110+ vi.advanceTimersByTime(100);
111+ expect(FakeWebSocket.instances).toHaveLength(3);
112+ });
113+
114+ it("does not reconnect after close()", () => {
115+ const socket = connect();
116+ FakeWebSocket.instances[0].serverOpens();
117+ socket.close();
118+
119+ vi.advanceTimersByTime(10_000);
120+ expect(FakeWebSocket.instances).toHaveLength(1);
121+ });
122+
123+ it("survives malformed frames", () => {
124+ connect();
125+ const ws = FakeWebSocket.instances[0];
126+ ws.serverOpens();
127+ ws.onmessage?.({ data: "{not json" });
128+ ws.serverSends({ type: "turn_started" });
129+
130+ expect(events).toContainEqual({
131+ type: "server",
132+ message: { type: "turn_started" },
133+ });
134+ });
135+});
new file mode 100644
@@ -0,0 +1,135 @@
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+import { connectSocket } from "./ws";
3+import type { ChatEvent } from "./reducer";
4+
5+/** A minimal scriptable WebSocket double. */
6+class FakeWebSocket {
7+ static OPEN = 1;
8+ static instances: FakeWebSocket[] = [];
9+
10+ url: string;
11+ readyState = 0;
12+ sent: string[] = [];
13+ onopen: (() => void) | null = null;
14+ onmessage: ((event: { data: string }) => void) | null = null;
15+ onclose: (() => void) | null = null;
16+
17+ constructor(url: string) {
18+ this.url = url;
19+ FakeWebSocket.instances.push(this);
20+ }
21+
22+ send(data: string) {
23+ this.sent.push(data);
24+ }
25+
26+ close() {
27+ this.readyState = 3;
28+ this.onclose?.();
29+ }
30+
31+ /* test helpers */
32+ serverOpens() {
33+ this.readyState = FakeWebSocket.OPEN;
34+ this.onopen?.();
35+ }
36+
37+ serverSends(payload: unknown) {
38+ this.onmessage?.({ data: JSON.stringify(payload) });
39+ }
40+
41+ serverDrops() {
42+ this.readyState = 3;
43+ this.onclose?.();
44+ }
45+}
46+
47+describe("connectSocket", () => {
48+ let events: ChatEvent[];
49+ const dispatch = (event: ChatEvent) => events.push(event);
50+
51+ beforeEach(() => {
52+ events = [];
53+ FakeWebSocket.instances = [];
54+ vi.useFakeTimers();
55+ });
56+
57+ afterEach(() => {
58+ vi.useRealTimers();
59+ });
60+
61+ const connect = () =>
62+ connectSocket({
63+ url: "ws://test/ws",
64+ dispatch,
65+ webSocketImpl: FakeWebSocket as unknown as typeof WebSocket,
66+ initialRetryMs: 100,
67+ });
68+
69+ it("dispatches parsed server messages", () => {
70+ connect();
71+ const ws = FakeWebSocket.instances[0];
72+ ws.serverOpens();
73+ ws.serverSends({ type: "hello", sessionId: "s1" });
74+
75+ expect(events).toContainEqual({
76+ type: "server",
77+ message: { type: "hello", sessionId: "s1" },
78+ });
79+ });
80+
81+ it("sends only while the socket is open", () => {
82+ const socket = connect();
83+ const ws = FakeWebSocket.instances[0];
84+
85+ socket.send({ type: "prompt", text: "too early" });
86+ expect(ws.sent).toHaveLength(0);
87+
88+ ws.serverOpens();
89+ socket.send({ type: "prompt", text: "hello" });
90+ expect(ws.sent).toEqual([
91+ JSON.stringify({ type: "prompt", text: "hello" }),
92+ ]);
93+ });
94+
95+ it("reconnects with backoff after a drop", () => {
96+ connect();
97+ FakeWebSocket.instances[0].serverOpens();
98+ FakeWebSocket.instances[0].serverDrops();
99+
100+ expect(events).toContainEqual({ type: "connection", status: "offline" });
101+ expect(FakeWebSocket.instances).toHaveLength(1);
102+
103+ vi.advanceTimersByTime(100);
104+ expect(FakeWebSocket.instances).toHaveLength(2);
105+
106+ // second drop: the delay doubles
107+ FakeWebSocket.instances[1].serverDrops();
108+ vi.advanceTimersByTime(100);
109+ expect(FakeWebSocket.instances).toHaveLength(2);
110+ vi.advanceTimersByTime(100);
111+ expect(FakeWebSocket.instances).toHaveLength(3);
112+ });
113+
114+ it("does not reconnect after close()", () => {
115+ const socket = connect();
116+ FakeWebSocket.instances[0].serverOpens();
117+ socket.close();
118+
119+ vi.advanceTimersByTime(10_000);
120+ expect(FakeWebSocket.instances).toHaveLength(1);
121+ });
122+
123+ it("survives malformed frames", () => {
124+ connect();
125+ const ws = FakeWebSocket.instances[0];
126+ ws.serverOpens();
127+ ws.onmessage?.({ data: "{not json" });
128+ ws.serverSends({ type: "turn_started" });
129+
130+ expect(events).toContainEqual({
131+ type: "server",
132+ message: { type: "turn_started" },
133+ });
134+ });
135+});
added ui/src/ws.ts +85 -0
new file mode 100644
@@ -0,0 +1,85 @@
1+/**
2+ * WebSocket client for the ori backend: parses server messages into store
3+ * events and reconnects automatically with a capped backoff. The server
4+ * replays the session history on every connection, and the reducer resets on
5+ * "hello", so a reconnection rebuilds the exact same thread.
6+ */
7+
8+import type { ClientMessage, ServerMessage } from "./protocol";
9+import type { ChatEvent } from "./reducer";
10+
11+/** What the UI needs from the connection. */
12+export interface OriSocket {
13+ send(message: ClientMessage): void;
14+ close(): void;
15+}
16+
17+interface ConnectOptions {
18+ url: string;
19+ dispatch: (event: ChatEvent) => void;
20+ /** Injectable WebSocket constructor, for tests. Defaults to the browser's. */
21+ webSocketImpl?: typeof WebSocket;
22+ /** First reconnection delay in ms; doubles up to 16x. Defaults to 500. */
23+ initialRetryMs?: number;
24+}
25+
26+/**
27+ * connectSocket opens the connection and keeps it alive until close().
28+ *
29+ * @example
30+ * const socket = connectSocket({ url: wsUrl(), dispatch });
31+ * socket.send({ type: "prompt", text: "hello" });
32+ */
33+export function connectSocket(options: ConnectOptions): OriSocket {
34+ const Impl = options.webSocketImpl ?? WebSocket;
35+ const initialRetryMs = options.initialRetryMs ?? 500;
36+
37+ let socket: WebSocket | null = null;
38+ let retryMs = initialRetryMs;
39+ let closedByUser = false;
40+
41+ const open = () => {
42+ socket = new Impl(options.url);
43+ options.dispatch({ type: "connection", status: "connecting" });
44+
45+ socket.onopen = () => {
46+ retryMs = initialRetryMs;
47+ };
48+ socket.onmessage = (event: MessageEvent) => {
49+ let message: ServerMessage;
50+ try {
51+ message = JSON.parse(String(event.data)) as ServerMessage;
52+ } catch {
53+ return; // a malformed frame must not kill the connection handling
54+ }
55+ options.dispatch({ type: "server", message });
56+ };
57+ socket.onclose = () => {
58+ if (closedByUser) {
59+ return;
60+ }
61+ options.dispatch({ type: "connection", status: "offline" });
62+ setTimeout(open, retryMs);
63+ retryMs = Math.min(retryMs * 2, initialRetryMs * 16);
64+ };
65+ };
66+ open();
67+
68+ return {
69+ send(message: ClientMessage) {
70+ if (socket && socket.readyState === Impl.OPEN) {
71+ socket.send(JSON.stringify(message));
72+ }
73+ },
74+ close() {
75+ closedByUser = true;
76+ socket?.close();
77+ },
78+ };
79+}
80+
81+/** wsUrl derives the backend WebSocket URL from the page's own origin. */
82+export function wsUrl(): string {
83+ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
84+ return `${scheme}://${window.location.host}/ws`;
85+}
new file mode 100644
@@ -0,0 +1,85 @@
1+/**
2+ * WebSocket client for the ori backend: parses server messages into store
3+ * events and reconnects automatically with a capped backoff. The server
4+ * replays the session history on every connection, and the reducer resets on
5+ * "hello", so a reconnection rebuilds the exact same thread.
6+ */
7+
8+import type { ClientMessage, ServerMessage } from "./protocol";
9+import type { ChatEvent } from "./reducer";
10+
11+/** What the UI needs from the connection. */
12+export interface OriSocket {
13+ send(message: ClientMessage): void;
14+ close(): void;
15+}
16+
17+interface ConnectOptions {
18+ url: string;
19+ dispatch: (event: ChatEvent) => void;
20+ /** Injectable WebSocket constructor, for tests. Defaults to the browser's. */
21+ webSocketImpl?: typeof WebSocket;
22+ /** First reconnection delay in ms; doubles up to 16x. Defaults to 500. */
23+ initialRetryMs?: number;
24+}
25+
26+/**
27+ * connectSocket opens the connection and keeps it alive until close().
28+ *
29+ * @example
30+ * const socket = connectSocket({ url: wsUrl(), dispatch });
31+ * socket.send({ type: "prompt", text: "hello" });
32+ */
33+export function connectSocket(options: ConnectOptions): OriSocket {
34+ const Impl = options.webSocketImpl ?? WebSocket;
35+ const initialRetryMs = options.initialRetryMs ?? 500;
36+
37+ let socket: WebSocket | null = null;
38+ let retryMs = initialRetryMs;
39+ let closedByUser = false;
40+
41+ const open = () => {
42+ socket = new Impl(options.url);
43+ options.dispatch({ type: "connection", status: "connecting" });
44+
45+ socket.onopen = () => {
46+ retryMs = initialRetryMs;
47+ };
48+ socket.onmessage = (event: MessageEvent) => {
49+ let message: ServerMessage;
50+ try {
51+ message = JSON.parse(String(event.data)) as ServerMessage;
52+ } catch {
53+ return; // a malformed frame must not kill the connection handling
54+ }
55+ options.dispatch({ type: "server", message });
56+ };
57+ socket.onclose = () => {
58+ if (closedByUser) {
59+ return;
60+ }
61+ options.dispatch({ type: "connection", status: "offline" });
62+ setTimeout(open, retryMs);
63+ retryMs = Math.min(retryMs * 2, initialRetryMs * 16);
64+ };
65+ };
66+ open();
67+
68+ return {
69+ send(message: ClientMessage) {
70+ if (socket && socket.readyState === Impl.OPEN) {
71+ socket.send(JSON.stringify(message));
72+ }
73+ },
74+ close() {
75+ closedByUser = true;
76+ socket?.close();
77+ },
78+ };
79+}
80+
81+/** wsUrl derives the backend WebSocket URL from the page's own origin. */
82+export function wsUrl(): string {
83+ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
84+ return `${scheme}://${window.location.host}/ws`;
85+}
added ui/tsconfig.json +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2022",
4+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5+ "module": "ESNext",
6+ "moduleResolution": "bundler",
7+ "jsx": "react-jsx",
8+ "strict": true,
9+ "noUnusedLocals": true,
10+ "noUnusedParameters": true,
11+ "noFallthroughCasesInSwitch": true,
12+ "skipLibCheck": true,
13+ "isolatedModules": true,
14+ "noEmit": true,
15+ "types": ["vitest/globals"]
16+ },
17+ "include": ["src", "vite.config.ts"]
18+}
new file mode 100644
@@ -0,0 +1,18 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2022",
4+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5+ "module": "ESNext",
6+ "moduleResolution": "bundler",
7+ "jsx": "react-jsx",
8+ "strict": true,
9+ "noUnusedLocals": true,
10+ "noUnusedParameters": true,
11+ "noFallthroughCasesInSwitch": true,
12+ "skipLibCheck": true,
13+ "isolatedModules": true,
14+ "noEmit": true,
15+ "types": ["vitest/globals"]
16+ },
17+ "include": ["src", "vite.config.ts"]
18+}
added ui/tsconfig.tsbuildinfo +1 -0
new file mode 100644
@@ -0,0 +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
new file mode 100644
@@ -0,0 +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
added ui/vite.config.ts +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+/// <reference types="vitest/config" />
2+import { defineConfig } from "vite";
3+import react from "@vitejs/plugin-react";
4+
5+// In development the SPA runs on Vite's dev server while the Go backend runs
6+// on :8888; the proxy below forwards the API and WebSocket routes to it. In
7+// production the Go binary serves the built SPA itself and no proxy exists.
8+export default defineConfig({
9+ plugins: [react()],
10+ server: {
11+ proxy: {
12+ "/ws": { target: "ws://localhost:8888", ws: true },
13+ "/healthz": { target: "http://localhost:8888" },
14+ },
15+ },
16+ test: {
17+ environment: "jsdom",
18+ globals: true,
19+ },
20+});
new file mode 100644
@@ -0,0 +1,20 @@
1+/// <reference types="vitest/config" />
2+import { defineConfig } from "vite";
3+import react from "@vitejs/plugin-react";
4+
5+// In development the SPA runs on Vite's dev server while the Go backend runs
6+// on :8888; the proxy below forwards the API and WebSocket routes to it. In
7+// production the Go binary serves the built SPA itself and no proxy exists.
8+export default defineConfig({
9+ plugins: [react()],
10+ server: {
11+ proxy: {
12+ "/ws": { target: "ws://localhost:8888", ws: true },
13+ "/healthz": { target: "http://localhost:8888" },
14+ },
15+ },
16+ test: {
17+ environment: "jsdom",
18+ globals: true,
19+ },
20+});