feat: add Dev and Preview title prefixes (#2464)

This commit is contained in:
Carlos Florêncio
2026-08-09 18:12:08 +01:00
committed by GitHub
parent e035a0c355
commit f4bc7f0a8c
13 changed files with 388 additions and 4 deletions
+1
View File
@@ -182,6 +182,7 @@ cd /app
export KANDEV_HOME_DIR=/data
export KANDEV_DOCKER_ENABLED=false
export KANDEV_LOG_LEVEL=info
export KANDEV_WEB_TITLE_PREFIX=Preview
export KANDEV_WEB_DIST_DIR=/app/apps/web/dist
# Preview mode: keep the mock agent while loading the built-in agent catalogue
# so Settings can show agents that are available to install.
@@ -154,6 +154,9 @@ func TestBuildExtractScript(t *testing.T) {
if !strings.Contains(script, "KANDEV_WEB_DIST_DIR=/app/apps/web/dist") {
t.Errorf("expected KANDEV_WEB_DIST_DIR to point at packaged Vite dist")
}
if !strings.Contains(script, "KANDEV_WEB_TITLE_PREFIX=Preview") {
t.Errorf("expected preview title prefix in script")
}
if !strings.Contains(script, "ln -sf /app/apps/backend/bin/kandev /usr/local/bin/kandev") {
t.Errorf("expected native kandev symlink in script")
}
@@ -158,6 +158,15 @@ debug:
dev: "true"
e2e: ""
# ── Browser identity ─────────────────────────────────────────────
# Distinguish the local development tab from release and preview tabs without
# changing the title of production or e2e environments.
ui:
KANDEV_WEB_TITLE_PREFIX:
prod: ""
dev: "Dev"
e2e: ""
# ── E2E test tuning ───────────────────────────────────────────────
# Behaviour knobs that only matter when running playwright. These
# don't belong in dev (would mask real interactions) and never in
@@ -52,6 +52,9 @@ func TestApplyProfile_DefaultsToProd(t *testing.T) {
"false",
)
}
if _, ok := os.LookupEnv("KANDEV_WEB_TITLE_PREFIX"); ok {
t.Errorf("KANDEV_WEB_TITLE_PREFIX is set in prod; want it unset")
}
}
// TestApplyProfile_DevUsesDevelopmentDefaults verifies the mixed dev profile:
@@ -86,6 +89,23 @@ func TestApplyProfile_DevUsesDevelopmentDefaults(t *testing.T) {
"false",
)
}
if v := os.Getenv("KANDEV_WEB_TITLE_PREFIX"); v != "Dev" {
t.Errorf("KANDEV_WEB_TITLE_PREFIX = %q in dev; want %q", v, "Dev")
}
}
func TestApplyProfile_ExplicitTitlePrefixWinsInDev(t *testing.T) {
clearProfileSelectors(t)
clearProfilesYAMLVars(t)
t.Setenv("KANDEV_DEBUG_DEV_MODE", "true")
t.Setenv("KANDEV_WEB_TITLE_PREFIX", "Custom")
if _, _, err := ApplyProfile(); err != nil {
t.Fatalf("ApplyProfile: %v", err)
}
if v := os.Getenv("KANDEV_WEB_TITLE_PREFIX"); v != "Custom" {
t.Errorf("ApplyProfile overwrote explicit title prefix: got %q; want %q", v, "Custom")
}
}
// TestApplyProfile_E2EWinsOverDev checks the documented detection
@@ -118,6 +138,9 @@ func TestApplyProfile_E2EWinsOverDev(t *testing.T) {
"false",
)
}
if _, ok := os.LookupEnv("KANDEV_WEB_TITLE_PREFIX"); ok {
t.Errorf("KANDEV_WEB_TITLE_PREFIX is set in e2e; want it unset")
}
}
// TestApplyProfile_ExistingEnvWins is the precedence guarantee that
@@ -230,6 +253,7 @@ func TestProfilesYAML_ContainsRequiredSections(t *testing.T) {
"KANDEV_FEATURES_OFFICE:",
"KANDEV_FEATURES_APP_STATUS_BAR:",
"KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF:",
"KANDEV_WEB_TITLE_PREFIX:",
} {
if !strings.Contains(yaml, section) {
t.Errorf("embedded profiles.yaml missing section/key %q; embed broken or file truncated", section)
+4
View File
@@ -29,6 +29,7 @@ describe("supervisor launch manifest", () => {
KANDEV_HOME_DIR: home,
KANDEV_SERVER_PORT: "38429",
KANDEV_DESKTOP_HEALTH_TOKEN: "health-token",
KANDEV_WEB_TITLE_PREFIX: "Preview",
SECRET_TOKEN: "do-not-write",
},
home_dir: home,
@@ -45,6 +46,7 @@ describe("supervisor launch manifest", () => {
KANDEV_HOME_DIR: home,
KANDEV_SERVER_PORT: "38429",
KANDEV_DESKTOP_HEALTH_TOKEN: "health-token",
KANDEV_WEB_TITLE_PREFIX: "Preview",
});
if (process.platform !== "win32") {
expect((fs.statSync(target).mode & 0o777).toString(8)).toBe("600");
@@ -75,6 +77,7 @@ describe("supervisor launch manifest", () => {
KANDEV_HOME_DIR: "/tmp/kandev",
KANDEV_CONSOLE_LOG_LEVEL: "warn",
KANDEV_DESKTOP_HEALTH_TOKEN: "health-token",
KANDEV_WEB_TITLE_PREFIX: "Dev",
AWS_SECRET_ACCESS_KEY: "secret",
GITHUB_TOKEN: "secret",
}),
@@ -82,6 +85,7 @@ describe("supervisor launch manifest", () => {
KANDEV_HOME_DIR: "/tmp/kandev",
KANDEV_CONSOLE_LOG_LEVEL: "warn",
KANDEV_DESKTOP_HEALTH_TOKEN: "health-token",
KANDEV_WEB_TITLE_PREFIX: "Dev",
});
});
});
+1
View File
@@ -29,6 +29,7 @@ const ENV_ALLOWLIST = new Set([
"KANDEV_AGENT_STANDALONE_PORT",
"KANDEV_LOG_LEVEL",
"KANDEV_CONSOLE_LOG_LEVEL",
"KANDEV_WEB_TITLE_PREFIX",
"KANDEV_DEBUG_DEV_MODE",
"KANDEV_DEBUG_AGENT_MESSAGES",
"KANDEV_DEBUG_PPROF_ENABLED",
+9 -3
View File
@@ -438,8 +438,9 @@ export const backendFixture = base.extend<object, { backend: BackendContext }>({
// process.env.X before spawn — that already flows through the
// `...sanitizeInheritedEnv(process.env)` spread above, and the
// backend's ApplyProfile leaves already-set vars alone. (Note:
// KANDEV_FEATURES_* is the exception — it's stripped from the
// inherited env so the profile always governs feature flags.)
// KANDEV_FEATURES_* and KANDEV_WEB_TITLE_PREFIX are exceptions —
// they are stripped from the inherited env so the e2e profile
// always controls the baseline.)
GIT_AUTHOR_NAME: "E2E Test",
GIT_AUTHOR_EMAIL: "e2e@test.local",
GIT_COMMITTER_NAME: "E2E Test",
@@ -539,6 +540,9 @@ function writeGitShimLauncher(shimDir: string, shimScript: string): void {
// in the test backend → /api/v1/office/* 404s. Dropping the whole
// KANDEV_FEATURES_* namespace lets the e2e profile govern feature flags so
// the suite always exercises them, regardless of where it's launched.
// - KANDEV_WEB_TITLE_PREFIX — the browser identity is profile-managed in
// this suite. Explicit per-test values are applied after this baseline is
// sanitized through `backend.restart(overrides)`.
// - PATH casing aliases — Windows commonly inherits `Path`; retaining it
// beside the fixture's new `PATH` makes child-process lookup order
// ambiguous. The caller restores one canonical PATH after sanitizing.
@@ -547,7 +551,9 @@ function sanitizeInheritedEnv(env: Record<string, string>): Record<string, strin
delete cleaned.GH_TOKEN;
delete cleaned.GITHUB_TOKEN;
for (const key of Object.keys(cleaned)) {
if (key.startsWith("KANDEV_FEATURES_")) delete cleaned[key];
if (key === "KANDEV_WEB_TITLE_PREFIX" || key.startsWith("KANDEV_FEATURES_")) {
delete cleaned[key];
}
if (key.toUpperCase() === "PATH") delete cleaned[key];
}
return cleaned;
@@ -0,0 +1,28 @@
import { expect, test } from "../../fixtures/test-base";
test.describe.serial("environment browser title prefixes", () => {
test("uses Dev for the development profile", async ({ backend, testPage }) => {
await backend.restart({ KANDEV_E2E_MOCK: "", KANDEV_DEBUG_DEV_MODE: "true" });
try {
await testPage.goto("/");
await expect(testPage).toHaveTitle("Dev Kandev");
} finally {
await backend.restart();
}
});
test("uses Preview for an explicitly configured preview backend", async ({
backend,
testPage,
}) => {
await backend.restart({ KANDEV_WEB_TITLE_PREFIX: "Preview" });
try {
await testPage.goto("/");
await expect(testPage).toHaveTitle("Preview Kandev");
} finally {
await backend.restart();
}
});
});
+4
View File
@@ -34,6 +34,10 @@ All env vars use the `KANDEV_` prefix. Nested keys map by replacing `.` with `_`
The aliases on the right are explicit bindings - see `LoadWithPath` in `config.go` for the full list. New keys should follow the deterministic rule (`KANDEV_<SECTION>_<KEYUPPERCASE>`) unless there is a reason to add an alias.
The `make dev` launcher uses `Dev` as the default browser title prefix, and PR
preview environments use `Preview`. An explicit `KANDEV_WEB_TITLE_PREFIX`
value overrides those environment defaults.
## Full `config.yaml` example
Every key shown here has a default - copying the whole file changes nothing. Use it as a starting point and delete what you don't need to override.
@@ -0,0 +1,116 @@
---
spec: docs/specs/dev-preview-title-prefixes/spec.md
created: 2026-08-09
status: complete
---
# Implementation Plan: Environment-specific browser tab title prefixes
## Overview
Reuse the merged PR #2459 title-prefix contract and supply environment-specific
defaults at the launch boundaries. The development profile will provide `Dev`,
the PR preview startup script will provide `Preview`, and the CLI supervisor
will preserve the prefix across restarts. Focused Go, CLI, browser, and docs
checks will cover the launcher contracts and the visible browser result.
## Backend and launchers
### Development profile
- `apps/backend/internal/profiles/profiles.yaml` (the target of the root
`profiles.yaml` symlink): add `KANDEV_WEB_TITLE_PREFIX` with an empty `prod`
and `e2e` value and `Dev` as the `dev` value.
- `apps/backend/internal/profiles/profiles_test.go`: assert the dev profile
supplies `Dev` while the existing profile precedence remains intact.
### PR preview launcher
- `apps/backend/cmd/preview/sprite_ops.go`: export
`KANDEV_WEB_TITLE_PREFIX=Preview` in the generated preview startup script.
- `apps/backend/cmd/preview/sprite_ops_test.go`: pin the generated script to
the `Preview` prefix alongside its existing startup assertions.
### Supervisor environment contract
- `apps/cli/src/supervisor/manifest.ts`: add `KANDEV_WEB_TITLE_PREFIX` to the
restart-manifest allowlist so any configured prefix survives a supervised
backend restart.
- `apps/cli/src/supervisor/manifest.test.ts`: verify the title-prefix variable
is retained while arbitrary variables remain excluded.
## Frontend and browser behavior
No new frontend implementation is required. The existing PR #2459 path already
renders the configured prefix in the server shell and applies it for the
`/api/v1/app-state` boot path.
Add `apps/web/e2e/tests/system/title-prefix.spec.ts` to restart the fixture
backend with the development profile and with an explicit preview prefix, then
assert both visible browser titles through the UI. The test restores the worker
environment afterward.
### Mobile parity contract
The affected surface is browser metadata (`document.title`), not layout,
navigation, touch input, scrolling, or viewport-bound UI. The desktop and
mobile outcomes are identical, so no mobile-specific interaction test is
needed; the E2E test and existing title helper contract cover the same
viewport-independent behavior.
## Public documentation
- `docs/public/configuration.md`: explain that `make dev` defaults the title
prefix to `Dev` and PR previews use `Preview`, while explicit environment
values still override defaults.
- `docs/configuration.md`: keep the internal configuration reference aligned.
## Tests
- **What:** the development profile resolves the title prefix to `Dev` and
leaves production/e2e defaults blank.
**File:** `apps/backend/internal/profiles/profiles_test.go`.
**How:** profile application unit test with isolated environment variables.
- **What:** the preview startup script exports `Preview`.
**File:** `apps/backend/cmd/preview/sprite_ops_test.go`.
**How:** generated-script contract assertion.
- **What:** the supervisor retains the title-prefix environment variable.
**File:** `apps/cli/src/supervisor/manifest.test.ts`.
**How:** allowlist unit test.
- **What:** a configured runtime prefix is visible in the browser title.
**File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`.
**How:** worker backend restart plus Playwright UI assertion.
## E2E Tests
- **Scenarios:** GIVEN a fixture backend restarted with the development profile,
WHEN the browser opens Kandev, THEN the page title is `Dev Kandev`; GIVEN a
fixture backend restarted with `KANDEV_WEB_TITLE_PREFIX=Preview`, THEN the
page title is `Preview Kandev`.
- **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`.
- **What to verify:** the user-visible browser tab title after a fresh page
navigation.
## Verification Results
- `cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview` —
passed, 30 tests in 2 packages.
- `cd apps && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts` —
passed, 2 files and 6 tests.
- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` —
passed, 2 Playwright tests (`Dev Kandev` and `Preview Kandev`).
- `node --test scripts/validate-public-docs.test.mjs` — passed, 58 tests.
- `node scripts/validate-public-docs.mjs` — passed, 41 published pages.
- `pnpm exec prettier --check ...` — passed for the changed TypeScript files.
- `git diff --check` — passed.
## Implementation Waves And Parallel Candidates
Wave 1 (sequential; the launch contracts and their tests are one cohesive
change):
- [x] [task-01-launch-prefixes](task-01-launch-prefixes.md)
## Open Questions
None.
@@ -0,0 +1,104 @@
---
id: "01-launch-prefixes"
title: "Set environment title prefixes"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/dev-preview-title-prefixes/spec.md"
---
# Task 01: Set environment title prefixes
## Acceptance
- `make dev` selects a backend profile that produces `Dev Kandev` by default,
while normal production/start and e2e profiles remain unprefixed unless an
explicit environment override is supplied.
- PR preview startup produces `Preview Kandev`, and the CLI supervisor retains
`KANDEV_WEB_TITLE_PREFIX` when it writes and reuses its restart manifest.
- Focused browser, Go, TypeScript, and public-doc checks pass for the new
launcher behavior.
## Verification
Run the focused unit checks:
```bash
cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview
cd ../ && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts
```
Run the browser contract against the managed production E2E build:
```bash
cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts
```
Validate public documentation:
```bash
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
```
On a fresh worktree, install workspace dependencies first with
`cd apps && pnpm install --frozen-lockfile`.
## Files likely touched
- `apps/backend/internal/profiles/profiles.yaml`
- `apps/backend/internal/profiles/profiles_test.go`
- `apps/backend/cmd/preview/sprite_ops.go`
- `apps/backend/cmd/preview/sprite_ops_test.go`
- `apps/cli/src/supervisor/manifest.ts`
- `apps/cli/src/supervisor/manifest.test.ts`
- `apps/web/e2e/tests/system/title-prefix.spec.ts`
- `docs/configuration.md`
- `docs/public/configuration.md`
## Dependencies
None.
## Parallelism
Sequential. The profile, preview script, supervisor allowlist, and browser test
all implement one environment-to-title contract, and the tests share the same
behavioral acceptance criteria.
## Inputs
- `docs/specs/dev-preview-title-prefixes/spec.md`
- `docs/plans/dev-preview-title-prefixes/plan.md`
- The merged PR #2459 `KANDEV_WEB_TITLE_PREFIX` contract.
- Existing profile precedence: explicit shell/launcher environment overrides
profile defaults.
- Existing backend and web E2E fixtures.
## Output contract
Report the changed files, exact test commands and results, any E2E cleanup, and
whether the task and plan statuses were updated. Record blockers and remaining
risks explicitly.
## Results
Implemented the profile, preview launcher, and supervisor environment
contracts. Added browser coverage for both environment defaults and updated the
public and internal configuration references.
Verification completed:
- `cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview` —
passed, 30 tests in 2 packages.
- `cd apps && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts` —
passed, 2 files and 6 tests.
- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` —
passed, 2 Playwright tests.
- `node --test scripts/validate-public-docs.test.mjs` — passed, 58 tests.
- `node scripts/validate-public-docs.mjs` — passed, 41 published pages.
- `pnpm exec prettier --check ...` and `git diff --check` — passed.
The managed E2E runner restored the fixture backend after each test; no
generated files are tracked in the worktree.
+1 -1
View File
@@ -58,7 +58,7 @@ Some common camelCase keys have explicit compatibility aliases. Use the document
| `server.readTimeout` | `KANDEV_SERVER_READTIMEOUT` | `30` | HTTP read timeout in seconds. |
| `server.writeTimeout` | `KANDEV_SERVER_WRITETIMEOUT` | `30` | HTTP write timeout in seconds. |
| `server.webInternalUrl` | `KANDEV_WEB_INTERNAL_URL` | empty | Development reverse-proxy target for a separately running web app. Installed releases normally serve embedded assets. |
| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as `<prefix> Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. Empty keeps the plain `Kandev` title. |
| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as `<prefix> Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. `make dev` defaults to `Dev`; PR previews use `Preview`. An explicit value overrides these defaults. Empty keeps the plain `Kandev` title. |
The default host exposes the server on every interface even though the CLI prints a `localhost` URL. The current local product path must not be treated as an authenticated multi-user perimeter. For remote access, bind to loopback and use a trusted authenticated tunnel/proxy, or isolate the network at the deployment layer.
@@ -0,0 +1,84 @@
---
status: shipped
created: 2026-08-09
owner: Kandev
---
# Environment-specific browser tab title prefixes
## Why
Developers often keep a local development instance and one or more pull-request
preview instances open at the same time. Identical `Kandev` browser tabs make it
easy to use the wrong instance.
## What
- A Kandev browser tab title uses the existing `<prefix> Kandev` composition.
- A backend started in the development profile, including `make dev`, uses the
default prefix `Dev`, so its tab title is `Dev Kandev`.
- A PR preview environment uses the prefix `Preview`, so its tab title is
`Preview Kandev`.
- An explicitly supplied `KANDEV_WEB_TITLE_PREFIX` continues to take precedence
over a profile default. Its value is trimmed by the existing title-prefix
contract.
- A normal production/start launch with no explicit prefix keeps the plain
`Kandev` title.
- The prefix is present both on the initial server-rendered page and after a
client boot from `/api/v1/app-state`, using the title behavior delivered by
PR #2459.
- A supervised backend restart keeps the configured prefix.
## API surface
The existing environment contract remains the public configuration surface:
| Environment | `KANDEV_WEB_TITLE_PREFIX` | Result |
|---|---|---|
| Development profile (`make dev`) | `Dev` by default | `Dev Kandev` |
| PR preview launcher | `Preview` | `Preview Kandev` |
| Normal start with no override | unset | `Kandev` |
| Any launch with an explicit override | caller value | `<value> Kandev` |
The prefix is process configuration. It is not stored in the database and is
not changed from the Kandev UI.
## Failure modes
- If the prefix is unset or blank, the title remains `Kandev`.
- If the preview launcher cannot set the environment variable, the preview
falls back to the existing plain-title behavior rather than failing startup.
- If a supervised restart occurs, the launcher preserves the allowlisted prefix
in the restart manifest so the restarted backend does not silently change its
title.
## Persistence guarantees
The title prefix lasts for the lifetime of the launched environment and its
supervised restarts. It is not persisted across separate launches unless the
caller supplies the same environment or profile again.
## Scenarios
- **GIVEN** a clean checkout with no title-prefix override, **WHEN** the user
runs `make dev` and opens the Kandev URL, **THEN** the browser title is
`Dev Kandev`.
- **GIVEN** a PR preview deployment, **WHEN** its startup service launches
Kandev, **THEN** the browser title is `Preview Kandev`.
- **GIVEN** a development profile and `KANDEV_WEB_TITLE_PREFIX=Custom`, **WHEN**
the backend starts, **THEN** the browser title is `Custom Kandev`.
- **GIVEN** a normal start with no title-prefix environment variable, **WHEN**
the user opens Kandev, **THEN** the browser title is `Kandev`.
- **GIVEN** a preview or development backend with a configured prefix, **WHEN**
its supervisor restarts the backend, **THEN** the browser title retains the
same prefix.
- **GIVEN** a phone-sized browser viewport, **WHEN** either environment is
opened, **THEN** the same title is shown; no viewport-specific interaction is
required.
## Out of scope
- Changing the title dynamically from the Kandev UI.
- Changing desktop window titles, CLI output, logs, or task titles.
- Automatically labeling arbitrary self-hosted instances beyond the existing
explicit environment variable.