7 Commits

Author SHA1 Message Date
pftom 19b5272f38 Merge branch 'main' into feat/optimize-naming 2026-04-28 16:23:44 +08:00
pftom 1337907df3 Merge branch 'main' of github.com:nexu-io/open-design 2026-04-28 16:20:14 +08:00
pftom 490bbe29c9 Merge branch 'feat/optimize-naming' of github.com:nexu-io/open-design into feat/optimize-naming 2026-04-28 16:16:51 +08:00
pftom 0eef347336 Update README and documentation for deck framework directives
- Clarified DECK_FRAMEWORK_DIRECTIVE description in both English and Chinese README files to specify conditions for deck kind without a skill seed.
- Added detailed workflow instructions in deck-framework.ts to emphasize the importance of copying the framework before adding content.
- Enhanced discovery.ts to reinforce the framework-first approach for deck projects.
- Updated system.ts to ensure proper handling of deck projects with and without bound skills, preventing re-authorship of scaling and navigation logic.
2026-04-28 16:11:46 +08:00
pftom 243e611eeb Update README and documentation for deck framework directives
- Clarified DECK_FRAMEWORK_DIRECTIVE description in both English and Chinese README files to specify conditions for deck kind without a skill seed.
- Added detailed workflow instructions in deck-framework.ts to emphasize the importance of copying the framework before adding content.
- Enhanced discovery.ts to reinforce the framework-first approach for deck projects.
- Updated system.ts to ensure proper handling of deck projects with and without bound skills, preventing re-authorship of scaling and navigation logic.
2026-04-28 16:07:52 +08:00
pftom 985238403f Add contributing guidelines in English and Chinese
- Introduced CONTRIBUTING.md and CONTRIBUTING.zh-CN.md to provide clear instructions for contributors.
- Outlined contribution types, local setup instructions, and merging criteria for skills and design systems.
- Enhanced README files to reference the new contributing guidelines.
2026-04-28 16:02:17 +08:00
pftom af3f96379a Refactor project name from "Open Claude Design" to "Open Design"
- Updated project name in package.json, package-lock.json, and README files.
- Changed CLI commands and references from "ocd" to "od".
- Adjusted file structure references in documentation and code to reflect new naming conventions.
- Enhanced .gitignore to include new runtime data files.
- Updated metadata in LICENSE file to match new project name.
2026-04-28 14:48:45 +08:00
26 changed files with 65 additions and 1521 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

