0a86dfb8c5
A new **AI Router** tab in Settings, next to Codex, stores named router
endpoints (9Router, OpenRouter, a self-hosted gateway) as profiles: a base
URL, an API key, a default model and an optional per-tier override. The tab
only stores them; a lane CHOOSES one in the Add-lane modal, via a Native /
AI Router radio, and that choice is written into the lane's own config.
The two CLIs are configured differently, and the difference is theirs:
- a claude lane gets eight `env` keys merged into its own
`.claude/settings.local.json` (`.local` because the value holds a key);
- a codex lane gets `$CODEX_HOME/<name>.config.toml` — the layer
`codex -p <name>` loads — because Codex has NO project-level config.
Its base `config.toml` is never touched.
Native is a built-in profile rather than a code branch, so "go back to
native" is the same write path. It blanks the two connection variables
instead of removing them (an absent key at project scope lets a USER-level
router apply) and removes the model names instead of writing them (writing
them would override the user's own global model choice).
Key material never leaves the server: profiles live one file each under
`<CLAUDE_HOME>/agenthub-routers/` (0700/0600), `GET /api/ai-routers`
reports only `hasKey`, and there is deliberately no route returning one
profile in full.
Three things this had to get right, each found by review and verified
against the real tools rather than assumed:
- `wire_api = "chat"` is rejected outright by codex-cli 0.149 ("no longer
supported"), so every codex lane on a router would have failed to start.
The layer writes "responses", and writes `env_key` only when the profile
actually has a key — naming an unset variable is itself a startup error.
- The key must never be TYPED into a pane. `tmux.sendCommand` used
`send-keys` with a `KEY='…' cmd` prefix, which the dashboard streams to
every attached browser and bash appends to history; it now takes over the
idle pane with `respawn-pane -k -e`, passing env through argv the way
`newSession` always has. Verified on tmux 3.4: multi-element argv arrives
intact and the pane renders no trace of the value.
- Attaching COPIES a profile, so editing or deleting one in Settings has to
reach the lanes already using it. An edit re-applies it (a rotated key
would otherwise never reach the lane still holding the revoked one); a
delete resets those lanes to native and removes the Codex layer file.
Both report per-lane failures, which the tab surfaces.
`.claude/settings.local.json` is added to the lane's git exclude, for both
native and router lanes: `agenthub lanes reset` runs `git clean -fd` without
`-x`, so an un-excluded settings file is deleted on reset — and untracked
meant the key was committable.
DB: `lanes.router_profile`, nullable, additive. Written only by
`setRouterProfile` beside the disk write, never through `PATCHABLE`.
147 lines
9.4 KiB
JSON
147 lines
9.4 KiB
JSON
{
|
||
"action.clear": "Dọn trạng thái",
|
||
"action.forget": "Xóa lane",
|
||
"action.purge": "Xóa lịch sử",
|
||
"action.remove": "Xóa làn + worktree",
|
||
"action.reset": "Đặt lại worktree",
|
||
"action.start": "Bắt đầu",
|
||
"action.stop": "Dừng",
|
||
"actionError": "Thao tác làn đường thất bại: {{message}}",
|
||
"actionErrorUnknown": "Lỗi không xác định",
|
||
"actions.agentsInstall": "Cài agent",
|
||
"actions.agentsInstallResult": "Đã cài: {{files}}",
|
||
"actions.busy": "…",
|
||
"actions.mcpSync": "Đồng bộ MCP",
|
||
"actions.mcpSyncResult": "Đã đồng bộ: {{servers}}",
|
||
"actions.syncCheck": "Kiểm tra đồng bộ dev",
|
||
"actions.syncCheckClean": "DEV_DELTA: {{count}} file, trùng: {{overlap}}",
|
||
"actions.syncCheckCollision": "Trùng migration: {{file}} → đổi tên thành {{suggestion}}",
|
||
"actions.tools": "Công cụ",
|
||
"add": "Thêm lane",
|
||
"addLane": "Tạo lane từ một thư mục làm việc",
|
||
"addLaneBaseLabel": "Nhánh để tạo nhánh mới",
|
||
"addLaneBranchHint": "Phải là tên nhánh git hợp lệ và chưa tồn tại.",
|
||
"addLaneBranchLabel": "Tên nhánh mới",
|
||
"addLaneBranchPlaceholder": "feat/tinh-nang-cua-toi",
|
||
"addLaneNoBranches": "Repo này chưa có commit nào — worktree sẽ bắt đầu trống.",
|
||
"addLaneNotARepo": "Chưa phải repo git (hoặc không có quyền đọc).",
|
||
"addLanePipelineHint": "Bản đồ lane này sẽ hiện những stage nào. Chọn đúng cái mà skill điều khiển nó khai báo — sau này không màn hình nào đổi được.",
|
||
"addLanePipelineLabel": "Mẫu pipeline",
|
||
"addLaneProvisionFailed": "Tạo worktree thất bại hoặc quá thời gian chờ — lane vẫn tồn tại nhưng tự động thiết lập đã bị bỏ qua. Kiểm tra git facts của lane, rồi chạy thiết lập agents/mcp thủ công.",
|
||
"addLaneRepoHint": "Một repo git có sẵn. Dashboard tự tạo worktree mới cho lane, không phải thư mục bạn chọn.",
|
||
"addLaneRepoHintAdopt": "Một thư mục có sẵn. Dashboard theo dõi nguyên trạng — không tạo worktree, không tạo nhánh mới.",
|
||
"addLaneRepoLabel": "Repo nguồn",
|
||
"addLaneRepoLabelAdopt": "Thư mục",
|
||
"addLaneSetupAgents": "Agent",
|
||
"addLaneSetupMcp": "MCP server",
|
||
"addLaneSetupProfile": "Profile",
|
||
"addLaneSetupTitle": "Thiết lập",
|
||
"addLaneTitleLabel": "Tiêu đề",
|
||
"addLaneToolClaude": "Claude Code",
|
||
"addLaneToolCodex": "Codex",
|
||
"addLaneToolHint": "CLI nào điều khiển lane này. Không thể thay đổi sau này.",
|
||
"addLaneWriteDocsHint": "Khi tạo lane sẽ mở terminal của lane và chạy skill /sg-all:sg-md-writing tại đó.",
|
||
"addLaneWriteDocsLabel": "Tạo/cập nhật file readme",
|
||
"mode.repo": "Repo",
|
||
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
|
||
"mode.worktree": "Worktree",
|
||
"mode.worktreeHint": "Tạo worktree git mới + nhánh mới từ repo nguồn.",
|
||
"browse.button": "Duyệt",
|
||
"browse.empty": "Thư mục này trống.",
|
||
"browse.select": "Chọn thư mục này",
|
||
"browse.title": "Duyệt chọn thư mục",
|
||
"worktrees.heading_one": "{{count}} worktree",
|
||
"worktrees.heading_other": "{{count}} worktree",
|
||
"addLaneTitlePlaceholder": "Không bắt buộc",
|
||
"autoStage": "tự động: {{stage}}",
|
||
"cardId": "Làn đường {{id}}",
|
||
"confirmRemoveCancel": "Hủy",
|
||
"confirmRemoveConfirm": "Xóa",
|
||
"confirmRemoveMessage": "Hành động này không thể hoàn tác.",
|
||
"confirmRemoveTitle": "Xóa làn đường?",
|
||
"countDead": "đã chết",
|
||
"countNeedsYou": "cần bạn",
|
||
"countRunning": "đang chạy",
|
||
"countTotal": "làn đường",
|
||
"destructive.blocked.adopted": "Đây là thư mục đã nhận và không thể thay đổi bằng thao tác worktree.",
|
||
"destructive.blocked.missing": "Thư mục làn đường không còn, nên không thể tiếp tục thao tác này.",
|
||
"destructive.blocked.unreadable": "Không thể đọc thư mục làn đường như một Git worktree, nên không thể tiếp tục thao tác này.",
|
||
"destructive.cancel": "Hủy",
|
||
"destructive.count.bytesEstimate": "Dung lượng giải phóng (ước tính)",
|
||
"destructive.count.dirty": "Tệp đã sửa",
|
||
"destructive.count.events": "Sự kiện",
|
||
"destructive.count.head": "HEAD",
|
||
"destructive.count.sessions": "Phiên",
|
||
"destructive.count.tokenRows": "Dòng token",
|
||
"destructive.count.unpushed": "Commit chưa đẩy",
|
||
"destructive.count.untracked": "Tệp chưa theo dõi",
|
||
"destructive.force": "Tôi hiểu các commit chưa đẩy này sẽ bị loại bỏ.",
|
||
"destructive.loading": "Đang tải trạng thái hiện tại…",
|
||
"destructive.notice.activeSessionSkipped": "Một hoặc nhiều phiên trong làn đường này vẫn đang hoạt động và sẽ được giữ lại, không bị dọn.",
|
||
"destructive.notice.adopted": "Làn đường này trỏ tới thư mục của bạn. Chỉ bản ghi trên bảng điều khiển bị xóa — thư mục và các tệp bên trong không bị thay đổi.",
|
||
"destructive.notice.missing": "Thư mục làn đường đã không còn. Việc xóa chỉ bỏ bản ghi trên bảng điều khiển và dọn mục worktree cũ trong Git; không tệp nào bị xóa khỏi đĩa.",
|
||
"destructive.notice.unreadable": "Không thể đọc thư mục làn đường như một Git worktree. Hệ thống sẽ thử xóa; nếu Git tự chối, mục ghi worktree của nó sẽ được dọn trực tiếp thay thế. Trong mọi trường hợp, thư mục vẫn không bị thay đổi.",
|
||
"destructive.purge.confirm": "Xóa lịch sử",
|
||
"destructive.purge.message": "Hãy xác nhận các số liệu hiện tại trước khi xóa lịch sử phiên đã lưu.",
|
||
"destructive.purge.title": "Xóa lịch sử làn đường?",
|
||
"destructive.remove.confirm": "Xóa làn đường",
|
||
"destructive.remove.message": "Hãy xác nhận các số liệu hiện tại trước khi thực hiện thao tác này.",
|
||
"destructive.remove.title": "Xóa làn đường?",
|
||
"destructive.reset.confirm": "Đặt lại worktree",
|
||
"destructive.reset.message": "Hãy xác nhận các số liệu hiện tại trước khi đặt lại worktree này.",
|
||
"destructive.reset.survives": "Các tệp Git bỏ qua như node_modules và .env được giữ lại; các tệp chưa theo dõi sẽ bị xóa.",
|
||
"destructive.reset.title": "Đặt lại worktree được quản lý?",
|
||
"destructive.warning.no-remote": "Chưa cấu hình Git remote, nên không có bản sao lưu ở xa. Bạn vẫn có thể tiếp tục.",
|
||
"emptyState": "Chưa có làn đường nào. Tạo một từ thư mục làm việc:",
|
||
"git.uncommitted": "{{dirty}} đã sửa · {{untracked}} chưa theo dõi",
|
||
"kind.adopted": "đã nhận",
|
||
"kind.managed": "được quản lý",
|
||
"laneDetail.hide": "Ẩn chi tiết",
|
||
"laneDetail.show": "Chi tiết lane",
|
||
"laneHeader": "Làn đường {{id}} · {{title}} · {{pipeline}}",
|
||
"locks.held_one": "Đang giữ {{count}} khóa",
|
||
"locks.held_other": "Đang giữ {{count}} khóa",
|
||
"moreActions": "Thêm hành động",
|
||
"pipelinePicker.label": "Mẫu pipeline",
|
||
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" không khớp node nào trong \"{{pipeline}}\" — khai báo một trong: {{nodes}}",
|
||
"preflightError": "Không thể tải trạng thái làn đường hiện tại.",
|
||
"preflightErrorWithMessage": "Không thể tải trạng thái làn đường hiện tại: {{message}}",
|
||
"runtime.boot": "▶ chạy",
|
||
"runtime.busy.down": "đang dừng…",
|
||
"runtime.busy.up": "đang khởi động…",
|
||
"runtime.down": "tắt",
|
||
"runtime.healthy": "khoẻ",
|
||
"runtime.partial": "một phần",
|
||
"runtime.slot": "slot {{slot}}",
|
||
"runtime.steppedAsideTitle": "Đã lùi khỏi {{expected}} vì cổng đó đang bận",
|
||
"runtime.stop": "■ dừng",
|
||
"runtime.toggleTitle": "Chạy hoặc dừng stack ứng dụng của lane này (khác với phiên Claude của nó)",
|
||
"integrations.ci_wait": "CI wait",
|
||
"integrations.dev_qc": "dev QC",
|
||
"integrations.tracker": "tracker",
|
||
"stageUndeclared": "chưa khai báo",
|
||
"status.failed": "thất bại",
|
||
"status.idle": "rảnh",
|
||
"status.provisioning": "đang khởi tạo",
|
||
"status.running": "đang chạy",
|
||
"features.live": "Phiên bản trực tiếp",
|
||
"features.archived": "đã lưu trữ",
|
||
"features.viewingArchived": "Xem tính năng đã lưu trữ \"{{slug}}\" — lane tiếp tục chạy; đây là ảnh chụp nhanh chỉ đọc.",
|
||
"proof.ticketReport": "Báo cáo nhiệm vụ",
|
||
"splitView.emptyPane": "Chưa chọn lane cho ô này.",
|
||
"splitView.paneLaneLabel": "Bộ chọn lane cho ô",
|
||
"splitView.paneCount": "{{count}} ô",
|
||
"splitView.pickLane": "Chọn lane",
|
||
"statusDead": "ĐÃ CHẾT",
|
||
"title": "Làn đường",
|
||
"tooltipStart": "Tạo một lần chạy ở chế độ hội thoại mà không có lời nhắc ban đầu; được điều khiển từ CLI hoặc qua tin nhắn",
|
||
"addLaneRouterLabel": "Endpoint model",
|
||
"addLaneRouterNative": "Native",
|
||
"addLaneRouterCustom": "AI Router",
|
||
"addLaneRouterProfileLabel": "Profile router",
|
||
"addLaneRouterHint": "Native là gọi thẳng API của hãng. Chọn profile AI Router thì cấu hình được ghi vào config riêng của lane này lúc tạo.",
|
||
"addLaneRouterEmptyHint": "Chưa lưu profile router nào — thêm ở Settings › AI Router.",
|
||
"addLaneSetupRouter": "Endpoint model",
|
||
"addLaneRouterLoading": "Đang tải danh sách router…"
|
||
}
|