feat(router-app): UX improvements — i18n, session grouping, date formatting, detail panel polish#28478
Closed
mrsimpson wants to merge 639 commits into
Closed
feat(router-app): UX improvements — i18n, session grouping, date formatting, detail panel polish#28478mrsimpson wants to merge 639 commits into
mrsimpson wants to merge 639 commits into
Conversation
… api /user in pod init
This sets up permission-based control for MCP tools: - Add default denies in opencode.json for all workflow and office_workflow tools - Update ade.md to explicitly allow workflow tools (with ask for start_development/proceed_to_phase) and deny office_workflow tools - Update office.md to allow office_workflow tools (with ask for start_development/proceed_to_phase) This enables per-agent MCP tool access control via the permission system. Note: The code currently bypasses permission filtering for MCP tools. See the PR description for the tracking issue.
- Add PVC watcher to delete DNS only on session termination (PVC delete) - Keep DNS on pod delete to allow instant resumption without DNS cache issues - Add integration tests for full session lifecycle (27 tests) - Add test job to CI workflow
- Pin vitest to 4.1.4 (same as homelab package) - Fix import paths in tests (../src/ instead of ./src/) - Move mocks to setup.ts for proper hoisting - Fix tunnel ingress reference handling in mocks
- Move all vi.mock calls before imports in test file - This ensures mocks are applied before modules are loaded - Fixes CI failure where mocks weren't taking effect
…ter sessions ## Intent Allow local opencode clients to attach to a running router session without going through OAuth, so developers can connect their local CLI directly to a cloud-hosted session. ## Decisions - Use a dedicated attach server on a separate port (ATTACH_PORT, default 4096) that is intentionally not behind oauth2-proxy, so attach subdomain requests can bypass OAuth. - Authenticate via a per-session password stored in a PVC annotation (opencode.ai/attach-password) rather than OAuth tokens. Password is generated at session creation and lazy-created for existing sessions. - Accept the password via HTTP Basic Auth, ?password= query param, or X-Attach-Password header to support different client integration styles. - Restrict the /api/sessions/:hash/attach-info endpoint to the session owner only (403 for everyone else) so passwords are never leaked across users. ## Key changes - packages/opencode-router/src/index.ts: add attach subdomain routing with password auth; extract shared proxyToPod helper; fix wsHandler type (upgrade event signature, not RequestListener); start attachServer on ATTACH_PORT alongside the main server - packages/opencode-router/src/pod-manager.ts: add getOrCreateAttachPassword, generateAttachPassword, getAttachUrl; store attach password in PVC at creation time; expose attachUrl/attachPassword in SessionInfo; remove unused req parameter from getSessionInfo - packages/opencode-router/src/api.ts: add GET /api/sessions/:hash/attach-info endpoint returning attachUrl and attachPassword for session owner - packages/opencode-router/src/config.ts: add attachPort and attachRoutePrefix config options - packages/opencode-router-app/src/session-item.tsx: add Attach button that copies the ready-to-run opencode attach command to clipboard - packages/opencode-router-app/src/api.ts: expose attachUrl/attachPassword in SessionSchema - packages/opencode-router/src/*.test.ts: add unit tests for attach-info API, hostname parsing, password generation, and config defaults
Make repoUrl optional in the SessionKey API — when absent, the init container runs `git init` instead of `git clone`, enabling new project workflows starting from an empty workspace. Backend changes: - SessionKey: repoUrl, branch, sourceBranch are now optional - getSessionHash: generates random SHA256(email+UUID)[:12] when repoUrl is absent (non-deterministic identity for new projects) - ensurePVC/ensurePod: conditionally omit repo annotations when repoUrl is absent - ensurePod init script: if repoUrl present → git clone flow; if absent → git init + initial commit (GITHUB_TOKEN block runs unconditionally in both paths) - getSessionProgress: renamed stage 'cloning' → 'preparing' (unified stage name for both git clone and git init) - resumeSession: reconstructs SessionKey without repo fields when PVC lacks ANNOTATION_REPO_URL - POST /api/sessions: conditional validation — branch/sourceBranch/ remoteBranchExists only required when repoUrl is present Frontend changes: - Setup form: segmented tab switcher [Git Repository] [New Project] - Git tab: existing repo URL + branch autocomplete fields - New Project tab: only prompt textarea - i18n: added form.tab.git, form.tab.newProject keys; renamed loading.stage.cloning → loading.stage.preparing - setup-form-utils: added buildNewProjectKey() for validation - LoadingScreen: STAGES array uses 'preparing' instead of 'cloning' Tests: - pod-manager.test.ts: 8 new tests for random hash, PVC without repo annotations, git init script, GITHUB_TOKEN injection, resume - api.test.ts: 5 new tests for new project API endpoint - setup-form.test.ts: 4 new tests for buildNewProjectKey
…e sessions - Add attachRoutePrefix, attachServicePort, attachServiceName config fields and sessionAttachHostname() to operator config - Add createAttachIngressRoute / deleteAttachIngressRoute in ingressroute.ts (no oauth2 middleware, targets code-attach Service on port 4096) - Wire attach IngressRoute into pod add/delete lifecycle and startup reconcile loop in index.ts - Create a dedicated code-attach Kubernetes Service in homelab/src/index.ts that exposes port 4096 so Traefik can route without the main Service - Add vitest test fixtures and describe block for attach IngressRoute create/delete behaviour
toMatchObject does not match missing keys against undefined; use not.toHaveProperty instead to assert the middlewares field is absent.
## Intent For sessions without a repo URL, getSessionHash uses crypto.randomUUID() so each call produces a different hash. Calling ensurePVC and ensurePod independently meant each got a distinct hash — the Pod referenced a PVC that didn't exist. ## Decisions - Hash is the first required parameter of ensurePVC/ensurePod (hash = identity, SessionKey = data). Callers must compute the hash once and pass it in; the functions no longer call getSessionHash internally. - startSession (new export) freezes the hash exactly once for no-repo sessions then passes it to both ensurePVC and ensurePod. api.ts no-repo path now calls startSession instead of ensurePVC + ensurePod directly. - Git-repo sessions remain deterministic; api.ts still calls ensurePVC/ensurePod directly with a pre-computed hash. - Hash is kept internal to pod-manager — not accepted from untrusted callers, preventing a malicious consumer from targeting another user's PVC. ## Major changes - pod-manager.ts: ensurePVC(hash, session) / ensurePod(hash, session, githubToken?, image?) - pod-manager.ts: startSession(session, githubToken?) — new export - pod-manager.ts: prepullImage / resumeSession updated to pre-compute hash - api.ts: no-repo path uses startSession; git-repo path passes hash explicitly - pod-manager.test.ts / api.test.ts: all signatures and mock index assertions updated - Added regression tests: startSession — stable hash for no-repo session
## Intent For sessions without a repo URL, getSessionHash uses crypto.randomUUID() so each call produces a different hash. Calling ensurePVC and ensurePod independently meant each got a distinct hash — the Pod referenced a PVC that didn't exist. ## Decisions - Hash is the first required parameter of ensurePVC/ensurePod (hash = identity, SessionKey = data). Callers must compute the hash once and pass it in; the functions no longer call getSessionHash internally. - startSession (new export) freezes the hash exactly once for no-repo sessions then passes it to both ensurePVC and ensurePod. api.ts no-repo path now calls startSession instead of ensurePVC + ensurePod directly. - Git-repo sessions remain deterministic; api.ts still calls ensurePVC/ensurePod directly with a pre-computed hash. - Hash is kept internal to pod-manager — not accepted from untrusted callers, preventing a malicious consumer from targeting another user's PVC. ## Major changes - pod-manager.ts: ensurePVC(hash, session) / ensurePod(hash, session, githubToken?, image?) - pod-manager.ts: startSession(session, githubToken?) — new export - pod-manager.ts: prepullImage / resumeSession updated to pre-compute hash - api.ts: no-repo path uses startSession; git-repo path passes hash explicitly - pod-manager.test.ts / api.test.ts: all signatures and mock index assertions updated - Added regression tests: startSession — stable hash for no-repo session
…s for dev-server port-forward URLs The public domain at which the router is reachable (e.g. no-panic.org) is now injected into session pods as OPENCODE_ROUTER_EXTERNAL_DOMAIN, so the dev-server skill can construct port-forward URLs without asking the user. - config.ts: new optional opencodeRouterExternalDomain field - pod-manager.ts: conditional env injection into session pods - deployment/homelab/src/index.ts: pass domain as the new env var - SKILL.md: replace 'ask user for domain' with one-liner using the env var - config.test.ts: test defaults to undefined when env var absent - pod-manager.test.ts: test env var is injected when configured
… metadata (#67) * fix: parse flinker /v1/models response using OpenAI-compatible shape fetchFlinkerModels() was reading data.models[].name but the OpenAI-compatible endpoint returns data.data[].id, causing flinker/Qwen models to always resolve to an empty object and never appear in the generated ConfigMap. Extract --ctx-size for limit.context/output, filter out embedding models (--embeddings flag) and placeholder entries (no --model/--hf-repo arg), set tool_call:true, and include a loaded/unloaded label in the model name.
## Summary
Allow users to maintain their own API keys as environment variables that are automatically injected into all their sessions.
### Features
- **K8s Secret pattern**: `opencode-user-<email-hash>` mirroring existing GitHub token pattern
- **Multi-key format**: Users can set multiple env vars (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) — keys map directly to environment variable names
- **API endpoints**:
- `GET /api/user/secret` — returns `{ hasSecret, keys[] }` (keys only, no values exposed)
- `POST /api/user/secret` — sets secrets via `{ secrets: { KEY: value } }`
- `DELETE /api/user/secret` — removes all user secrets
- **Settings UI**: Gear icon (⚙️) opens settings dialog at `/settings` route
- Add env var name + value pairs
- View existing keys (masked)
- Delete individual secrets or all at once
- **Auto-injection**: User's secrets mounted via `envFrom` to all their session pods
…ster Add an in-memory fake Kubernetes client activated via MOCK_K8S=true. This lets the opencode-router SPA be developed without a live k8s cluster. - src/index.ts: conditional import of mock-k8s.ts when MOCK_K8S is set - src/mock-k8s.ts: in-memory fake k8s client with 3 pre-seeded sessions - .env.local.example: add MOCK_K8S block with usage instructions
…atting, and detail panel polish
- Fix German i18n: replace stale loading.stage.cloning key, add all missing
translations (settings.*, form.tab.*, session group/meta labels, autocomplete)
- Add 8 new i18n keys to en.ts/de.ts: session.group.{current,active,stopped},
session.meta.{started,stopped,created}, session.messages.count, autocomplete.loading
- Fix Autocomplete onBlur bug: no longer calls onSelect unconditionally on blur,
preventing partial-text from overwriting a clicked item selection
- Add Autocomplete loading prop: shows translated 'Loading…' hint while repos fetch
- Add session branch display with data-testid for E2E test compatibility
- Fix settings dialog: button no longer pushes /settings to history; add typed
secretMessageIsError signal replacing fragile string.includes('error') check
- Add formatDateTime (Intl.DateTimeFormat) and sortedAndGroupedSessions helpers
to session-utils; overloaded signature supports 2-group (list) and 3-group
(sidebar: current/active/stopped) layouts
- session-list: each group (Active/Stopped) rendered as its own rounded card with
a proper uppercase section header; Sessions title moved above the cards
- session-sidebar: 3-group layout (Current → Active → Stopped) with small
uppercase group labels; drops inline 'current' badge that consumed too much space;
compact meta date wraps to two lines instead of truncating
- session-item: locale date+time via Intl.DateTimeFormat replaces relative 'X ago';
action buttons (Attach/Terminate) appear above the border-top divider when
expanded; messages list gets scroll-to-top/bottom controls when >3 messages
- session-sidebar: Home button replaces New Session (icon arrow-left)
Contributor
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
loading.stage.cloning→loading.stage.preparing), add all missing German translations, and replace every hardcoded UI string in the router app with typedt()calls (8 new keys: session group labels, meta date strings, message count, autocomplete loading hint)Intl.DateTimeFormattimestamps throughout (compact meta and expanded panel footer)border-topdivider when the panel is expanded, so they're immediately accessible without scrolling; messages list gets scroll-to-top/bottom jump controls when >3 messagesonBlurno longer unconditionally callsonSelect(fixes partial-text overwriting a clicked item);loadingprop shows translated hint while repos are being fetched/settingsto history (avoids stale URL); error/success message colour uses a typedsecretMessageIsErrorsignal instead ofstring.includes("error")data-testid="session-branch-display"added to satisfy existing E2E test expectationarrow-lefticon — accurately reflects that clicking it navigates back to the home screen