feat(desktop): account-wide naming instructions for workspaces and branches

Draft
AA
2h ago
+19495-214

What & why

Closes #9197. Third of three PRs. Depends on #9387 and #9388. This is a fork PR, so it also contains their commits. Review only the top commit, feat(desktop): account-wide naming instructions…. Per rule 9, it should merge only after #9388's API is live.

Today naming instructions can only be set per project, and they are stored per host. This PR adds an account-wide "Naming instructions" setting that applies in every project, on every host.

Prompt layering (ai-workspace-names.ts):

  • buildInstructions now takes { global, project } and emits <global-naming-instructions> first, then <project-naming-instructions>.
  • When both are set, it tells the model that the project block wins where the two conflict.
  • The custom-branch rules (60 characters, / allowed) apply when either block is set. Without this, a global-only "prefix with feat/" rule would come out as featfoo.

How the host gets the global text (option 2 in the issue): the new resolveNamingInstructions reads userSettings.get through the host's existing API client when naming runs.

  • This also covers CLI and automation creation paths, not only the desktop.
  • The read has a 3 s timeout. If it fails, or the API doesn't have the field yet, naming uses the project text only.
  • workspaces.aiRename does the read in its background chain, so the mutation still returns immediately.

Ignore toggle:

  • New host column projects.ignore_global_naming_instructions (host SQLite migration 0039, NOT NULL DEFAULT false).
  • New procedure project.setIgnoreGlobalNamingInstructions. project.get returns the new field.
  • When the toggle is on, the API isn't called at all.

Desktop UI:

  • Settings → Git & worktrees gets a "Naming instructions" field that saves to the account. It is also listed in settings search.
  • The project field shows the inherited global text as its placeholder, with the hint "…Empty uses the global instructions.", and has the "Ignore global naming instructions" switch.
  • The debounced save logic moved into a shared settings/components/NamingInstructionsField, which both fields use.
  • Older hosts: those that don't return ignoreGlobalNamingInstructions get the old UI (no toggle, no inherited text), because they don't apply global text either.

Fallback: deriving the name from the prompt (when the agent CLI can't run) still ignores all instructions. The docs (workspaces.mdx) now say so.

Compatibility: every host-service change is additive (a new procedure and a new output field). Existing English copy is unchanged; the new hint is a separate message, so existing translations stay valid. New strings are translated in all 17 locales.

How I tested it

Unit tests:

  • ai-workspace-names.test.ts:
    • prompts with global only, project only, and both (checks global-then-project order and the conflict line)
    • no custom block for blank input
    • a global-only feat/ reply keeps feat/add-login, while the same reply without instructions becomes featadd-login
  • naming-instructions.test.ts:
    • global and project text are returned together
    • the ignore toggle skips the API call
    • an unreachable API, or one without the field, means no global text
  • NamingInstructionsSection.test.tsx: inherited placeholder and hint; ignored state; an old host shows no toggle and no inherited text

In the dev desktop app (this worktree's renderer, driven over CDP with real clicks and key input, against the local API and host-service):

  1. Settings → Git & worktrees: typed "Prefix branches with feat/ or fix/ based on the task." The field showed "Saving…", and the DB row user_settings.naming_instructions for the signed-in user then held that text.
  2. Settings → Projects → a project: the empty project field showed the global text as its placeholder, with the "Empty uses the global instructions." hint and the toggle off.
  3. Clicked the toggle: it turned on, project.get on the host returned ignoreGlobalNamingInstructions: true, and the placeholder and hint went back to the defaults.
  4. Navigated away and back: the toggle was still on.

Not verified end to end: an actual AI naming run picking up the global text through the host's API client. That path is covered by the unit tests above.

Checks: bun run lint, typecheck (host-service, desktop, trpc), and bun run check:i18n pass. Desktop settings tests and the full host-service suite pass, apart from one ordering failure in runtime/pull-requests/pull-requests.test.ts (PR history order). That test passes on its own and this PR doesn't touch that code.

Screenshots of steps 1–3 were captured over CDP and will be attached here.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs
  • I have signed the CLA (the bot asks on your first PR)

Summary by cubic

Adds account-wide naming instructions for AI-generated workspace and branch names, applied in every project and on every host. Global instructions are layered before the project's, and the project's win where the two conflict.

  • The host reads the global text from the user's cloud settings at naming time (3 s timeout); a failed or missing read falls back to project text only.
  • Projects can toggle "Ignore global naming instructions," which skips the API call entirely; older hosts keep the old UI without the toggle.
  • Either block being set enables the custom branch rules, so a global feat/ prefix keeps its slash.
  • Settings → Git & worktrees gets the new "Naming instructions" field; both it and the project field share a debounced NamingInstructionsField component that shows the global text as the project field's placeholder.
  • All host-service changes are additive (new procedure, new output field, new host column and migration), and existing translations stay valid.