+1 -3
View File
@@ -15,6 +15,4 @@ dist
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
.claude-sessions/* .claude-sessions/*
.cursor/*
+7 -37
View File
@@ -39,7 +39,7 @@ OD stands on four open-source shoulders:
| | What you get | | | What you get |
|---|---| |---|---|
| **Coding agents supported** | Claude Code · Codex CLI · Cursor Agent · Gemini CLI · OpenCode · Qwen Code · Hosted-API BYOK fallback (Anthropic · OpenAI-compatible · Azure · Google Gemini, plus AWS Bedrock & GCP Vertex via proxy) | | **Coding agents supported** | Claude Code · Codex CLI · Cursor Agent · Gemini CLI · OpenCode · Qwen Code · Anthropic API (BYOK fallback) |
| **Design systems built-in** | **71** — 2 hand-authored starters + 69 product systems (Linear, Stripe, Vercel, Airbnb, Tesla, Notion, Anthropic, Apple, Cursor, Supabase, Figma, …) imported from [`awesome-design-md`][acd2] | | **Design systems built-in** | **71** — 2 hand-authored starters + 69 product systems (Linear, Stripe, Vercel, Airbnb, Tesla, Notion, Anthropic, Apple, Cursor, Supabase, Figma, …) imported from [`awesome-design-md`][acd2] |
| **Skills built-in** | **19** — prototype, deck, mobile, dashboard, pricing, docs, blog, SaaS landing, plus 10 document/work-product templates (PM spec, weekly update, OKRs, runbook, kanban, …) | | **Skills built-in** | **19** — prototype, deck, mobile, dashboard, pricing, docs, blog, SaaS landing, plus 10 document/work-product templates (PM spec, weekly update, OKRs, runbook, kanban, …) |
| **Visual directions** | 5 curated schools (Editorial Monocle · Modern Minimal · Tech Utility · Brutalist · Soft Warm) — each ships a deterministic OKLch palette + font stack | | **Visual directions** | 5 curated schools (Editorial Monocle · Modern Minimal · Tech Utility · Brutalist · Soft Warm) — each ships a deterministic OKLch palette + font stack |
@@ -180,7 +180,7 @@ Adding a skill takes one folder. Read [`docs/skills-protocol.md`](docs/skills-pr
### 1 · We don't ship an agent. Yours is good enough. ### 1 · We don't ship an agent. Yours is good enough.
The daemon scans your `PATH` for [`claude`](https://docs.anthropic.com/en/docs/claude-code), [`codex`](https://github.com/openai/codex), [`cursor-agent`](https://www.cursor.com/cli), [`gemini`](https://github.com/google-gemini/gemini-cli), [`opencode`](https://opencode.ai/), and [`qwen`](https://github.com/QwenLM/qwen-code) on startup. Whichever it finds becomes the design engine — driven via stdio, with one adapter per CLI. Inspired by [`multica`](https://github.com/multica-ai/multica) and [`cc-switch`](https://github.com/farion1231/cc-switch). No CLI? The `Hosted API · BYOK` fallback streams directly from the browser to **Anthropic**, any **OpenAI-compatible** endpoint (OpenRouter / LiteLLM / DeepSeek / Groq / Together / Mistral …), **Azure OpenAI**, or **Google Gemini** — pick the provider in Settings, paste a key, go. AWS Bedrock and GCP Vertex Anthropic models work the same way through a server-side LiteLLM (or equivalent) proxy pointed at the `Anthropic` provider, since SigV4 / GCP JWT signing belongs on the server, not the browser. The daemon scans your `PATH` for [`claude`](https://docs.anthropic.com/en/docs/claude-code), [`codex`](https://github.com/openai/codex), [`cursor-agent`](https://www.cursor.com/cli), [`gemini`](https://github.com/google-gemini/gemini-cli), [`opencode`](https://opencode.ai/), and [`qwen`](https://github.com/QwenLM/qwen-code) on startup. Whichever it finds becomes the design engine — driven via stdio, with one adapter per CLI. Inspired by [`multica`](https://github.com/multica-ai/multica) and [`cc-switch`](https://github.com/farion1231/cc-switch). No CLI? `Anthropic API · BYOK` is the same pipeline minus the spawn.
### 2 · Skills are files, not plugins. ### 2 · Skills are files, not plugins.
@@ -227,10 +227,8 @@ Every layer is composable. Every layer is a file you can edit. Read [`src/prompt
│ /api/* (proxied in dev) │ direct (BYOK) │ /api/* (proxied in dev) │ direct (BYOK)
▼ ▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Local daemon │ │ Hosted-API router │ Local daemon │ │ Anthropic SDK
│ (Express + SQLite) │ │ Anthropic · OpenAI- │ (Express + SQLite) │ │ (browser fallback)
│ │ │ compatible · Azure │
│ │ │ · Google Gemini │
│ │ └──────────────────────┘ │ │ └──────────────────────┘
│ /api/agents │ │ /api/agents │
│ /api/skills │ │ /api/skills │
@@ -270,7 +268,7 @@ The first load:
1. Detects which agent CLIs you have on `PATH` and picks one automatically. 1. Detects which agent CLIs you have on `PATH` and picks one automatically.
2. Loads 19 skills + 71 design systems. 2. Loads 19 skills + 71 design systems.
3. Pops the welcome dialog so you can pick a hosted-API provider — **Anthropic**, **OpenAI-compatible** (OpenRouter / LiteLLM / DeepSeek / Groq / Together / Mistral / OpenAI), **Azure OpenAI**, or **Google Gemini** — and paste the matching key (only needed for the BYOK fallback path; for AWS Bedrock or GCP Vertex Anthropic models, run a server-side LiteLLM proxy and point the `Anthropic` provider at it). 3. Pops the welcome dialog so you can paste an Anthropic key (only needed for the BYOK fallback path).
4. **Auto-creates `./.od/`** — the local runtime folder for the SQLite project DB, per-project artifacts, and saved renders. There is no `od init` step; the daemon `mkdir`s everything it needs on boot. 4. **Auto-creates `./.od/`** — the local runtime folder for the SQLite project DB, per-project artifacts, and saved renders. There is no `od init` step; the daemon `mkdir`s everything it needs on boot.
Type a prompt, hit **Send**, watch the question form arrive, fill it, watch the todo card stream, watch the artifact render. Click **Save to disk** or download as a project ZIP. Type a prompt, hit **Send**, watch the question form arrive, fill it, watch the todo card stream, watch the artifact render. Click **Save to disk** or download as a project ZIP.
@@ -336,12 +334,7 @@ open-design/
│ │ └── zip.ts ← project archive │ │ └── zip.ts ← project archive
│ ├── providers/ │ ├── providers/
│ │ ├── daemon.ts ← /api/chat SSE stream consumer │ │ ├── daemon.ts ← /api/chat SSE stream consumer
│ │ ├── model.ts ← BYOK provider router (anthropic / openai / azure / google) │ │ ├── anthropic.ts ← BYOK Anthropic SDK path
│ │ ├── anthropic.ts ← Anthropic SDK path (also covers any Anthropic-compatible proxy)
│ │ ├── openai.ts ← OpenAI-compatible SSE (OpenRouter / LiteLLM / DeepSeek / Groq / Together)
│ │ ├── azure.ts ← Azure OpenAI deployment URLs + api-key header
│ │ ├── google.ts ← Google Generative Language streamGenerateContent
│ │ ├── presets.ts ← per-provider defaults shown in Settings
│ │ └── registry.ts ← /api/agents, /api/skills, /api/design-systems │ │ └── registry.ts ← /api/agents, /api/skills, /api/design-systems
│ └── state/ ← config + projects (localStorage + daemon-backed) │ └── state/ ← config + projects (localStorage + daemon-backed)
@@ -506,25 +499,10 @@ Auto-detected from `PATH` on daemon boot. No config required.
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | line-buffered | `gemini -p` | | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | line-buffered | `gemini -p` |
| [OpenCode](https://opencode.ai/) | `opencode` | line-buffered | `opencode run` | | [OpenCode](https://opencode.ai/) | `opencode` | line-buffered | `opencode run` |
| [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | line-buffered | `qwen -p` | | [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | line-buffered | `qwen -p` |
| Hosted API · BYOK | n/a | SSE direct | Browser fallback when no CLI is on PATH — pick any of the providers below | | Anthropic API · BYOK | n/a | SSE direct | Browser fallback when no CLI is on PATH |
Adding a new CLI is one entry in [`daemon/agents.js`](daemon/agents.js). Streaming format is one of `claude-stream-json` (typed events) or `plain` (raw text). Adding a new CLI is one entry in [`daemon/agents.js`](daemon/agents.js). Streaming format is one of `claude-stream-json` (typed events) or `plain` (raw text).
### Hosted-API providers (BYOK fallback)
When no CLI is detected, OD streams directly from the browser to a hosted endpoint. Pick one in **Settings → Hosted API**, paste a key, optionally tweak the base URL.
| Provider | Wire format | What it covers |
|---|---|---|
| **Anthropic** | `@anthropic-ai/sdk` | `api.anthropic.com`, plus any Anthropic-compatible proxy (LiteLLM, custom gateways, **AWS Bedrock** & **GCP Vertex** via a server-side proxy) |
| **OpenAI-compatible** | `/chat/completions` SSE | OpenAI proper, [OpenRouter](https://openrouter.ai), [LiteLLM proxy](https://docs.litellm.ai/), [DeepSeek](https://platform.deepseek.com/), [Groq](https://groq.com/), [Together](https://together.ai/), [Mistral](https://mistral.ai/), and any other OpenAI-shaped endpoint |
| **Azure OpenAI** | `/openai/deployments/<deployment>/chat/completions` SSE + `api-key` header | Azure-hosted OpenAI deployments. Base URL is the resource endpoint, Model is the deployment name, plus the Azure `api-version` |
| **Google Gemini** | `:streamGenerateContent?alt=sse` | Google Generative Language API direct (Gemini family) |
**On AWS Bedrock & GCP Vertex with Anthropic models:** Both require credential signing (SigV4 / GCP service-account JWT) which is unsafe to do from a browser with long-lived BYOK credentials. The recommended path is to run a server-side proxy ([LiteLLM](https://docs.litellm.ai/) works well — it speaks Anthropic-compatible *and* OpenAI-compatible) and point either the `Anthropic` or `OpenAI-compatible` provider at the proxy URL. The signing stays on the server where it belongs.
Adding a fifth wire format is mechanical: a row in `ModelProvider`, an entry in [`src/providers/presets.ts`](src/providers/presets.ts), a `stream<X>` function alongside [`anthropic.ts` / `openai.ts` / `azure.ts` / `google.ts`](src/providers/), one more `case` in [`src/providers/model.ts`](src/providers/model.ts).
## References & lineage ## References & lineage
Every external project this repo borrows from. Each link goes to the source so you can verify the provenance. Every external project this repo borrows from. Each link goes to the source so you can verify the provenance.
@@ -560,14 +538,6 @@ Phased delivery → [`docs/roadmap.md`](docs/roadmap.md).
This is an early implementation — the closed loop (detect → pick skill + design system → chat → parse `<artifact>` → preview → save) runs end-to-end. The prompt stack and skill library are where most of the value lives, and they're stable. The component-level UI is shipping daily. This is an early implementation — the closed loop (detect → pick skill + design system → chat → parse `<artifact>` → preview → save) runs end-to-end. The prompt stack and skill library are where most of the value lives, and they're stable. The component-level UI is shipping daily.
## Star us
<p align="center">
<a href="https://github.com/nexu-io/open-design"><img src="docs/assets/star-us.png" alt="Star Open Design on GitHub — github.com/nexu-io/open-design" width="100%" /></a>
</p>
If this saved you thirty minutes — give it a ★. Stars don't pay rent, but they tell the next designer, agent, and contributor that this experiment is worth their attention. One click, three seconds, real signal: [github.com/nexu-io/open-design](https://github.com/nexu-io/open-design).
## Contributing ## Contributing
Issues, PRs, new skills, and new design systems are all welcome. The highest-leverage contributions are usually one folder, one Markdown file, or one PR-sized adapter: Issues, PRs, new skills, and new design systems are all welcome. The highest-leverage contributions are usually one folder, one Markdown file, or one PR-sized adapter:
+7 -37
View File
@@ -39,7 +39,7 @@ OD 站在四个开源项目的肩膀上:
| | 你拿到的 | | | 你拿到的 |
|---|---| |---|---|
| **支持的 coding agent** | Claude Code · Codex CLI · Cursor Agent · Gemini CLI · OpenCode · Qwen Code · 托管 API · BYOK 兜底(Anthropic · OpenAI 兼容 · Azure · Google GeminiAWS Bedrock 与 GCP Vertex 通过代理接入 | | **支持的 coding agent** | Claude Code · Codex CLI · Cursor Agent · Gemini CLI · OpenCode · Qwen Code · Anthropic APIBYOK 兜底 |
| **内置 design system** | **71 套** —— 2 套手写起手 + 69 套从 [`awesome-design-md`][acd2] 导入的产品系统(Linear、Stripe、Vercel、Airbnb、Tesla、Notion、Anthropic、Apple、Cursor、Supabase、Figma…) | | **内置 design system** | **71 套** —— 2 套手写起手 + 69 套从 [`awesome-design-md`][acd2] 导入的产品系统(Linear、Stripe、Vercel、Airbnb、Tesla、Notion、Anthropic、Apple、Cursor、Supabase、Figma…) |
| **内置 skill** | **19 个** —— 原型 / deck / 移动端 / dashboard / pricing / docs / blog / SaaS landing,外加 10 个文档与办公产物模板(PM 规范、周报、OKR、runbook、看板…) | | **内置 skill** | **19 个** —— 原型 / deck / 移动端 / dashboard / pricing / docs / blog / SaaS landing,外加 10 个文档与办公产物模板(PM 规范、周报、OKR、runbook、看板…) |
| **视觉方向** | 5 套精选流派(Editorial Monocle · Modern Minimal · Tech Utility · Brutalist · Soft Warm),每一套自带 OKLch 色板 + 字体栈 | | **视觉方向** | 5 套精选流派(Editorial Monocle · Modern Minimal · Tech Utility · Brutalist · Soft Warm),每一套自带 OKLch 色板 + 字体栈 |
@@ -180,7 +180,7 @@ OD 站在四个开源项目的肩膀上:
### 1 · 我们不带 agent,你的就够好 ### 1 · 我们不带 agent,你的就够好
Daemon 启动时扫 `PATH`,找 [`claude`](https://docs.anthropic.com/en/docs/claude-code)、[`codex`](https://github.com/openai/codex)、[`cursor-agent`](https://www.cursor.com/cli)、[`gemini`](https://github.com/google-gemini/gemini-cli)、[`opencode`](https://opencode.ai/)、[`qwen`](https://github.com/QwenLM/qwen-code)。哪个在就用哪个 —— 通过 stdio 驱动,每个 CLI 一个 adapter。灵感来自 [`multica`](https://github.com/multica-ai/multica) 和 [`cc-switch`](https://github.com/farion1231/cc-switch)。一个 CLI 都没有?`托管 API · BYOK` 就是同一条管线减去 spawn —— 浏览器直连 **Anthropic**、任意 **OpenAI 兼容**端点(OpenRouter / LiteLLM / DeepSeek / Groq / Together / Mistral …)、**Azure OpenAI**、或 **Google Gemini**。在 Settings 里选渠道、贴 Key、走起。AWS Bedrock 和 GCP Vertex 上的 Anthropic 模型按同样的方式接入:服务端跑一个 LiteLLM(或同类)代理,再把 `Anthropic` 渠道指向它 —— SigV4 / GCP JWT 签名应留在服务器,不放进浏览器 Daemon 启动时扫 `PATH`,找 [`claude`](https://docs.anthropic.com/en/docs/claude-code)、[`codex`](https://github.com/openai/codex)、[`cursor-agent`](https://www.cursor.com/cli)、[`gemini`](https://github.com/google-gemini/gemini-cli)、[`opencode`](https://opencode.ai/)、[`qwen`](https://github.com/QwenLM/qwen-code)。哪个在就用哪个 —— 通过 stdio 驱动,每个 CLI 一个 adapter。灵感来自 [`multica`](https://github.com/multica-ai/multica) 和 [`cc-switch`](https://github.com/farion1231/cc-switch)。一个 CLI 都没有?`Anthropic API · BYOK` 就是同一条管线减去 spawn。
### 2 · Skill 是文件,不是插件 ### 2 · Skill 是文件,不是插件
@@ -227,10 +227,8 @@ DISCOVERY 指令 turn-1 表单、turn-2 品牌分支、TodoWrite、
│ /api/* dev 走代理) │ direct (BYOK) │ /api/* dev 走代理) │ direct (BYOK)
▼ ▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ 本地 daemon │ │ 托管 API 路由器 │ 本地 daemon │ │ Anthropic SDK
Express + SQLite)│ │ Anthropic · OpenAI Express + SQLite)│ │ (浏览器兜底)
│ │ │ 兼容 · Azure · Google│
│ │ │ Gemini │
│ │ └──────────────────────┘ │ │ └──────────────────────┘
│ /api/agents │ │ /api/agents │
│ /api/skills │ │ /api/skills │
@@ -270,7 +268,7 @@ open http://localhost:5173
1. 检测你 `PATH` 上有哪些 agent CLI,自动选一个。 1. 检测你 `PATH` 上有哪些 agent CLI,自动选一个。
2. 加载 19 个 skill + 71 套 design system。 2. 加载 19 个 skill + 71 套 design system。
3. 弹欢迎对话框,让你挑一个托管 API 渠道 —— **Anthropic**、**OpenAI 兼容**OpenRouter / LiteLLM / DeepSeek / Groq / Together / Mistral / OpenAI)、**Azure OpenAI** 或 **Google Gemini** —— 并贴上对应的 Key(仅 BYOK 兜底路径需要;要在浏览器里调 AWS Bedrock / GCP Vertex 上的 Anthropic 模型,建议在服务器跑一个 LiteLLM 代理,再把 `Anthropic` 渠道指向它)。 3. 弹欢迎对话框,让你贴 Anthropic key(仅 BYOK 兜底路径需要)。
4. **自动创建 `./.od/`** —— 本地运行时目录,存放 SQLite 项目库、各项目工作区、保存下来的 artifact。**没有** `od init` 这一步,daemon 启动时会自己 `mkdir` 4. **自动创建 `./.od/`** —— 本地运行时目录,存放 SQLite 项目库、各项目工作区、保存下来的 artifact。**没有** `od init` 这一步,daemon 启动时会自己 `mkdir`
输入需求,回车,看 question form 跳出来,填,看 todo 卡片流动,看 artifact 渲染。点 **Save to disk** 或导出整个项目 ZIP。 输入需求,回车,看 question form 跳出来,填,看 todo 卡片流动,看 artifact 渲染。点 **Save to disk** 或导出整个项目 ZIP。
@@ -336,12 +334,7 @@ open-design/
│ │ └── zip.ts ← 项目打包 │ │ └── zip.ts ← 项目打包
│ ├── providers/ │ ├── providers/
│ │ ├── daemon.ts ← /api/chat SSE 流消费者 │ │ ├── daemon.ts ← /api/chat SSE 流消费者
│ │ ├── model.ts ← BYOK 渠道路由(anthropic / openai / azure / google │ │ ├── anthropic.ts ← BYOK Anthropic SDK 路径
│ │ ├── anthropic.ts ← Anthropic SDK 路径(也涵盖任意 Anthropic 兼容代理)
│ │ ├── openai.ts ← OpenAI 兼容 SSEOpenRouter / LiteLLM / DeepSeek / Groq / Together
│ │ ├── azure.ts ← Azure OpenAI 部署 URL + api-key 头
│ │ ├── google.ts ← Google Generative Language streamGenerateContent
│ │ ├── presets.ts ← Settings 中各渠道的默认值
│ │ └── registry.ts ← /api/agents、/api/skills、/api/design-systems │ │ └── registry.ts ← /api/agents、/api/skills、/api/design-systems
│ └── state/ ← config + projectslocalStorage + daemon 持久化) │ └── state/ ← config + projectslocalStorage + daemon 持久化)
@@ -506,25 +499,10 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | line-buffered | `gemini -p` | | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | line-buffered | `gemini -p` |
| [OpenCode](https://opencode.ai/) | `opencode` | line-buffered | `opencode run` | | [OpenCode](https://opencode.ai/) | `opencode` | line-buffered | `opencode run` |
| [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | line-buffered | `qwen -p` | | [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | line-buffered | `qwen -p` |
| 托管 API · BYOK | n/a | SSE 直连 | 没装任何 CLI 时的浏览器兜底 —— 在下方任意一个渠道里挑一个 | | Anthropic API · BYOK | n/a | SSE 直连 | 没装任何 CLI 时的浏览器兜底 |
加一个新 CLI = 在 [`daemon/agents.js`](daemon/agents.js) 里加一项。流式格式从 `claude-stream-json`(类型化事件)和 `plain`(原始文本)两种里选一个。 加一个新 CLI = 在 [`daemon/agents.js`](daemon/agents.js) 里加一项。流式格式从 `claude-stream-json`(类型化事件)和 `plain`(原始文本)两种里选一个。
### 托管 API 渠道(BYOK 兜底)
没检测到 CLI 时,OD 会从浏览器直连一个托管端点。在 **Settings → 托管 API** 里挑一个渠道,贴 Key,按需改 Base URL。
| 渠道 | 报文格式 | 覆盖范围 |
|---|---|---|
| **Anthropic** | `@anthropic-ai/sdk` | `api.anthropic.com`,以及任意 Anthropic 兼容代理(LiteLLM、自建网关、**AWS Bedrock** 与 **GCP Vertex** 通过服务端代理接入) |
| **OpenAI 兼容** | `/chat/completions` SSE | OpenAI 官方、[OpenRouter](https://openrouter.ai)、[LiteLLM 代理](https://docs.litellm.ai/)、[DeepSeek](https://platform.deepseek.com/)、[Groq](https://groq.com/)、[Together](https://together.ai/)、[Mistral](https://mistral.ai/),以及任意 OpenAI 形态的端点 |
| **Azure OpenAI** | `/openai/deployments/<deployment>/chat/completions` SSE + `api-key` 头 | Azure 托管的 OpenAI 部署。Base URL 是资源终结点,Model 是部署名,再加 Azure 的 `api-version` |
| **Google Gemini** | `:streamGenerateContent?alt=sse` | Google Generative Language API 直连(Gemini 系列) |
**关于 AWS Bedrock 与 GCP Vertex 上的 Anthropic 模型:**两者都需要凭证签名(SigV4 / GCP service-account JWT),用浏览器里长期存放的 BYOK 凭证去签是不安全的。推荐做法:在服务器端跑一个代理([LiteLLM](https://docs.litellm.ai/) 同时支持 Anthropic 兼容和 OpenAI 兼容),把 `Anthropic``OpenAI 兼容` 渠道的 Base URL 指向代理,签名留在服务器端。
加第五种报文格式很机械:`ModelProvider` 里加一行、[`src/providers/presets.ts`](src/providers/presets.ts) 里加一项、和 [`anthropic.ts` / `openai.ts` / `azure.ts` / `google.ts`](src/providers/) 一起放一个 `stream<X>` 函数、[`src/providers/model.ts`](src/providers/model.ts) 里再加一个 `case`
## 引用与师承 ## 引用与师承
每一个被借鉴的开源项目都列在这里。点链接可以验证师承。 每一个被借鉴的开源项目都列在这里。点链接可以验证师承。
@@ -560,14 +538,6 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。
这是一个早期实现 —— 闭环(检测 → 选 skill + design system → 对话 → 解析 `<artifact>` → 预览 → 保存)已经端到端跑通。提示词栈和 skill 库是价值最重的部分,目前已稳定。组件级 UI 仍在每天迭代。 这是一个早期实现 —— 闭环(检测 → 选 skill + design system → 对话 → 解析 `<artifact>` → 预览 → 保存)已经端到端跑通。提示词栈和 skill 库是价值最重的部分,目前已稳定。组件级 UI 仍在每天迭代。
## 给我们点个 Star
<p align="center">
<a href="https://github.com/nexu-io/open-design"><img src="docs/assets/star-us.png" alt="给 Open Design 点个 Star —— github.com/nexu-io/open-design" width="100%" /></a>
</p>
如果这套东西帮你省了半小时,给它一个 ★。Star 不付房租,但它告诉下一个设计师、Agent 和贡献者:这个实验值得他们的注意力。一次点击、三秒钟、真实信号:[github.com/nexu-io/open-design](https://github.com/nexu-io/open-design)。
## 贡献 ## 贡献
欢迎 issue、PR、新 skill、新 design system。收益最高的贡献往往就是一个文件夹、一份 Markdown,或者一个 PR 大小的 adapter: 欢迎 issue、PR、新 skill、新 design system。收益最高的贡献往往就是一个文件夹、一份 Markdown,或者一个 PR 大小的 adapter:
+9 -23
View File
@@ -7,12 +7,7 @@ import path from 'node:path';
const execFileP = promisify(execFile); const execFileP = promisify(execFile);
// Each entry defines how to invoke the agent in non-interactive "one-shot" mode. // Each entry defines how to invoke the agent in non-interactive "one-shot" mode.
// `buildArgs(prompt, imagePaths, extraAllowedDirs)` returns argv for the child // `buildArgs(prompt, imagePaths)` returns argv for the child process.
// process. `extraAllowedDirs` is a list of absolute directories the agent must
// be permitted to read files from (skill seeds, design-system specs) that live
// outside the project cwd. Currently only Claude Code wires this through
// (`--add-dir`); other agents either inherit broader access or run with cwd
// boundaries we can't widen via flags.
// `streamFormat` hints to the daemon how to interpret stdout: // `streamFormat` hints to the daemon how to interpret stdout:
// - 'claude-stream-json' : line-delimited JSON emitted by Claude Code's // - 'claude-stream-json' : line-delimited JSON emitted by Claude Code's
// `--output-format stream-json`. Daemon parses it into typed events // `--output-format stream-json`. Daemon parses it into typed events
@@ -24,23 +19,14 @@ export const AGENT_DEFS = [
name: 'Claude Code', name: 'Claude Code',
bin: 'claude', bin: 'claude',
versionArgs: ['--version'], versionArgs: ['--version'],
buildArgs: (prompt, _imagePaths, extraAllowedDirs = []) => { buildArgs: (prompt) => [
const args = [ '-p',
'-p', prompt,
prompt, '--output-format',
'--output-format', 'stream-json',
'stream-json', '--verbose',
'--verbose', '--include-partial-messages',
'--include-partial-messages', ],
];
const dirs = (extraAllowedDirs || []).filter(
(d) => typeof d === 'string' && d.length > 0,
);
if (dirs.length > 0) {
args.push('--add-dir', ...dirs);
}
return args;
},
streamFormat: 'claude-stream-json', streamFormat: 'claude-stream-json',
}, },
{ {
+1 -11
View File
@@ -769,17 +769,7 @@ export async function startServer({ port = 7456 } = {}) {
safeImages.length ? `\n\n${safeImages.map((p) => `@${p}`).join(' ')}` : '', safeImages.length ? `\n\n${safeImages.map((p) => `@${p}`).join(' ')}` : '',
].join(''); ].join('');
// Skill seeds (`skills/<id>/assets/template.html`) and design-system const args = def.buildArgs(composed, safeImages);
// specs (`design-systems/<id>/DESIGN.md`) live outside the project cwd.
// The composed system prompt asks the agent to Read them via absolute
// paths in the skill-root preamble — without an explicit allowlist,
// Claude Code blocks those reads (issue #6: "no permission to read
// skills template"). We surface both roots so any agent that honours
// `--add-dir` can resolve those side files.
const extraAllowedDirs = [SKILLS_DIR, DESIGN_SYSTEMS_DIR].filter(
(d) => fs.existsSync(d),
);
const args = def.buildArgs(composed, safeImages, extraAllowedDirs);
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Cache-Control', 'no-cache, no-transform');
-743
View File
@@ -1,743 +0,0 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>Open Design — Star us on GitHub</title>
<link rel='preconnect' href='https://fonts.googleapis.com'>
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>
<link href='https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,500;0,9..144,700;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600&display=swap' rel='stylesheet'>
<style>
:root {
--bg: #0d0a06;
--bg-2: #14100a;
--ink: #f3ead8;
--muted: #9b8f78;
--rule: #4a3f2c;
--accent: #f0833f;
--accent-2: #e85a2c;
--gold: #ffc83d;
--gold-2: #ffb000;
--paper: #f6efdd;
--gh-bg: #0d1117;
--gh-fg: #e6edf3;
--gh-muted: #8b949e;
--gh-border: #30363d;
--gh-border-2: #21262d;
--gh-btn: #21262d;
--gh-btn-hover: #30363d;
--gh-pill: #1f2328;
--gh-link: #4493f8;
--gh-tag-bg: #15295a;
--gh-tag-fg: #79c0ff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
background: var(--bg);
color: var(--ink);
font-family: 'Inter', -apple-system, system-ui, sans-serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background:
radial-gradient(1200px 760px at 80% 18%, rgba(255,200,61,0.10), transparent 70%),
radial-gradient(900px 600px at 12% 80%, rgba(240,131,63,0.06), transparent 70%),
linear-gradient(180deg, #0e0b07 0%, #0a0805 100%);
padding: 56px 72px;
display: flex;
flex-direction: column;
}
.mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding-bottom: 22px;
border-bottom: 1px solid var(--rule);
}
header .l { text-align: left; }
header .r { text-align: right; }
header .center {
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 500;
font-size: 22px;
color: var(--ink);
letter-spacing: 0.01em;
}
header .center .star {
color: var(--gold);
margin-right: 6px;
font-size: 20px;
vertical-align: 1px;
}
main {
flex: 1;
display: grid;
grid-template-columns: 1.05fr 1fr;
column-gap: 64px;
padding-top: 56px;
align-items: stretch;
}
/* --------- LEFT: editorial copy --------- */
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.eyebrow {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 28px;
}
.pill {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.22em;
color: #f6e9d4;
background: rgba(240,131,63,0.16);
border: 1px solid rgba(240,131,63,0.45);
padding: 6px 12px;
border-radius: 999px;
text-transform: uppercase;
}
.pill.alt {
color: var(--muted);
background: transparent;
border-color: var(--rule);
}
.pill.gold {
color: #2a1a05;
background: linear-gradient(180deg, var(--gold), var(--gold-2));
border: 1px solid rgba(255,200,61,0.85);
}
h1 {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 100px;
line-height: 0.94;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 32px 0;
}
h1 em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
h1 .star-glyph {
color: var(--gold);
font-style: normal;
font-weight: 500;
display: inline-block;
transform: translateY(8px);
text-shadow:
0 0 24px rgba(255,200,61,0.55),
0 0 60px rgba(255,176,0,0.35);
}
h1 .underline-accent {
position: relative;
display: inline-block;
}
h1 .underline-accent::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -6px;
height: 6px;
background: var(--accent);
border-radius: 4px;
opacity: 0.85;
}
.lede {
font-family: 'Fraunces', serif;
font-weight: 300;
font-size: 22px;
line-height: 1.5;
color: #d8cdb6;
max-width: 640px;
margin: 0 0 28px 0;
}
.lede b { font-weight: 500; color: #f3ead8; }
.url-card {
display: inline-flex;
align-items: center;
gap: 18px;
padding: 16px 22px;
border: 1px solid var(--rule);
border-radius: 14px;
background: rgba(255,255,255,0.02);
margin-bottom: 36px;
box-shadow: 0 14px 30px rgba(0,0,0,0.35);
}
.url-card .arrow {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
}
.url-card .url {
font-family: 'JetBrains Mono', monospace;
font-size: 22px;
color: var(--ink);
letter-spacing: 0.01em;
}
.url-card .url b {
color: var(--accent);
font-weight: 500;
}
.url-card .copybtn {
margin-left: 8px;
border: 1px solid var(--rule);
color: var(--muted);
padding: 4px 10px;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.meta {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-top: 1px solid var(--rule);
padding-top: 22px;
}
.meta .cell {
border-right: 1px solid var(--rule);
padding-right: 18px;
}
.meta .cell:last-child { border-right: none; }
.meta .num {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 42px;
line-height: 1;
color: var(--ink);
letter-spacing: -0.01em;
}
.meta .num em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
.meta .num .gold {
color: var(--gold);
text-shadow: 0 0 18px rgba(255,200,61,0.45);
}
.meta .lbl {
margin-top: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
letter-spacing: 0.18em;
color: var(--muted);
text-transform: uppercase;
line-height: 1.4;
}
/* --------- RIGHT: GitHub mock --------- */
.right {
position: relative;
}
.stage {
position: absolute;
inset: 0;
}
.window {
position: absolute;
top: 130px;
left: 0;
right: 0;
background: var(--gh-bg);
color: var(--gh-fg);
border-radius: 16px;
overflow: hidden;
border: 1px solid var(--gh-border);
box-shadow:
0 1px 0 rgba(255,255,255,0.04) inset,
0 24px 60px rgba(0,0,0,0.55),
0 6px 18px rgba(0,0,0,0.45);
transform: rotate(-1deg);
}
.winbar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--gh-border);
background: linear-gradient(180deg, #15191e, #0f1216);
}
.winbar .dots { display: flex; gap: 6px; margin-right: 8px; }
.winbar .dot { width: 12px; height: 12px; border-radius: 50%; background: #3a3a3a; }
.winbar .dot.r { background: #ff5f57; }
.winbar .dot.y { background: #febc2e; }
.winbar .dot.g { background: #28c840; }
.winbar .urlbar {
flex: 1;
background: #161b22;
border: 1px solid var(--gh-border);
border-radius: 8px;
padding: 6px 12px;
color: var(--gh-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.winbar .urlbar .lock { color: #6e7681; font-size: 10px; }
.winbar .urlbar b { color: var(--gh-fg); font-weight: 500; }
.ghnav {
display: flex;
align-items: center;
gap: 18px;
padding: 10px 22px;
border-bottom: 1px solid var(--gh-border);
background: var(--gh-bg);
font-size: 13px;
color: var(--gh-fg);
}
.ghnav .logo { width: 22px; height: 22px; }
.ghnav .crumbs { color: var(--gh-fg); font-weight: 500; }
.ghnav .crumbs .slash { color: var(--gh-muted); margin: 0 6px; font-weight: 300; }
.ghnav .crumbs .repo { color: var(--gh-link); }
.ghnav .private {
margin-left: 8px;
font-size: 11px;
color: var(--gh-muted);
border: 1px solid var(--gh-border);
border-radius: 999px;
padding: 2px 10px;
}
.ghhead {
padding: 22px 26px 6px;
}
.ghhead .row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: nowrap;
}
.ghhead .row > .repotitle { min-width: 0; flex-shrink: 1; }
.repotitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 500;
white-space: nowrap;
}
.repotitle .icon { color: var(--gh-muted); }
.repotitle .org { color: var(--gh-link); }
.repotitle .name { color: var(--gh-link); font-weight: 600; }
.repotitle .sep { color: var(--gh-muted); font-weight: 300; }
.repotitle .badge {
margin-left: 10px;
border: 1px solid var(--gh-border);
color: var(--gh-muted);
border-radius: 999px;
padding: 2px 10px;
font-size: 11px;
}
.actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.ghbtn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--gh-btn);
border: 1px solid var(--gh-border);
color: var(--gh-fg);
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
line-height: 1.2;
}
.ghbtn .icon { color: var(--gh-muted); width: 14px; height: 14px; }
.ghbtn .count {
margin-left: 4px;
padding: 1px 6px;
background: var(--gh-pill);
border-radius: 999px;
color: var(--gh-fg);
font-weight: 500;
font-size: 11px;
}
.ghbtn .caret {
width: 0; height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid var(--gh-muted);
margin-left: 6px;
}
.star-wrap {
position: relative;
padding: 6px;
margin: -6px;
border: 2px dashed var(--accent-2);
border-radius: 12px;
box-shadow:
0 0 0 4px rgba(232,90,44,0.10),
0 0 36px rgba(255,200,61,0.18);
background: rgba(232,90,44,0.04);
}
.ghbtn.star {
background: linear-gradient(180deg, #1c1f25, #14171c);
border-color: var(--gh-border);
}
.ghbtn.star .icon { color: var(--gold); }
.ghbtn.star .label { color: var(--gh-fg); font-weight: 600; }
/* CTA arrow & note pointing at the star */
.point {
position: absolute;
z-index: 10;
pointer-events: none;
}
.point .note {
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 500;
font-size: 28px;
color: var(--accent);
line-height: 1.15;
text-shadow: 0 6px 18px rgba(0,0,0,0.55);
}
.point .note .gold { color: var(--gold); }
.point .sub {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
margin-top: 8px;
}
.point.tap {
top: -20px;
right: 60px;
text-align: right;
}
.arrow-svg {
position: absolute;
overflow: visible;
z-index: 9;
pointer-events: none;
}
/* floating sparkle */
.sparkle {
position: absolute;
color: var(--gold);
text-shadow: 0 0 16px rgba(255,200,61,0.6);
font-size: 22px;
}
.stamp {
position: absolute;
bottom: 30px;
right: 0;
width: 110px;
height: 110px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffe27a, var(--gold-2) 80%);
color: #2a1a05;
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 700;
font-size: 14px;
line-height: 1.15;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(8deg);
box-shadow: 0 14px 30px rgba(255,176,0,0.45);
z-index: 5;
}
.stamp .big { font-size: 28px; line-height: 1; display: block; margin-bottom: 2px; }
/* secondary stat strip below window */
.ghbody {
display: grid;
grid-template-columns: 1fr 220px;
gap: 0;
padding: 18px 26px 22px;
border-top: 1px solid var(--gh-border);
background: var(--gh-bg);
}
.ghbody .desc {
color: var(--gh-fg);
font-size: 13px;
line-height: 1.55;
}
.ghbody .desc .em { color: var(--gh-link); }
.ghbody .topics {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.topic {
font-size: 11px;
color: var(--gh-tag-fg);
background: var(--gh-tag-bg);
padding: 2px 10px;
border-radius: 999px;
}
.ghbody .stats {
font-size: 12px;
color: var(--gh-muted);
text-align: right;
}
.ghbody .stats .row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
margin-bottom: 6px;
}
.ghbody .stats .row .icon { color: var(--gh-muted); }
.ghbody .stats .row b { color: var(--gh-fg); font-weight: 500; }
footer {
margin-top: 48px;
padding-top: 22px;
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
}
footer .l { text-align: left; }
footer .r { text-align: right; }
footer .c {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.28em;
color: var(--muted);
text-transform: uppercase;
}
</style>
</head>
<body>
<header>
<div class='mono l'>Open Design · Community · 2026 Edition</div>
<div class='center'><span class='star'></span>open.design</div>
<div class='mono r'>Cover · 03 / 08 · Star Us</div>
</header>
<main>
<section class='left'>
<div>
<div class='eyebrow'>
<span class='pill gold'>★ STAR US</span>
<span class='pill alt'>Open Source · Apache 2.0</span>
</div>
<h1>
If this saved<br/>
you <em>thirty</em><br/>
minutes,<br/>
give it a <span class='star-glyph'></span>.
</h1>
<p class='lede'>
Open Design is built and maintained in the open. <b>Stars don't pay rent —</b>
but they tell the next designer, agent, and contributor that this experiment
is worth their attention. One click, three seconds, real signal.
</p>
<div class='url-card'>
<span class='arrow'>· DROP BY →</span>
<span class='url'>github.com/<b>nexu-io/open-design</b></span>
<span class='copybtn'>· COPY</span>
</div>
</div>
<div class='meta'>
<div class='cell'>
<div class='num'>71</div>
<div class='lbl'>Design<br/>Systems</div>
</div>
<div class='cell'>
<div class='num'>19</div>
<div class='lbl'>Composable<br/>Skills</div>
</div>
<div class='cell'>
<div class='num'>06</div>
<div class='lbl'>Coding<br/>Agents</div>
</div>
<div class='cell'>
<div class='num'><span class='gold'></span></div>
<div class='lbl'>One click =<br/>one signal</div>
</div>
</div>
</section>
<!-- RIGHT: stylized GitHub repo header -->
<section class='right'>
<div class='stage'>
<!-- Pointing note above the star -->
<div class='point tap'>
<div class='note'>Tap the <span class='gold'></span> Star<br/>top-right.</div>
<div class='sub'>· three seconds · one click ·</div>
</div>
<!-- arc arrow from the note down to the Star button -->
<svg class='arrow-svg' style='top:30px; right:30px; width:200px; height:200px;' viewBox='0 0 200 200' fill='none'>
<path d='M 30 60 C 60 90, 120 120, 168 168' stroke='#f0833f' stroke-width='2.5' stroke-linecap='round' fill='none' stroke-dasharray='2 8'/>
<path d='M 152 156 L 168 170 L 162 150' stroke='#f0833f' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/>
</svg>
<!-- sparkles -->
<span class='sparkle' style='top:200px; right:30px; font-size:18px;'></span>
<span class='sparkle' style='top:260px; right:120px; font-size:14px; opacity:.7;'></span>
<span class='sparkle' style='top:150px; right:240px; font-size:12px; opacity:.55;'></span>
<div class='window'>
<div class='winbar'>
<div class='dots'>
<span class='dot r'></span><span class='dot y'></span><span class='dot g'></span>
</div>
<div class='urlbar'>
<span class='lock'>🔒</span>
<span>github.com/<b>nexu-io/open-design</b></span>
</div>
</div>
<div class='ghnav'>
<svg class='logo' viewBox='0 0 16 16' fill='currentColor'>
<path d='M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z'/>
</svg>
<div class='crumbs'>
<span class='org'>nexu-io</span>
<span class='slash'>/</span>
<span class='repo'>open-design</span>
</div>
<span class='private'>Public</span>
</div>
<div class='ghhead'>
<div class='row'>
<div class='repotitle'>
<svg class='icon' width='18' height='18' viewBox='0 0 16 16' fill='currentColor'>
<path fill-rule='evenodd' d='M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.692 1.72.75.75 0 0 1-1.034 1.084A2.5 2.5 0 0 1 2 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8zM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25z'/>
</svg>
<span class='org'>nexu-io</span>
<span class='sep'>/</span>
<span class='name'>open-design</span>
<span class='badge'>Public</span>
</div>
<div class='actions'>
<span class='ghbtn'>
<svg class='icon' viewBox='0 0 16 16' fill='currentColor'><path d='M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.756l8.61-8.61Z'/></svg>
<span>Edit Pins</span>
<span class='caret'></span>
</span>
<span class='ghbtn'>
<svg class='icon' viewBox='0 0 16 16' fill='currentColor'><path d='M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.678 1.367-1.932 2.637-3.023C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z'/></svg>
<span>Watch</span>
<span class='count'>0</span>
<span class='caret'></span>
</span>
<span class='ghbtn'>
<svg class='icon' viewBox='0 0 16 16' fill='currentColor'><path d='M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z'/></svg>
<span>Fork</span>
<span class='count'>0</span>
<span class='caret'></span>
</span>
<span class='star-wrap'>
<span class='ghbtn star'>
<svg class='icon' viewBox='0 0 16 16' fill='currentColor'><path d='M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z'/></svg>
<span class='label'>Star</span>
<span class='count'>1</span>
<span class='caret'></span>
</span>
</span>
</div>
</div>
</div>
<div class='ghbody'>
<div>
<div class='desc'>
<span class='em'></span> Local-first open replica of Anthropic's <b>Claude Design</b>.
⚡ 19 Skills · ✶ 71 brand-grade Design Systems · ⛁ sandboxed preview ·
⇩ HTML / PDF / PPTX export. Runs on Claude Code · Codex · Cursor · Gemini CLI · OpenCode · Qwen.
</div>
<div class='topics'>
<span class='topic'>react</span>
<span class='topic'>design</span>
<span class='topic'>design-systems</span>
<span class='topic'>typescript</span>
<span class='topic'>skills</span>
<span class='topic'>cursor</span>
<span class='topic'>local-first</span>
<span class='topic'>byok</span>
<span class='topic'>claude</span>
<span class='topic'>ai-agents</span>
</div>
</div>
<div class='stats'>
<div class='row'>
<span>Apache 2.0</span>
<svg class='icon' width='14' height='14' viewBox='0 0 16 16' fill='currentColor'><path d='M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Z'/></svg>
</div>
<div class='row'><b>3</b> Commits</div>
<div class='row'><b>2</b> Branches</div>
<div class='row'><b>0</b> Tags</div>
<div class='row'><b>0</b> Issues</div>
<div class='row' style='color:var(--gold);'><b style='color:var(--gold);'>1</b> Star · be the next</div>
</div>
</div>
</div>
<div class='stamp'>
<span><span class='big'></span>STAR<br/>US!</span>
</div>
</div>
</section>
</main>
<footer>
<div class='mono l'>Local-first · BYOK · Apache 2.0</div>
<div class='c'>· git clone · pnpm install · pnpm dev ·</div>
<div class='mono r'>github.com/nexu-io/open-design</div>
</footer>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

+1 -1
View File
@@ -11,7 +11,7 @@
"scripts": { "scripts": {
"daemon": "node daemon/cli.js --no-open", "daemon": "node daemon/cli.js --no-open",
"dev": "vite", "dev": "vite",
"dev:all": "node scripts/dev-all.mjs", "dev:all": "concurrently -k -n daemon,web -c cyan,magenta \"npm:daemon\" \"npm:dev\"",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc -b --noEmit", "typecheck": "tsc -b --noEmit",
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env node
// Launcher for `npm run dev:all`.
//
// Probes for free ports for the daemon (OD_PORT, default 7456) and the Vite
// dev server (VITE_PORT, default 5173) before spawning `concurrently`, so a
// stray process holding either port doesn't kill the whole boot. The
// resolved ports are exported into the child env, which means:
// * the daemon's cli.js sees the new OD_PORT and binds to it
// * vite.config.ts reads the same OD_PORT and points its /api proxy at
// the daemon's actual port
// * Vite itself binds to VITE_PORT
//
// If a port is busy we walk forward up to PORT_SEARCH_RANGE steps and log
// the switch so the user notices.
import { spawn } from 'node:child_process';
import net from 'node:net';
const HOST = '127.0.0.1';
const PORT_SEARCH_RANGE = 50;
function isPortFree(port, host = HOST) {
return new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.once('error', () => resolve(false));
server.listen({ port, host, exclusive: true }, () => {
server.close(() => resolve(true));
});
});
}
async function findFreePort(start, label) {
for (let port = start; port < start + PORT_SEARCH_RANGE; port++) {
if (await isPortFree(port)) return port;
}
throw new Error(
`[dev:all] could not find a free ${label} port near ${start} (tried ${PORT_SEARCH_RANGE})`,
);
}
const desiredDaemon = Number(process.env.OD_PORT) || 7456;
const desiredVite = Number(process.env.VITE_PORT) || 5173;
const daemonPort = await findFreePort(desiredDaemon, 'daemon');
const vitePort = await findFreePort(desiredVite, 'vite');
if (daemonPort !== desiredDaemon) {
console.log(
`[dev:all] daemon port ${desiredDaemon} is busy, switching to ${daemonPort}`,
);
}
if (vitePort !== desiredVite) {
console.log(
`[dev:all] vite port ${desiredVite} is busy, switching to ${vitePort}`,
);
}
const env = {
...process.env,
OD_PORT: String(daemonPort),
VITE_PORT: String(vitePort),
};
// We spawn the local `concurrently` bin via shell so Windows .cmd shims
// resolve correctly. The `npm:daemon` / `npm:dev` shorthand runs the
// matching package.json scripts, so any future tweak to those scripts is
// picked up automatically.
const child = spawn(
'concurrently',
['-k', '-n', 'daemon,web', '-c', 'cyan,magenta', 'npm:daemon', 'npm:dev'],
{ env, stdio: 'inherit', shell: true },
);
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
else process.exit(code ?? 0);
});
for (const sig of ['SIGINT', 'SIGTERM']) {
process.on(sig, () => {
if (!child.killed) child.kill(sig);
});
}
-1
View File
@@ -116,7 +116,6 @@ export function App() {
const withOnboarding: AppConfig = { ...next, onboardingCompleted: true }; const withOnboarding: AppConfig = { ...next, onboardingCompleted: true };
saveConfig(withOnboarding); saveConfig(withOnboarding);
setConfig(withOnboarding); setConfig(withOnboarding);
setSettingsOpen(false);
}, []); }, []);
const handleModeChange = useCallback( const handleModeChange = useCallback(
+2 -3
View File
@@ -1,6 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useT } from '../i18n'; import { useT } from '../i18n';
import { providerLabel } from '../providers/presets';
import { AgentIcon } from './AgentIcon'; import { AgentIcon } from './AgentIcon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import type { AgentInfo, AppConfig, ExecMode } from '../types'; import type { AgentInfo, AppConfig, ExecMode } from '../types';
@@ -83,11 +82,11 @@ export function AvatarMenu({
<span className="who"> <span className="who">
{config.mode === 'daemon' {config.mode === 'daemon'
? t('avatar.localCli') ? t('avatar.localCli')
: providerLabel(config.provider)} : t('avatar.anthropicApi')}
</span> </span>
<span className="where"> <span className="where">
{config.mode === 'api' {config.mode === 'api'
? `${config.model}${config.baseUrl ? ` · ${safeHost(config.baseUrl)}` : ''}` ? safeHost(config.baseUrl)
: currentAgent : currentAgent
? `${currentAgent.name}${currentAgent.version ? ` · ${currentAgent.version}` : ''}` ? `${currentAgent.name}${currentAgent.version ? ` · ${currentAgent.version}` : ''}`
: t('avatar.noAgentSelected')} : t('avatar.noAgentSelected')}
+3 -5
View File
@@ -1,6 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useT } from '../i18n'; import { useT } from '../i18n';
import { providerLabel } from '../providers/presets';
import type { import type {
AgentInfo, AgentInfo,
AppConfig, AppConfig,
@@ -83,17 +82,16 @@ export function EntryView({
const envMetaLine = useMemo(() => { const envMetaLine = useMemo(() => {
if (config.mode === 'api') { if (config.mode === 'api') {
const provider = providerLabel(config.provider);
try { try {
return `${provider} · ${config.model} · ${new URL(config.baseUrl).host}`; return `${config.model} · ${new URL(config.baseUrl).host}`;
} catch { } catch {
return `${provider} · ${config.model}`; return config.model;
} }
} }
return currentAgent return currentAgent
? `${currentAgent.name}${currentAgent.version ? ` · ${currentAgent.version}` : ''}` ? `${currentAgent.name}${currentAgent.version ? ` · ${currentAgent.version}` : ''}`
: t('settings.noAgentSelected'); : t('settings.noAgentSelected');
}, [config.mode, config.model, config.baseUrl, config.provider, currentAgent, t]); }, [config.mode, config.model, config.baseUrl, currentAgent, t]);
// 'Use this prompt' on an example card is a fast path — skip the form and // 'Use this prompt' on an example card is a fast path — skip the form and
// create the project immediately with sane defaults derived from the skill, // create the project immediately with sane defaults derived from the skill,
+2 -2
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createArtifactParser } from '../artifacts/parser'; import { createArtifactParser } from '../artifacts/parser';
import { useT } from '../i18n'; import { useT } from '../i18n';
import { streamMessage } from '../providers/anthropic';
import { streamViaDaemon } from '../providers/daemon'; import { streamViaDaemon } from '../providers/daemon';
import { streamModel } from '../providers/model';
import { import {
fetchDesignSystem, fetchDesignSystem,
fetchProjectFiles, fetchProjectFiles,
@@ -501,7 +501,7 @@ export function ProjectView({
}); });
} else { } else {
pushEvent({ kind: 'status', label: 'requesting', detail: config.model }); pushEvent({ kind: 'status', label: 'requesting', detail: config.model });
void streamModel(config, systemPrompt, nextHistory, controller.signal, { void streamMessage(config, systemPrompt, nextHistory, controller.signal, {
onDelta: (delta) => { onDelta: (delta) => {
handlers.onDelta(delta); handlers.onDelta(delta);
handlers.onAgentEvent({ kind: 'text', text: delta }); handlers.onAgentEvent({ kind: 'text', text: delta });
+15 -79
View File
@@ -1,9 +1,8 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { LOCALE_LABEL, LOCALES, useI18n } from '../i18n'; import { LOCALE_LABEL, LOCALES, useI18n } from '../i18n';
import type { Locale } from '../i18n'; import type { Locale } from '../i18n';
import { PROVIDER_ORDER, PROVIDER_PRESETS } from '../providers/presets';
import { AgentIcon } from './AgentIcon'; import { AgentIcon } from './AgentIcon';
import type { AgentInfo, AppConfig, ExecMode, ModelProvider } from '../types'; import type { AgentInfo, AppConfig, ExecMode } from '../types';
interface Props { interface Props {
initial: AppConfig; initial: AppConfig;
@@ -15,6 +14,12 @@ interface Props {
onRefreshAgents: () => void; onRefreshAgents: () => void;
} }
const SUGGESTED_MODELS = [
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
];
export function SettingsDialog({ export function SettingsDialog({
initial, initial,
agents, agents,
@@ -43,38 +48,10 @@ export function SettingsDialog({
const setMode = (mode: ExecMode) => setCfg((c) => ({ ...c, mode })); const setMode = (mode: ExecMode) => setCfg((c) => ({ ...c, mode }));
// Switching providers swaps in that provider's defaults, but preserves
// any non-empty values the user already typed — they may have a custom
// baseUrl (e.g. an OpenRouter URL while staying on the openai provider)
// they don't want clobbered. Empty fields fall back to the preset.
const setProvider = (provider: ModelProvider) => {
setCfg((c) => {
if (c.provider === provider) return c;
const preset = PROVIDER_PRESETS[provider];
return {
...c,
provider,
baseUrl: c.baseUrl?.trim() ? c.baseUrl : preset.baseUrl,
model: c.model?.trim() ? c.model : preset.defaultModel,
};
});
};
const activePreset = PROVIDER_PRESETS[cfg.provider];
const canSave = const canSave =
cfg.mode === 'daemon' cfg.mode === 'daemon'
? Boolean(cfg.agentId && agents.find((a) => a.id === cfg.agentId)?.available) ? Boolean(cfg.agentId && agents.find((a) => a.id === cfg.agentId)?.available)
: Boolean( : Boolean(cfg.apiKey.trim() && cfg.model.trim() && cfg.baseUrl.trim());
cfg.apiKey.trim() &&
cfg.model.trim() &&
// Azure has no global default base URL — require the user to
// paste their resource endpoint. Other providers ship a usable
// default so a blank field falls back to the preset.
(cfg.provider === 'azure'
? cfg.baseUrl.trim().length > 0
: true),
);
return ( return (
<div className="modal-backdrop" onClick={onClose}> <div className="modal-backdrop" onClick={onClose}>
@@ -210,41 +187,14 @@ export function SettingsDialog({
) : ( ) : (
<section className="settings-section"> <section className="settings-section">
<div className="section-head"> <div className="section-head">
<div> <h3>{t('settings.apiSection')}</h3>
<h3>{t('settings.apiSection')}</h3>
<p className="hint">{t('settings.providerHint')}</p>
</div>
</div>
<div
className="seg-control"
role="tablist"
aria-label={t('settings.providerLabel')}
>
{PROVIDER_ORDER.map((id) => {
const preset = PROVIDER_PRESETS[id];
const active = cfg.provider === id;
return (
<button
key={id}
type="button"
role="tab"
aria-selected={active}
className={'seg-btn' + (active ? ' active' : '')}
onClick={() => setProvider(id)}
title={preset.blurb}
>
<span className="seg-title">{preset.label}</span>
<span className="seg-meta">{preset.blurb}</span>
</button>
);
})}
</div> </div>
<label className="field"> <label className="field">
<span className="field-label">{t('settings.apiKey')}</span> <span className="field-label">{t('settings.apiKey')}</span>
<div className="field-row"> <div className="field-row">
<input <input
type={showApiKey ? 'text' : 'password'} type={showApiKey ? 'text' : 'password'}
placeholder={activePreset.apiKeyPlaceholder} placeholder="sk-ant-..."
value={cfg.apiKey} value={cfg.apiKey}
onChange={(e) => setCfg({ ...cfg, apiKey: e.target.value })} onChange={(e) => setCfg({ ...cfg, apiKey: e.target.value })}
autoFocus autoFocus
@@ -267,11 +217,10 @@ export function SettingsDialog({
type="text" type="text"
value={cfg.model} value={cfg.model}
list="suggested-models" list="suggested-models"
placeholder={activePreset.defaultModel}
onChange={(e) => setCfg({ ...cfg, model: e.target.value })} onChange={(e) => setCfg({ ...cfg, model: e.target.value })}
/> />
<datalist id="suggested-models"> <datalist id="suggested-models">
{activePreset.modelSuggestions.map((m) => ( {SUGGESTED_MODELS.map((m) => (
<option value={m} key={m} /> <option value={m} key={m} />
))} ))}
</datalist> </datalist>
@@ -281,26 +230,10 @@ export function SettingsDialog({
<input <input
type="text" type="text"
value={cfg.baseUrl} value={cfg.baseUrl}
placeholder={activePreset.baseUrl || 'https://...'}
onChange={(e) => setCfg({ ...cfg, baseUrl: e.target.value })} onChange={(e) => setCfg({ ...cfg, baseUrl: e.target.value })}
/> />
</label> </label>
{activePreset.needsApiVersion ? (
<label className="field">
<span className="field-label">{t('settings.apiVersion')}</span>
<input
type="text"
value={cfg.apiVersion ?? ''}
placeholder="2024-08-01-preview"
onChange={(e) =>
setCfg({ ...cfg, apiVersion: e.target.value })
}
/>
<span className="hint">{t('settings.apiVersionHint')}</span>
</label>
) : null}
<p className="hint">{t('settings.apiHint')}</p> <p className="hint">{t('settings.apiHint')}</p>
<p className="hint">{t('settings.proxyHint')}</p>
</section> </section>
)} )}
@@ -343,7 +276,10 @@ export function SettingsDialog({
type="button" type="button"
className="primary" className="primary"
disabled={!canSave} disabled={!canSave}
onClick={() => onSave(cfg)} onClick={() => {
onSave(cfg);
onClose();
}}
> >
{welcome ? t('settings.getStarted') : t('common.save')} {welcome ? t('settings.getStarted') : t('common.save')}
</button> </button>
+8 -16
View File
@@ -49,15 +49,15 @@ export const en: Dict = {
'settings.kicker': 'Settings', 'settings.kicker': 'Settings',
'settings.title': 'Execution & model', 'settings.title': 'Execution & model',
'settings.subtitle': 'settings.subtitle':
'Choose between a local code-agent CLI and a hosted model provider (BYOK). Anthropic, OpenAI-compatible (OpenRouter / LiteLLM / DeepSeek / Groq …), Azure OpenAI, and Google Gemini are supported. Your API key is stored only in this browser.', 'Choose between a local code-agent CLI and the Anthropic API (BYOK). Your API key is stored only in this browser.',
'settings.modeAria': 'Execution mode', 'settings.modeAria': 'Execution mode',
'settings.modeDaemon': 'Local CLI', 'settings.modeDaemon': 'Local CLI',
'settings.modeDaemonHelp': 'Run via a code-agent CLI on your machine', 'settings.modeDaemonHelp': 'Run via a code-agent CLI on your machine',
'settings.modeDaemonOffline': 'Daemon is not running', 'settings.modeDaemonOffline': 'Daemon is not running',
'settings.modeDaemonOfflineMeta': 'daemon offline', 'settings.modeDaemonOfflineMeta': 'daemon offline',
'settings.modeDaemonInstalledMeta': '{count} installed', 'settings.modeDaemonInstalledMeta': '{count} installed',
'settings.modeApi': 'Hosted API', 'settings.modeApi': 'Anthropic API',
'settings.modeApiMeta': 'BYOK · multi-provider', 'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Code agent', 'settings.codeAgent': 'Code agent',
'settings.codeAgentHint': 'settings.codeAgentHint':
'Detected by scanning your PATH. Pick the CLI you want generations to flow through.', 'Detected by scanning your PATH. Pick the CLI you want generations to flow through.',
@@ -65,7 +65,7 @@ export const en: Dict = {
'settings.rescanTitle': 'Re-scan PATH', 'settings.rescanTitle': 'Re-scan PATH',
'settings.noAgentsDetected': 'settings.noAgentsDetected':
'No agents detected yet. Install one of Claude Code, Codex, Gemini CLI, OpenCode, Cursor Agent, or Qwen, then click Rescan.', 'No agents detected yet. Install one of Claude Code, Codex, Gemini CLI, OpenCode, Cursor Agent, or Qwen, then click Rescan.',
'settings.apiSection': 'Model endpoint', 'settings.apiSection': 'Anthropic API',
'settings.apiKey': 'API key', 'settings.apiKey': 'API key',
'settings.showKey': 'Show key', 'settings.showKey': 'Show key',
'settings.hideKey': 'Hide key', 'settings.hideKey': 'Hide key',
@@ -75,19 +75,11 @@ export const en: Dict = {
'settings.baseUrl': 'Base URL', 'settings.baseUrl': 'Base URL',
'settings.apiHint': 'settings.apiHint':
'Calls go directly from this browser to the base URL you set. No proxy. The key never leaves localStorage.', 'Calls go directly from this browser to the base URL you set. No proxy. The key never leaves localStorage.',
'settings.providerLabel': 'Provider',
'settings.providerHint':
'Pick the wire format. Anthropic also covers any Anthropic-compatible proxy. OpenAI-compatible covers OpenRouter, LiteLLM, DeepSeek, Groq, Together, etc.',
'settings.apiVersion': 'API version',
'settings.apiVersionHint':
'Azure REST api-version (e.g. 2024-08-01-preview). Leave blank to use the default.',
'settings.proxyHint':
'Tip: For AWS Bedrock or Google Vertex with Anthropic models, run a server-side proxy (LiteLLM works well) and point the Anthropic provider at it — credential signing belongs on the server, not the browser.',
'settings.skipForNow': 'Skip for now', 'settings.skipForNow': 'Skip for now',
'settings.getStarted': 'Get started', 'settings.getStarted': 'Get started',
'settings.envConfigure': 'Configure execution mode', 'settings.envConfigure': 'Configure execution mode',
'settings.localCli': 'Local CLI', 'settings.localCli': 'Local CLI',
'settings.anthropicApi': 'Hosted API', 'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'no agent selected', 'settings.noAgentSelected': 'no agent selected',
'settings.language': 'Language', 'settings.language': 'Language',
'settings.languageHint': 'Switch the interface language. Saved to this browser.', 'settings.languageHint': 'Switch the interface language. Saved to this browser.',
@@ -208,9 +200,9 @@ export const en: Dict = {
'avatar.title': 'Account & settings', 'avatar.title': 'Account & settings',
'avatar.localCli': 'Local CLI', 'avatar.localCli': 'Local CLI',
'avatar.anthropicApi': 'Hosted API', 'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Use Local CLI', 'avatar.useLocal': 'Use Local CLI',
'avatar.useApi': 'Use hosted API', 'avatar.useApi': 'Use Anthropic API',
'avatar.codeAgent': 'Code agent', 'avatar.codeAgent': 'Code agent',
'avatar.rescan': 'Rescan PATH', 'avatar.rescan': 'Rescan PATH',
'avatar.settings': 'Settings', 'avatar.settings': 'Settings',
@@ -419,7 +411,7 @@ export const en: Dict = {
'agentPicker.modeChoose': 'Choose execution mode', 'agentPicker.modeChoose': 'Choose execution mode',
'agentPicker.localCli': 'Local CLI', 'agentPicker.localCli': 'Local CLI',
'agentPicker.daemonOff': 'daemon off', 'agentPicker.daemonOff': 'daemon off',
'agentPicker.byok': 'Hosted API · BYOK', 'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.selectAgent': 'Select a detected code-agent CLI', 'agentPicker.selectAgent': 'Select a detected code-agent CLI',
'agentPicker.noAgents': 'no agents on PATH', 'agentPicker.noAgents': 'no agents on PATH',
'agentPicker.notInstalled': 'not installed', 'agentPicker.notInstalled': 'not installed',
+8 -16
View File
@@ -49,22 +49,22 @@ export const zhCN: Dict = {
'settings.kicker': '设置', 'settings.kicker': '设置',
'settings.title': '执行模式与模型', 'settings.title': '执行模式与模型',
'settings.subtitle': 'settings.subtitle':
'在本机的代码代理 CLI 与托管模型 API(自带 Key)之间切换。支持 Anthropic、OpenAI 兼容(OpenRouter / LiteLLM / DeepSeek / Groq 等)、Azure OpenAI 与 Google Gemini。API Key 只保存在当前浏览器中。', '在本机的代码代理 CLI 与 Anthropic API(自带 Key)之间切换。API Key 只保存在当前浏览器中。',
'settings.modeAria': '执行模式', 'settings.modeAria': '执行模式',
'settings.modeDaemon': '本机 CLI', 'settings.modeDaemon': '本机 CLI',
'settings.modeDaemonHelp': '通过本机的代码代理 CLI 执行', 'settings.modeDaemonHelp': '通过本机的代码代理 CLI 执行',
'settings.modeDaemonOffline': '后台守护进程未运行', 'settings.modeDaemonOffline': '后台守护进程未运行',
'settings.modeDaemonOfflineMeta': '守护进程未运行', 'settings.modeDaemonOfflineMeta': '守护进程未运行',
'settings.modeDaemonInstalledMeta': '已安装 {count} 个', 'settings.modeDaemonInstalledMeta': '已安装 {count} 个',
'settings.modeApi': '托管 API', 'settings.modeApi': 'Anthropic API',
'settings.modeApiMeta': '自带 Key · 多渠道', 'settings.modeApiMeta': '自带 Key',
'settings.codeAgent': '代码代理', 'settings.codeAgent': '代码代理',
'settings.codeAgentHint': '通过扫描 PATH 自动检测,选择你希望使用的 CLI。', 'settings.codeAgentHint': '通过扫描 PATH 自动检测,选择你希望使用的 CLI。',
'settings.rescan': '↻ 重新扫描', 'settings.rescan': '↻ 重新扫描',
'settings.rescanTitle': '重新扫描 PATH', 'settings.rescanTitle': '重新扫描 PATH',
'settings.noAgentsDetected': 'settings.noAgentsDetected':
'尚未检测到任何代理。请安装 Claude Code、Codex、Gemini CLI、OpenCode、Cursor Agent 或 Qwen 中的一个,然后点击「重新扫描」。', '尚未检测到任何代理。请安装 Claude Code、Codex、Gemini CLI、OpenCode、Cursor Agent 或 Qwen 中的一个,然后点击「重新扫描」。',
'settings.apiSection': '模型端点', 'settings.apiSection': 'Anthropic API',
'settings.apiKey': 'API Key', 'settings.apiKey': 'API Key',
'settings.showKey': '显示 Key', 'settings.showKey': '显示 Key',
'settings.hideKey': '隐藏 Key', 'settings.hideKey': '隐藏 Key',
@@ -74,19 +74,11 @@ export const zhCN: Dict = {
'settings.baseUrl': 'Base URL', 'settings.baseUrl': 'Base URL',
'settings.apiHint': 'settings.apiHint':
'请求会从当前浏览器直连你设置的 Base URL,无中转代理。Key 只存放在 localStorage。', '请求会从当前浏览器直连你设置的 Base URL,无中转代理。Key 只存放在 localStorage。',
'settings.providerLabel': '渠道',
'settings.providerHint':
'选择请求格式。Anthropic 也涵盖任意 Anthropic 兼容代理;OpenAI 兼容涵盖 OpenRouter、LiteLLM、DeepSeek、Groq、Together 等。',
'settings.apiVersion': 'API version',
'settings.apiVersionHint':
'Azure 的 api-version 查询参数(如 2024-08-01-preview)。留空则使用默认值。',
'settings.proxyHint':
'提示:若要在浏览器里使用 AWS Bedrock 或 Google Vertex 上的 Anthropic 模型,建议在服务器端跑一个 LiteLLM 代理,再把 Anthropic 渠道指向它 —— 凭证签名应留在服务器,不放进浏览器。',
'settings.skipForNow': '暂时跳过', 'settings.skipForNow': '暂时跳过',
'settings.getStarted': '开始使用', 'settings.getStarted': '开始使用',
'settings.envConfigure': '配置执行模式', 'settings.envConfigure': '配置执行模式',
'settings.localCli': '本机 CLI', 'settings.localCli': '本机 CLI',
'settings.anthropicApi': '托管 API', 'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': '尚未选择代理', 'settings.noAgentSelected': '尚未选择代理',
'settings.language': '界面语言', 'settings.language': '界面语言',
'settings.languageHint': '切换界面语言,设置仅保存在当前浏览器。', 'settings.languageHint': '切换界面语言,设置仅保存在当前浏览器。',
@@ -205,9 +197,9 @@ export const zhCN: Dict = {
'avatar.title': '账户与设置', 'avatar.title': '账户与设置',
'avatar.localCli': '本机 CLI', 'avatar.localCli': '本机 CLI',
'avatar.anthropicApi': '托管 API', 'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': '使用本机 CLI', 'avatar.useLocal': '使用本机 CLI',
'avatar.useApi': '使用托管 API', 'avatar.useApi': '使用 Anthropic API',
'avatar.codeAgent': '代码代理', 'avatar.codeAgent': '代码代理',
'avatar.rescan': '重新扫描 PATH', 'avatar.rescan': '重新扫描 PATH',
'avatar.settings': '设置', 'avatar.settings': '设置',
@@ -408,7 +400,7 @@ export const zhCN: Dict = {
'agentPicker.modeChoose': '选择执行模式', 'agentPicker.modeChoose': '选择执行模式',
'agentPicker.localCli': '本机 CLI', 'agentPicker.localCli': '本机 CLI',
'agentPicker.daemonOff': '守护进程未运行', 'agentPicker.daemonOff': '守护进程未运行',
'agentPicker.byok': '托管 API · 自带 Key', 'agentPicker.byok': 'Anthropic API · 自带 Key',
'agentPicker.selectAgent': '选择已检测到的代码代理 CLI', 'agentPicker.selectAgent': '选择已检测到的代码代理 CLI',
'agentPicker.noAgents': 'PATH 中未发现代理', 'agentPicker.noAgents': 'PATH 中未发现代理',
'agentPicker.notInstalled': '未安装', 'agentPicker.notInstalled': '未安装',
-5
View File
@@ -85,11 +85,6 @@ export interface Dict {
'settings.model': string; 'settings.model': string;
'settings.baseUrl': string; 'settings.baseUrl': string;
'settings.apiHint': string; 'settings.apiHint': string;
'settings.providerLabel': string;
'settings.providerHint': string;
'settings.apiVersion': string;
'settings.apiVersionHint': string;
'settings.proxyHint': string;
'settings.skipForNow': string; 'settings.skipForNow': string;
'settings.getStarted': string; 'settings.getStarted': string;
'settings.envConfigure': string; 'settings.envConfigure': string;
-56
View File
@@ -1,56 +0,0 @@
/**
* Azure OpenAI streaming client. Wire format is OpenAI's (chat.completions
* SSE), but the URL embeds the deployment name and an api-version query
* string, and auth uses the `api-key` header rather than `Authorization:
* Bearer`. We reuse streamChatCompletions() from openai.ts for the SSE
* pump and only diverge on URL + headers.
*/
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
import { streamChatCompletions } from './openai';
const DEFAULT_API_VERSION = '2024-08-01-preview';
export async function streamAzure(
cfg: AppConfig,
system: string,
history: ChatMessage[],
signal: AbortSignal,
handlers: StreamHandlers,
): Promise<void> {
if (!cfg.apiKey) {
handlers.onError(new Error('Missing Azure key — open Settings and paste one in.'));
return;
}
if (!cfg.baseUrl) {
handlers.onError(
new Error('Missing Azure endpoint — set Base URL to https://<resource>.openai.azure.com.'),
);
return;
}
if (!cfg.model) {
handlers.onError(
new Error('Missing Azure deployment — set Model to your deployment name.'),
);
return;
}
const apiVersion = (cfg.apiVersion?.trim() || DEFAULT_API_VERSION);
const url = buildAzureUrl(cfg.baseUrl, cfg.model, apiVersion);
const body = {
stream: true,
max_tokens: 8192,
messages: [
...(system ? [{ role: 'system', content: system }] : []),
...history.map((m) => ({ role: m.role, content: m.content })),
],
};
await streamChatCompletions(url, cfg.apiKey, body, signal, handlers, 'azure');
}
function buildAzureUrl(baseUrl: string, deployment: string, apiVersion: string): string {
const base = baseUrl.replace(/\/+$/, '');
return `${base}/openai/deployments/${encodeURIComponent(deployment)}/chat/completions?api-version=${encodeURIComponent(apiVersion)}`;
}
-105
View File
@@ -1,105 +0,0 @@
/**
* Google Generative Language API streaming client (Gemini direct). The
* REST surface is at generativelanguage.googleapis.com and accepts an
* api key in the query string. We hit `:streamGenerateContent` with
* `alt=sse` so the response arrives as a server-sent event stream we
* can pump like the OpenAI one.
*/
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
export async function streamGoogle(
cfg: AppConfig,
system: string,
history: ChatMessage[],
signal: AbortSignal,
handlers: StreamHandlers,
): Promise<void> {
if (!cfg.apiKey) {
handlers.onError(new Error('Missing API key — open Settings and paste one in.'));
return;
}
if (!cfg.model) {
handlers.onError(new Error('Missing model — set one in Settings.'));
return;
}
const base = (cfg.baseUrl || 'https://generativelanguage.googleapis.com').replace(/\/+$/, '');
const url = `${base}/v1beta/models/${encodeURIComponent(cfg.model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(cfg.apiKey)}`;
const contents = history.map((m) => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text: m.content }],
}));
const body: Record<string, unknown> = { contents };
if (system) {
body.systemInstruction = { role: 'system', parts: [{ text: system }] };
}
let acc = '';
try {
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal,
});
if (!resp.ok || !resp.body) {
const text = await resp.text().catch(() => '');
handlers.onError(new Error(`upstream ${resp.status}: ${text || 'no body'}`));
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx: number;
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx).trim();
buf = buf.slice(idx + 2);
if (!frame) continue;
for (const line of frame.split('\n')) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (!payload) continue;
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
continue;
}
const delta = extractGeminiText(parsed);
if (delta) {
acc += delta;
handlers.onDelta(delta);
}
}
}
}
handlers.onDone(acc);
} catch (err) {
if ((err as Error).name === 'AbortError') return;
handlers.onError(err instanceof Error ? err : new Error(String(err)));
}
}
function extractGeminiText(payload: unknown): string {
if (!payload || typeof payload !== 'object') return '';
const candidates = (payload as { candidates?: unknown }).candidates;
if (!Array.isArray(candidates) || candidates.length === 0) return '';
const first = candidates[0] as { content?: { parts?: Array<{ text?: unknown }> } };
const parts = first?.content?.parts;
if (!Array.isArray(parts)) return '';
let out = '';
for (const p of parts) {
if (typeof p?.text === 'string') out += p.text;
}
return out;
}
-32
View File
@@ -1,32 +0,0 @@
/**
* BYOK model router. Picks a streaming client based on cfg.provider so
* the rest of the app can stay provider-agnostic. Adding a fifth provider
* later means: add an entry to ModelProvider, add a presets row, add a
* `stream<X>` function, and one more `case` here.
*/
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
import { streamMessage as streamAnthropic } from './anthropic';
import { streamAzure } from './azure';
import { streamGoogle } from './google';
import { streamOpenAI } from './openai';
export async function streamModel(
cfg: AppConfig,
system: string,
history: ChatMessage[],
signal: AbortSignal,
handlers: StreamHandlers,
): Promise<void> {
switch (cfg.provider) {
case 'openai':
return streamOpenAI(cfg, system, history, signal, handlers);
case 'azure':
return streamAzure(cfg, system, history, signal, handlers);
case 'google':
return streamGoogle(cfg, system, history, signal, handlers);
case 'anthropic':
default:
return streamAnthropic(cfg, system, history, signal, handlers);
}
}
-135
View File
@@ -1,135 +0,0 @@
/**
* OpenAI-compatible streaming client. Covers any endpoint that speaks the
* `/chat/completions` SSE wire format OpenAI proper, OpenRouter,
* LiteLLM proxy, DeepSeek, Groq, Together, Mistral. Azure has its own
* URL shape and lives in azure.ts.
*
* Browser fetch is fine here for the same BYOK reason streamMessage()
* uses dangerouslyAllowBrowser: this is a local-first tool, the key is
* the user's, it never leaves their machine. Move to a server proxy if
* you ever ship a hosted build.
*/
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
export async function streamOpenAI(
cfg: AppConfig,
system: string,
history: ChatMessage[],
signal: AbortSignal,
handlers: StreamHandlers,
): Promise<void> {
if (!cfg.apiKey) {
handlers.onError(new Error('Missing API key — open Settings and paste one in.'));
return;
}
if (!cfg.baseUrl) {
handlers.onError(new Error('Missing base URL — open Settings and set one.'));
return;
}
const url = joinUrl(cfg.baseUrl, '/chat/completions');
const body = {
model: cfg.model,
stream: true,
max_tokens: 8192,
messages: [
...(system ? [{ role: 'system', content: system }] : []),
...history.map((m) => ({ role: m.role, content: m.content })),
],
};
await streamChatCompletions(url, cfg.apiKey, body, signal, handlers, 'bearer');
}
// Shared SSE pump between the OpenAI and Azure clients — they only differ
// in URL shape and auth header.
export async function streamChatCompletions(
url: string,
apiKey: string,
body: Record<string, unknown>,
signal: AbortSignal,
handlers: StreamHandlers,
auth: 'bearer' | 'azure',
): Promise<void> {
let acc = '';
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (auth === 'bearer') headers['Authorization'] = `Bearer ${apiKey}`;
else headers['api-key'] = apiKey;
const resp = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
signal,
});
if (!resp.ok || !resp.body) {
const text = await resp.text().catch(() => '');
handlers.onError(new Error(`upstream ${resp.status}: ${text || 'no body'}`));
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
// Frames are separated by a blank line. Split on \n\n; the trailing
// partial frame stays in buf for the next iteration.
let idx: number;
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx).trim();
buf = buf.slice(idx + 2);
if (!frame) continue;
// Each frame is one or more `data: ...` lines plus optional
// `event:` / comments. We only care about `data:` payloads.
for (const line of frame.split('\n')) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (!payload || payload === '[DONE]') continue;
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
continue;
}
const delta = extractDelta(parsed);
if (delta) {
acc += delta;
handlers.onDelta(delta);
}
}
}
}
handlers.onDone(acc);
} catch (err) {
if ((err as Error).name === 'AbortError') return;
handlers.onError(err instanceof Error ? err : new Error(String(err)));
}
}
function extractDelta(payload: unknown): string {
if (!payload || typeof payload !== 'object') return '';
const choices = (payload as { choices?: unknown }).choices;
if (!Array.isArray(choices) || choices.length === 0) return '';
const first = choices[0] as { delta?: { content?: unknown }; text?: unknown };
if (first?.delta && typeof first.delta.content === 'string') {
return first.delta.content;
}
// Some legacy / completion-style proxies emit `text` instead of delta.
if (typeof first?.text === 'string') return first.text;
return '';
}
function joinUrl(base: string, path: string): string {
const b = base.replace(/\/+$/, '');
const p = path.startsWith('/') ? path : `/${path}`;
return `${b}${p}`;
}
-108
View File
@@ -1,108 +0,0 @@
/**
* Provider presets the BYOK side of the app supports four wire formats
* (Anthropic-native, OpenAI-compatible, Azure OpenAI, Google Generative
* Language). Each one ships with a default base URL and a short list of
* suggested model ids so the SettingsDialog datalist gives the user a
* head-start. The presets stay deliberately conservative: a user pointing
* an `openai` provider at LiteLLM / OpenRouter / DeepSeek just types a
* different baseUrl + model, no code change required.
*
* AWS Bedrock and Google Vertex aren't first-class providers here. Both
* require credential signing (SigV4 for AWS, GCP service-account JWT for
* Vertex) which is unsafe to do from the browser with long-lived BYOK
* credentials. The recommended path is to run LiteLLM (or a similar
* proxy) server-side and point the `anthropic` or `openai` provider at
* that proxy's URL the provider chooser surfaces this guidance.
*/
import type { ModelProvider } from '../types';
export interface ProviderPreset {
id: ModelProvider;
// Display name shown in the chooser and the env meta line.
label: string;
// Short marketing-style line shown under the provider card.
blurb: string;
// Default base URL preloaded into the form when the user picks this
// provider for the first time. Empty string means "the user must fill
// it in" (Azure has no global default).
baseUrl: string;
// Suggested model id (datalist anchor). The user can type anything.
defaultModel: string;
// Suggestions surfaced in the model field's <datalist>.
modelSuggestions: string[];
// Placeholder hint for the api key field.
apiKeyPlaceholder: string;
// Whether the provider requires the Azure-specific apiVersion field.
needsApiVersion?: boolean;
}
export const PROVIDER_PRESETS: Record<ModelProvider, ProviderPreset> = {
anthropic: {
id: 'anthropic',
label: 'Anthropic',
blurb: 'Direct to api.anthropic.com or any Anthropic-compatible proxy (LiteLLM, AWS Bedrock / GCP Vertex via proxy).',
baseUrl: 'https://api.anthropic.com',
defaultModel: 'claude-sonnet-4-5',
modelSuggestions: [
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
'claude-3-5-sonnet-latest',
],
apiKeyPlaceholder: 'sk-ant-...',
},
openai: {
id: 'openai',
label: 'OpenAI-compatible',
blurb: 'Any OpenAI /chat/completions endpoint — OpenAI, OpenRouter, LiteLLM proxy, DeepSeek, Groq, Together, Mistral.',
baseUrl: 'https://api.openai.com/v1',
defaultModel: 'gpt-4o-mini',
modelSuggestions: [
'gpt-4o',
'gpt-4o-mini',
'anthropic/claude-3.5-sonnet',
'google/gemini-2.0-flash',
'deepseek/deepseek-chat',
'meta-llama/llama-3.3-70b-instruct',
],
apiKeyPlaceholder: 'sk-...',
},
azure: {
id: 'azure',
label: 'Azure OpenAI',
blurb: 'Azure-hosted deployments. Base URL is your resource endpoint; Model is the deployment name.',
baseUrl: '',
defaultModel: '',
modelSuggestions: [],
apiKeyPlaceholder: 'azure key',
needsApiVersion: true,
},
google: {
id: 'google',
label: 'Google Gemini',
blurb: 'Google Generative Language API — Gemini family, key from aistudio.google.com.',
baseUrl: 'https://generativelanguage.googleapis.com',
defaultModel: 'gemini-2.0-flash',
modelSuggestions: [
'gemini-2.0-flash',
'gemini-2.0-flash-lite',
'gemini-1.5-pro',
'gemini-1.5-flash',
],
apiKeyPlaceholder: 'AIza...',
},
};
export const PROVIDER_ORDER: ModelProvider[] = [
'anthropic',
'openai',
'azure',
'google',
];
// True when the provider's wire format expects a deployment-specific URL
// rather than a generic baseUrl + path. Today only Azure qualifies — kept
// as a helper so callers don't have to memorize that.
export function providerLabel(provider: ModelProvider): string {
return PROVIDER_PRESETS[provider]?.label ?? provider;
}
-2
View File
@@ -4,11 +4,9 @@ const STORAGE_KEY = 'open-design:config';
export const DEFAULT_CONFIG: AppConfig = { export const DEFAULT_CONFIG: AppConfig = {
mode: 'daemon', mode: 'daemon',
provider: 'anthropic',
apiKey: '', apiKey: '',
baseUrl: 'https://api.anthropic.com', baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5', model: 'claude-sonnet-4-5',
apiVersion: '',
agentId: null, agentId: null,
skillId: null, skillId: null,
designSystemId: null, designSystemId: null,
-15
View File
@@ -1,25 +1,10 @@
export type ExecMode = 'daemon' | 'api'; export type ExecMode = 'daemon' | 'api';
// Which BYOK model endpoint to talk to in `mode === 'api'`. Each provider
// has its own request shape — see src/providers/{anthropic,openai,azure,
// google}.ts for the wire details. AWS Bedrock and Google Vertex are
// reached via the `anthropic` provider pointed at an Anthropic-compatible
// proxy (e.g. LiteLLM), which keeps signing on the server where the
// long-lived AWS / GCP credentials belong.
export type ModelProvider = 'anthropic' | 'openai' | 'azure' | 'google';
export interface AppConfig { export interface AppConfig {
mode: ExecMode; mode: ExecMode;
// Active provider when `mode === 'api'`. Older configs that predate the
// multi-provider rework default to 'anthropic' on load.
provider: ModelProvider;
apiKey: string; apiKey: string;
baseUrl: string; baseUrl: string;
model: string; model: string;
// Azure OpenAI only — the api-version query string the Azure REST
// surface requires (e.g. '2024-08-01-preview'). Ignored by every other
// provider so the same config can round-trip through localStorage.
apiVersion?: string;
agentId: string | null; agentId: string | null;
skillId: string | null; skillId: string | null;
designSystemId: string | null; designSystemId: string | null;
+1 -2
View File
@@ -2,12 +2,11 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
const DAEMON_PORT = Number(process.env.OD_PORT) || 7456; const DAEMON_PORT = Number(process.env.OD_PORT) || 7456;
const VITE_PORT = Number(process.env.VITE_PORT) || 5173;
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: VITE_PORT, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: `http://127.0.0.1:${DAEMON_PORT}`, target: `http://127.0.0.1:${DAEMON_PORT}`,