Skip to content

feat(router-app): UX improvements — i18n, session grouping, date formatting, detail panel polish#28478

Closed
mrsimpson wants to merge 639 commits into
anomalyco:devfrom
mrsimpson:feat/router-app-ux-improvements
Closed

feat(router-app): UX improvements — i18n, session grouping, date formatting, detail panel polish#28478
mrsimpson wants to merge 639 commits into
anomalyco:devfrom
mrsimpson:feat/router-app-ux-improvements

Conversation

@mrsimpson
Copy link
Copy Markdown

Summary

  • i18n completeness: Fix stale German key (loading.stage.cloningloading.stage.preparing), add all missing German translations, and replace every hardcoded UI string in the router app with typed t() calls (8 new keys: session group labels, meta date strings, message count, autocomplete loading hint)
  • Session list grouping: When both active and stopped sessions exist, each group is rendered as its own rounded card with an uppercase section header (no more blended-in header rows); single-group case stays as a plain card
  • Sidebar 3-group layout: Current → Active → Stopped groups with small uppercase labels; drops the inline "current" badge that consumed too much horizontal space in the narrow sidebar; compact session meta date wraps to two lines instead of truncating
  • Date formatting: Replace relative "X ago" strings with locale-aware Intl.DateTimeFormat timestamps throughout (compact meta and expanded panel footer)
  • Detail panel polish: Action buttons (Attach / Terminate) now appear above the border-top divider when the panel is expanded, so they're immediately accessible without scrolling; messages list gets scroll-to-top/bottom jump controls when >3 messages
  • Autocomplete fixes: onBlur no longer unconditionally calls onSelect (fixes partial-text overwriting a clicked item); loading prop shows translated hint while repos are being fetched
  • Settings: Dialog button no longer pushes /settings to history (avoids stale URL); error/success message colour uses a typed secretMessageIsError signal instead of string.includes("error")
  • Session branch display: data-testid="session-branch-display" added to satisfy existing E2E test expectation
  • Sidebar Home button: "New Session" relabelled to "Home" with arrow-left icon — accurately reflects that clicking it navigates back to the home screen

mrsimpson and others added 30 commits April 16, 2026 08:37
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
github-actions Bot and others added 25 commits May 7, 2026 16:53
…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)
@mrsimpson mrsimpson requested a review from adamdotdevin as a code owner May 20, 2026 14:35
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

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.

@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

The only result matching the current PR #28478 itself is the current PR. The unrelated PR #13885 (TUI status line template system) is not a duplicate.

No duplicate PRs found

@mrsimpson mrsimpson closed this May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:compliance This means the issue will auto-close after 2 hours.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants