specrails-hub 1.63.0 → 1.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -95
- package/client/dist/assets/{ActivityFeedPage-Ddtx0RoB.js → ActivityFeedPage-BUteOq2I.js} +1 -1
- package/client/dist/assets/{AgentsPage-fBwl0dem.js → AgentsPage-Dn_YfDs2.js} +1 -1
- package/client/dist/assets/AnalyticsPage-CZ0QKJf0.js +1 -0
- package/client/dist/assets/{BarChart-D5fROR-P.js → BarChart-Dvcb2eDw.js} +1 -1
- package/client/dist/assets/{CodePage-DLB4rtSG.js → CodePage-Dr3YBYUu.js} +1 -1
- package/client/dist/assets/{DocsDialog-jkgxFabu.js → DocsDialog-7rUEORUK.js} +1 -1
- package/client/dist/assets/{DocsPage-CNwelao8.js → DocsPage-3UlvX3uN.js} +1 -1
- package/client/dist/assets/{ExportDropdown-DE6Sf9lN.js → ExportDropdown-Czw3l5jw.js} +1 -1
- package/client/dist/assets/{HubAnalyticsPage-WkNw6FjF.js → HubAnalyticsPage-DjJF9oDK.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-BouSqxp_.js → IntegrationsPage-DpF2rB-j.js} +1 -1
- package/client/dist/assets/JobDetailPage-DREPKK81.js +16 -0
- package/client/dist/assets/{JobsPage-zia2PQx2.js → JobsPage-DuKic6gk.js} +1 -1
- package/client/dist/assets/{dist-js-DazNsMeg.js → dist-js-C-6o954n.js} +1 -1
- package/client/dist/assets/{dist-js-CLEItn6i.js → dist-js-DHZ3tbtl.js} +1 -1
- package/client/dist/assets/{index-B7bEifI4.js → index-k8jG-NWj.js} +6 -6
- package/client/dist/assets/{lib-CJzwGHcW.js → lib-mhqLpBMA.js} +1 -1
- package/client/dist/assets/{useProjectCache-DwDPh5Ek.js → useProjectCache-Dvz6idyy.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/dist/ai-invocations.js +10 -2
- package/server/dist/db.js +17 -1
- package/server/dist/pricing.js +9 -1
- package/server/dist/profiles-router.js +2 -1
- package/server/dist/project-router.js +21 -15
- package/server/dist/providers/codex-adapter.js +7 -2
- package/server/dist/queue-manager.js +1 -0
- package/server/dist/result-event.js +9 -0
- package/server/dist/spending.js +3 -0
- package/client/dist/assets/AnalyticsPage-uT5UQ1TZ.js +0 -1
- package/client/dist/assets/JobDetailPage-DfapkSOT.js +0 -16
package/README.md
CHANGED
|
@@ -1,169 +1,288 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# 🚄 specrails-hub
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### The local cockpit for shipping software with AI agents
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Draft specs by talking to Claude or Codex → drag them onto execution rails → watch the pipeline ship — all from one window, on your laptop, with every dollar tracked.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/specrails-hub)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://www.typescriptlang.org/)
|
|
13
|
+
[](https://react.dev)
|
|
14
|
+
[](https://tauri.app)
|
|
15
|
+
[](#-bring-your-own-agent)
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
[Quick start](#-quick-start) · [Features](#-what-you-can-do) · [Architecture](#%EF%B8%8F-how-its-built) · [Docs](#-documentation) · [Desktop app](#%EF%B8%8F-desktop-app)
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
- **Generate a spec in one shot** — Quick mode for when you already know what you want. Optionally enrich it with a "Contract Layer" of names, shapes, invariants, and a file touch list.
|
|
15
|
-
- **Turn a live website into a spec** — *Add Spec → From a website* opens an embedded browser. Navigate to a page, hover-select an element or drag a rectangle, and the screenshot + rich DOM + applied CSS become attachments that feed Quick or Explore. The desktop app ships its own Chromium, so it works offline.
|
|
16
|
-
- **Drag specs onto execution rails** — each rail is an independent lane. Run multiple specs in parallel, with different agent profiles per rail.
|
|
17
|
-
- **Compare two specs side by side** — drag any spec modal to the edge of the screen; a picker of your todo specs appears on the other side. Pick one and they live next to each other. Tablet-style.
|
|
18
|
-
- **Split a big epic** — SMASH a parent spec into a family of sub-specs in one click; the children carry short summaries on their cards.
|
|
19
|
-
- **Refine a spec in place** — *Continue Editing* reopens any draft / todo / backlog spec in Explore, with the original conversation resumed if there was one.
|
|
20
|
-
- **Track every AI cost** — every AI CLI invocation across rails, Quick spec, Explore turns, and AI edits is recorded — across **both** Claude and Codex. Codex cost is estimated from a local rate-card since the CLI doesn't report it natively. The Analytics page shows your burn rate, top tickets, breaks down spend per provider, and lets you export CSV.
|
|
21
|
-
- **Customise everything** — three themes, font sizes, terminal preferences, agent profiles, plugin integrations (Serena bundled today).
|
|
19
|
+
</div>
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🌟 What is this?
|
|
24
|
+
|
|
25
|
+
**specrails-hub** turns *"I'll just let the AI do it"* into a workflow you can **see, steer, and trust**. It's a local-first dashboard and CLI that sits on top of [**specrails-core**](https://github.com/fjpulidop/specrails-core) and gives you **one window for all your projects**:
|
|
26
|
+
|
|
27
|
+
- 💬 Shape a spec in conversation with an AI, or generate one in a single shot.
|
|
28
|
+
- 🛤️ Drag specs onto **execution rails** and run them — many in parallel.
|
|
29
|
+
- 🤖 Watch the **Architect → Developer → Reviewer → Ship** pipeline stream live.
|
|
30
|
+
- 💰 See exactly **what each agent cost you** this week, per provider, per ticket.
|
|
31
|
+
|
|
32
|
+
> 🔒 **100% local. Single user. No accounts. No telemetry leaves your machine.**
|
|
33
|
+
> Your code stays on your laptop — nothing moves unless **you** spawn an agent against it.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ✨ What you can do
|
|
38
|
+
|
|
39
|
+
### 📝 Turn ideas into specs
|
|
40
|
+
|
|
41
|
+
| | |
|
|
42
|
+
|---|---|
|
|
43
|
+
| 🗣️ **Explore** | Describe what you want in a chat; a **live draft** rebuilds itself every turn. Save it as a *draft ticket* and resume later, or commit when it looks right. First-token latency is tuned to feel electric. |
|
|
44
|
+
| ⚡ **Quick** | Already know what you want? Generate a full spec in one shot — optionally enriched with a **Contract Layer** of exact names, data shapes, invariants, and a file-touch list so the downstream agents don't reinvent anything. |
|
|
45
|
+
| 🌐 **From a website** | *Add Spec → From a website* opens an **embedded browser**. Navigate, hover-select an element or drag a rectangle, and the screenshot + DOM + applied CSS become attachments. The desktop build ships its own Chromium, so it works **offline**. |
|
|
46
|
+
| 💥 **SMASH** | Explode a big epic into a family of sub-specs in one click — children carry short summaries on their cards. |
|
|
47
|
+
| 🔁 **Continue Editing** | Reopen any draft / todo / backlog spec back in Explore, resuming the original conversation if there was one. |
|
|
48
|
+
| 🪟 **Compare** | Drag a spec modal to the screen edge → a picker of your todo specs slides in on the other side. Pick one and review them **side by side**, tablet-style. |
|
|
49
|
+
|
|
50
|
+
### 🚀 Run the pipeline
|
|
51
|
+
|
|
52
|
+
| | |
|
|
53
|
+
|---|---|
|
|
54
|
+
| 🛤️ **Execution rails** | Each rail is an independent lane. Run multiple specs **in parallel**, each with its own **agent profile** (which agents, which models, how tasks route). |
|
|
55
|
+
| 🧩 **Agent profiles** | A per-project, declarative catalog that tells the implement pipeline which agents to run and at what model — snapshotted per job so concurrent rails stay isolated. |
|
|
56
|
+
| 📡 **Live job detail** | A premium ticket-identity header, live duration ticker, incremental turns/tokens, and authoritative cost on exit. |
|
|
57
|
+
| 🔌 **Plugins** | A per-project marketplace of MCP-based integrations (**Serena** semantic code-nav bundled today). Additive by design — installing plugin N+1 never disturbs plugin N. |
|
|
58
|
+
|
|
59
|
+
### 💸 Track every cent
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| 📊 **Analytics** | Every billable AI invocation — across rails, Quick, Explore, AI edits, and file summaries — is recorded for **both Claude and Codex**. Burn-rate hero, daily timeline, top tickets, model breakdown, cost-vs-turns scatter. |
|
|
64
|
+
| 🧮 **Honest numbers** | Claude cost is the provider-billed figure; Codex cost is **estimated** from a local rate-card (the CLI doesn't report it) and clearly flagged with a `~`. Token totals include the cache tiers, so the figures actually reconcile. |
|
|
65
|
+
| 📤 **Exports** | One-click CSV/JSON, plus per-ticket spending deep-links. |
|
|
66
|
+
|
|
67
|
+
### 🛠️ Make it yours
|
|
68
|
+
|
|
69
|
+
| | |
|
|
70
|
+
|---|---|
|
|
71
|
+
| 🖥️ **Terminal panel** | A real VS-Code-style bottom panel (`Cmd/Ctrl+J`) powered by `node-pty` + xterm.js — WebGL rendering, search, ligatures, image inline, drag-drop paths, and OSC 133 shell integration. |
|
|
72
|
+
| 🎨 **Themes** | Three built-ins — `dracula`, `aurora-light`, `obsidian-dark` — applied before React hydrates (no flash). |
|
|
73
|
+
| 🧭 **Code explorer** | A read-only, non-developer-friendly file tree + Monaco viewer with plain-language AI summaries and *"touched by AI"* provenance chips. |
|
|
74
|
+
| 💬 **Minimizable chats** | Park an Explore or AI-Edit session into a dock chip and pick it back up later — never lost across refreshes or project switches. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🤖 Bring your own agent
|
|
79
|
+
|
|
80
|
+
specrails-hub treats **Claude Code** and **Codex CLI** as first-class, interchangeable providers through a single `ProviderAdapter` contract — no manager ever branches on `provider === 'X'`.
|
|
81
|
+
|
|
82
|
+
| | 🟣 Claude Code | 🟢 Codex CLI |
|
|
83
|
+
|---|:---:|:---:|
|
|
84
|
+
| Native streaming | ✅ | ✅ |
|
|
85
|
+
| Native session resume | ✅ | ✅ |
|
|
86
|
+
| Native cost reporting | ✅ | ⚠️ estimated via rate-card |
|
|
87
|
+
| Native OTEL telemetry | ✅ | 🔧 synthesized by the hub |
|
|
88
|
+
| Agent profiles | ✅ | — |
|
|
89
|
+
|
|
90
|
+
A project can install **one or both**. When both are present, the UI lets you pick the engine per spec, per rail, or per terminal launch. Adding a third provider is *one file + one registry entry* — see [`docs/internals/`](docs/internals/).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🖼️ How it looks
|
|
24
95
|
|
|
25
96
|
```
|
|
26
97
|
┌──────────┬───────────────────────────────────────────────────┐
|
|
27
98
|
│ │ Dashboard · Jobs · Analytics · Agents · ⚙ │
|
|
28
99
|
│ Arc │ ──────────────────────────────────────────────── │
|
|
29
100
|
│ side- │ │
|
|
30
|
-
│ bar │ SpecsBoard
|
|
101
|
+
│ bar │ 📋 SpecsBoard │ 🛤️ Rails │
|
|
31
102
|
│ │ (your specs) │ (execution lanes) │
|
|
32
103
|
│ projects │ │ │
|
|
33
|
-
│
|
|
34
|
-
│
|
|
104
|
+
│ ● proj │ #1 Login flow ● │ ▶ Rail 1 #1 #2 │
|
|
105
|
+
│ ○ proj │ #2 Webhook retry │ [profile: default] │
|
|
35
106
|
│ │ #3 Cost limits ● │ │
|
|
36
|
-
│
|
|
107
|
+
│ ➕ Add │ #4 Draft idea ✎ │ ▶ Rail 2 running │
|
|
37
108
|
│ │ │ [profile: budget] │
|
|
38
109
|
│ ⚙ │ │ │
|
|
39
110
|
└──────────┴───────────────────────────────────────────────────┘
|
|
40
|
-
|
|
111
|
+
⌨️ Terminal panel (Cmd/Ctrl + J)
|
|
41
112
|
```
|
|
42
113
|
|
|
43
|
-
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🚀 Quick start
|
|
44
117
|
|
|
45
118
|
```bash
|
|
46
|
-
# 1
|
|
119
|
+
# 1️⃣ Install
|
|
47
120
|
npm install -g specrails-hub
|
|
48
121
|
|
|
49
|
-
# 2
|
|
122
|
+
# 2️⃣ Start the hub
|
|
50
123
|
specrails-hub start
|
|
51
124
|
|
|
52
|
-
# 3
|
|
125
|
+
# 3️⃣ Add a project from the CLI…
|
|
53
126
|
specrails-hub add /path/to/your/project
|
|
54
127
|
|
|
55
|
-
# …or click
|
|
128
|
+
# …or click “➕ Add project” in the dashboard sidebar at
|
|
56
129
|
# http://127.0.0.1:4200
|
|
57
130
|
```
|
|
58
131
|
|
|
59
|
-
If
|
|
132
|
+
If a project doesn't have specrails-core yet, a **5-step setup wizard** installs it for you — one flow, no tier picker, ~1 minute on a warm cache.
|
|
60
133
|
|
|
61
|
-
**Prefer a desktop app?**
|
|
134
|
+
> 💡 **Prefer a desktop app?** Grab a signed macOS or Windows build — see [Desktop app](#%EF%B8%8F-desktop-app). It bundles the server, so there's no separate `start`.
|
|
62
135
|
|
|
63
|
-
|
|
136
|
+
### 🧑💻 The `specrails-hub` CLI
|
|
64
137
|
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
138
|
+
```bash
|
|
139
|
+
specrails-hub start | stop | add | remove | list # manage the hub
|
|
140
|
+
specrails-hub implement #42 # run a specrails verb
|
|
141
|
+
specrails-hub --status # manager status
|
|
142
|
+
specrails-hub --jobs # recent job history
|
|
143
|
+
specrails-hub --project <name|path> # target a project
|
|
144
|
+
specrails-hub --help # full reference
|
|
145
|
+
```
|
|
71
146
|
|
|
72
|
-
|
|
73
|
-
creation. On macOS, the desktop app handles Homebrew/Volta/nvm paths
|
|
74
|
-
automatically — see [docs/platforms/macos.md](docs/platforms/macos.md).
|
|
75
|
-
**Windows users:** see [docs/platforms/windows.md](docs/platforms/windows.md)
|
|
76
|
-
for Windows 10/11 specifics. For Codex-specific topics — auth, sandbox config,
|
|
77
|
-
estimated cost caveats, plugin support, and emergency rollback — see
|
|
78
|
-
[docs/codex.md](docs/codex.md).
|
|
147
|
+
When the hub is running, the CLI talks to it over HTTP + WebSocket; when it isn't, it spawns the agent directly. Either way you get streamed logs.
|
|
79
148
|
|
|
80
|
-
|
|
149
|
+
---
|
|
81
150
|
|
|
82
|
-
|
|
151
|
+
## 📦 Prerequisites
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| [Terminal panel](docs/terminal.md) | Keyboard shortcuts, shell integration, drag-and-drop |
|
|
92
|
-
| [CLI reference](docs/cli.md) | Every command grouped by task |
|
|
153
|
+
- 🟩 **Node.js 20+**
|
|
154
|
+
- 🤖 **At least one AI CLI** on your `PATH`:
|
|
155
|
+
- **[Claude Code](https://claude.com/claude-code)** — the `claude` binary. Authenticate with `ANTHROPIC_API_KEY`.
|
|
156
|
+
- **[Codex CLI](https://developers.openai.com/codex)** ≥ 0.128.0 — the `codex` binary. Run `codex login` or set `OPENAI_API_KEY`.
|
|
157
|
+
- 🌿 **git**
|
|
158
|
+
- 🧪 *(optional)* **`uv`** — only if you want the Serena plugin
|
|
159
|
+
- 📦 **specrails-core** ≥ 4.6.0 in the project for Codex, ≥ 4.2.0 for Claude-only *(the wizard installs it)*
|
|
93
160
|
|
|
94
|
-
|
|
161
|
+
The provider is chosen **per project at install time** and is immutable afterward. On macOS the desktop app resolves Homebrew/Volta/nvm paths for you.
|
|
95
162
|
|
|
96
|
-
|
|
97
|
-
- [Windows](docs/platforms/windows.md) — installer formats, SmartScreen, ConPTY
|
|
163
|
+
---
|
|
98
164
|
|
|
99
|
-
|
|
165
|
+
## 🏗️ How it's built
|
|
100
166
|
|
|
101
|
-
-
|
|
167
|
+
A clean **three-layer TypeScript monorepo** — one Express process runs in *hub mode* and manages every project.
|
|
102
168
|
|
|
103
|
-
|
|
169
|
+
```
|
|
170
|
+
🗄️ server/ Express 5 + WebSocket + SQLite (better-sqlite3) · the brain
|
|
171
|
+
🎨 client/ React 18 + Vite + Tailwind v4 · the dashboard
|
|
172
|
+
⌨️ cli/ specrails-hub command bridge · the terminal door
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
~/.specrails/
|
|
177
|
+
├── hub.sqlite # project registry
|
|
178
|
+
├── manager.pid # running server PID
|
|
179
|
+
└── projects/<slug>/
|
|
180
|
+
├── jobs.sqlite # per-project DB (jobs, analytics, chats)
|
|
181
|
+
└── … # snapshots, telemetry, terminals, summaries
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Highlights under the hood:**
|
|
185
|
+
|
|
186
|
+
- 🔗 **One WebSocket** multiplexes everything; every project-scoped message carries a `projectId`, injected by a `boundBroadcast` closure — managers need zero changes.
|
|
187
|
+
- 🧱 **Per-project isolation** — each project gets its own SQLite, queue manager, and chat manager.
|
|
188
|
+
- 🖥️ **Terminals** stream over a *dedicated* WebSocket so PTY throughput can't starve the event stream.
|
|
189
|
+
- 🌐 **Embedded browser** via Playwright (CDP capture) for the *"From a website"* flow.
|
|
190
|
+
- 📦 **Desktop** via Tauri, with optionally-bundled Node, Git, and Chromium runtimes so it runs fully offline.
|
|
191
|
+
|
|
192
|
+
> 📖 Want the deep dive? CLAUDE.md and [`docs/internals/`](docs/internals/) document the adapter contract, REST surface, migrations, and the OpenSpec workflow.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🛠️ Development
|
|
104
197
|
|
|
105
198
|
```bash
|
|
106
199
|
git clone https://github.com/fjpulidop/specrails-hub.git
|
|
107
200
|
cd specrails-hub
|
|
108
201
|
npm install # root deps (server + CLI)
|
|
109
202
|
cd client && npm install && cd .. # client deps (separate tree)
|
|
110
|
-
npm run dev # server
|
|
203
|
+
npm run dev # 🚀 server :4200 + client :4201, hot reload
|
|
111
204
|
```
|
|
112
205
|
|
|
113
|
-
| Script |
|
|
114
|
-
|
|
206
|
+
| Script | What it does |
|
|
207
|
+
|--------|--------------|
|
|
115
208
|
| `npm run dev` | Server (4200) + client (4201) with hot reload |
|
|
116
209
|
| `npm run dev:desktop` | Desktop app (Tauri) in dev mode |
|
|
117
|
-
| `npm run build` | Production build (
|
|
118
|
-
| `npm test` |
|
|
210
|
+
| `npm run build` | Production build (server + client + CLI) |
|
|
211
|
+
| `npm test` | Vitest suite (server + CLI) + core-compat check |
|
|
119
212
|
| `npm run test:coverage` | Server coverage (mirrors the CI gate) |
|
|
120
|
-
| `npm run
|
|
121
|
-
| `npm run
|
|
213
|
+
| `npm run test:client` | Client Vitest suite |
|
|
214
|
+
| `npm run ci` | Everything CI runs: typecheck + tests + both coverage gates |
|
|
215
|
+
|
|
216
|
+
🛡️ **Coverage is a hard gate** — **70 % global**, **80 % server**, **80 % client**. Local runs must clear the same bars before pushing.
|
|
217
|
+
|
|
218
|
+
- 🌍 `4200` — Express API + WebSocket
|
|
219
|
+
- ⚡ `4201` — Vite dev server (proxies `/api` and `/hooks` to 4200)
|
|
220
|
+
|
|
221
|
+
---
|
|
122
222
|
|
|
123
|
-
|
|
223
|
+
## 🖥️ Desktop app
|
|
124
224
|
|
|
125
|
-
|
|
225
|
+
Signed, notarized builds for **macOS (Apple Silicon)** and **Windows (x64)** are published at:
|
|
126
226
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
first use. To build a self-contained app the way CI does (**macOS arm64 only**):
|
|
227
|
+
> 📥 `https://specrails.dev/downloads/specrails-hub/latest/`
|
|
228
|
+
|
|
229
|
+
`npm run build:desktop` produces the `.app` / `.dmg` / `.exe`, but does **not** assemble the bundled runtimes — that app falls back to your system PATH and downloads a Playwright Chromium on first use. To build a self-contained app the way CI does (**macOS arm64 only**):
|
|
131
230
|
|
|
132
231
|
```bash
|
|
133
|
-
# Bundle Node 22 + a relocatable Git
|
|
232
|
+
# Bundle Node 22 + a relocatable Git, then build
|
|
134
233
|
npm run build:desktop:local
|
|
135
234
|
|
|
136
|
-
# …and bundle Chromium too, so "Add Spec from a website" works fully offline
|
|
137
|
-
#
|
|
235
|
+
# …and bundle Chromium too, so "Add Spec from a website" works fully offline
|
|
236
|
+
# (one-time ~150 MB Playwright Chromium download)
|
|
138
237
|
BUNDLE_CHROMIUM=true npm run build:desktop:local
|
|
139
238
|
```
|
|
140
239
|
|
|
141
|
-
|
|
240
|
+
> ℹ️ Local builds are **unsigned** — Gatekeeper warns; right-click → Open, or `xattr -dr com.apple.quarantine <App>.app`. Signed + notarized installers come only from the `desktop-release` CI workflow.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 🔒 Security model
|
|
245
|
+
|
|
246
|
+
- 🏠 Binds to `127.0.0.1` only — **do not expose to a network**.
|
|
247
|
+
- 🙅 No authentication (single-user local tool by design).
|
|
248
|
+
- 🧷 Parameterised SQL everywhere — never string-interpolated.
|
|
249
|
+
- 🧬 Reserved files in your project (`.mcp.json`, `.specrails/plugins/state.json`, `.specrails/profiles/.user-preferred.json`) are mutated **surgically** — read → modify only owned keys → atomic temp+rename — so adding plugin N+1 never disturbs plugin N.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 📚 Documentation
|
|
142
254
|
|
|
143
|
-
|
|
144
|
-
right-click → Open, or run `xattr -dr com.apple.quarantine <App>.app`.
|
|
145
|
-
- Locally, Chromium is bundled **unpacked**; the app launches it straight from
|
|
146
|
-
`Contents/Resources/runtimes/chromium`.
|
|
147
|
-
- The signed + notarized installers are produced only by the `desktop-release` CI
|
|
148
|
-
workflow. There, Chromium is shipped as an obfuscated `chromium.pak` blob — the
|
|
149
|
-
notarization service recurses into plain archives and would reject Chromium's
|
|
150
|
-
ad-hoc-signed binaries, so the magic bytes are XOR-broken to make it opaque. The
|
|
151
|
-
app reverses the XOR and extracts Chromium to `~/.specrails/runtimes/chromium`
|
|
152
|
-
on first use, where Google's ad-hoc signature is enough to run it.
|
|
255
|
+
**User guides**
|
|
153
256
|
|
|
154
|
-
|
|
257
|
+
| Guide | What it covers |
|
|
258
|
+
|-------|----------------|
|
|
259
|
+
| 🏁 [Getting started](docs/getting-started.md) | Install, register a project, run your first pipeline |
|
|
260
|
+
| 📝 [Creating specs](docs/creating-specs.md) | Quick vs Explore, drafts, SMASH, Compare, Continue Editing |
|
|
261
|
+
| 🚀 [Running pipelines](docs/running-pipelines.md) | Rails, jobs, agent profiles, plugins |
|
|
262
|
+
| 💰 [Tracking cost](docs/tracking-cost.md) | Analytics, exports, per-ticket spending |
|
|
263
|
+
| 🎨 [Customising the hub](docs/customizing.md) | Themes, settings, telemetry, kill switches |
|
|
264
|
+
| ⌨️ [Terminal panel](docs/terminal.md) | Shortcuts, shell integration, drag-and-drop |
|
|
265
|
+
| 🧑💻 [CLI reference](docs/cli.md) | Every command grouped by task |
|
|
266
|
+
| 🟢 [Codex notes](docs/codex.md) | Auth, sandbox, estimated-cost caveats, rollback |
|
|
267
|
+
|
|
268
|
+
**Platform notes** — 🍎 [macOS](docs/platforms/macos.md) · 🪟 [Windows](docs/platforms/windows.md)
|
|
155
269
|
|
|
156
|
-
|
|
157
|
-
- No authentication (single-user local tool).
|
|
158
|
-
- Parameterised SQL throughout.
|
|
159
|
-
- Reserved paths in your project (`.mcp.json`, `.specrails/plugins/state.json`, `.specrails/profiles/.user-preferred.json`) are mutated surgically — read, modify owned keys, atomic rename — so adding plugin N+1 never disturbs plugin N's state.
|
|
270
|
+
**Extending** — 🧩 [`docs/internals/`](docs/internals/): architecture, REST reference, ops runbook, adding a provider.
|
|
160
271
|
|
|
161
|
-
|
|
272
|
+
---
|
|
162
273
|
|
|
163
|
-
|
|
274
|
+
## ☕ Support
|
|
275
|
+
|
|
276
|
+
If specrails-hub saves you time, you can buy me a coffee on **Ko-fi** — it funds the open-source ecosystem. 💜
|
|
164
277
|
|
|
165
278
|
[](https://ko-fi.com/D1D81Y002C)
|
|
166
279
|
|
|
167
|
-
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 📄 License
|
|
283
|
+
|
|
284
|
+
[MIT](LICENSE) © [Javier Pulido](https://github.com/fjpulidop)
|
|
168
285
|
|
|
169
|
-
|
|
286
|
+
<div align="center">
|
|
287
|
+
<sub>Built with TypeScript, React, Express & a lot of ☕ — and shipped by the agents it orchestrates. 🚄</sub>
|
|
288
|
+
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{A as t,G as n,St as r,Vt as i,_t as a,at as o,bt as s,gt as c,it as l,xt as u,yt as d}from"./index-
|
|
1
|
+
import{r as e}from"./chunk-CilyBKbf.js";import{A as t,G as n,St as r,Vt as i,_t as a,at as o,bt as s,gt as c,it as l,xt as u,yt as d}from"./index-k8jG-NWj.js";var f=a(`ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M4.929 4.929 19.07 19.071`,key:`196cmz`}]]),p=e(i(),1);function m(e){let t=new Set;return e.filter(e=>{let n=`${e.type}:${e.jobId}`;return t.has(n)?!1:(t.add(n),!0)})}function h({activeProjectId:e,limit:t=50}){let[n,i]=(0,p.useState)([]),[a,o]=(0,p.useState)(!1),[c,l]=(0,p.useState)(!0),u=(0,p.useRef)(e);(0,p.useEffect)(()=>{u.current=e},[e]);let{registerHandler:d,unregisterHandler:f}=s();(0,p.useEffect)(()=>{if(!e){i([]),l(!0);return}let n=!1;o(!0),i([]),l(!0);async function a(){let e=r();try{let r=await fetch(`${e}/activity?limit=${t}`);if(!r.ok||n)return;let a=await r.json();if(n)return;i(a),l(a.length===t)}catch{}finally{n||o(!1)}}return a(),()=>{n=!0}},[e,t]);let h=(0,p.useCallback)(async()=>{if(a||!c)return;o(!0);let e=r();try{i(n=>{let r=n[n.length-1];if(!r)return n;let a=encodeURIComponent(r.timestamp);return fetch(`${e}/activity?limit=${t}&before=${a}`).then(e=>e.json()).then(e=>{i(t=>m([...t,...e])),l(e.length===t),o(!1)}).catch(()=>o(!1)),n})}catch{o(!1)}},[a,c,t]),g=(0,p.useCallback)(e=>{let t=e,n=u.current;if(!(!t||typeof t.type!=`string`)&&!(t.projectId&&t.projectId!==n)){if(t.type===`queue`&&Array.isArray(t.jobs)){let e=[];for(let n of t.jobs){let t=n.status===`completed`?`job_completed`:n.status===`failed`?`job_failed`:n.status===`canceled`?`job_canceled`:`job_started`;e.push({id:`${t}:${n.id}`,type:t,jobId:n.id,jobCommand:n.command??``,timestamp:n.startedAt??new Date().toISOString(),summary:`${t.replace(`_`,` `)}: ${n.command??``}`,costUsd:null})}e.length>0&&i(t=>m([...e,...t]))}if(t.type===`phase`&&t.phase&&t.state&&t.timestamp){let e={id:`phase:${t.phase}:${t.state}:${t.timestamp}`,type:`job_started`,jobId:`phase-${t.phase}`,jobCommand:`Phase: ${t.phase} → ${t.state}`,timestamp:t.timestamp,summary:`Phase ${t.phase} is ${t.state}`,costUsd:null};i(t=>m([e,...t]))}}},[]);return(0,p.useLayoutEffect)(()=>(d(`activity`,g),()=>f(`activity`)),[g,d,f]),{items:n,loading:a,hasMore:c,loadMore:h}}var g=u();function _(e){let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/1e3);if(n<60)return`${n}s ago`;let r=Math.floor(n/60);if(r<60)return`${r}m ago`;let i=Math.floor(r/60);return i<24?`${i}h ago`:`${Math.floor(i/24)}d ago`}function v({type:e}){switch(e){case`job_completed`:return(0,g.jsx)(o,{className:`w-4 h-4 text-green-500 flex-shrink-0`});case`job_failed`:return(0,g.jsx)(l,{className:`w-4 h-4 text-red-500 flex-shrink-0`});case`job_canceled`:return(0,g.jsx)(f,{className:`w-4 h-4 text-muted-foreground flex-shrink-0`});default:return(0,g.jsx)(t,{className:`w-4 h-4 text-blue-500 flex-shrink-0`})}}function y(e){switch(e){case`job_completed`:return`Completed`;case`job_failed`:return`Failed`;case`job_canceled`:return`Canceled`;default:return`Started`}}function b(e){switch(e){case`job_completed`:return`text-green-500`;case`job_failed`:return`text-red-500`;case`job_canceled`:return`text-muted-foreground`;default:return`text-blue-500`}}function x(){let{activeProjectId:e}=d(),{items:t,loading:r,hasMore:i,loadMore:a}=h({activeProjectId:e}),o=(0,p.useRef)(null);return(0,p.useEffect)(()=>{let e=o.current;if(!e)return;let t=new IntersectionObserver(e=>{e[0].isIntersecting&&i&&!r&&a()},{threshold:.1});return t.observe(e),()=>t.disconnect()},[i,r,a]),(0,g.jsxs)(`div`,{className:`flex flex-col h-full overflow-hidden`,children:[(0,g.jsxs)(`div`,{className:`flex items-center gap-2 px-4 py-3 border-b border-border bg-background/50`,children:[(0,g.jsx)(c,{className:`w-4 h-4 text-muted-foreground`}),(0,g.jsx)(`h1`,{className:`text-sm font-medium`,children:`Activity`})]}),(0,g.jsxs)(`div`,{className:`flex-1 overflow-y-auto`,children:[r&&t.length===0?(0,g.jsx)(`div`,{className:`flex items-center justify-center h-32`,children:(0,g.jsx)(n,{className:`w-4 h-4 animate-spin text-muted-foreground`})}):t.length===0?(0,g.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground`,children:[(0,g.jsx)(c,{className:`w-8 h-8 opacity-40`}),(0,g.jsx)(`p`,{className:`text-sm`,children:`No activity yet`}),(0,g.jsx)(`p`,{className:`text-xs opacity-70`,children:`Job events will appear here when jobs run`})]}):(0,g.jsx)(`ul`,{className:`divide-y divide-border/50`,children:t.map(e=>(0,g.jsxs)(`li`,{className:`flex items-center gap-3 px-4 py-2.5 hover:bg-accent/30 transition-colors`,children:[(0,g.jsx)(v,{type:e.type}),(0,g.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,g.jsx)(`p`,{className:`text-xs text-foreground truncate`,title:e.jobCommand,children:e.jobCommand}),(0,g.jsxs)(`div`,{className:`flex items-center gap-2 mt-0.5`,children:[(0,g.jsx)(`span`,{className:`text-xs font-medium ${b(e.type)}`,children:y(e.type)}),e.costUsd!=null&&(0,g.jsxs)(`span`,{className:`text-xs text-muted-foreground`,children:[`$`,e.costUsd.toFixed(4)]})]})]}),(0,g.jsx)(`span`,{className:`text-xs text-muted-foreground flex-shrink-0 tabular-nums`,children:_(e.timestamp)})]},`${e.type}:${e.jobId}`))}),(0,g.jsx)(`div`,{ref:o,className:`h-1`}),r&&t.length>0&&(0,g.jsx)(`div`,{className:`flex justify-center py-3`,children:(0,g.jsx)(n,{className:`w-4 h-4 animate-spin text-muted-foreground`})}),!i&&t.length>0&&(0,g.jsx)(`p`,{className:`text-center text-xs text-muted-foreground py-3`,children:`All activity loaded`})]})]})}export{x as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{$ as t,At as n,B as r,C as i,Ct as a,Dt as o,E as s,Et as c,F as l,Ft as u,G as d,H as f,I as p,L as m,M as h,Mt as g,N as _,Nt as v,O as y,Ot as b,P as x,Pt as S,S as C,St as w,T,Tt as E,U as D,Vt as O,W as k,X as A,_t as j,a as M,b as N,bt as P,ct as F,ft as I,ht as L,j as R,jt as z,kt as ee,lt as B,mt as V,ot as te,q as H,tt as ne,ut as U,w as W,wt as G,x as re,xt as ie,y as K,yt as ae}from"./index-B7bEifI4.js";var oe=j(`flask-conical`,[[`path`,{d:`M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2`,key:`18mbvz`}],[`path`,{d:`M6.453 15h11.094`,key:`3shlmq`}],[`path`,{d:`M8.5 2h7`,key:`csnxdl`}]]),q=j(`history`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}],[`path`,{d:`M12 7v5l4 2`,key:`1fdv2h`}]]),se=j(`pin`,[[`path`,{d:`M12 17v5`,key:`bb1du9`}],[`path`,{d:`M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z`,key:`1nkz8b`}]]),ce=j(`wand-sparkles`,[[`path`,{d:`m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72`,key:`ul74o6`}],[`path`,{d:`m14 7 3 3`,key:`1r5n42`}],[`path`,{d:`M5 6v4`,key:`ilb8ba`}],[`path`,{d:`M19 14v4`,key:`blhpug`}],[`path`,{d:`M10 2v2`,key:`7u0qdc`}],[`path`,{d:`M7 8H3`,key:`zfb6yr`}],[`path`,{d:`M21 16h-4`,key:`1cnmox`}],[`path`,{d:`M11 3H9`,key:`1obp7u`}]]),J=e(O(),1),Y=ie(),X=/^[a-z0-9][a-z0-9-]*$/;function le({open:e,mode:t=`add`,initial:n,chainAgents:r,onConfirm:a,onCancel:o}){let[s,c]=(0,J.useState)(``),[l,u]=(0,J.useState)(r[0]??``);(0,J.useEffect)(()=>{e&&(t===`edit`&&n?(c(n.tags.join(`, `)),u(r.includes(n.agent)?n.agent:r[0]??``)):(c(``),u(r[0]??``)))},[e,t,n,r]);let d=s.split(`,`).map(e=>e.trim()).filter(Boolean),f=d.filter(e=>!X.test(e)),p=d.length>0&&f.length===0&&r.includes(l);return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||o()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsxs)(i,{children:[(0,Y.jsx)(W,{children:t===`edit`?`Edit routing rule`:`Add routing rule`}),(0,Y.jsx)(re,{children:t===`edit`?`Update the tags or target agent for this rule.`:`Add a tag-matched routing rule using lowercase kebab-case tags.`})]}),(0,Y.jsxs)(`div`,{className:`py-2 space-y-3`,children:[(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Tags`}),(0,Y.jsx)(M,{autoFocus:!0,value:s,onChange:e=>c(e.target.value),placeholder:`frontend, ui`,"aria-label":`Tags`,className:`text-sm font-mono`}),(0,Y.jsxs)(`p`,{className:`text-[11px] text-muted-foreground mt-1`,children:[`Comma-separated. Use lowercase kebab-case like `,(0,Y.jsx)(`code`,{children:`frontend`}),` or `,(0,Y.jsx)(`code`,{children:`api-design`}),`.`]}),f.length>0&&(0,Y.jsxs)(`p`,{className:`text-[11px] text-red-400 mt-1`,children:[`Invalid tag`,f.length===1?``:`s`,`: `,f.join(`, `),`. Tags must use lowercase letters, digits, and hyphens only.`]})]}),(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Route to`}),(0,Y.jsx)(`select`,{value:l,onChange:e=>u(e.target.value),"aria-label":`Route to`,className:`w-full h-9 px-2 text-sm rounded-md border border-border bg-background`,children:r.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))}),(0,Y.jsx)(`p`,{className:`text-[11px] text-muted-foreground mt-1`,children:`Only agents in this profile's chain can be routing targets.`})]})]}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:o,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:()=>a(d,l),disabled:!p,children:t===`edit`?`Save changes`:`Add rule`})]})]})})}var Z=new Set([`sr-architect`,`sr-developer`,`sr-reviewer`,`sr-merge-resolver`]),ue=[`sonnet`,`opus`,`haiku`],de=/^[a-z0-9][a-z0-9-]*$/;function fe({profile:e,onChange:t,footer:r,onValidityChange:i,onSoftWarningsChange:s}){let[c,l]=(0,J.useState)([]),[u,d]=(0,J.useState)(!1),[p,m]=(0,J.useState)(!1),[h,_]=(0,J.useState)(null);(0,J.useEffect)(()=>{let e=!1;return fetch(`${w()}/profiles/catalog`).then(e=>e.ok?e.json():{agents:[]}).then(t=>{e||l(t.agents)}).catch(()=>{e||l([])}),()=>{e=!0}},[]);let x=n=>{let r=JSON.parse(JSON.stringify(e));n(r),t(r)},S=new Set(e.agents.map(e=>e.id)),C=c.filter(e=>!S.has(e.id)),T=(0,J.useMemo)(()=>{let t=[];for(let n of Z)e.agents.some(e=>e.id===n)||t.push(`Missing required baseline agent: ${n}`);let n=e.routing.filter(e=>`default`in e&&e.default===!0);if(n.length>1&&t.push(`Routing may have at most one default rule (found ${n.length})`),n.length===1){let n=e.routing[e.routing.length-1];`default`in n&&n.default===!0||t.push(`The default routing rule must be the last entry`)}for(let n of e.routing)if(e.agents.some(e=>e.id===n.agent)||t.push(`Routing references agent not in the chain: ${n.agent}`),`tags`in n){let e=n.tags.filter(e=>!de.test(e));e.length>0&&t.push(`Routing rule ${n.agent} has invalid tags: ${e.join(`, `)} (use lowercase kebab-case)`)}return t},[e]),D=(0,J.useMemo)(()=>{if(e.routing.length===0)return[];let t=new Set(e.routing.map(e=>e.agent));return e.agents.filter(e=>!Z.has(e.id)&&!t.has(e.id)).map(e=>e.id)},[e]),O=(0,J.useMemo)(()=>e.routing.some(e=>`default`in e&&e.default===!0),[e.routing]);(0,J.useEffect)(()=>{i&&i(T)},[T,i]),(0,J.useEffect)(()=>{s&&s({agentsMissingRouting:D})},[D,s]);let k=e=>{x(t=>{let n={id:e,model:`sonnet`},r=t.agents.findIndex(e=>e.id===`sr-merge-resolver`);r>=0?t.agents.splice(r,0,n):t.agents.push(n)}),d(!1)},A=t=>{let n=e.agents[t];Z.has(n.id)||x(e=>{e.agents.splice(t,1),e.routing=e.routing.filter(e=>e.agent!==n.id)})},j=(t,n)=>{if(t===n)return;let r=e.agents.findIndex(e=>e.id===t),i=e.agents.findIndex(e=>e.id===n);if(r<0||i<0)return;let a=G(e.agents,r,i),o=a.findIndex(e=>e.id===`sr-architect`);if(o>0){let[e]=a.splice(o,1);a.unshift(e)}let s=a.findIndex(e=>e.id===`sr-merge-resolver`);if(s>=0&&s!==a.length-1){let[e]=a.splice(s,1);a.push(e)}x(e=>{e.agents=a})},N=v(g(n,{activationConstraint:{distance:4}}),g(ee,{coordinateGetter:E})),P=e=>{let{active:t,over:n}=e;n&&j(String(t.id),String(n.id))},F=(e,t)=>{x(n=>{n.agents[e].model=t})},I=(e,t)=>{x(n=>{let r={tags:e,agent:t},i=n.routing.findIndex(e=>`default`in e&&e.default===!0);i>=0?n.routing.splice(i,0,r):n.routing.push(r)}),m(!1)},L=()=>{x(e=>{e.routing.some(e=>`default`in e&&e.default===!0)||e.agents.some(e=>e.id===`sr-developer`)&&e.routing.push({default:!0,agent:`sr-developer`})})},B=e=>`default`in e&&e.default===!0,V=(e,t)=>{x(n=>{let r=n.routing[e];r&&(B(r)||(r.agent=t))})},te=(e,t,n)=>{x(r=>{let i=r.routing[e];i&&(B(i)||(i.tags=t,i.agent=n))})},H=e=>{x(t=>{let n=t.routing[e];n&&(B(n)||t.routing.splice(e,1))})},ne=(t,n)=>{let r=t+n;if(r<0||r>=e.routing.length)return;let i=e.routing[t],a=e.routing[r];B(i)||B(a)||x(e=>{let[n]=e.routing.splice(t,1);e.routing.splice(r,0,n)})};return(0,Y.jsxs)(`div`,{className:`p-6 space-y-6 max-w-3xl`,children:[(0,Y.jsx)(le,{open:p,mode:`add`,chainAgents:e.agents.map(e=>e.id),onConfirm:I,onCancel:()=>m(!1)}),(0,Y.jsx)(le,{open:h!==null,mode:`edit`,initial:h!==null&&e.routing[h]&&!(`default`in e.routing[h])?{tags:e.routing[h].tags,agent:e.routing[h].agent}:void 0,chainAgents:e.agents.map(e=>e.id),onConfirm:(e,t)=>{h!==null&&(te(h,e,t),_(null))},onCancel:()=>_(null)}),T.length>0&&(0,Y.jsxs)(`div`,{className:`px-3 py-2 text-xs rounded-md border border-yellow-500/30 bg-yellow-500/10 text-yellow-500`,children:[(0,Y.jsxs)(`div`,{className:`font-medium mb-1`,children:[T.length,` validation `,T.length===1?`issue`:`issues`]}),(0,Y.jsx)(`ul`,{className:`list-disc list-inside space-y-0.5`,children:T.map((e,t)=>(0,Y.jsx)(`li`,{children:e},t))})]}),(0,Y.jsxs)(`section`,{className:`space-y-3`,children:[(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Name`}),(0,Y.jsx)(M,{value:e.name,disabled:!0,className:`text-sm`})]}),(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Description`}),(0,Y.jsx)(M,{value:e.description??``,onChange:e=>x(t=>{t.description=e.target.value}),className:`text-sm`,placeholder:`What is this profile for?`})]})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsx)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2`,children:`Orchestrator`}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-3 p-3 rounded-md border border-border`,children:[(0,Y.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,Y.jsx)(`div`,{className:`text-sm font-mono text-foreground truncate`,children:`/specrails:implement · /specrails:batch-implement`}),(0,Y.jsx)(`div`,{className:`text-[11px] text-muted-foreground mt-0.5`,children:`Top-level model for both commands. batch-implement delegates to implement per feature, so every rail it spawns inherits this profile's agent chain.`})]}),(0,Y.jsx)(he,{value:e.orchestrator.model,onChange:e=>x(t=>{t.orchestrator.model=e})})]})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-2`,children:[(0,Y.jsxs)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:[`Agent chain (`,e.agents.length,`)`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:()=>d(e=>!e),disabled:C.length===0,title:C.length===0?`All catalog agents are already in the chain`:`Add an agent from the catalog`,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add`]})]}),u&&(0,Y.jsxs)(`div`,{className:`mb-2 p-2 rounded-md border border-border bg-muted/30`,children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-1.5 px-1`,children:[(0,Y.jsxs)(`span`,{className:`text-[11px] text-muted-foreground`,children:[`Pick from catalog (`,C.length,` available)`]}),(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:()=>d(!1),title:`Close`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]}),C.length===0?(0,Y.jsx)(`div`,{className:`px-2 py-3 text-xs text-muted-foreground text-center`,children:`No more agents in the catalog. Add custom agents from the Agents Catalog tab.`}):(0,Y.jsx)(`div`,{className:`space-y-0.5 max-h-64 overflow-auto`,children:C.map(e=>(0,Y.jsxs)(`button`,{type:`button`,onClick:()=>k(e.id),className:`w-full flex items-center justify-between gap-2 px-2 py-1.5 text-left rounded hover:bg-accent transition-colors`,children:[(0,Y.jsx)(`span`,{className:`text-sm font-mono`,children:e.id}),(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded `+(e.kind===`custom`?`bg-purple-500/15 text-purple-400`:`bg-muted text-muted-foreground`),children:e.kind})]},e.id))})]}),(0,Y.jsx)(b,{sensors:N,collisionDetection:z,onDragEnd:P,children:(0,Y.jsx)(a,{items:e.agents.map(e=>e.id),strategy:o,children:(0,Y.jsx)(`div`,{className:`space-y-1.5`,children:e.agents.map((e,t)=>(0,Y.jsx)(pe,{agent:e,canRemove:!Z.has(e.id),onModel:e=>F(t,e),onRemove:()=>A(t)},e.id))})})})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-2`,children:[(0,Y.jsxs)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:[`Routing (`,e.routing.length,`)`]}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-1`,children:[!O&&(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:L,disabled:e.agents.length===0,title:e.agents.length===0?`Add at least one agent before creating routing rules`:void 0,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add default`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:()=>m(!0),disabled:e.agents.length===0,title:e.agents.length===0?`Add at least one agent before creating routing rules`:void 0,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add rule`]})]})]}),(0,Y.jsx)(`p`,{className:`text-[11px] text-muted-foreground mb-2`,children:`First matching rule wins. Rules are editable and removable, and the default catch-all stays last when present. If you leave routing empty, the pipeline falls back to the first developer-shaped agent in the chain.`}),D.length>0&&(0,Y.jsxs)(`div`,{className:`mb-2 px-3 py-2 text-xs rounded-md border border-yellow-500/30 bg-yellow-500/10 text-yellow-500`,children:[(0,Y.jsx)(`div`,{className:`font-medium mb-1`,children:`Untargeted agents in the chain`}),(0,Y.jsxs)(`div`,{children:[`No routing rule points to: `,(0,Y.jsx)(`span`,{className:`font-mono`,children:D.join(`, `)}),`. Add a tag rule or retarget the default rule if you want them to run.`]})]}),(0,Y.jsx)(`div`,{className:`space-y-1.5`,children:e.routing.map((t,n)=>{let r=`default`in t&&t.default===!0;return(0,Y.jsx)(me,{rule:t,ordinal:n+1,isLast:n===e.routing.length-1,canMove:!r,canRemove:!r,canEdit:!r,chainAgents:e.agents.map(e=>e.id),onAgentChange:e=>V(n,e),onEdit:()=>_(n),onUp:()=>ne(n,-1),onDown:()=>ne(n,1),onRemove:()=>H(n)},n)})})]}),r&&(0,Y.jsx)(`div`,{className:`pt-3 border-t border-border`,children:r})]})}function pe({agent:e,canRemove:t,onModel:n,onRemove:r}){let i=Z.has(e.id),a=e.id===`sr-architect`,o=e.id===`sr-merge-resolver`,{attributes:s,listeners:l,setNodeRef:u,transform:d,transition:f,isDragging:p}=c({id:e.id});return(0,Y.jsxs)(`div`,{ref:u,style:{transform:S.Transform.toString(d),transition:f,opacity:p?.5:void 0,zIndex:p?10:void 0},className:`flex items-center gap-2 px-2 py-1.5 rounded-md border border-border group hover:bg-accent/30 transition-colors bg-background`,children:[(0,Y.jsx)(`button`,{type:`button`,className:`flex-shrink-0 p-0.5 rounded text-muted-foreground cursor-grab active:cursor-grabbing hover:text-foreground`,title:`Drag to reorder`,"aria-label":`Drag handle`,...s,...l,children:(0,Y.jsx)(H,{className:`w-3.5 h-3.5`})}),(0,Y.jsx)(`span`,{className:`text-sm font-mono flex-1 truncate`,children:e.id}),a&&(0,Y.jsxs)(`span`,{className:`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-accent-primary/15 text-accent-primary`,title:`Pinned to first position — pipeline always starts with sr-architect`,children:[(0,Y.jsx)(se,{className:`w-2.5 h-2.5 rotate-[135deg]`}),` first`]}),o&&(0,Y.jsxs)(`span`,{className:`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-accent-primary/15 text-accent-primary`,title:`Pinned to last position — merge phase always runs last`,children:[(0,Y.jsx)(se,{className:`w-2.5 h-2.5 rotate-45`}),` last`]}),i&&!a&&!o&&(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground`,children:`required`}),(0,Y.jsx)(he,{value:e.model??`sonnet`,onChange:n}),(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded disabled:opacity-30 disabled:cursor-not-allowed opacity-0 group-hover:opacity-100 transition-opacity`,onClick:r,disabled:!t,title:t?`Remove`:`Required baseline agent — the pipeline depends on this row`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]})}function me({rule:e,ordinal:t,isLast:n,canMove:r,canRemove:i,canEdit:a,chainAgents:o,onAgentChange:s,onEdit:c,onUp:l,onDown:u,onRemove:d}){let f=`default`in e&&e.default===!0;return(0,Y.jsxs)(`div`,{className:`flex items-center gap-2 px-2 py-1.5 rounded-md border border-border group hover:bg-accent/30 transition-colors`,children:[(0,Y.jsxs)(`span`,{className:`text-[10px] font-mono text-muted-foreground w-5 text-center flex-shrink-0`,children:[t,`.`]}),f?(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground flex-1`,children:`everything else`}):(0,Y.jsx)(`span`,{className:`text-xs flex-1 flex gap-1 flex-wrap items-center`,children:e.tags.map(e=>(0,Y.jsx)(`span`,{className:`px-1.5 py-0.5 rounded bg-muted font-mono text-[11px]`,children:e},e))}),(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:`→`}),f?(0,Y.jsx)(`span`,{className:`h-7 max-w-[15rem] px-2 text-xs font-mono rounded border border-border bg-muted/40 text-muted-foreground inline-flex items-center`,"aria-label":`Default routing target (core, read-only)`,title:`Core fallback — not editable`,children:e.agent}):(0,Y.jsx)(`select`,{value:e.agent,onChange:e=>s(e.target.value),"aria-label":`Routing target for rule ${t}`,className:`h-7 max-w-[15rem] px-2 text-xs font-mono rounded border border-border bg-background`,children:o.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))}),(0,Y.jsxs)(`div`,{className:`flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity`,children:[a&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:c,title:`Edit rule`,"aria-label":`Edit rule ${t}`,children:(0,Y.jsx)(D,{className:`w-3 h-3`})}),r&&!n&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:u,title:`Move down`,children:(0,Y.jsx)(L,{className:`w-3 h-3`})}),r&&t>1&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:l,title:`Move up`,children:(0,Y.jsx)(I,{className:`w-3 h-3`})}),i&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded`,onClick:d,title:`Remove`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]}),f&&(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground`,title:`Core fallback — the pipeline's last-resort rule, pinned to sr-developer`,children:`core · default`})]})}function he({value:e,onChange:t}){return(0,Y.jsx)(`select`,{value:e,onChange:e=>t(e.target.value),className:`h-7 px-2 text-xs rounded border border-border bg-background`,children:ue.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))})}function ge({open:e,title:t,description:n,placeholder:r,initialValue:a=``,confirmLabel:o=`Confirm`,inputPattern:s,inputInvalidHint:c,onConfirm:l,onCancel:u}){let[d,f]=(0,J.useState)(a);(0,J.useEffect)(()=>{e&&f(a)},[e,a]);let p=d.trim(),m=!s||s.test(p),h=p.length>0&&m;return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||u()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsx)(i,{children:(0,Y.jsx)(W,{children:t})}),(0,Y.jsxs)(`div`,{className:`py-2 space-y-2`,children:[n&&(0,Y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:n}),(0,Y.jsx)(M,{autoFocus:!0,value:d,placeholder:r,onChange:e=>f(e.target.value),onKeyDown:e=>{e.key===`Enter`&&h&&l(p)},className:`text-sm font-mono`}),p.length>0&&!m&&c&&(0,Y.jsx)(`p`,{className:`text-[11px] text-yellow-500`,children:c})]}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:u,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:()=>l(p),disabled:!h,children:o})]})]})})}function _e({open:e,title:t,description:n,confirmLabel:r=`Confirm`,destructive:a=!1,onConfirm:o,onCancel:s}){return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||s()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsx)(i,{children:(0,Y.jsx)(W,{children:t})}),n&&(0,Y.jsx)(`div`,{className:`py-2`,children:(0,Y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:n})}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:s,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:o,className:a?`bg-red-500 hover:bg-red-600 text-white`:void 0,children:r})]})]})})}function ve(){let[e,t]=(0,J.useState)([]),[n,r]=(0,J.useState)(null),[i,a]=(0,J.useState)(null),[o,s]=(0,J.useState)(!0),[c,l]=(0,J.useState)(null),[d,p]=(0,J.useState)(!1),[h,g]=(0,J.useState)([]),[v,b]=(0,J.useState)([]),[x,S]=(0,J.useState)(!1),[C,T]=(0,J.useState)(null),[E,D]=(0,J.useState)(null),O=(0,J.useCallback)(async()=>{s(!0),l(null);try{let e=await fetch(`${w()}/profiles`);if(!e.ok)throw Error(`List failed: ${e.status}`);let i=await e.json();t(i.profiles),i.profiles.length>0&&!n&&r(i.profiles[0].name)}catch(e){l(e.message)}finally{s(!1)}},[n]);(0,J.useEffect)(()=>{O()},[O]),(0,J.useEffect)(()=>{if(!n){a(null);return}let e=!1;return fetch(`${w()}/profiles/${encodeURIComponent(n)}`).then(e=>{if(!e.ok)throw Error(`Load failed: ${e.status}`);return e.json()}).then(t=>{e||a(t.profile)}).catch(t=>{e||l(t.message)}),()=>{e=!0}},[n]);let k=(0,J.useCallback)(async()=>{p(!0),l(null);try{let e=await fetch(`${w()}/profiles/migrate-from-settings`,{method:`POST`});if(!e.ok){let t=await e.json().catch(()=>({}));throw Error(t.error??`Migration failed: ${e.status}`)}await O(),r(`default`),u.success(`Profile migrated`,{description:`default profile created from your current agents`})}catch(e){let t=e.message;l(t),u.error(`Migration failed`,{description:t})}finally{p(!1)}},[O]),A=(0,J.useCallback)(async e=>{S(!1),p(!0),l(null);try{let t={schemaVersion:1,name:e,description:``,orchestrator:{model:`sonnet`},agents:[{id:`sr-architect`,model:`sonnet`,required:!0},{id:`sr-developer`,model:`sonnet`,required:!0},{id:`sr-reviewer`,model:`sonnet`,required:!0},{id:`sr-merge-resolver`,model:`sonnet`,required:!0}],routing:[{default:!0,agent:`sr-developer`}]},n=await fetch(`${w()}/profiles`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Create failed: ${n.status}`)}await O(),r(e),u.success(`Profile created`,{description:e})}catch(e){let t=e.message;l(t),u.error(`Failed to create profile`,{description:t})}finally{p(!1)}},[O]),j=(0,J.useCallback)(async(e,t)=>{T(null),p(!0),l(null);try{let n=await fetch(`${w()}/profiles/${encodeURIComponent(e)}/duplicate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Duplicate failed: ${n.status}`)}await O(),r(t),u.success(`Profile duplicated`,{description:`${e} → ${t}`})}catch(e){let t=e.message;l(t),u.error(`Failed to duplicate profile`,{description:t})}finally{p(!1)}},[O]),M=(0,J.useCallback)(async e=>{D(null),p(!0),l(null);try{let t=await fetch(`${w()}/profiles/${encodeURIComponent(e)}`,{method:`DELETE`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error??`Delete failed: ${t.status}`)}r(t=>t===e?null:t),await O(),u.success(`Profile deleted`,{description:e})}catch(e){let t=e.message;l(t),u.error(`Failed to delete profile`,{description:t})}finally{p(!1)}},[O]),N=(0,J.useCallback)(()=>S(!0),[]),P=(0,J.useCallback)(e=>T({from:e}),[]),F=(0,J.useCallback)(e=>D({name:e}),[]),I=(0,J.useCallback)(async(e,t)=>{p(!0),l(null);try{let n=await fetch(`${w()}/profiles/${encodeURIComponent(e.name)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e)});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Save failed: ${n.status}`)}a(e),t.length>0?u.warning(`Profile saved with untargeted agents`,{description:`No routing rule points to: ${t.join(`, `)}. Add a tag rule or retarget the default rule if you want them to run.`,duration:6e3}):u.success(`Profile saved`,{description:e.name})}catch(e){let t=e.message;l(t),u.error(`Failed to save profile`,{description:t})}finally{p(!1)}},[]),L=(0,Y.jsxs)(Y.Fragment,{children:[(0,Y.jsx)(ge,{open:x,title:`New profile`,description:`Pick a lowercase kebab-case name (letters, digits, and hyphens).`,placeholder:`my-profile`,confirmLabel:`Create`,inputPattern:/^[a-z0-9][a-z0-9-]*$/,inputInvalidHint:`Must start with a letter or digit and contain only lowercase letters, digits, and hyphens.`,onConfirm:e=>void A(e),onCancel:()=>S(!1)}),C&&(0,Y.jsx)(ge,{open:!0,title:`Duplicate "${C.from}"`,description:`Name for the new profile.`,placeholder:`${C.from}-copy`,initialValue:`${C.from}-copy`,confirmLabel:`Duplicate`,inputPattern:/^[a-z0-9][a-z0-9-]*$/,inputInvalidHint:`Lowercase kebab-case only.`,onConfirm:e=>void j(C.from,e),onCancel:()=>T(null)}),E&&(0,Y.jsx)(_e,{open:!0,title:`Delete profile "${E.name}"?`,description:`This cannot be undone. Jobs already launched with this profile keep their snapshot; future launches will fall back to the resolution order.`,confirmLabel:`Delete`,destructive:!0,onConfirm:()=>void M(E.name),onCancel:()=>D(null)})]});return o?(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Loading profiles…`})}):e.length===0?(0,Y.jsxs)(Y.Fragment,{children:[L,(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsxs)(`div`,{className:`text-center max-w-md`,children:[(0,Y.jsx)(`div`,{className:`text-sm font-medium text-foreground`,children:`No profiles yet`}),(0,Y.jsx)(`div`,{className:`text-xs text-muted-foreground mt-1 mb-4`,children:`Profiles let you save orchestrator + agent + routing combinations and pick one per rail.`}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-2 justify-center`,children:[(0,Y.jsxs)(y,{size:`sm`,onClick:k,disabled:d,children:[(0,Y.jsx)(ce,{className:`w-3.5 h-3.5 mr-1.5`}),` Migrate from current agents`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:N,disabled:d,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1.5`}),` Blank profile`]})]}),(0,Y.jsxs)(`div`,{className:`text-[11px] text-muted-foreground/70 mt-3`,children:[`"Migrate" reads your existing `,(0,Y.jsx)(`code`,{className:`text-foreground`,children:`.claude/agents/`}),` `,`frontmatter models and creates a `,(0,Y.jsx)(`code`,{className:`text-foreground`,children:`default`}),` profile mirroring today's behavior — zero-loss.`]}),c&&(0,Y.jsx)(`div`,{className:`mt-3 text-xs text-red-400`,children:c})]})})]}):(0,Y.jsxs)(`div`,{className:`flex flex-col h-full`,children:[L,(0,Y.jsxs)(`div`,{className:`flex flex-1 min-h-0`,children:[(0,Y.jsxs)(`aside`,{className:`w-64 flex-shrink-0 border-r border-border flex flex-col`,children:[(0,Y.jsxs)(`div`,{className:`p-3 flex items-center justify-between`,children:[(0,Y.jsx)(`div`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:`Profiles`}),(0,Y.jsx)(y,{size:`sm`,variant:`ghost`,onClick:N,disabled:d,children:(0,Y.jsx)(f,{className:`w-3.5 h-3.5`})})]}),(0,Y.jsx)(`div`,{className:`flex-1 overflow-auto px-2 pb-3`,children:e.map(e=>(0,Y.jsxs)(`div`,{className:`group mb-1 rounded-md px-2 py-1.5 text-xs cursor-pointer transition-colors flex items-center justify-between `+(e.name===n?`bg-accent text-foreground`:`text-muted-foreground hover:bg-accent/50 hover:text-foreground`),onClick:()=>r(e.name),role:`button`,tabIndex:0,onKeyDown:t=>{(t.key===`Enter`||t.key===` `)&&r(e.name)},children:[(0,Y.jsxs)(`div`,{className:`flex items-center gap-1.5 min-w-0`,children:[(0,Y.jsx)(`span`,{className:`truncate font-medium`,children:e.name}),e.isDefault&&(0,Y.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:`(team default)`})]}),(0,Y.jsxs)(`div`,{className:`flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity`,children:[(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,title:`Duplicate`,onClick:t=>{t.stopPropagation(),P(e.name)},children:(0,Y.jsx)(ne,{className:`w-3 h-3`})}),!e.isDefault&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded`,title:`Delete`,onClick:t=>{t.stopPropagation(),F(e.name)},children:(0,Y.jsx)(_,{className:`w-3 h-3`})})]})]},e.name))})]}),(0,Y.jsxs)(`main`,{className:`flex-1 overflow-auto`,children:[c&&(0,Y.jsx)(`div`,{className:`mx-4 mt-4 px-3 py-2 text-xs rounded border border-red-500/30 bg-red-500/10 text-red-400`,children:c}),i?(0,Y.jsx)(fe,{profile:i,onChange:a,onValidityChange:g,onSoftWarningsChange:e=>b(e.agentsMissingRouting),footer:(0,Y.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Y.jsxs)(y,{size:`sm`,onClick:()=>void I(i,v),disabled:d||h.length>0,title:h.length>0?`Fix validation issues before saving`:void 0,children:[(0,Y.jsx)(m,{className:`w-3.5 h-3.5 mr-1.5`}),`Save`]}),d&&(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:`Saving…`}),h.length>0&&(0,Y.jsxs)(`span`,{className:`text-xs text-yellow-500`,children:[h.length,` `,h.length===1?`issue`:`issues`,` to resolve`]})]})},i.name):(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Select a profile to edit`})})]})]})]})}function Q(e){let t=e.workflow.map((e,t)=>`${t+1}. ${e}`).join(`
|
|
1
|
+
import{r as e}from"./chunk-CilyBKbf.js";import{$ as t,At as n,B as r,C as i,Ct as a,Dt as o,E as s,Et as c,F as l,Ft as u,G as d,H as f,I as p,L as m,M as h,Mt as g,N as _,Nt as v,O as y,Ot as b,P as x,Pt as S,S as C,St as w,T,Tt as E,U as D,Vt as O,W as k,X as A,_t as j,a as M,b as N,bt as P,ct as F,ft as I,ht as L,j as R,jt as z,kt as ee,lt as B,mt as V,ot as te,q as H,tt as ne,ut as U,w as W,wt as G,x as re,xt as ie,y as K,yt as ae}from"./index-k8jG-NWj.js";var oe=j(`flask-conical`,[[`path`,{d:`M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2`,key:`18mbvz`}],[`path`,{d:`M6.453 15h11.094`,key:`3shlmq`}],[`path`,{d:`M8.5 2h7`,key:`csnxdl`}]]),q=j(`history`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}],[`path`,{d:`M12 7v5l4 2`,key:`1fdv2h`}]]),se=j(`pin`,[[`path`,{d:`M12 17v5`,key:`bb1du9`}],[`path`,{d:`M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z`,key:`1nkz8b`}]]),ce=j(`wand-sparkles`,[[`path`,{d:`m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72`,key:`ul74o6`}],[`path`,{d:`m14 7 3 3`,key:`1r5n42`}],[`path`,{d:`M5 6v4`,key:`ilb8ba`}],[`path`,{d:`M19 14v4`,key:`blhpug`}],[`path`,{d:`M10 2v2`,key:`7u0qdc`}],[`path`,{d:`M7 8H3`,key:`zfb6yr`}],[`path`,{d:`M21 16h-4`,key:`1cnmox`}],[`path`,{d:`M11 3H9`,key:`1obp7u`}]]),J=e(O(),1),Y=ie(),X=/^[a-z0-9][a-z0-9-]*$/;function le({open:e,mode:t=`add`,initial:n,chainAgents:r,onConfirm:a,onCancel:o}){let[s,c]=(0,J.useState)(``),[l,u]=(0,J.useState)(r[0]??``);(0,J.useEffect)(()=>{e&&(t===`edit`&&n?(c(n.tags.join(`, `)),u(r.includes(n.agent)?n.agent:r[0]??``)):(c(``),u(r[0]??``)))},[e,t,n,r]);let d=s.split(`,`).map(e=>e.trim()).filter(Boolean),f=d.filter(e=>!X.test(e)),p=d.length>0&&f.length===0&&r.includes(l);return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||o()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsxs)(i,{children:[(0,Y.jsx)(W,{children:t===`edit`?`Edit routing rule`:`Add routing rule`}),(0,Y.jsx)(re,{children:t===`edit`?`Update the tags or target agent for this rule.`:`Add a tag-matched routing rule using lowercase kebab-case tags.`})]}),(0,Y.jsxs)(`div`,{className:`py-2 space-y-3`,children:[(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Tags`}),(0,Y.jsx)(M,{autoFocus:!0,value:s,onChange:e=>c(e.target.value),placeholder:`frontend, ui`,"aria-label":`Tags`,className:`text-sm font-mono`}),(0,Y.jsxs)(`p`,{className:`text-[11px] text-muted-foreground mt-1`,children:[`Comma-separated. Use lowercase kebab-case like `,(0,Y.jsx)(`code`,{children:`frontend`}),` or `,(0,Y.jsx)(`code`,{children:`api-design`}),`.`]}),f.length>0&&(0,Y.jsxs)(`p`,{className:`text-[11px] text-red-400 mt-1`,children:[`Invalid tag`,f.length===1?``:`s`,`: `,f.join(`, `),`. Tags must use lowercase letters, digits, and hyphens only.`]})]}),(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Route to`}),(0,Y.jsx)(`select`,{value:l,onChange:e=>u(e.target.value),"aria-label":`Route to`,className:`w-full h-9 px-2 text-sm rounded-md border border-border bg-background`,children:r.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))}),(0,Y.jsx)(`p`,{className:`text-[11px] text-muted-foreground mt-1`,children:`Only agents in this profile's chain can be routing targets.`})]})]}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:o,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:()=>a(d,l),disabled:!p,children:t===`edit`?`Save changes`:`Add rule`})]})]})})}var Z=new Set([`sr-architect`,`sr-developer`,`sr-reviewer`,`sr-merge-resolver`]),ue=[`sonnet`,`opus`,`haiku`],de=/^[a-z0-9][a-z0-9-]*$/;function fe({profile:e,onChange:t,footer:r,onValidityChange:i,onSoftWarningsChange:s}){let[c,l]=(0,J.useState)([]),[u,d]=(0,J.useState)(!1),[p,m]=(0,J.useState)(!1),[h,_]=(0,J.useState)(null);(0,J.useEffect)(()=>{let e=!1;return fetch(`${w()}/profiles/catalog`).then(e=>e.ok?e.json():{agents:[]}).then(t=>{e||l(t.agents)}).catch(()=>{e||l([])}),()=>{e=!0}},[]);let x=n=>{let r=JSON.parse(JSON.stringify(e));n(r),t(r)},S=new Set(e.agents.map(e=>e.id)),C=c.filter(e=>!S.has(e.id)),T=(0,J.useMemo)(()=>{let t=[];for(let n of Z)e.agents.some(e=>e.id===n)||t.push(`Missing required baseline agent: ${n}`);let n=e.routing.filter(e=>`default`in e&&e.default===!0);if(n.length>1&&t.push(`Routing may have at most one default rule (found ${n.length})`),n.length===1){let n=e.routing[e.routing.length-1];`default`in n&&n.default===!0||t.push(`The default routing rule must be the last entry`)}for(let n of e.routing)if(e.agents.some(e=>e.id===n.agent)||t.push(`Routing references agent not in the chain: ${n.agent}`),`tags`in n){let e=n.tags.filter(e=>!de.test(e));e.length>0&&t.push(`Routing rule ${n.agent} has invalid tags: ${e.join(`, `)} (use lowercase kebab-case)`)}return t},[e]),D=(0,J.useMemo)(()=>{if(e.routing.length===0)return[];let t=new Set(e.routing.map(e=>e.agent));return e.agents.filter(e=>!Z.has(e.id)&&!t.has(e.id)).map(e=>e.id)},[e]),O=(0,J.useMemo)(()=>e.routing.some(e=>`default`in e&&e.default===!0),[e.routing]);(0,J.useEffect)(()=>{i&&i(T)},[T,i]),(0,J.useEffect)(()=>{s&&s({agentsMissingRouting:D})},[D,s]);let k=e=>{x(t=>{let n={id:e,model:`sonnet`},r=t.agents.findIndex(e=>e.id===`sr-merge-resolver`);r>=0?t.agents.splice(r,0,n):t.agents.push(n)}),d(!1)},A=t=>{let n=e.agents[t];Z.has(n.id)||x(e=>{e.agents.splice(t,1),e.routing=e.routing.filter(e=>e.agent!==n.id)})},j=(t,n)=>{if(t===n)return;let r=e.agents.findIndex(e=>e.id===t),i=e.agents.findIndex(e=>e.id===n);if(r<0||i<0)return;let a=G(e.agents,r,i),o=a.findIndex(e=>e.id===`sr-architect`);if(o>0){let[e]=a.splice(o,1);a.unshift(e)}let s=a.findIndex(e=>e.id===`sr-merge-resolver`);if(s>=0&&s!==a.length-1){let[e]=a.splice(s,1);a.push(e)}x(e=>{e.agents=a})},N=v(g(n,{activationConstraint:{distance:4}}),g(ee,{coordinateGetter:E})),P=e=>{let{active:t,over:n}=e;n&&j(String(t.id),String(n.id))},F=(e,t)=>{x(n=>{n.agents[e].model=t})},I=(e,t)=>{x(n=>{let r={tags:e,agent:t},i=n.routing.findIndex(e=>`default`in e&&e.default===!0);i>=0?n.routing.splice(i,0,r):n.routing.push(r)}),m(!1)},L=()=>{x(e=>{e.routing.some(e=>`default`in e&&e.default===!0)||e.agents.some(e=>e.id===`sr-developer`)&&e.routing.push({default:!0,agent:`sr-developer`})})},B=e=>`default`in e&&e.default===!0,V=(e,t)=>{x(n=>{let r=n.routing[e];r&&(B(r)||(r.agent=t))})},te=(e,t,n)=>{x(r=>{let i=r.routing[e];i&&(B(i)||(i.tags=t,i.agent=n))})},H=e=>{x(t=>{let n=t.routing[e];n&&(B(n)||t.routing.splice(e,1))})},ne=(t,n)=>{let r=t+n;if(r<0||r>=e.routing.length)return;let i=e.routing[t],a=e.routing[r];B(i)||B(a)||x(e=>{let[n]=e.routing.splice(t,1);e.routing.splice(r,0,n)})};return(0,Y.jsxs)(`div`,{className:`p-6 space-y-6 max-w-3xl`,children:[(0,Y.jsx)(le,{open:p,mode:`add`,chainAgents:e.agents.map(e=>e.id),onConfirm:I,onCancel:()=>m(!1)}),(0,Y.jsx)(le,{open:h!==null,mode:`edit`,initial:h!==null&&e.routing[h]&&!(`default`in e.routing[h])?{tags:e.routing[h].tags,agent:e.routing[h].agent}:void 0,chainAgents:e.agents.map(e=>e.id),onConfirm:(e,t)=>{h!==null&&(te(h,e,t),_(null))},onCancel:()=>_(null)}),T.length>0&&(0,Y.jsxs)(`div`,{className:`px-3 py-2 text-xs rounded-md border border-yellow-500/30 bg-yellow-500/10 text-yellow-500`,children:[(0,Y.jsxs)(`div`,{className:`font-medium mb-1`,children:[T.length,` validation `,T.length===1?`issue`:`issues`]}),(0,Y.jsx)(`ul`,{className:`list-disc list-inside space-y-0.5`,children:T.map((e,t)=>(0,Y.jsx)(`li`,{children:e},t))})]}),(0,Y.jsxs)(`section`,{className:`space-y-3`,children:[(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Name`}),(0,Y.jsx)(M,{value:e.name,disabled:!0,className:`text-sm`})]}),(0,Y.jsxs)(`div`,{children:[(0,Y.jsx)(`label`,{className:`block text-xs font-medium text-muted-foreground mb-1`,children:`Description`}),(0,Y.jsx)(M,{value:e.description??``,onChange:e=>x(t=>{t.description=e.target.value}),className:`text-sm`,placeholder:`What is this profile for?`})]})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsx)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2`,children:`Orchestrator`}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-3 p-3 rounded-md border border-border`,children:[(0,Y.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,Y.jsx)(`div`,{className:`text-sm font-mono text-foreground truncate`,children:`/specrails:implement · /specrails:batch-implement`}),(0,Y.jsx)(`div`,{className:`text-[11px] text-muted-foreground mt-0.5`,children:`Top-level model for both commands. batch-implement delegates to implement per feature, so every rail it spawns inherits this profile's agent chain.`})]}),(0,Y.jsx)(he,{value:e.orchestrator.model,onChange:e=>x(t=>{t.orchestrator.model=e})})]})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-2`,children:[(0,Y.jsxs)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:[`Agent chain (`,e.agents.length,`)`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:()=>d(e=>!e),disabled:C.length===0,title:C.length===0?`All catalog agents are already in the chain`:`Add an agent from the catalog`,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add`]})]}),u&&(0,Y.jsxs)(`div`,{className:`mb-2 p-2 rounded-md border border-border bg-muted/30`,children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-1.5 px-1`,children:[(0,Y.jsxs)(`span`,{className:`text-[11px] text-muted-foreground`,children:[`Pick from catalog (`,C.length,` available)`]}),(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:()=>d(!1),title:`Close`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]}),C.length===0?(0,Y.jsx)(`div`,{className:`px-2 py-3 text-xs text-muted-foreground text-center`,children:`No more agents in the catalog. Add custom agents from the Agents Catalog tab.`}):(0,Y.jsx)(`div`,{className:`space-y-0.5 max-h-64 overflow-auto`,children:C.map(e=>(0,Y.jsxs)(`button`,{type:`button`,onClick:()=>k(e.id),className:`w-full flex items-center justify-between gap-2 px-2 py-1.5 text-left rounded hover:bg-accent transition-colors`,children:[(0,Y.jsx)(`span`,{className:`text-sm font-mono`,children:e.id}),(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded `+(e.kind===`custom`?`bg-purple-500/15 text-purple-400`:`bg-muted text-muted-foreground`),children:e.kind})]},e.id))})]}),(0,Y.jsx)(b,{sensors:N,collisionDetection:z,onDragEnd:P,children:(0,Y.jsx)(a,{items:e.agents.map(e=>e.id),strategy:o,children:(0,Y.jsx)(`div`,{className:`space-y-1.5`,children:e.agents.map((e,t)=>(0,Y.jsx)(pe,{agent:e,canRemove:!Z.has(e.id),onModel:e=>F(t,e),onRemove:()=>A(t)},e.id))})})})]}),(0,Y.jsxs)(`section`,{children:[(0,Y.jsxs)(`div`,{className:`flex items-center justify-between mb-2`,children:[(0,Y.jsxs)(`h2`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:[`Routing (`,e.routing.length,`)`]}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-1`,children:[!O&&(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:L,disabled:e.agents.length===0,title:e.agents.length===0?`Add at least one agent before creating routing rules`:void 0,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add default`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:()=>m(!0),disabled:e.agents.length===0,title:e.agents.length===0?`Add at least one agent before creating routing rules`:void 0,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1`}),` Add rule`]})]})]}),(0,Y.jsx)(`p`,{className:`text-[11px] text-muted-foreground mb-2`,children:`First matching rule wins. Rules are editable and removable, and the default catch-all stays last when present. If you leave routing empty, the pipeline falls back to the first developer-shaped agent in the chain.`}),D.length>0&&(0,Y.jsxs)(`div`,{className:`mb-2 px-3 py-2 text-xs rounded-md border border-yellow-500/30 bg-yellow-500/10 text-yellow-500`,children:[(0,Y.jsx)(`div`,{className:`font-medium mb-1`,children:`Untargeted agents in the chain`}),(0,Y.jsxs)(`div`,{children:[`No routing rule points to: `,(0,Y.jsx)(`span`,{className:`font-mono`,children:D.join(`, `)}),`. Add a tag rule or retarget the default rule if you want them to run.`]})]}),(0,Y.jsx)(`div`,{className:`space-y-1.5`,children:e.routing.map((t,n)=>{let r=`default`in t&&t.default===!0;return(0,Y.jsx)(me,{rule:t,ordinal:n+1,isLast:n===e.routing.length-1,canMove:!r,canRemove:!r,canEdit:!r,chainAgents:e.agents.map(e=>e.id),onAgentChange:e=>V(n,e),onEdit:()=>_(n),onUp:()=>ne(n,-1),onDown:()=>ne(n,1),onRemove:()=>H(n)},n)})})]}),r&&(0,Y.jsx)(`div`,{className:`pt-3 border-t border-border`,children:r})]})}function pe({agent:e,canRemove:t,onModel:n,onRemove:r}){let i=Z.has(e.id),a=e.id===`sr-architect`,o=e.id===`sr-merge-resolver`,{attributes:s,listeners:l,setNodeRef:u,transform:d,transition:f,isDragging:p}=c({id:e.id});return(0,Y.jsxs)(`div`,{ref:u,style:{transform:S.Transform.toString(d),transition:f,opacity:p?.5:void 0,zIndex:p?10:void 0},className:`flex items-center gap-2 px-2 py-1.5 rounded-md border border-border group hover:bg-accent/30 transition-colors bg-background`,children:[(0,Y.jsx)(`button`,{type:`button`,className:`flex-shrink-0 p-0.5 rounded text-muted-foreground cursor-grab active:cursor-grabbing hover:text-foreground`,title:`Drag to reorder`,"aria-label":`Drag handle`,...s,...l,children:(0,Y.jsx)(H,{className:`w-3.5 h-3.5`})}),(0,Y.jsx)(`span`,{className:`text-sm font-mono flex-1 truncate`,children:e.id}),a&&(0,Y.jsxs)(`span`,{className:`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-accent-primary/15 text-accent-primary`,title:`Pinned to first position — pipeline always starts with sr-architect`,children:[(0,Y.jsx)(se,{className:`w-2.5 h-2.5 rotate-[135deg]`}),` first`]}),o&&(0,Y.jsxs)(`span`,{className:`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-accent-primary/15 text-accent-primary`,title:`Pinned to last position — merge phase always runs last`,children:[(0,Y.jsx)(se,{className:`w-2.5 h-2.5 rotate-45`}),` last`]}),i&&!a&&!o&&(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground`,children:`required`}),(0,Y.jsx)(he,{value:e.model??`sonnet`,onChange:n}),(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded disabled:opacity-30 disabled:cursor-not-allowed opacity-0 group-hover:opacity-100 transition-opacity`,onClick:r,disabled:!t,title:t?`Remove`:`Required baseline agent — the pipeline depends on this row`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]})}function me({rule:e,ordinal:t,isLast:n,canMove:r,canRemove:i,canEdit:a,chainAgents:o,onAgentChange:s,onEdit:c,onUp:l,onDown:u,onRemove:d}){let f=`default`in e&&e.default===!0;return(0,Y.jsxs)(`div`,{className:`flex items-center gap-2 px-2 py-1.5 rounded-md border border-border group hover:bg-accent/30 transition-colors`,children:[(0,Y.jsxs)(`span`,{className:`text-[10px] font-mono text-muted-foreground w-5 text-center flex-shrink-0`,children:[t,`.`]}),f?(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground flex-1`,children:`everything else`}):(0,Y.jsx)(`span`,{className:`text-xs flex-1 flex gap-1 flex-wrap items-center`,children:e.tags.map(e=>(0,Y.jsx)(`span`,{className:`px-1.5 py-0.5 rounded bg-muted font-mono text-[11px]`,children:e},e))}),(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:`→`}),f?(0,Y.jsx)(`span`,{className:`h-7 max-w-[15rem] px-2 text-xs font-mono rounded border border-border bg-muted/40 text-muted-foreground inline-flex items-center`,"aria-label":`Default routing target (core, read-only)`,title:`Core fallback — not editable`,children:e.agent}):(0,Y.jsx)(`select`,{value:e.agent,onChange:e=>s(e.target.value),"aria-label":`Routing target for rule ${t}`,className:`h-7 max-w-[15rem] px-2 text-xs font-mono rounded border border-border bg-background`,children:o.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))}),(0,Y.jsxs)(`div`,{className:`flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity`,children:[a&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:c,title:`Edit rule`,"aria-label":`Edit rule ${t}`,children:(0,Y.jsx)(D,{className:`w-3 h-3`})}),r&&!n&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:u,title:`Move down`,children:(0,Y.jsx)(L,{className:`w-3 h-3`})}),r&&t>1&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,onClick:l,title:`Move up`,children:(0,Y.jsx)(I,{className:`w-3 h-3`})}),i&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded`,onClick:d,title:`Remove`,children:(0,Y.jsx)(R,{className:`w-3 h-3`})})]}),f&&(0,Y.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground`,title:`Core fallback — the pipeline's last-resort rule, pinned to sr-developer`,children:`core · default`})]})}function he({value:e,onChange:t}){return(0,Y.jsx)(`select`,{value:e,onChange:e=>t(e.target.value),className:`h-7 px-2 text-xs rounded border border-border bg-background`,children:ue.map(e=>(0,Y.jsx)(`option`,{value:e,children:e},e))})}function ge({open:e,title:t,description:n,placeholder:r,initialValue:a=``,confirmLabel:o=`Confirm`,inputPattern:s,inputInvalidHint:c,onConfirm:l,onCancel:u}){let[d,f]=(0,J.useState)(a);(0,J.useEffect)(()=>{e&&f(a)},[e,a]);let p=d.trim(),m=!s||s.test(p),h=p.length>0&&m;return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||u()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsx)(i,{children:(0,Y.jsx)(W,{children:t})}),(0,Y.jsxs)(`div`,{className:`py-2 space-y-2`,children:[n&&(0,Y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:n}),(0,Y.jsx)(M,{autoFocus:!0,value:d,placeholder:r,onChange:e=>f(e.target.value),onKeyDown:e=>{e.key===`Enter`&&h&&l(p)},className:`text-sm font-mono`}),p.length>0&&!m&&c&&(0,Y.jsx)(`p`,{className:`text-[11px] text-yellow-500`,children:c})]}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:u,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:()=>l(p),disabled:!h,children:o})]})]})})}function _e({open:e,title:t,description:n,confirmLabel:r=`Confirm`,destructive:a=!1,onConfirm:o,onCancel:s}){return(0,Y.jsx)(K,{open:e,onOpenChange:e=>{e||s()},children:(0,Y.jsxs)(N,{className:`max-w-md`,children:[(0,Y.jsx)(i,{children:(0,Y.jsx)(W,{children:t})}),n&&(0,Y.jsx)(`div`,{className:`py-2`,children:(0,Y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:n})}),(0,Y.jsxs)(C,{children:[(0,Y.jsx)(y,{variant:`ghost`,size:`sm`,onClick:s,children:`Cancel`}),(0,Y.jsx)(y,{size:`sm`,onClick:o,className:a?`bg-red-500 hover:bg-red-600 text-white`:void 0,children:r})]})]})})}function ve(){let[e,t]=(0,J.useState)([]),[n,r]=(0,J.useState)(null),[i,a]=(0,J.useState)(null),[o,s]=(0,J.useState)(!0),[c,l]=(0,J.useState)(null),[d,p]=(0,J.useState)(!1),[h,g]=(0,J.useState)([]),[v,b]=(0,J.useState)([]),[x,S]=(0,J.useState)(!1),[C,T]=(0,J.useState)(null),[E,D]=(0,J.useState)(null),O=(0,J.useCallback)(async()=>{s(!0),l(null);try{let e=await fetch(`${w()}/profiles`);if(!e.ok)throw Error(`List failed: ${e.status}`);let i=await e.json();t(i.profiles),i.profiles.length>0&&!n&&r(i.profiles[0].name)}catch(e){l(e.message)}finally{s(!1)}},[n]);(0,J.useEffect)(()=>{O()},[O]),(0,J.useEffect)(()=>{if(!n){a(null);return}let e=!1;return fetch(`${w()}/profiles/${encodeURIComponent(n)}`).then(e=>{if(!e.ok)throw Error(`Load failed: ${e.status}`);return e.json()}).then(t=>{e||a(t.profile)}).catch(t=>{e||l(t.message)}),()=>{e=!0}},[n]);let k=(0,J.useCallback)(async()=>{p(!0),l(null);try{let e=await fetch(`${w()}/profiles/migrate-from-settings`,{method:`POST`});if(!e.ok){let t=await e.json().catch(()=>({}));throw Error(t.error??`Migration failed: ${e.status}`)}await O(),r(`default`),u.success(`Profile migrated`,{description:`default profile created from your current agents`})}catch(e){let t=e.message;l(t),u.error(`Migration failed`,{description:t})}finally{p(!1)}},[O]),A=(0,J.useCallback)(async e=>{S(!1),p(!0),l(null);try{let t={schemaVersion:1,name:e,description:``,orchestrator:{model:`sonnet`},agents:[{id:`sr-architect`,model:`sonnet`,required:!0},{id:`sr-developer`,model:`sonnet`,required:!0},{id:`sr-reviewer`,model:`sonnet`,required:!0},{id:`sr-merge-resolver`,model:`sonnet`,required:!0}],routing:[{default:!0,agent:`sr-developer`}]},n=await fetch(`${w()}/profiles`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Create failed: ${n.status}`)}await O(),r(e),u.success(`Profile created`,{description:e})}catch(e){let t=e.message;l(t),u.error(`Failed to create profile`,{description:t})}finally{p(!1)}},[O]),j=(0,J.useCallback)(async(e,t)=>{T(null),p(!0),l(null);try{let n=await fetch(`${w()}/profiles/${encodeURIComponent(e)}/duplicate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Duplicate failed: ${n.status}`)}await O(),r(t),u.success(`Profile duplicated`,{description:`${e} → ${t}`})}catch(e){let t=e.message;l(t),u.error(`Failed to duplicate profile`,{description:t})}finally{p(!1)}},[O]),M=(0,J.useCallback)(async e=>{D(null),p(!0),l(null);try{let t=await fetch(`${w()}/profiles/${encodeURIComponent(e)}`,{method:`DELETE`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error??`Delete failed: ${t.status}`)}r(t=>t===e?null:t),await O(),u.success(`Profile deleted`,{description:e})}catch(e){let t=e.message;l(t),u.error(`Failed to delete profile`,{description:t})}finally{p(!1)}},[O]),N=(0,J.useCallback)(()=>S(!0),[]),P=(0,J.useCallback)(e=>T({from:e}),[]),F=(0,J.useCallback)(e=>D({name:e}),[]),I=(0,J.useCallback)(async(e,t)=>{p(!0),l(null);try{let n=await fetch(`${w()}/profiles/${encodeURIComponent(e.name)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e)});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Save failed: ${n.status}`)}a(e),t.length>0?u.warning(`Profile saved with untargeted agents`,{description:`No routing rule points to: ${t.join(`, `)}. Add a tag rule or retarget the default rule if you want them to run.`,duration:6e3}):u.success(`Profile saved`,{description:e.name})}catch(e){let t=e.message;l(t),u.error(`Failed to save profile`,{description:t})}finally{p(!1)}},[]),L=(0,Y.jsxs)(Y.Fragment,{children:[(0,Y.jsx)(ge,{open:x,title:`New profile`,description:`Pick a lowercase kebab-case name (letters, digits, and hyphens).`,placeholder:`my-profile`,confirmLabel:`Create`,inputPattern:/^[a-z0-9][a-z0-9-]*$/,inputInvalidHint:`Must start with a letter or digit and contain only lowercase letters, digits, and hyphens.`,onConfirm:e=>void A(e),onCancel:()=>S(!1)}),C&&(0,Y.jsx)(ge,{open:!0,title:`Duplicate "${C.from}"`,description:`Name for the new profile.`,placeholder:`${C.from}-copy`,initialValue:`${C.from}-copy`,confirmLabel:`Duplicate`,inputPattern:/^[a-z0-9][a-z0-9-]*$/,inputInvalidHint:`Lowercase kebab-case only.`,onConfirm:e=>void j(C.from,e),onCancel:()=>T(null)}),E&&(0,Y.jsx)(_e,{open:!0,title:`Delete profile "${E.name}"?`,description:`This cannot be undone. Jobs already launched with this profile keep their snapshot; future launches will fall back to the resolution order.`,confirmLabel:`Delete`,destructive:!0,onConfirm:()=>void M(E.name),onCancel:()=>D(null)})]});return o?(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Loading profiles…`})}):e.length===0?(0,Y.jsxs)(Y.Fragment,{children:[L,(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsxs)(`div`,{className:`text-center max-w-md`,children:[(0,Y.jsx)(`div`,{className:`text-sm font-medium text-foreground`,children:`No profiles yet`}),(0,Y.jsx)(`div`,{className:`text-xs text-muted-foreground mt-1 mb-4`,children:`Profiles let you save orchestrator + agent + routing combinations and pick one per rail.`}),(0,Y.jsxs)(`div`,{className:`flex items-center gap-2 justify-center`,children:[(0,Y.jsxs)(y,{size:`sm`,onClick:k,disabled:d,children:[(0,Y.jsx)(ce,{className:`w-3.5 h-3.5 mr-1.5`}),` Migrate from current agents`]}),(0,Y.jsxs)(y,{size:`sm`,variant:`ghost`,onClick:N,disabled:d,children:[(0,Y.jsx)(f,{className:`w-3.5 h-3.5 mr-1.5`}),` Blank profile`]})]}),(0,Y.jsxs)(`div`,{className:`text-[11px] text-muted-foreground/70 mt-3`,children:[`"Migrate" reads your existing `,(0,Y.jsx)(`code`,{className:`text-foreground`,children:`.claude/agents/`}),` `,`frontmatter models and creates a `,(0,Y.jsx)(`code`,{className:`text-foreground`,children:`default`}),` profile mirroring today's behavior — zero-loss.`]}),c&&(0,Y.jsx)(`div`,{className:`mt-3 text-xs text-red-400`,children:c})]})})]}):(0,Y.jsxs)(`div`,{className:`flex flex-col h-full`,children:[L,(0,Y.jsxs)(`div`,{className:`flex flex-1 min-h-0`,children:[(0,Y.jsxs)(`aside`,{className:`w-64 flex-shrink-0 border-r border-border flex flex-col`,children:[(0,Y.jsxs)(`div`,{className:`p-3 flex items-center justify-between`,children:[(0,Y.jsx)(`div`,{className:`text-xs font-medium text-muted-foreground uppercase tracking-wide`,children:`Profiles`}),(0,Y.jsx)(y,{size:`sm`,variant:`ghost`,onClick:N,disabled:d,children:(0,Y.jsx)(f,{className:`w-3.5 h-3.5`})})]}),(0,Y.jsx)(`div`,{className:`flex-1 overflow-auto px-2 pb-3`,children:e.map(e=>(0,Y.jsxs)(`div`,{className:`group mb-1 rounded-md px-2 py-1.5 text-xs cursor-pointer transition-colors flex items-center justify-between `+(e.name===n?`bg-accent text-foreground`:`text-muted-foreground hover:bg-accent/50 hover:text-foreground`),onClick:()=>r(e.name),role:`button`,tabIndex:0,onKeyDown:t=>{(t.key===`Enter`||t.key===` `)&&r(e.name)},children:[(0,Y.jsxs)(`div`,{className:`flex items-center gap-1.5 min-w-0`,children:[(0,Y.jsx)(`span`,{className:`truncate font-medium`,children:e.name}),e.isDefault&&(0,Y.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:`(team default)`})]}),(0,Y.jsxs)(`div`,{className:`flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity`,children:[(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-accent rounded`,title:`Duplicate`,onClick:t=>{t.stopPropagation(),P(e.name)},children:(0,Y.jsx)(ne,{className:`w-3 h-3`})}),!e.isDefault&&(0,Y.jsx)(`button`,{type:`button`,className:`p-1 hover:bg-red-500/20 text-red-400 rounded`,title:`Delete`,onClick:t=>{t.stopPropagation(),F(e.name)},children:(0,Y.jsx)(_,{className:`w-3 h-3`})})]})]},e.name))})]}),(0,Y.jsxs)(`main`,{className:`flex-1 overflow-auto`,children:[c&&(0,Y.jsx)(`div`,{className:`mx-4 mt-4 px-3 py-2 text-xs rounded border border-red-500/30 bg-red-500/10 text-red-400`,children:c}),i?(0,Y.jsx)(fe,{profile:i,onChange:a,onValidityChange:g,onSoftWarningsChange:e=>b(e.agentsMissingRouting),footer:(0,Y.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Y.jsxs)(y,{size:`sm`,onClick:()=>void I(i,v),disabled:d||h.length>0,title:h.length>0?`Fix validation issues before saving`:void 0,children:[(0,Y.jsx)(m,{className:`w-3.5 h-3.5 mr-1.5`}),`Save`]}),d&&(0,Y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:`Saving…`}),h.length>0&&(0,Y.jsxs)(`span`,{className:`text-xs text-yellow-500`,children:[h.length,` `,h.length===1?`issue`:`issues`,` to resolve`]})]})},i.name):(0,Y.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,Y.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Select a profile to edit`})})]})]})]})}function Q(e){let t=e.workflow.map((e,t)=>`${t+1}. ${e}`).join(`
|
|
2
2
|
`);return`---
|
|
3
3
|
name: ${e.id}
|
|
4
4
|
description: "${e.descriptionOneLine}"
|