umans-gate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/dashboard/dist/assets/alert-dialog-DlhAyxD3.js +2 -0
- package/dashboard/dist/assets/alert-dialog-DlhAyxD3.js.map +1 -0
- package/dashboard/dist/assets/clear-confirm-dialog-BOcl93wD.js +2 -0
- package/dashboard/dist/assets/clear-confirm-dialog-BOcl93wD.js.map +1 -0
- package/dashboard/dist/assets/config-tab-pnBjiSyC.js +4 -0
- package/dashboard/dist/assets/config-tab-pnBjiSyC.js.map +1 -0
- package/dashboard/dist/assets/economics-tab-BJXa5ydD.js +2 -0
- package/dashboard/dist/assets/economics-tab-BJXa5ydD.js.map +1 -0
- package/dashboard/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/dist/assets/headers-viewer-C9FKdG-_.js +2 -0
- package/dashboard/dist/assets/headers-viewer-C9FKdG-_.js.map +1 -0
- package/dashboard/dist/assets/index-72wCsW4_.js +3 -0
- package/dashboard/dist/assets/index-72wCsW4_.js.map +1 -0
- package/dashboard/dist/assets/index-DFhG4t-C.css +1 -0
- package/dashboard/dist/assets/json-viewer-CXkZMoVn.js +2 -0
- package/dashboard/dist/assets/json-viewer-CXkZMoVn.js.map +1 -0
- package/dashboard/dist/assets/mode-toggle-BxkGFiYG.js +2 -0
- package/dashboard/dist/assets/mode-toggle-BxkGFiYG.js.map +1 -0
- package/dashboard/dist/assets/models-tab-Deqk3WT3.js +2 -0
- package/dashboard/dist/assets/models-tab-Deqk3WT3.js.map +1 -0
- package/dashboard/dist/assets/performance-meter-Bd152C4C.js +2 -0
- package/dashboard/dist/assets/performance-meter-Bd152C4C.js.map +1 -0
- package/dashboard/dist/assets/select-mZPf2BR3.js +2 -0
- package/dashboard/dist/assets/select-mZPf2BR3.js.map +1 -0
- package/dashboard/dist/assets/sheet-BgQXtbdw.js +2 -0
- package/dashboard/dist/assets/sheet-BgQXtbdw.js.map +1 -0
- package/dashboard/dist/assets/sonner-DGu1DGeO.js +2 -0
- package/dashboard/dist/assets/sonner-DGu1DGeO.js.map +1 -0
- package/dashboard/dist/assets/sse-viewer-BHnn3FcY.js +3 -0
- package/dashboard/dist/assets/sse-viewer-BHnn3FcY.js.map +1 -0
- package/dashboard/dist/assets/use-capture-done-listener-u5RxfYQP.js +2 -0
- package/dashboard/dist/assets/use-capture-done-listener-u5RxfYQP.js.map +1 -0
- package/dashboard/dist/assets/use-models-B4MLkfpt.js +2 -0
- package/dashboard/dist/assets/use-models-B4MLkfpt.js.map +1 -0
- package/dashboard/dist/assets/use-polling-resource-D_vWHFwW.js +2 -0
- package/dashboard/dist/assets/use-polling-resource-D_vWHFwW.js.map +1 -0
- package/dashboard/dist/assets/vendor-CVKsgXJU.js +312 -0
- package/dashboard/dist/assets/vendor-CVKsgXJU.js.map +1 -0
- package/dashboard/dist/assets/vendor-DtX4s2-2.css +1 -0
- package/dashboard/dist/assets/vendor-react-Dtud3GyP.js +41 -0
- package/dashboard/dist/assets/vendor-react-Dtud3GyP.js.map +1 -0
- package/dashboard/dist/assets/vision-calls-BquNiDzm.js +3 -0
- package/dashboard/dist/assets/vision-calls-BquNiDzm.js.map +1 -0
- package/dashboard/dist/index.html +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +6788 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1094 -0
- package/dist/index.js +6443 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Vision handoff pipeline**: replaces image blocks with text descriptions
|
|
15
|
+
generated by a separate vision model (`umans-flash`). Images are transcoded
|
|
16
|
+
to JPEG/PNG, sent to the vision model, and descriptions are cached (7-day TTL)
|
|
17
|
+
with persistent storage. Strategies: `always`, `catalog`, `never`.
|
|
18
|
+
- Modules: `src/vision/` (`handoff.ts`, `detect.ts`, `cache.ts`,
|
|
19
|
+
`persistent-cache.ts`, `transcode.ts`, `wrapper.ts`, `sink.ts`)
|
|
20
|
+
- `src/vision-description-store.ts` — persistent description storage in SQLite
|
|
21
|
+
- **Concurrency gate** (`src/limiter/`): semaphore + circuit breaker with
|
|
22
|
+
intention-based reservations (main vs vision), hard cap, soft limit driven by
|
|
23
|
+
`/v1/usage`, queue timeout, and over-subscription fallback.
|
|
24
|
+
- `src/limiter/gate.ts` — `ConcurrencyGate`, `Semaphore`, `CircuitBreaker`
|
|
25
|
+
- `src/limiter/types.ts` — gate option types
|
|
26
|
+
- **Rate limiting** (`src/rate.ts`): sliding-window weighted rate limiter for
|
|
27
|
+
pro-tier request limits. Auto-derives from `/v1/usage` or explicit config.
|
|
28
|
+
- **Connection warmer** (`src/warmer.ts`): periodic `/v1/models` pings to keep
|
|
29
|
+
TLS warm. Skips when real traffic occurred in the last interval.
|
|
30
|
+
- **Usage tracking** (`src/usage.ts`, `src/usage-extract.ts`): fetches and
|
|
31
|
+
reconciles `/v1/usage` data to size concurrency limits, detect rate-boxing,
|
|
32
|
+
and demote priority. Extractors for Anthropic + OpenAI streaming/non-streaming.
|
|
33
|
+
- **Economics module** (`src/economics.ts`): token cost calculation per capture.
|
|
34
|
+
- **Metrics module** (`src/metrics.ts`): aggregated runtime statistics.
|
|
35
|
+
- **Model info parser** (`src/model-info-parser.ts`): parses `/v1/models`
|
|
36
|
+
response to determine vision support per model.
|
|
37
|
+
- **Model policy** (`src/model-policy.ts`): model-aware stamping decisions.
|
|
38
|
+
- **Compressed capture storage** (`src/compress.ts`): zstd compression for
|
|
39
|
+
request/response bodies (default: on).
|
|
40
|
+
- **Worker-based capture pipeline** (`src/workers/`): offload capture writes to
|
|
41
|
+
a worker thread for non-blocking streaming.
|
|
42
|
+
- **Stamp pipeline** (`src/stamp-pipeline.ts`): unified stamping orchestrator
|
|
43
|
+
that applies TTL, `top_k`, `max_tokens`, `thinking`, `output_config`,
|
|
44
|
+
`context_management`, and `temperature` stamps in the correct order.
|
|
45
|
+
- **Individual stamp modules**:
|
|
46
|
+
- `src/stamp-temperature.ts` — forces `temperature` value
|
|
47
|
+
- `src/stamp-thinking.ts` — injects `thinking`, `max_tokens`, `output_config`
|
|
48
|
+
- `src/stamp-topk.ts` — injects `top_k`
|
|
49
|
+
- `src/stamp-reasoning.ts` — OpenAI-compatible `reasoning_effort` stamping
|
|
50
|
+
- **Bundled stamp toggle**: `stamp_claude_code_enabled` replaces individual
|
|
51
|
+
stamp toggles — one switch applies the full Claude Code stamp bundle.
|
|
52
|
+
- **Dashboard config validation**: dispatch-table-based validation with
|
|
53
|
+
hot-reload support and restart-required field detection.
|
|
54
|
+
- **Dashboard polling consolidation**: `usePollingResource` hook unifies
|
|
55
|
+
triplicated polling logic.
|
|
56
|
+
- **WebSocket backpressure management**: configurable limit and auto-close.
|
|
57
|
+
- **Upstream timeout** (`upstream_timeout_ms`): configurable fetch timeout.
|
|
58
|
+
- **Capture body size limit** (`capture_body_max_bytes`).
|
|
59
|
+
- **Logger** (`src/logger.ts`): structured scoped logging replacing `console.*`.
|
|
60
|
+
- **Shared utilities** (`src/shared/`): extracted domain helpers for
|
|
61
|
+
model-name parsing, usage fetching, dashboard constants, and vision sink.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **SOLID refactor (Waves 1–5)**: split monolithic modules into focused,
|
|
66
|
+
single-responsibility units. Helpers split into domain modules, `ConcurrencyGate`
|
|
67
|
+
decomposed into `Semaphore` + `CircuitBreaker`, `useCaptures` split into
|
|
68
|
+
`useCaptureList` + `useCaptureDetail` + `useGateStats`, config validation
|
|
69
|
+
replaced if/else chains with dispatch tables.
|
|
70
|
+
- **Stamp toggles consolidated**: individual `stamp_cache_ttl_enabled`,
|
|
71
|
+
`stamp_top_k_enabled`, `stamp_max_tokens_enabled`, `stamp_thinking_enabled`,
|
|
72
|
+
`stamp_output_config_enabled` → single `stamp_claude_code_enabled`.
|
|
73
|
+
- **Vision strategy default**: `always` → `catalog` (only intercept when model
|
|
74
|
+
lacks vision support).
|
|
75
|
+
- **Vision image format default**: `jpeg` → `png` (higher fidelity for OCR).
|
|
76
|
+
- **Vision cache TTL default**: 24h → 7 days (`604800000ms`).
|
|
77
|
+
- **`max_tokens` stamp values**: GLM models get `131071`, non-GLM get `32767`.
|
|
78
|
+
- **Config validation**: now uses dispatch table instead of if/else chains.
|
|
79
|
+
- **Dashboard capture detail**: copy-status derived from source state.
|
|
80
|
+
- **Usage extraction**: replaced provider×streaming dispatch with `EXTRACTORS`
|
|
81
|
+
lookup table.
|
|
82
|
+
- **ConnectionWarmer**: narrowed constructor to `Pick<ProxyConfig, ...>`.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- Streaming `duration_ms` now floors at wall-clock to prevent timing collapse
|
|
87
|
+
- Limiter allows permit acquisition when reservations are over-subscribed
|
|
88
|
+
- `stamp-reasoning` correctly removes `max_tokens`/`thinking` before injection
|
|
89
|
+
|
|
90
|
+
## [0.0.1] - 2025-07-02
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- TypeScript rewrite of the capture proxy (modular `src/` structure)
|
|
95
|
+
- Vite + React + Tailwind + shadcn/ui dashboard
|
|
96
|
+
- `createProxyServer()` programmatic API
|
|
97
|
+
- Biome for linting and formatting
|
|
98
|
+
- GitHub Actions CI (matrix: ubuntu + macOS, Bun 1.1 + latest)
|
|
99
|
+
- npm publish workflow with Sigstore provenance
|
|
100
|
+
- Issue templates, PR template, dependabot config
|
|
101
|
+
- `.env.example` documenting all environment variables
|
|
102
|
+
- MIT license
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- Single-file `capture.js` → modular TypeScript in `src/`
|
|
107
|
+
- Vanilla JS dashboard → React + shadcn/ui dashboard
|
|
108
|
+
- Test helpers reference new entry point (no more `proxy.js`/`capture-only.js`)
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- Broken tests that spawned non-existent `proxy.js` file
|
|
113
|
+
- Runtime artifacts (`capture.db*`) now gitignored
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Umans AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# umans-gate
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/umans-gate)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://bun.sh)
|
|
6
|
+
[](https://github.com/umans-ai/umans-gate/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
> **Disclaimer:** This is a community project. The author is not an official
|
|
9
|
+
> Umans developer — just a contributor building tools around the Umans API.
|
|
10
|
+
> "umans" and related marks belong to their respective owners.
|
|
11
|
+
|
|
12
|
+
LLM capture proxy with Anthropic `cache_control` TTL stamping, vision handoff,
|
|
13
|
+
concurrency gating, rate limiting, and a live inspection dashboard.
|
|
14
|
+
|
|
15
|
+
Point your LLM harness at `http://localhost:1945`, and every request/response is
|
|
16
|
+
captured to SQLite with a live WebSocket-updating inspector UI at
|
|
17
|
+
`http://localhost:1945/dashboard/`.
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Features](#features)
|
|
22
|
+
- [Quick Start](#quick-start)
|
|
23
|
+
- [Configuration](#configuration)
|
|
24
|
+
- [Config Fields](#config-fields)
|
|
25
|
+
- [Hot Reload and Restart](#hot-reload-and-restart)
|
|
26
|
+
- [Stamp Pipeline](#stamp-pipeline)
|
|
27
|
+
- [Vision Handoff](#vision-handoff)
|
|
28
|
+
- [Concurrency Gate](#concurrency-gate)
|
|
29
|
+
- [Rate Limiting](#rate-limiting)
|
|
30
|
+
- [Programmatic API](#programmatic-api)
|
|
31
|
+
- [Inspector Dashboard](#inspector-dashboard)
|
|
32
|
+
- [Development](#development)
|
|
33
|
+
- [Project Structure](#project-structure)
|
|
34
|
+
- [Documentation](#documentation)
|
|
35
|
+
- [Contributing](#contributing)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Capture proxy**: intercepts all LLM API traffic (Anthropic, OpenAI-compatible) and stores it in SQLite with optional zstd compression
|
|
41
|
+
- **Stamp pipeline**: unified stamping orchestrator that applies TTL, `top_k`, `max_tokens`, `thinking`, `output_config`, `context_management`, and `temperature` stamps in the correct order — toggled by a single `stamp_claude_code_enabled` switch
|
|
42
|
+
- **Vision handoff**: replaces image blocks with text descriptions generated by a vision model (`umans-flash`), enabling text-only models to "see" images. Descriptions are cached (7-day TTL) with persistent SQLite storage
|
|
43
|
+
- **Concurrency gate**: semaphore + circuit breaker with intention-based reservations (main vs vision), hard cap, soft limit driven by `/v1/usage`, queue timeout, and over-subscription fallback
|
|
44
|
+
- **Rate limiting**: sliding-window weighted rate limiter for pro-tier request limits, auto-derived from `/v1/usage` or explicitly configured
|
|
45
|
+
- **Connection warmer**: periodic `/v1/models` pings keep TLS warm, skipping when real traffic occurred recently
|
|
46
|
+
- **Usage tracking**: fetches and reconciles `/v1/usage` to size concurrency limits, detect rate-boxing, and manage priority demotion
|
|
47
|
+
- **Live inspector dashboard**: React + shadcn/ui dashboard with WebSocket live updates
|
|
48
|
+
- **SSE rendering**: streaming responses are captured and rendered with expandable event previews
|
|
49
|
+
- **Ring buffer storage**: keeps the last N captures (default 200) with WAL mode SQLite
|
|
50
|
+
- **Write-behind queue**: batched database writes to minimize blocking during streaming
|
|
51
|
+
- **Worker-based capture pipeline**: offloads capture writes to a worker thread for non-blocking streaming
|
|
52
|
+
- **Hop-by-hop header stripping**: correct HTTP proxy behavior
|
|
53
|
+
- **Protocol flexibility**: configurable upstream HTTP/1.1 (default) or HTTP/2
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Install
|
|
59
|
+
bun install
|
|
60
|
+
|
|
61
|
+
# Start the proxy (reads config.json + env overrides)
|
|
62
|
+
bun src/cli.ts
|
|
63
|
+
|
|
64
|
+
# Or after build:
|
|
65
|
+
bun run build
|
|
66
|
+
./dist/cli.js
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Point your harness base URL to `http://localhost:1945` and open the inspector at `http://localhost:1945/dashboard/`.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
> **⚠️ Dashboard-first:** The recommended way to change configuration is via
|
|
74
|
+
> the **Config** tab in the dashboard at `http://localhost:1945/dashboard/`.
|
|
75
|
+
> Editing `config.json` directly works, but the dashboard validates against
|
|
76
|
+
> the upstream `/v1/usage` hard cap, shows field descriptions, and can hot-reload
|
|
77
|
+
> or restart the server for you. If you do edit the file, restart the server
|
|
78
|
+
> afterward (the dashboard auto-reloads on save).
|
|
79
|
+
|
|
80
|
+
Configuration is loaded from a JSON file with environment variable overrides.
|
|
81
|
+
|
|
82
|
+
**Config file path** (auto-created on first run):
|
|
83
|
+
|
|
84
|
+
| OS | Path |
|
|
85
|
+
|----|------|
|
|
86
|
+
| Linux/macOS | `$XDG_CONFIG_HOME/umans-gate/config.json` or `~/.config/umans-gate/config.json` |
|
|
87
|
+
| Windows | `%APPDATA%/umans-gate/config.json` |
|
|
88
|
+
|
|
89
|
+
**Precedence**: environment variables > JSON config file > built-in defaults.
|
|
90
|
+
|
|
91
|
+
On first run, `loadConfig()` writes a `config.json` with defaults to the
|
|
92
|
+
resolved path if it does not already exist. Existing configs are never
|
|
93
|
+
overwritten.
|
|
94
|
+
|
|
95
|
+
All env vars from `.env.example` have JSON equivalents using `snake_case`
|
|
96
|
+
(e.g., `UPSTREAM_PROTOCOL` → `upstream_protocol`).
|
|
97
|
+
|
|
98
|
+
### Config Fields
|
|
99
|
+
|
|
100
|
+
| Variable | Default | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `PORT` | `1945` | Listen port |
|
|
103
|
+
| `MAX_CAPTURES` | `200` | Ring buffer size (keeps last N) |
|
|
104
|
+
| `DB_PATH` | `./umans-gate.db` | SQLite database path |
|
|
105
|
+
| `IDLE_TIMEOUT` | `255` | Bun.serve idleTimeout — HTTP connection idle timeout in seconds (1–255) |
|
|
106
|
+
| `UPSTREAM_PROTOCOL` | `http1.1` | Upstream protocol: `http1.1` or `http2` |
|
|
107
|
+
| `UPSTREAM_TIMEOUT_MS` | `300000` | Upstream fetch timeout in ms (5 min) |
|
|
108
|
+
| `STAMP_CLAUDE_CODE_ENABLED` | `false` | Toggle full Claude Code stamp bundle (TTL, `top_k`, `max_tokens`, `thinking`, `output_config`, `context_management`, `temperature`) |
|
|
109
|
+
| `STAMP_REASONING_EFFORT_ENABLED` | `false` | Toggle OpenAI-compatible `reasoning_effort` stamping (`high`, `max` for `umans-glm*`; removes `max_tokens`/`thinking`) |
|
|
110
|
+
| `WARMER_ENABLED` | `true` | Toggle TLS connection warmer |
|
|
111
|
+
| `WARMER_INTERVAL_MS` | `20000` | Warmer ping interval in ms |
|
|
112
|
+
| `UMANS_API_KEY` | _(empty)_ | Required for `/v1/usage` fetch, concurrency gate sizing, and rate-limit validation |
|
|
113
|
+
| `USAGE_REFRESH_MS` | `60000` | `/v1/usage` poll interval in ms |
|
|
114
|
+
| `MODELS_REFRESH_MS` | `3600000` | `/v1/models` poll interval in ms |
|
|
115
|
+
| `CONCURRENCY_HARD_CAP` | `1` | Maximum concurrent upstream requests (hard ceiling) |
|
|
116
|
+
| `CONCURRENCY_SOFT_LIMIT` | `1` | Soft limit (driven by `/v1/usage`, adjustable at runtime) |
|
|
117
|
+
| `CONCURRENCY_MAIN_RESERVATION` | `1` | Reserved slots for main requests |
|
|
118
|
+
| `CONCURRENCY_VISION_RESERVATION` | `1` | Reserved slots for vision requests |
|
|
119
|
+
| `RATE_LIMIT_REQUESTS` | `0` | Pro-tier rolling-window limit. `-1` = unlimited, `0` = auto-derive from `/v1/usage`, `>0` = explicit |
|
|
120
|
+
| `QUEUE_TIMEOUT_MS` | `30000` | Max wait time for queued permits |
|
|
121
|
+
| `MAX_QUEUE_DEPTH` | `256` | Max queued permits |
|
|
122
|
+
| `RELEASE_COOLDOWN_MS` | `1000` | Cooldown after releasing a permit |
|
|
123
|
+
| `BREAKER_THRESHOLD` | `5` | Circuit breaker failure threshold |
|
|
124
|
+
| `BREAKER_WINDOW_MS` | `300000` | Circuit breaker failure window (5 min) |
|
|
125
|
+
| `BREAKER_COOLDOWN_MS` | `60000` | Circuit breaker cooldown (1 min) |
|
|
126
|
+
| `VISION_STRATEGY` | `catalog` | When to intercept images: `never`, `catalog` (only if model lacks vision), `always` |
|
|
127
|
+
| `VISION_MODEL` | `umans-flash` | Vision model used for image description |
|
|
128
|
+
| `VISION_MAX_IMAGES` | `5` | Max images processed per request |
|
|
129
|
+
| `VISION_MAX_DESCRIPTION_TOKENS` | `4096` | Max tokens for vision descriptions |
|
|
130
|
+
| `VISION_CONCURRENCY` | `1` | Vision model call concurrency |
|
|
131
|
+
| `VISION_MAX_DIMENSION` | `2048` | Max image dimension before resize |
|
|
132
|
+
| `VISION_JPEG_QUALITY` | `92` | JPEG compression quality |
|
|
133
|
+
| `VISION_IMAGE_FORMAT` | `png` | Image format sent to vision model: `jpeg` or `png` |
|
|
134
|
+
| `VISION_CACHE_SIZE` | `1000` | In-memory description cache size |
|
|
135
|
+
| `VISION_CACHE_TTL_MS` | `604800000` | Description cache TTL (7 days) |
|
|
136
|
+
| `VISION_PERSISTENT_CACHE` | `true` | Persist vision descriptions to SQLite |
|
|
137
|
+
| `VISION_TIMEOUT_MS` | `0` | Vision call timeout (0 = no timeout) |
|
|
138
|
+
| `CAPTURE_BODY_MAX_BYTES` | `10000000` | Max captured request/response body size (10 MB, 0 = unlimited) |
|
|
139
|
+
| `COMPRESSION_ENABLED` | `true` | zstd compression for stored bodies |
|
|
140
|
+
| `QUEUE_MAX_DEPTH` | `100` | Max write-behind response queue depth |
|
|
141
|
+
| `WS_BACKPRESSURE_LIMIT` | `1048576` | WebSocket backpressure limit in bytes (1 MB) |
|
|
142
|
+
| `WS_CLOSE_ON_BACKPRESSURE_LIMIT` | `true` | Close WebSocket connections exceeding backpressure limit |
|
|
143
|
+
|
|
144
|
+
The upstream target (`https://api.code.umans.ai`), OpenAI chat path
|
|
145
|
+
(`chat/completions`), warmer path (`/v1/models`), and vision target are
|
|
146
|
+
hardcoded — not configurable.
|
|
147
|
+
|
|
148
|
+
### Hot Reload and Restart
|
|
149
|
+
|
|
150
|
+
The dashboard's Config tab can save changes and trigger a hot reload
|
|
151
|
+
via `POST /dashboard/api/config/reload`. Hot-reloadable fields (e.g.
|
|
152
|
+
`stamp_claude_code_enabled`, `breaker_*`, `rate_limit_*`, `vision_*`)
|
|
153
|
+
apply live; fields marked `restartRequired` (e.g. `port`, `db_path`,
|
|
154
|
+
`upstream_protocol`) require a server restart.
|
|
155
|
+
|
|
156
|
+
The dashboard also has a **Restart** button (`POST /dashboard/api/restart`)
|
|
157
|
+
that calls `process.exit(0)`. This requires an external process manager
|
|
158
|
+
to restart the server automatically:
|
|
159
|
+
|
|
160
|
+
| Manager | Command |
|
|
161
|
+
|---------|---------|
|
|
162
|
+
| `bun --watch` | `bun --watch src/cli.ts` |
|
|
163
|
+
| systemd | `Restart=always` in the unit file |
|
|
164
|
+
| pm2 | `pm2 start src/cli.ts --interpreter bun --watch` |
|
|
165
|
+
|
|
166
|
+
Without a process manager, the server exits and stays down until you
|
|
167
|
+
start it manually.
|
|
168
|
+
|
|
169
|
+
### Stamp Pipeline
|
|
170
|
+
|
|
171
|
+
When `STAMP_CLAUDE_CODE_ENABLED` is on, the proxy applies the full Claude
|
|
172
|
+
Code stamp bundle to Anthropic requests:
|
|
173
|
+
|
|
174
|
+
1. **TTL stamping**: adds `"ttl": "1h"` to every `cache_control: {type:"ephemeral"}` block
|
|
175
|
+
2. **`top_k` injection**: injects `"top_k": 20` after the `model` field
|
|
176
|
+
3. **`temperature` stamping**: forces `temperature: 1.0`
|
|
177
|
+
4. **`max_tokens` stamping**: `131071` for `umans-glm*` models, `32767` for others
|
|
178
|
+
5. **`thinking` injection**: `{ "type": "adaptive" }` for `umans-coder`, `umans-flash`, `umans-kimi*`, `umans-qwen*`
|
|
179
|
+
6. **`output_config` injection**: `{ "effort": "high" }` for most models, `{ "effort": "max" }` for `umans-glm*`
|
|
180
|
+
7. **`context_management` injection**: `{ "edits": [{ "type": "clear_thinking_20251015", "keep": "all" }] }`
|
|
181
|
+
|
|
182
|
+
For OpenAI-compatible requests, `STAMP_REASONING_EFFORT_ENABLED` injects
|
|
183
|
+
`"reasoning_effort": "high"` (or `"max"` for `umans-glm*`) and removes
|
|
184
|
+
`max_tokens`/`thinking` from the body.
|
|
185
|
+
|
|
186
|
+
The stamped body is what gets forwarded upstream AND what gets captured — so
|
|
187
|
+
the inspector shows exactly what went to the API.
|
|
188
|
+
|
|
189
|
+
### Vision Handoff
|
|
190
|
+
|
|
191
|
+
The vision handoff pipeline replaces image blocks in the request body with text
|
|
192
|
+
descriptions generated by a vision model:
|
|
193
|
+
|
|
194
|
+
1. Detects image blocks in Anthropic and OpenAI request bodies
|
|
195
|
+
2. Transcodes images to the configured format (PNG/JPEG) with max dimension
|
|
196
|
+
3. Sends to the vision model (`umans-flash`) with a detailed OCR prompt
|
|
197
|
+
4. Caches descriptions in-memory (1000 entries) and persistently in SQLite
|
|
198
|
+
5. Replaces image blocks with text descriptions
|
|
199
|
+
|
|
200
|
+
Strategies:
|
|
201
|
+
- `catalog` (default): intercept only if the model is known to lack vision support
|
|
202
|
+
- `always`: intercept all images regardless of model
|
|
203
|
+
- `never`: disabled
|
|
204
|
+
|
|
205
|
+
Vision calls are serialized by the concurrency gate (default concurrency=1)
|
|
206
|
+
because the upstream has limited vision slots.
|
|
207
|
+
|
|
208
|
+
### Concurrency Gate
|
|
209
|
+
|
|
210
|
+
The concurrency gate (`src/limiter/`) prevents overwhelming the upstream:
|
|
211
|
+
|
|
212
|
+
- **Semaphore**: enforces a soft limit (driven by `/v1/usage`) and hard cap
|
|
213
|
+
- **Circuit breaker**: opens after `breaker_threshold` 429s in `breaker_window_ms`,
|
|
214
|
+
blocks traffic for `breaker_cooldown_ms`, then half-opens to test
|
|
215
|
+
- **Intention-based reservations**: main and vision requests have reserved slots
|
|
216
|
+
- **Queue**: over-limit requests are queued up to `max_queue_depth` with
|
|
217
|
+
`queue_timeout_ms` timeout
|
|
218
|
+
- **Over-subscription fallback**: when reservations exceed capacity, permits
|
|
219
|
+
are still granted if actual usage allows
|
|
220
|
+
|
|
221
|
+
### Rate Limiting
|
|
222
|
+
|
|
223
|
+
The sliding-window rate limiter (`src/rate.ts`) enforces pro-tier request limits:
|
|
224
|
+
|
|
225
|
+
- `rate_limit_requests: 0` — auto-derive from `/v1/usage` (default)
|
|
226
|
+
- `rate_limit_requests: -1` — unlimited (no limiter)
|
|
227
|
+
- `rate_limit_requests: N` — explicit limit with weighted sliding window
|
|
228
|
+
|
|
229
|
+
The window size is derived from `/v1/usage` and is not configurable.
|
|
230
|
+
|
|
231
|
+
## Programmatic API
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { createProxyServer } from "umans-gate";
|
|
235
|
+
|
|
236
|
+
const server = createProxyServer({
|
|
237
|
+
config: { port: 8080 },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// server.db — CaptureDB instance
|
|
241
|
+
// server.ws — WsBroadcaster instance
|
|
242
|
+
// server.queue — WriteQueue instance
|
|
243
|
+
// server.shutdown() — graceful shutdown
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Inspector Dashboard
|
|
247
|
+
|
|
248
|
+
The dashboard is a Vite + React + TypeScript + Tailwind + shadcn/ui app in `dashboard/`.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
cd dashboard
|
|
252
|
+
bun install
|
|
253
|
+
bun run dev # dev server at localhost:5173
|
|
254
|
+
bun run build # production build to dashboard/dist/
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The dashboard connects to:
|
|
258
|
+
- `GET /dashboard/api/captures` — capture list
|
|
259
|
+
- `GET /dashboard/api/captures/:id` — full capture detail
|
|
260
|
+
- `POST /dashboard/api/clear` — clear all captures
|
|
261
|
+
- `WS /dashboard/ws` — live updates (`new`, `update`, `clear` messages)
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
bun run dev # start proxy server
|
|
267
|
+
bun run typecheck # TypeScript checking
|
|
268
|
+
bun run lint # Biome lint
|
|
269
|
+
bun run lint:fix # Biome lint + auto-fix
|
|
270
|
+
bun run test # run server tests
|
|
271
|
+
bun run test:dashboard # run dashboard tests (vitest + jsdom)
|
|
272
|
+
bun run test:all # run server + dashboard tests
|
|
273
|
+
bun run build # build server (tsup) + dashboard (vite)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Project Structure
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
umans-gate/
|
|
280
|
+
├── src/
|
|
281
|
+
│ ├── cli.ts # CLI entry point
|
|
282
|
+
│ ├── index.ts # createProxyServer() factory + exports
|
|
283
|
+
│ ├── config.ts # env-driven configuration
|
|
284
|
+
│ ├── db.ts # SQLite capture store (WAL, ring buffer)
|
|
285
|
+
│ ├── proxy.ts # proxy handler (capture + stamping + streaming)
|
|
286
|
+
│ ├── stamp.ts # cache_control TTL stamping logic
|
|
287
|
+
│ ├── stamp-pipeline.ts # unified stamp orchestrator
|
|
288
|
+
│ ├── stamp-temperature.ts # temperature stamping
|
|
289
|
+
│ ├── stamp-thinking.ts # thinking / max_tokens / output_config stamping
|
|
290
|
+
│ ├── stamp-topk.ts # top_k injection
|
|
291
|
+
│ ├── stamp-reasoning.ts # OpenAI reasoning_effort stamping
|
|
292
|
+
│ ├── viewer.ts # inspector dashboard + REST API router
|
|
293
|
+
│ ├── ws.ts # WebSocket broadcast manager
|
|
294
|
+
│ ├── queue.ts # write-behind batched flush queue
|
|
295
|
+
│ ├── rate.ts # sliding-window rate limiter
|
|
296
|
+
│ ├── warmer.ts # TLS connection warmer
|
|
297
|
+
│ ├── metrics.ts # aggregated runtime statistics
|
|
298
|
+
│ ├── economics.ts # token cost calculation
|
|
299
|
+
│ ├── usage.ts # /v1/usage fetch + reconciliation
|
|
300
|
+
│ ├── usage-extract.ts # usage extraction (Anthropic + OpenAI)
|
|
301
|
+
│ ├── model-info-parser.ts # /v1/models response parser
|
|
302
|
+
│ ├── model-policy.ts # model-aware stamping decisions
|
|
303
|
+
│ ├── compress.ts # zstd body compression
|
|
304
|
+
│ ├── vision-description-store.ts # persistent vision description storage
|
|
305
|
+
│ ├── helpers.ts # shared utilities
|
|
306
|
+
│ ├── logger.ts # structured scoped logging
|
|
307
|
+
│ ├── banner.ts # startup banner
|
|
308
|
+
│ ├── types.ts # shared types
|
|
309
|
+
│ ├── limiter/
|
|
310
|
+
│ │ ├── gate.ts # ConcurrencyGate (Semaphore + CircuitBreaker)
|
|
311
|
+
│ │ ├── circuit-breaker.ts # CircuitBreaker implementation
|
|
312
|
+
│ │ ├── types.ts # gate option types
|
|
313
|
+
│ │ └── index.ts # re-exports
|
|
314
|
+
│ ├── vision/
|
|
315
|
+
│ │ ├── handoff.ts # vision handoff orchestrator
|
|
316
|
+
│ │ ├── detect.ts # image block detection
|
|
317
|
+
│ │ ├── cache.ts # in-memory description cache
|
|
318
|
+
│ │ ├── persistent-cache.ts # SQLite-backed description cache
|
|
319
|
+
│ │ ├── transcode.ts # image transcoding
|
|
320
|
+
│ │ ├── wrapper.ts # description wrapper + policy
|
|
321
|
+
│ │ └── sink.ts # vision record sink
|
|
322
|
+
│ ├── workers/ # worker-based capture pipeline
|
|
323
|
+
│ └── shared/ # extracted domain helpers
|
|
324
|
+
├── dashboard/ # React + shadcn/ui dashboard
|
|
325
|
+
├── test/ # bun:test test suite
|
|
326
|
+
├── docs/ # additional documentation
|
|
327
|
+
│ ├── ARCHITECTURE.md # system architecture
|
|
328
|
+
│ ├── TROUBLESHOOTING.md # troubleshooting guide
|
|
329
|
+
│ ├── BENCHMARKS.md # benchmark results
|
|
330
|
+
│ └── proxy-modifications.md # proxy modification inventory
|
|
331
|
+
├── benchmark/ # benchmark scripts + results
|
|
332
|
+
├── .github/ # issue templates, workflows, PR template
|
|
333
|
+
└── dist/ # build output (gitignored)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Documentation
|
|
337
|
+
|
|
338
|
+
- [Architecture](docs/ARCHITECTURE.md) — system design and data flow
|
|
339
|
+
- [Proxy Modifications](docs/proxy-modifications.md) — complete inventory of proxy modifications
|
|
340
|
+
- [Troubleshooting](docs/TROUBLESHOOTING.md) — common issues and solutions
|
|
341
|
+
- [Benchmarks](docs/BENCHMARKS.md) — benchmark methodology and results
|
|
342
|
+
- [Dashboard Design System](dashboard/DESIGN.md) — design system documentation
|
|
343
|
+
- [Changelog](CHANGELOG.md) — version history
|
|
344
|
+
- [Contributing](CONTRIBUTING.md) — how to contribute
|
|
345
|
+
- [Roadmap](ROADMAP.md) — future plans
|
|
346
|
+
|
|
347
|
+
## Contributing
|
|
348
|
+
|
|
349
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style,
|
|
350
|
+
testing, and release instructions.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{j as e,r as d}from"./vendor-react-Dtud3GyP.js";import{c as o,B as n}from"./index-72wCsW4_.js";import{aC as u,aD as c,aE as f,aF as m,aG as g,aH as p,aI as x,aJ as D}from"./vendor-CVKsgXJU.js";function h({...a}){return e.jsx(u,{"data-slot":"alert-dialog",...a})}function v({...a}){return e.jsx(D,{"data-slot":"alert-dialog-trigger",...a})}function j({...a}){return e.jsx(p,{"data-slot":"alert-dialog-portal",...a})}function A({className:a,...t}){return e.jsx(x,{"data-slot":"alert-dialog-overlay",className:o("fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",a),...t})}function N({className:a,size:t="default",...l}){return e.jsxs(j,{children:[e.jsx(A,{}),e.jsx(c,{"data-slot":"alert-dialog-content","data-size":t,className:o("group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",a),...l})]})}function y({className:a,...t}){return e.jsx("div",{"data-slot":"alert-dialog-header",className:o("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",a),...t})}function C({className:a,...t}){return e.jsx("div",{"data-slot":"alert-dialog-footer",className:o("-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",a),...t})}function k({className:a,...t}){return e.jsx(f,{"data-slot":"alert-dialog-title",className:o("font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",a),...t})}function R({className:a,...t}){return e.jsx(m,{"data-slot":"alert-dialog-description",className:o("text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",a),...t})}const T=d.forwardRef(function({className:t,variant:l="default",size:r="default",...s},i){return e.jsx(g,{ref:i,"data-slot":"alert-dialog-action",className:o(t),render:e.jsx(n,{variant:l,size:r}),...s})}),_=d.forwardRef(function({className:t,variant:l="outline",size:r="default",...s},i){return e.jsx(g,{ref:i,"data-slot":"alert-dialog-cancel",className:o(t),render:e.jsx(n,{variant:l,size:r}),...s})});export{h as A,v as a,N as b,y as c,k as d,R as e,C as f,_ as g,T as h};
|
|
2
|
+
//# sourceMappingURL=alert-dialog-DlhAyxD3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alert-dialog-DlhAyxD3.js","sources":["../../src/components/ui/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertDialog as AlertDialogPrimitive } from \"@base-ui/react/alert-dialog\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nfunction AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nfunction AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nfunction AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {\n return <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />;\n}\n\nfunction AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {\n return (\n <AlertDialogPrimitive.Backdrop\n data-slot=\"alert-dialog-overlay\"\n className={cn(\n \"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogContent({\n className,\n size = \"default\",\n ...props\n}: AlertDialogPrimitive.Popup.Props & {\n size?: \"default\" | \"sm\";\n}) {\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Popup\n data-slot=\"alert-dialog-content\"\n data-size={size}\n className={cn(\n \"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className,\n )}\n {...props}\n />\n </AlertDialogPortal>\n );\n}\n\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\n \"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\n \"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogMedia({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-media\"\n className={cn(\n \"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\n \"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\n \"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst AlertDialogAction = React.forwardRef<\n HTMLButtonElement,\n Omit<AlertDialogPrimitive.Close.Props, \"render\"> &\n Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\">\n>(function AlertDialogAction({ className, variant = \"default\", size = \"default\", ...props }, ref) {\n return (\n <AlertDialogPrimitive.Close\n ref={ref}\n data-slot=\"alert-dialog-action\"\n className={cn(className)}\n render={<Button variant={variant} size={size} />}\n {...props}\n />\n );\n});\n\nconst AlertDialogCancel = React.forwardRef<\n HTMLButtonElement,\n AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\">\n>(function AlertDialogCancel({ className, variant = \"outline\", size = \"default\", ...props }, ref) {\n return (\n <AlertDialogPrimitive.Close\n ref={ref}\n data-slot=\"alert-dialog-cancel\"\n className={cn(className)}\n render={<Button variant={variant} size={size} />}\n {...props}\n />\n );\n});\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogMedia,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n"],"names":["AlertDialog","props","AlertDialogPrimitive.Root","AlertDialogTrigger","AlertDialogPrimitive.Trigger","AlertDialogPortal","AlertDialogPrimitive.Portal","AlertDialogOverlay","className","jsx","AlertDialogPrimitive.Backdrop","cn","AlertDialogContent","size","AlertDialogPrimitive.Popup","AlertDialogHeader","AlertDialogFooter","AlertDialogTitle","AlertDialogPrimitive.Title","AlertDialogDescription","AlertDialogPrimitive.Description","AlertDialogAction","React.forwardRef","variant","ref","AlertDialogPrimitive.Close","Button","AlertDialogCancel"],"mappings":"uMAQA,SAASA,EAAY,CAAE,GAAGC,GAA0C,CAClE,aAAQC,EAAA,CAA0B,YAAU,eAAgB,GAAGD,EAAO,CACxE,CAEA,SAASE,EAAmB,CAAE,GAAGF,GAA6C,CAC5E,aAAQG,EAAA,CAA6B,YAAU,uBAAwB,GAAGH,EAAO,CACnF,CAEA,SAASI,EAAkB,CAAE,GAAGJ,GAA4C,CAC1E,aAAQK,EAAA,CAA4B,YAAU,sBAAuB,GAAGL,EAAO,CACjF,CAEA,SAASM,EAAmB,CAAE,UAAAC,EAAW,GAAGP,GAA8C,CACxF,OACEQ,EAAAA,IAACC,EAAA,CACC,YAAU,uBACV,UAAWC,EACT,wLACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASW,EAAmB,CAC1B,UAAAJ,EACA,KAAAK,EAAO,UACP,GAAGZ,CACL,EAEG,CACD,cACGI,EAAA,CACC,SAAA,CAAAI,EAAAA,IAACF,EAAA,EAAmB,EACpBE,EAAAA,IAACK,EAAA,CACC,YAAU,uBACV,YAAWD,EACX,UAAWF,EACT,6aACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CACN,EACF,CAEJ,CAEA,SAASc,EAAkB,CAAE,UAAAP,EAAW,GAAGP,GAAsC,CAC/E,OACEQ,EAAAA,IAAC,MAAA,CACC,YAAU,sBACV,UAAWE,EACT,qZACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASe,EAAkB,CAAE,UAAAR,EAAW,GAAGP,GAAsC,CAC/E,OACEQ,EAAAA,IAAC,MAAA,CACC,YAAU,sBACV,UAAWE,EACT,gNACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAeA,SAASgB,EAAiB,CACxB,UAAAT,EACA,GAAGP,CACL,EAA4D,CAC1D,OACEQ,EAAAA,IAACS,EAAA,CACC,YAAU,qBACV,UAAWP,EACT,iKACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASkB,EAAuB,CAC9B,UAAAX,EACA,GAAGP,CACL,EAAkE,CAChE,OACEQ,EAAAA,IAACW,EAAA,CACC,YAAU,2BACV,UAAWT,EACT,iIACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,MAAMoB,EAAoBC,EAAAA,WAIxB,SAA2B,CAAE,UAAAd,EAAW,QAAAe,EAAU,UAAW,KAAAV,EAAO,UAAW,GAAGZ,CAAA,EAASuB,EAAK,CAChG,OACEf,EAAAA,IAACgB,EAAA,CACC,IAAAD,EACA,YAAU,sBACV,UAAWb,EAAGH,CAAS,EACvB,OAAQC,EAAAA,IAACiB,EAAA,CAAO,QAAAH,EAAkB,KAAAV,CAAA,CAAY,EAC7C,GAAGZ,CAAA,CAAA,CAGV,CAAC,EAEK0B,EAAoBL,EAAAA,WAGxB,SAA2B,CAAE,UAAAd,EAAW,QAAAe,EAAU,UAAW,KAAAV,EAAO,UAAW,GAAGZ,CAAA,EAASuB,EAAK,CAChG,OACEf,EAAAA,IAACgB,EAAA,CACC,IAAAD,EACA,YAAU,sBACV,UAAWb,EAAGH,CAAS,EACvB,OAAQC,EAAAA,IAACiB,EAAA,CAAO,QAAAH,EAAkB,KAAAV,CAAA,CAAY,EAC7C,GAAGZ,CAAA,CAAA,CAGV,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as h,j as e}from"./vendor-react-Dtud3GyP.js";import{A as j,b as g,c as u,d as m,e as D,f,g as A,h as w}from"./alert-dialog-DlhAyxD3.js";import{T as n,a,b as o}from"./index-72wCsW4_.js";import"./vendor-CVKsgXJU.js";function k({open:t,onConfirm:d,onClose:s,count:l,title:c="Clear all captures?",itemLabel:p="captured request",confirmTooltip:x="Permanently delete all captured requests"}){return h.useEffect(()=>{if(!t)return;const r=i=>{i.key==="Escape"&&(i.preventDefault(),i.stopPropagation(),s())};return window.addEventListener("keydown",r,!0),()=>window.removeEventListener("keydown",r,!0)},[t,s]),e.jsx(j,{open:t,onOpenChange:r=>{r||s()},children:e.jsxs(g,{children:[e.jsxs(u,{children:[e.jsx(m,{children:c}),e.jsxs(D,{children:["Delete ",l," ",p,l===1?"":"s","? This cannot be undone."]})]}),e.jsxs(f,{children:[e.jsxs(n,{children:[e.jsx(a,{render:e.jsx(A,{}),children:"Cancel"}),e.jsx(o,{side:"top",children:"Dismiss without deleting"})]}),e.jsxs(n,{children:[e.jsx(a,{render:e.jsx(w,{onClick:d}),children:"Clear"}),e.jsx(o,{side:"top",className:"max-w-[220px]",children:x})]})]})]})})}export{k as ClearConfirmDialog};
|
|
2
|
+
//# sourceMappingURL=clear-confirm-dialog-BOcl93wD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clear-confirm-dialog-BOcl93wD.js","sources":["../../src/components/clear-confirm-dialog.tsx"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\n\ninterface ClearConfirmDialogProps {\n open: boolean;\n onConfirm: () => void;\n onClose: () => void;\n count: number;\n title?: string;\n itemLabel?: string;\n confirmTooltip?: string;\n}\n\nexport function ClearConfirmDialog({\n open,\n onConfirm,\n onClose,\n count,\n title = \"Clear all captures?\",\n itemLabel = \"captured request\",\n confirmTooltip = \"Permanently delete all captured requests\",\n}: ClearConfirmDialogProps) {\n useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n onClose();\n }\n };\n\n // Use capture phase so we intercept Escape before Base UI's AlertDialog\n // (which blocks Escape to enforce non-dismissable behavior) can swallow it.\n window.addEventListener(\"keydown\", handleKeyDown, true);\n return () => window.removeEventListener(\"keydown\", handleKeyDown, true);\n }, [open, onClose]);\n\n return (\n <AlertDialog\n open={open}\n onOpenChange={(v) => {\n if (!v) onClose();\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>\n Delete {count} {itemLabel}\n {count === 1 ? \"\" : \"s\"}? This cannot be undone.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <Tooltip>\n <TooltipTrigger render={<AlertDialogCancel />}>Cancel</TooltipTrigger>\n <TooltipContent side=\"top\">Dismiss without deleting</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger render={<AlertDialogAction onClick={onConfirm} />}>\n Clear\n </TooltipTrigger>\n <TooltipContent side=\"top\" className=\"max-w-[220px]\">\n {confirmTooltip}\n </TooltipContent>\n </Tooltip>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"names":["ClearConfirmDialog","open","onConfirm","onClose","count","title","itemLabel","confirmTooltip","useEffect","handleKeyDown","e","jsx","AlertDialog","v","AlertDialogContent","jsxs","AlertDialogHeader","AlertDialogTitle","AlertDialogDescription","AlertDialogFooter","Tooltip","TooltipTrigger","AlertDialogCancel","TooltipContent","AlertDialogAction"],"mappings":"+NAwBO,SAASA,EAAmB,CACjC,KAAAC,EACA,UAAAC,EACA,QAAAC,EACA,MAAAC,EACA,MAAAC,EAAQ,sBACR,UAAAC,EAAY,mBACZ,eAAAC,EAAiB,0CACnB,EAA4B,CAC1BC,OAAAA,EAAAA,UAAU,IAAM,CACd,GAAI,CAACP,EAAM,OAEX,MAAMQ,EAAiBC,GAAqB,CACtCA,EAAE,MAAQ,WACZA,EAAE,eAAA,EACFA,EAAE,gBAAA,EACFP,EAAA,EAEJ,EAIA,cAAO,iBAAiB,UAAWM,EAAe,EAAI,EAC/C,IAAM,OAAO,oBAAoB,UAAWA,EAAe,EAAI,CACxE,EAAG,CAACR,EAAME,CAAO,CAAC,EAGhBQ,EAAAA,IAACC,EAAA,CACC,KAAAX,EACA,aAAeY,GAAM,CACdA,GAAGV,EAAA,CACV,EAEA,gBAACW,EAAA,CACC,SAAA,CAAAC,OAACC,EAAA,CACC,SAAA,CAAAL,EAAAA,IAACM,GAAkB,SAAAZ,CAAA,CAAM,SACxBa,EAAA,CAAuB,SAAA,CAAA,UACdd,EAAM,IAAEE,EACfF,IAAU,EAAI,GAAK,IAAI,0BAAA,CAAA,CAC1B,CAAA,EACF,SACCe,EAAA,CACC,SAAA,CAAAJ,OAACK,EAAA,CACC,SAAA,CAAAT,MAACU,EAAA,CAAe,OAAQV,EAAAA,IAACW,EAAA,EAAkB,EAAI,SAAA,SAAM,EACrDX,EAAAA,IAACY,EAAA,CAAe,KAAK,MAAM,SAAA,0BAAA,CAAwB,CAAA,EACrD,SACCH,EAAA,CACC,SAAA,CAAAT,EAAAA,IAACU,GAAe,OAAQV,EAAAA,IAACa,GAAkB,QAAStB,CAAA,CAAW,EAAI,SAAA,OAAA,CAEnE,QACCqB,EAAA,CAAe,KAAK,MAAM,UAAU,gBAClC,SAAAhB,CAAA,CACH,CAAA,CAAA,CACF,CAAA,CAAA,CACF,CAAA,CAAA,CACF,CAAA,CAAA,CAGN"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{j as t,r as h}from"./vendor-react-Dtud3GyP.js";import{T as w,a as j,B as _,b as R,I as U,c as C,L as ne,d as L,e as ae,f as oe,P as le,u as ue,g as de,S as ce}from"./index-72wCsW4_.js";import{Y as me,Z as pe,_ as he,$ as fe,a0 as be,a1 as G,a2 as ge,a3 as xe,a4 as ve,m as b,H as ye,R as H,a5 as ke}from"./vendor-CVKsgXJU.js";import{S as we,a as je,b as _e,c as Re,d as Se}from"./select-mZPf2BR3.js";import{A as qe,a as Ne,b as De,c as Ce,d as Me,e as Te,f as Ie,g as Ee,h as Ae}from"./alert-dialog-DlhAyxD3.js";import{u as Le}from"./use-models-B4MLkfpt.js";import{u as ze}from"./use-polling-resource-D_vWHFwW.js";import"./use-capture-done-listener-u5RxfYQP.js";function Pe({className:e,min:r,max:s,suffix:i,onChange:a,value:n,disabled:o,...d}){const u=typeof n=="string"||typeof n=="number"?n:"";function l(c){if(o)return;const m=typeof u=="string"?Number(u):u;if(Number.isNaN(m))return;let f=m+c;r!==void 0&&f<r&&(f=r),s!==void 0&&f>s&&(f=s),a==null||a(String(f))}return t.jsxs("div",{className:"flex items-stretch",children:[t.jsxs(w,{children:[t.jsx(j,{render:t.jsx(_,{type:"button",variant:"outline",size:"icon",className:"h-8 w-8 rounded-r-none border-r-0 text-muted-foreground hover:text-foreground",onClick:()=>l(-1),disabled:o||r!==void 0&&typeof u=="number"&&u<=r,"aria-label":"Decrease",tabIndex:-1}),children:t.jsx(me,{className:"h-3.5 w-3.5"})}),t.jsx(R,{side:"top",children:"Decrease value"})]}),t.jsxs("div",{className:"relative flex-1",children:[t.jsx(U,{type:"number",inputMode:"numeric",className:C("no-spinner rounded-none text-center",i&&"pr-9",e),value:n,onChange:c=>a==null?void 0:a(c.target.value),disabled:o,...d}),i?t.jsx("span",{className:"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-muted-foreground",children:i}):null]}),t.jsxs(w,{children:[t.jsx(j,{render:t.jsx(_,{type:"button",variant:"outline",size:"icon",className:"h-8 w-8 rounded-l-none border-l-0 text-muted-foreground hover:text-foreground",onClick:()=>l(1),disabled:o||s!==void 0&&typeof u=="number"&&u>=s,"aria-label":"Increase",tabIndex:-1}),children:t.jsx(pe,{className:"h-3.5 w-3.5"})}),t.jsx(R,{side:"top",children:"Increase value"})]})]})}function J({className:e,orientation:r="horizontal",...s}){return t.jsx(he,{"data-slot":"separator",orientation:r,className:C("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",e),...s})}function Q({className:e,size:r="default",...s}){return t.jsx(fe,{"data-slot":"switch","data-size":r,className:C("peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",e),...s,children:t.jsx(be,{"data-slot":"switch-thumb",className:"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"})})}function K({className:e,...r}){return t.jsx("textarea",{"data-slot":"textarea",className:C("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",e),...r})}function z(e,r){return e==null?"":String(e)}function $(e){if(e==null)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}}function Fe({id:e,value:r,onChange:s}){return t.jsx(Q,{id:e,checked:!!r,onCheckedChange:i=>s(i)})}function Oe({id:e,value:r,onChange:s}){return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(Q,{id:e,checked:!!r,onCheckedChange:i=>s(i)}),t.jsx("span",{className:"text-xs text-muted-foreground",children:r?"On":"Off"})]})}function Be({def:e,value:r,onChange:s,id:i,isInvalid:a}){return t.jsxs(we,{value:String(r??""),onValueChange:n=>s(e.nullable&&n===""?null:n),children:[t.jsx(je,{id:i,"aria-invalid":a||void 0,children:t.jsx(_e,{placeholder:e.placeholder??"Select…"})}),t.jsx(Re,{children:(e.options??[]).map(n=>t.jsx(Se,{value:n.value,children:n.label},n.value))})]})}function We({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(K,{id:i,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),rows:3,"aria-invalid":a||void 0})}function He({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(le,{id:i,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),disabled:e.disabled,"aria-invalid":a||void 0})}function $e({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(Pe,{id:i,min:e.min,max:e.max,suffix:e.suffix,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o),disabled:e.disabled,"aria-invalid":a||void 0})}function Ve({def:e,value:r,onChange:s,id:i,isInvalid:a}){return t.jsx(Je,{def:e,value:r,onChange:s,id:i,isInvalid:a})}function Ue({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsxs("div",{className:"relative flex items-center",children:[t.jsx(U,{id:i,type:e.inputMode==="url"?"url":"text",value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),disabled:e.disabled,"aria-invalid":a||void 0}),e.suffix?t.jsx("span",{className:"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-muted-foreground",children:e.suffix}):null]})}const Ge={boolean:Fe,toggle:Oe,select:Be,textarea:We,password:He,number:$e,json:Ve,text:Ue};function Je({def:e,value:r,onChange:s,id:i,isInvalid:a}){const[n,o]=h.useState(()=>$(r)),[d,u]=h.useState(null),l=h.useRef(r);h.useEffect(()=>{r!==l.current&&(l.current=r,o($(r)),u(null))},[r]);const c=m=>{if(o(m),m.trim()===""){u(null);const f=e.nullable?null:{};l.current!==f&&(l.current=f,s(f));return}try{const f=JSON.parse(m);u(null),l.current=f,s(f)}catch(f){u(f instanceof Error?f.message:"Invalid JSON")}};return t.jsxs(t.Fragment,{children:[t.jsx(K,{id:i,value:n,placeholder:e.placeholder??"JSON object",onChange:m=>c(m.target.value),rows:6,"aria-invalid":a||!!d||void 0,className:"font-mono text-xs"}),d?t.jsx("p",{className:"text-xs font-medium text-destructive",role:"alert",children:d}):null]})}function Qe({def:e,value:r,onChange:s,dirty:i,error:a,warning:n,onRefreshSource:o,refreshing:d}){const u=`cfg-${e.key}`,l=!!a,c=l,m=e.placeholder??e.patternHint??void 0;return t.jsxs("div",{className:"grid grid-cols-1 gap-1.5 py-2.5 sm:grid-cols-[minmax(200px,1fr)_2fr] sm:items-start sm:gap-4",children:[t.jsxs("div",{className:"flex flex-col gap-1",children:[t.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[t.jsx(ne,{htmlFor:u,className:C(i&&"text-primary",l&&"text-destructive"),children:e.label}),e.required?t.jsx(L,{variant:"destructive",size:"sm",children:"Required"}):null,e.restartRequired?t.jsxs(L,{variant:"secondary",size:"sm",children:[t.jsx(G,{className:"h-2.5 w-2.5"}),"restart"]}):null,e.experimental?t.jsxs(L,{variant:"secondary",size:"sm",className:ae,children:[t.jsx(ge,{className:"h-2.5 w-2.5"}),"experimental"]}):null,e.umansSourced?t.jsxs(L,{variant:"secondary",size:"sm",className:oe,children:[t.jsx(xe,{className:"h-2.5 w-2.5"}),"Umans API"]}):null]}),e.description?t.jsx("span",{className:"text-xs text-muted-foreground",children:e.description}):null]}),t.jsxs("div",{className:"flex flex-col gap-1",children:[t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"flex-1",children:Ge[e.kind]({def:e,value:r,onChange:s,id:u,isInvalid:c,placeholder:m})}),e.refreshSource&&o?t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{type:"button",variant:"ghost",size:"icon",className:"h-8 w-8 shrink-0",disabled:d,onClick:o,"aria-label":`Refresh ${e.label} from source`,children:t.jsx(ve,{className:C("h-3.5 w-3.5",d&&"animate-pulse")})})}),t.jsx(R,{side:"left",children:"Re-fetch from upstream rate-limit headers"})]}):null]}),l?t.jsx("p",{className:"text-xs font-medium text-destructive",role:"alert",children:a}):null,n&&!l?t.jsx("p",{className:"text-xs font-medium text-amber-600 dark:text-amber-400",children:n}):null]})]})}function Ke({section:e,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o,onRefreshSource:d,refreshingSource:u}){return t.jsxs("section",{className:"space-y-0.5",children:[t.jsx("div",{className:"flex items-baseline justify-between gap-3 pb-1",children:t.jsxs("div",{children:[t.jsx("h4",{className:"text-sm font-semibold tracking-tight",children:e.title}),t.jsx("p",{className:"text-xs text-muted-foreground",children:e.description})]})}),e.fields.map(l=>t.jsx(Qe,{def:l,value:r[l.key],onChange:c=>i(l.key,c),dirty:JSON.stringify(r[l.key]??null)!==JSON.stringify(s[l.key]??null),error:a[l.key],warning:n[l.key],onRefreshSource:l.refreshSource?d:void 0,refreshing:u},l.key)),o?null:t.jsx(J,{className:"my-2"})]})}function Ye({group:e,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o,onRefreshSource:d,refreshingSource:u}){return t.jsxs("div",{className:"space-y-1",children:[t.jsxs("div",{className:"pb-1",children:[t.jsx("h3",{className:"text-base font-semibold tracking-tight",children:e.title}),t.jsx("p",{className:"text-xs text-muted-foreground",children:e.description})]}),e.sections.map((l,c)=>t.jsx(Ke,{section:l,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o&&c===e.sections.length-1,onRefreshSource:d,refreshingSource:u},l.title)),o?null:t.jsx(J,{className:"my-3"})]})}const Ze=[{key:"vision_strategy",label:"Strategy",kind:"select",description:'Controls when the proxy intercepts image-bearing requests to generate text descriptions via a vision model. "never" disables interception entirely; "catalog" uses a cache-first strategy where vision-capable models process images themselves and only non-capable models get descriptions; "always" forces interception for all requests regardless of model capability. Default: catalog.',options:[{value:"never",label:"Never"},{value:"catalog",label:"Catalog"},{value:"always",label:"Always"}],restartRequired:!0,required:!0},{key:"vision_model",label:"Model",kind:"select",description:"Model used to generate image descriptions during vision interception. Must be a vision-capable model. The dropdown is populated from /v1/models/info at runtime. Default: umans-flash.",restartRequired:!0,required:!0}],Xe=[{key:"vision_prompt",label:"Prompt",kind:"textarea",description:"System prompt sent to the vision model when generating image descriptions. Changing this increments vision_prompt_version to bust the cache. Default: a built-in prompt that produces exhaustive, structured descriptions (image type, OCR, visual elements, data/charts, contextual clues, quality notes).",restartRequired:!0,required:!0},{key:"vision_prompt_version",label:"Prompt Version",kind:"number",description:"Version tag for the vision prompt. Bump this when you edit vision_prompt to invalidate cached descriptions generated with an older prompt. Cached entries with a mismatched version are treated as misses. Default: 2.",restartRequired:!0,required:!0,min:1},{key:"vision_max_images",label:"Max Images",kind:"number",description:"Maximum number of images processed in a single vision interception request. Additional images in the same request are ignored. Range: 1–100. Default: 5.",restartRequired:!0,required:!0,min:1,max:100},{key:"vision_max_description_tokens",label:"Max Description Tokens",kind:"number",description:"Maximum number of tokens the vision model may generate per image description. Higher values allow more detailed descriptions at the cost of latency. Range: 1–200,000. Default: 4,096.",restartRequired:!0,required:!0,min:1,max:2e5},{key:"vision_reasoning_effort",label:"Reasoning Effort",kind:"select",description:`Reasoning effort for the vision model. "none" disables reasoning; "low"/"medium"/"high" increase reasoning depth at the cost of latency. Selecting "Default (null)" uses the model's built-in default. Default: none.`,options:[{value:"",label:"Default (null)"},{value:"none",label:"None"},{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"}],nullable:!0,restartRequired:!0},{key:"vision_timeout_ms",label:"Timeout",kind:"number",description:"Timeout for vision interception requests in milliseconds. 0 means no timeout — the proxy waits indefinitely for the vision model to respond. Default: 0 (no timeout).",restartRequired:!0,required:!0,min:0,suffix:"ms"},{key:"vision_cache_size",label:"Cache Size",kind:"number",description:"Maximum number of entries in the in-memory vision description cache. When the cache is full, oldest entries are evicted. Must be ≥ 100. Default: 1,000.",restartRequired:!0,required:!0,min:100},{key:"vision_cache_ttl_ms",label:"Cache TTL",kind:"number",description:"Time-to-live for vision description cache entries in milliseconds. Expired entries are treated as misses and re-fetched from the vision model. Must be ≥ 1,000 ms. Default: 604,800,000 ms (7 days).",restartRequired:!0,min:1e3,suffix:"ms"},{key:"vision_cache_max_rows",label:"Cache Max Rows",kind:"number",description:"Maximum number of rows in the persistent (on-disk SQLite) vision cache. When the table is full, oldest entries are evicted. Must be ≥ 100. Default: 10,000.",restartRequired:!0,min:100},{key:"vision_persistent_cache",label:"Persistent Cache",kind:"boolean",description:"When on, vision description cache is persisted to disk (SQLite) and survives restarts. When off, cache is in-memory only and lost on restart. Default: true.",restartRequired:!0},{key:"vision_concurrency",label:"Concurrency",kind:"number",description:"Maximum number of concurrent vision interception requests. Higher values allow parallel image processing but increase upstream load. Range: 1–20. Default: 1.",restartRequired:!0,required:!0,min:1,max:20},{key:"vision_max_dimension",label:"Max Dimension",kind:"number",description:"Maximum width or height (in pixels) for preprocessed images. Images exceeding this in either dimension are downscaled proportionally before being sent to the vision model. Range: 256–8,192. Default: 2,048 px.",restartRequired:!0,min:256,max:8192,suffix:"px"},{key:"vision_jpeg_quality",label:"JPEG Quality",kind:"number",description:'JPEG encoding quality (1–100) for preprocessed images when vision_image_format is "jpeg". Higher values produce sharper images at larger file sizes. Ignored when format is "png". Range: 1–100. Default: 92.',restartRequired:!0,min:1,max:100},{key:"vision_image_format",label:"Image Format",kind:"select",description:'Output image format for preprocessed images sent to the vision model. "jpeg" produces smaller payloads with lossy compression; "png" is lossless but larger. Default: png.',options:[{value:"jpeg",label:"JPEG"},{value:"png",label:"PNG"}],restartRequired:!0},{key:"vision_image_detail",label:"Image Detail",kind:"select",description:'OpenAI-style image detail level sent to the vision model. "low" uses a single low-resolution pass; "high" processes the image at higher detail (more tokens, more accurate); "auto" lets the model decide based on image size. Default: high.',options:[{value:"auto",label:"Auto"},{value:"low",label:"Low"},{value:"high",label:"High"}],restartRequired:!0},{key:"vision_pending_max_batch",label:"Pending Max Batch",kind:"number",description:"Maximum number of pending vision requests to batch together into a single processing cycle. Higher values increase throughput under load but add latency for the first request in the batch. Must be ≥ 1. Default: 50.",restartRequired:!0,min:1}],et=[{key:"port",label:"Port",kind:"number",description:"TCP port the proxy listens on. Range: 1–65,535. Default: 1,945.",restartRequired:!0,required:!0,min:1,max:65535},{key:"max_captures",label:"Max Captures",kind:"number",description:"Maximum number of captures kept in the SQLite ring buffer. When this limit is reached, the oldest entries are evicted to make room for new ones. Must be ≥ 1. Default: 200.",restartRequired:!0,required:!0,min:1},{key:"db_path",label:"DB Path",kind:"text",description:"Filesystem path to the SQLite database used for storing captures. Relative paths are resolved from the proxy's working directory. Default: ./umans-gate.db.",restartRequired:!0,required:!0},{key:"idle_timeout",label:"Idle Timeout",kind:"number",description:"Bun.serve idleTimeout — seconds before an idle HTTP connection is closed. Range: 1–255 (Bun's maximum). Default: 255 s.",restartRequired:!0,required:!0,min:1,max:255,suffix:"s"},{key:"upstream_protocol",label:"Upstream Protocol",kind:"select",description:'HTTP protocol used for upstream connections. "http1.1" uses HTTP/1.1 (default, broadest compatibility); "http2" uses HTTP/2 (multiplexing, lower latency for concurrent requests). Default: http1.1.',options:[{value:"http1.1",label:"HTTP/1.1"},{value:"http2",label:"HTTP/2"}],restartRequired:!0,required:!0},{key:"compression_enabled",label:"Compression",kind:"toggle",restartRequired:!1,description:"When on, stored request/response bodies are compressed with zstd to reduce SQLite database size. Hot-reloadable — changes take effect immediately without restart. Default: on."}],tt=[{key:"stamp_claude_code_enabled",label:"Claude Code Style",kind:"toggle",description:"When on, applies the full Claude Code stamp bundle on Anthropic requests: cache TTL=1h, temperature=1.0, top_k=20 (GLM only), max_tokens (131,071 GLM / 32,767 others), thinking={type:'adaptive'}, output_config (effort=max GLM / high others), and context_management injection (requires anthropic-version: 2023-06-01). Off by default.",experimental:!0},{key:"stamp_reasoning_effort_enabled",label:"Reasoning Effort (OpenAI)",kind:"toggle",description:"When on, stamps reasoning_effort='high' onto OpenAI-compatible requests (umans-glm* models get 'max') and removes max_tokens/thinking from the request body. Off by default.",experimental:!0}],rt=[{key:"warmer_enabled",label:"Enabled",kind:"boolean",description:"When on, the proxy periodically pings the upstream /v1/models endpoint to keep connections warm, avoiding ~750 ms cold-start latency on the first request after an idle period. Default: true.",restartRequired:!0},{key:"warmer_interval_ms",label:"Interval",kind:"number",description:"Interval between upstream keep-alive pings in milliseconds. Shorter intervals keep connections warmer but increase background traffic. Must be ≥ 1,000 ms. Default: 20,000 ms (20 s).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"}],st=[{key:"concurrency_hard_cap",label:"Hard Cap",kind:"number",description:"Absolute maximum number of concurrent upstream requests the proxy will allow. Excess requests are queued. This value is validated against the upstream /v1/usage limit when an Umans API key is set. Must be ≥ 1. Default: 1.",required:!0,min:1,refreshSource:!0,umansSourced:!0},{key:"concurrency_soft_limit",label:"Soft Limit",kind:"number",description:"Soft concurrency limit read from the upstream /v1/usage endpoint. Read-only — auto-derived from the upstream and cannot be edited directly. Use the refresh button to re-fetch from source. Default: 1.",disabled:!0,min:1,refreshSource:!0,umansSourced:!0},{key:"concurrency_main_reservation",label:"Main Reservation",kind:"number",description:"Number of concurrency slots reserved exclusively for main (non-vision) requests. Ensures vision traffic cannot starve main requests. Must be ≥ 1 and ≤ (hard_cap − 2) when hard_cap ≥ 3. Default: 1.",required:!0,min:1},{key:"concurrency_vision_reservation",label:"Vision Reservation",kind:"number",description:"Number of concurrency slots reserved exclusively for vision interception requests. Ensures main traffic cannot starve vision handoffs. Must be ≥ 1 and ≤ (hard_cap − 2) when hard_cap ≥ 3. Default: 1.",required:!0,min:1},{key:"release_cooldown_ms",label:"Release Cooldown",kind:"number",description:"Delay in milliseconds before releasing a concurrency slot back to the pool after a response completes. A non-zero value adds a brief cooldown to prevent immediate burst re-use. Must be ≥ 0. Default: 1,000 ms (1 s).",required:!0,min:0,suffix:"ms"},{key:"rate_limit_requests",label:"Rate Limit",kind:"number",description:"Controls upstream request rate limiting. -1 = unlimited (no limiter at all), 0 = auto-derive the limit from the upstream /v1/usage endpoint (requires umans_api_key), >0 = use this explicit value as the max requests per rate-limit window. Default: 0 (auto-derive).",min:-1,umansSourced:!0,refreshSource:!0}],it=[{key:"breaker_threshold",label:"Breaker Threshold",kind:"number",description:"Number of upstream failures within the breaker window required to open (trip) the circuit breaker. When open, the proxy short-circuits upstream requests instead of retrying. Must be ≥ 1. Default: 5.",required:!0,min:1},{key:"breaker_window_ms",label:"Breaker Window",kind:"number",description:"Sliding time window in milliseconds during which upstream failures are counted toward the breaker threshold. Must be ≥ 1,000 ms. Default: 300,000 ms (5 minutes).",required:!0,min:1e3,suffix:"ms"},{key:"breaker_cooldown_ms",label:"Breaker Cooldown",kind:"number",description:"Duration in milliseconds the circuit breaker stays open before allowing a half-open probe request through. If the probe succeeds, the breaker closes; if it fails, the timer restarts. Must be ≥ 1,000 ms. Default: 60,000 ms (1 minute).",required:!0,min:1e3,suffix:"ms"}],nt=[{key:"queue_timeout_ms",label:"Queue Timeout",kind:"number",description:"Maximum time in milliseconds a request can wait in the concurrency queue before timing out and returning an error to the client. Must be ≥ 100 ms. Default: 30,000 ms (30 s).",required:!0,min:100,suffix:"ms"},{key:"max_queue_depth",label:"Max Queue Depth",kind:"number",description:"Maximum number of requests that can be waiting in the concurrency queue simultaneously. When the queue is full, new requests are rejected immediately. Must be ≥ 1. Default: 256.",required:!0,min:1},{key:"queue_max_depth",label:"Write Queue Depth",kind:"number",description:"Maximum depth of the write-behind database flush queue (distinct from the concurrency waiters queue above). Controls how many capture writes can be buffered before the queue applies backpressure. Must be ≥ 1. Default: 100.",min:1}],at=[{key:"capture_body_max_bytes",label:"Capture Body Max Bytes",kind:"number",description:"Maximum size (in bytes) of captured request/response bodies stored in SQLite. Bodies exceeding this limit are truncated. 0 = no limit (capture full bodies). Must be ≥ 0. Default: 10,000,000 (10 MB).",min:0},{key:"ws_backpressure_limit",label:"WS Backpressure Limit",kind:"number",description:"Maximum buffered bytes per WebSocket connection before backpressure is applied. When a client falls behind, the proxy stops sending until the client catches up. 0 = use Bun's default limit. Must be ≥ 0. Default: 1,048,576 (1 MB).",min:0,suffix:"bytes"},{key:"ws_close_on_backpressure_limit",label:"Close on Backpressure Limit",kind:"boolean",description:"When on, WebSocket connections that exceed the backpressure limit are forcibly closed instead of merely pausing. This prevents slow clients from accumulating unbounded buffered data. Default: true."}],ot=[{key:"umans_api_key",label:"Umans API Key",kind:"password",description:"Umans API key used to authenticate upstream requests and fetch /v1/usage data (concurrency limits, rate-limit source). When empty, the proxy runs in fail-safe mode with worst-case limits and priority_low=true. Leave empty to disable upstream usage tracking. Default: empty.",restartRequired:!0},{key:"usage_refresh_ms",label:"Usage Refresh",kind:"number",description:"Interval in milliseconds between background /v1/usage fetches. The usage data drives concurrency limit auto-derivation and rate-limit source values. Shorter intervals keep limits fresher but increase API calls. Must be ≥ 1,000 ms. Default: 60,000 ms (1 minute).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"},{key:"models_refresh_ms",label:"Models Refresh",kind:"number",description:"Interval in milliseconds between background /v1/models fetches. The models list populates the vision model dropdown and validates model names. Must be ≥ 1,000 ms. Default: 3,600,000 ms (1 hour).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"}],Y=[{title:"General",description:"Core proxy, credentials, vision, and concurrency settings.",sections:[{title:"Server",description:"Core network settings and SQLite storage for captures.",fields:et},{title:"Credentials",description:"API key and upstream usage/model refresh intervals.",fields:ot},{title:"Vision",description:"Vision image-to-description interception pipeline.",fields:Ze},{title:"Warmer",description:"Background keep-alive pings to the upstream /v1/models endpoint.",fields:rt},{title:"Concurrency & Gate",description:"Concurrency limits, reservations, and rate limiting.",fields:st}]},{title:"Experimental",description:"Optional request-body stamping features that may change behavior.",sections:[{title:"Request Stamp",description:"Optional request-body stamping for Anthropic and OpenAI-compatible routes.",fields:tt}]},{title:"Advanced",description:"Circuit breaker, queue tuning, vision parameters, and storage internals.",sections:[{title:"Circuit Breaker",description:"Failure detection and circuit breaker thresholds.",fields:it},{title:"Queue",description:"Request queue timeouts, depths, and write-behind buffering.",fields:nt},{title:"Vision Tuning",description:"Vision prompt, cache, concurrency, and image processing parameters.",fields:Xe},{title:"Capture & Storage",description:"Body capture limits and WebSocket backpressure controls.",fields:at}]}];Y.flatMap(e=>e.sections);function lt(e){const[r,s]=h.useState(null);h.useEffect(()=>{e&&s({...e})},[e]);const i=h.useMemo(()=>{if(!r||!e)return new Set;const o=new Set;for(const d of Object.keys(r))JSON.stringify(r[d]??null)!==JSON.stringify(e[d]??null)&&o.add(d);return o},[r,e]);function a(){e&&s({...e})}function n(o,d){s(u=>u&&{...u,[o]:d})}return{draft:r,setDraft:s,resetDraft:a,updateField:n,isDirty:i.size>0,dirtyKeys:i}}function ut(e){const[r,s]=h.useState(!1),[i,a]=h.useState(!1),[n,o]=h.useState(!1),[d,u]=h.useState(!1),[l,c]=h.useState([]),m=h.useCallback(async()=>{if(!e.draft)return;if(!e.isDirty){b.info("Nothing to save",{description:"No changes since last load."});return}s(!0);const p={};for(const y of e.dirtyKeys)p[y]=e.draft[y];const g=await e.save(p);if(!g){s(!1),b.error("Save failed",{description:"Server unreachable."}),c(["Server unreachable."]);return}if(g.ok){const y=await e.reloadFromDisk();s(!1),y!=null&&y.ok?b.success("Config saved",{description:g.warnings.length?`${g.warnings.length} warning(s)`:"Saved and applied live."}):b.warning("Config saved",{description:"Saved to disk, but live reload failed. Restart to apply."}),c([])}else s(!1),b.error("Save rejected",{description:g.errors.join(`
|
|
2
|
+
`)}),c(g.errors);if(g.warnings.length)for(const y of g.warnings)b.warning("Warning",{description:y})},[e]),f=h.useCallback(()=>{e.resetDraft(),b.info("Draft reset",{description:"Reverted to last loaded config."})},[e]),P=h.useCallback(async()=>{u(!0);const p=await e.resetToDefault();if(u(!1),!p){b.error("Reset failed",{description:"Server unreachable."});return}p.ok?(b.success("Config reset to defaults",{description:"API key preserved. Limits refetched from upstream."}),c([])):b.error("Reset failed",{description:p.errors.join(`
|
|
3
|
+
`)})},[e]),D=h.useCallback(async()=>{a(!0);const p=await e.refreshFromSource();if(a(!1),!p){b.error("Refresh failed",{description:"Server unreachable."});return}if(p.ok){const g=[`Hard cap set to ${p.hardCap}`,`soft limit set to ${p.softLimit}`];p.requestsHardCap!==null&&p.requestsHardCap!==void 0?g.push(`rate limit hard cap set to ${p.requestsHardCap}`):g.push("rate limit: unlimited"),b.success("Limits reloaded from source",{description:`${g.join(", ")}.`})}else b.error("Refresh failed",{description:p.error})},[e]),M=h.useCallback(async()=>{o(!0);const p=await e.restart();if(!p){b.error("Restart failed",{description:"Server unreachable."}),o(!1);return}p.ok?(b.success("Restarting",{description:p.message??"Server is restarting. Reconnect in a few seconds. Requires a process manager (bun --watch, systemd, pm2) to auto-restart."}),setTimeout(()=>o(!1),5e3)):(b.error("Restart failed",{description:p.error}),o(!1))},[e]);return{save:m,refreshFromSource:D,restart:M,reset:f,resetToDefault:P,loading:{saving:r,refreshingSource:i,restarting:n,resetting:d},validationErrors:l,setValidationErrors:c}}const dt=3e4;function ct(){const e=n=>n??null,{data:r,loading:s,error:i,refresh:a}=ze({endpoint:"/dashboard/api/usage",pollInterval:dt,errorMessage:"Failed to fetch usage",parse:e});return{data:r,loading:s,error:i,refresh:a}}const mt={number:pt,text:V,password:V,json:ht,boolean:()=>({}),select:()=>({}),textarea:()=>({}),toggle:()=>({})};function pt(e,r){const s=r==null?"":String(r),i=Number(s);return s.length===0?{}:Number.isInteger(i)?e.min!==void 0&&i<e.min?{error:`${e.label} must be ≥ ${e.min}`}:e.max!==void 0&&i>e.max?{error:`${e.label} must be ≤ ${e.max}`}:{}:{error:`${e.label} must be an integer`}}function V(e,r){const s=r==null?"":String(r);if(e.pattern&&s.length>0&&!new RegExp(e.pattern).test(s))return{error:`${e.label} must match ${e.patternHint??e.pattern}`};if(e.inputMode==="url"&&s.length>0)try{const i=new URL(s);if(i.protocol!=="http:"&&i.protocol!=="https:")return{error:`${e.label} must use http: or https: protocol`}}catch{return{error:`${e.label} is not a valid URL`}}return{}}function ht(e,r){if(r==null)return{};if(typeof r!="object"||Array.isArray(r))return{error:`${e.label} must be a JSON object`};for(const[s,i]of Object.entries(r))if(typeof i!="number"||!Number.isFinite(i)||i<=0)return{error:`${e.label}: value for "${s}" must be a positive number`};return{}}const ft={rate_limit_requests:e=>Number(e)===-1?"Unlimited — no request cap is enforced. The upstream may still reject excessive traffic.":null};function bt(e,r){const s=r==null?"":String(r);if(e.required&&!e.nullable&&s.length===0)return{error:`${e.label} is required`};if(e.nullable&&s.length===0)return{};const i=mt[e.kind];return i?i(e,r):{}}function gt(e,r){const s={},i={};for(const a of r)for(const n of a.fields){if(n.disabled)continue;const o=bt(n,e[n.key]);o.error&&(s[n.key]=o.error);const d=ft[n.key];if(d){const u=d(e[n.key]);u&&(i[n.key]=u)}}return{errors:s,warnings:i}}function St(){const{config:e,loading:r,error:s,reload:i,save:a,reloadFromDisk:n,refreshFromSource:o,restart:d,resetToDefault:u}=ue(),{data:l}=Le(),{data:c}=ct(),{draft:m,updateField:f,resetDraft:P,isDirty:D,dirtyKeys:M}=lt(e),{save:p,refreshFromSource:g,restart:y,reset:Z,resetToDefault:X,loading:S,validationErrors:T,setValidationErrors:ee}=ut({draft:m,dirtyKeys:M,isDirty:D,resetDraft:P,save:a,reloadFromDisk:n,refreshFromSource:o,restart:d,resetToDefault:u}),[te,F]=h.useState(!1),E=h.useMemo(()=>{const x=((l==null?void 0:l.models)??[]).filter(k=>{var N;return((N=k.info)==null?void 0:N.capabilities.supports_vision)===!0}).map(k=>({value:k.id,label:k.id})).sort((k,N)=>k.label.localeCompare(N.label)),v=(c==null?void 0:c.concurrencyHardCap)??void 0,B=v!==void 0?v-2:void 0,W=(c==null?void 0:c.requestsHardCap)??void 0,q={};return v!==void 0&&(q.concurrency_hard_cap=v,q.concurrency_soft_limit=v,q.concurrency_main_reservation=B,q.concurrency_vision_reservation=B),W!==void 0&&(q.rate_limit_requests=W),Y.map(k=>({...k,sections:k.sections.map(N=>({...N,fields:N.fields.map(A=>{let I=A;return A.key==="vision_model"&&(I={...I,options:x}),A.key in q&&(I={...I,max:q[A.key]}),I})}))}))},[l,c]),{errors:O,warnings:re}=h.useMemo(()=>{if(!m)return{errors:{},warnings:{}};const x=E.flatMap(v=>v.sections);return gt(m,x)},[m,E]),se=Object.keys(O).length>0;function ie(x,v){m&&f(x,v),T.length>0&&ee([])}return r&&!m?t.jsx("div",{className:"flex h-full items-center justify-center",children:t.jsx(de,{})}):s&&!m?t.jsxs("div",{className:"flex h-full flex-col items-center justify-center gap-3 p-8 text-center text-muted-foreground",children:[t.jsx(ye,{className:"h-12 w-12 text-destructive"}),t.jsx("p",{className:"text-sm font-medium text-destructive",children:"Something went wrong"}),t.jsx("p",{className:"text-xs",children:s}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsxs(_,{variant:"outline",size:"sm",onClick:i,children:[t.jsx(H,{className:"mr-1.5 h-3.5 w-3.5"}),"Retry"]})}),t.jsx(R,{side:"top",children:"Retry loading config"})]})]}):m?t.jsxs("div",{className:"flex h-full flex-col",children:[t.jsxs("header",{className:"flex items-center justify-between border-b border-border bg-card px-4 py-3",children:[t.jsx("h2",{className:"sr-only",children:"Configuration"}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{size:"sm",variant:"default",disabled:S.saving||!D||se,onClick:p,children:S.saving?"Saving…":"Save"})}),t.jsx(R,{side:"bottom",children:"Save changes to disk and apply live fields automatically"})]}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{size:"sm",variant:"ghost",disabled:!D,onClick:Z,children:"Reset Draft"})}),t.jsx(R,{side:"bottom",children:"Discard unsaved changes"})]}),t.jsxs(qe,{open:te,onOpenChange:F,children:[t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(Ne,{render:t.jsxs(_,{size:"sm",variant:"outline",disabled:S.resetting,children:[t.jsx(H,{className:"mr-1.5 h-3.5 w-3.5"}),S.resetting?"Resetting…":"Reset to Default"]})})}),t.jsx(R,{side:"bottom",children:"Reset all config fields to defaults (API key is preserved)"})]}),t.jsxs(De,{children:[t.jsxs(Ce,{children:[t.jsx(Me,{children:"Reset all config to defaults?"}),t.jsx(Te,{children:"This writes the default configuration to disk, replacing all current values. Your API key is preserved. A restart will be needed for some fields to take effect."})]}),t.jsxs(Ie,{children:[t.jsx(Ee,{children:"Cancel"}),t.jsx(Ae,{variant:"destructive",onClick:async()=>{F(!1),await X()},children:"Reset to Default"})]})]})]}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsxs(_,{size:"sm",variant:"destructive",disabled:S.restarting,onClick:y,children:[t.jsx(ke,{className:"mr-1.5 h-3.5 w-3.5"}),S.restarting?"Restarting…":"Restart"]})}),t.jsx(R,{side:"bottom",className:"max-w-[260px]",children:"Restart the server. Requires an external process manager (bun --watch, systemd, pm2)."})]})]}),D?t.jsxs("span",{className:"text-xs text-muted-foreground",children:[M.size," unsaved change",M.size>1?"s":""]}):t.jsx("span",{className:"text-xs text-muted-foreground",children:"All changes saved"})]}),T.length>0&&t.jsxs("div",{role:"alert","aria-live":"polite",className:"border-b border-destructive/30 bg-destructive/5 px-4 py-2",children:[t.jsx("p",{className:"text-xs font-medium text-destructive",children:T.length===1?"1 issue":`${T.length} issues`}),t.jsx("ul",{className:"mt-1 space-y-0.5",children:T.map(x=>t.jsx("li",{className:"text-xs text-destructive/80",children:x},x))})]}),t.jsx(ce,{className:"min-h-0 flex-1",children:t.jsxs("div",{className:"mx-auto w-full max-w-3xl px-6 py-6",children:[E.map((x,v)=>t.jsx(Ye,{group:x,values:m,originals:e??{},onField:ie,errors:O,warnings:re,isLast:v===E.length-1,onRefreshSource:g,refreshingSource:S.refreshingSource},x.title)),t.jsxs("div",{className:"pt-6 text-xs text-muted-foreground",children:["Fields marked"," ",t.jsxs("span",{className:"text-muted-foreground",children:[t.jsx(G,{className:"inline h-3 w-3 mr-1","aria-hidden":!0}),"restart"]})," ","require a server restart to take effect. Other fields are applied live when you save."]})]})})]}):null}export{St as ConfigTab};
|
|
4
|
+
//# sourceMappingURL=config-tab-pnBjiSyC.js.map
|