Rename runs in the UI: user-set title as pure display overlay #111

Closed
opened 2026-07-10 18:24:22 +02:00 by dominik.polakovics · 1 comment

Goal

Let the user rename any run (live or finished, manual or AFK) from the UI. The rename is a pure display overlay — a user-set title stored on the run — and never touches identity.

Motivation (all three): telling running agents apart at a glance, correcting a bad/missing spawn label, and meaningful names in History.

Decisions (from grill session, 2026-07-10)

  1. Overlay only — identity is immutable. session_name, git branch, worktree path, and tmux session are never re-keyed. The label is structurally load-bearing (internal/gitx/session.go:12-18): it seeds tmux identity, branch (AFK claim = branch existence), and worktree path. Renaming any of those on a live run breaks the chat tailer, transcript cwd matching, and AFK claims.
  2. Vocabulary: "title". New nullable column runs.title (TEXT, NULL = no override). Displayed name = title || parsed-label title || branch (extend instanceTitle chain in web/src/lib/instanceLabel.ts). This is renaming an instance — never say "session" (CONTEXT.md ambiguity rule). Add a one-line Title entry to CONTEXT.md: user-set display name on a run; overrides the label-derived title; never part of identity.
  3. Scope: any run. Live or finished, manual or AFK. No liveness check — it is just a display field.
  4. API: PATCH /api/v1/runs/{id} with body {"title": "..."} or {"title": null} to clear. Auth + CSRF like the existing repos/credentials PATCH routes (internal/httpapi/server.go). Thin handler + store method; no internal/instance/rename.go ceremony. Emit an SSE event on change so the rail updates in other tabs.
  5. Validation: trim whitespace; empty-after-trim = clear (store NULL); reject > ~120 chars; otherwise free text — SanitizeLabel does NOT apply since the title never reaches git/tmux/paths.
  6. UI affordance: chat header only (web/src/routes/RunChat.tsx ChatHeader). Click title / pencil-on-hover, inline edit; mirror the mode signal pattern from web/src/routes/Credentials.tsx rename. Saving an empty field reverts to the generated title (no separate reset button). No rail context menu, no History inline edit — rail and History just render the new fallback chain.
  7. Original label stays visible: when a title is set, the chat header shows the raw label as secondary text/tooltip (e.g. afk-83 · <repo>~afk-83) for correlating with branch/worktree/tmux in a terminal. Rail and History show the title alone.
  8. Downstream consumers: push-notification payload builder uses the same title || label fallback. PR titles, commit messages, deep links: untouched.

Deliverables

  • SQLite migration adding nullable runs.title.
  • Store method + PATCH /api/v1/runs/{id} handler (auth/CSRF) + SSE emit.
  • Frontend: API client field, fallback chain in instanceLabel.ts, inline rename in ChatHeader, secondary raw-label text when title set, rail/History pick up titles.
  • Notification payload uses title when set.
  • CONTEXT.md Title vocabulary entry.
  • ADR 0037 (next free number): "Run title is a display overlay; identity is immutable after spawn." One page: context = label is load-bearing identity; decision = nullable runs.title overlay; consequences = raw identifiers still show in git/tmux, secondary text in header, rename never re-keys tmux/branch/worktree.
  • Tests: store round-trip incl. NULL-clear, handler validation (trim, length, null), title precedence in label helper.
## Goal Let the user rename any run (live or finished, manual or AFK) from the UI. The rename is a **pure display overlay** — a user-set **title** stored on the run — and never touches identity. Motivation (all three): telling running agents apart at a glance, correcting a bad/missing spawn label, and meaningful names in History. ## Decisions (from grill session, 2026-07-10) 1. **Overlay only — identity is immutable.** `session_name`, git branch, worktree path, and tmux session are never re-keyed. The label is structurally load-bearing (`internal/gitx/session.go:12-18`): it seeds tmux identity, branch (AFK claim = branch existence), and worktree path. Renaming any of those on a live run breaks the chat tailer, transcript cwd matching, and AFK claims. 2. **Vocabulary: "title".** New nullable column `runs.title` (`TEXT`, `NULL` = no override). Displayed name = `title || parsed-label title || branch` (extend `instanceTitle` chain in `web/src/lib/instanceLabel.ts`). This is *renaming an instance* — never say "session" (CONTEXT.md ambiguity rule). Add a one-line **Title** entry to CONTEXT.md: user-set display name on a run; overrides the label-derived title; never part of identity. 3. **Scope: any run.** Live or finished, manual or AFK. No liveness check — it is just a display field. 4. **API:** `PATCH /api/v1/runs/{id}` with body `{"title": "..."}` or `{"title": null}` to clear. Auth + CSRF like the existing repos/credentials PATCH routes (`internal/httpapi/server.go`). Thin handler + store method; no `internal/instance/rename.go` ceremony. Emit an SSE event on change so the rail updates in other tabs. 5. **Validation:** trim whitespace; empty-after-trim = clear (store NULL); reject > ~120 chars; otherwise free text — `SanitizeLabel` does NOT apply since the title never reaches git/tmux/paths. 6. **UI affordance: chat header only** (`web/src/routes/RunChat.tsx` ChatHeader). Click title / pencil-on-hover, inline edit; mirror the `mode` signal pattern from `web/src/routes/Credentials.tsx` rename. Saving an empty field reverts to the generated title (no separate reset button). No rail context menu, no History inline edit — rail and History just render the new fallback chain. 7. **Original label stays visible:** when a title is set, the chat header shows the raw label as secondary text/tooltip (e.g. `afk-83 · <repo>~afk-83`) for correlating with branch/worktree/tmux in a terminal. Rail and History show the title alone. 8. **Downstream consumers:** push-notification payload builder uses the same `title || label` fallback. PR titles, commit messages, deep links: untouched. ## Deliverables - SQLite migration adding nullable `runs.title`. - Store method + `PATCH /api/v1/runs/{id}` handler (auth/CSRF) + SSE emit. - Frontend: API client field, fallback chain in `instanceLabel.ts`, inline rename in ChatHeader, secondary raw-label text when title set, rail/History pick up titles. - Notification payload uses title when set. - CONTEXT.md **Title** vocabulary entry. - **ADR 0037** (next free number): "Run title is a display overlay; identity is immutable after spawn." One page: context = label is load-bearing identity; decision = nullable `runs.title` overlay; consequences = raw identifiers still show in git/tmux, secondary text in header, rename never re-keys tmux/branch/worktree. - Tests: store round-trip incl. NULL-clear, handler validation (trim, length, null), title precedence in label helper.
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PR #113 (afk/111): verdict PASS

  • CI: green — ci / native success (5m3s). Relied on, not re-run.
  • Convention: title feat(runs): rename runs in the UI … is Conventional Commits ✓
  • AFK contract: head afk/111, body carries a valid Closes #111
  • Mergeable: clean 3-way merge into current main (advanced +2 by #112); git merge-tree reports no conflicts ✓
  • Diff review (24 files, +718/−18):
    • PATCH /api/v1/runs/{id} correctly distinguishes JSON null (clear) from absent (leave) via map[string]json.RawMessage; trims, whitespace-only clears to NULL, >120 runes → 400, unknown field → 400; run.changed published only when title is set.
    • Dual-dialect migration 0008_run_title (sqlite + postgres), nullable column, reversible.
    • store.Run.DisplayName() (Go) and runDisplayTitle() (TS) share title-wins semantics; push builders (afk/notify, chat/notify) now name runs via DisplayName(), so a push matches what the operator sees.
    • Instance SSE payload flows title through (instances.go embeds runJSON), so a live rename refetches into the rail. History + rail render title-alone by design.
    • Inline rename UI: maxlength=120 matches the server cap, Escape cancels, empty-save clears (the reset path), errors ride the page banner.
  • ADR: 0040 (0037–0039 already taken) documents identity-immutable / display-overlay decision.

Merge is gated on the operator’\s explicit free-text go-ahead.

> *This was generated by AI while landing a PR.* **Landing audit — PR #113 (`afk/111`): verdict PASS** - **CI**: green — `ci / native` success (5m3s). Relied on, not re-run. - **Convention**: title `feat(runs): rename runs in the UI …` is Conventional Commits ✓ - **AFK contract**: head `afk/111`, body carries a valid `Closes #111` ✓ - **Mergeable**: clean 3-way merge into current `main` (advanced +2 by #112); `git merge-tree` reports no conflicts ✓ - **Diff review** (24 files, +718/−18): - PATCH `/api/v1/runs/{id}` correctly distinguishes JSON `null` (clear) from absent (leave) via `map[string]json.RawMessage`; trims, whitespace-only clears to NULL, >120 **runes** → 400, unknown field → 400; `run.changed` published only when title is set. - Dual-dialect migration `0008_run_title` (sqlite + postgres), nullable column, reversible. - `store.Run.DisplayName()` (Go) and `runDisplayTitle()` (TS) share title-wins semantics; push builders (`afk/notify`, `chat/notify`) now name runs via `DisplayName()`, so a push matches what the operator sees. - Instance SSE payload flows `title` through (`instances.go` embeds `runJSON`), so a live rename refetches into the rail. History + rail render title-alone by design. - Inline rename UI: `maxlength=120` matches the server cap, Escape cancels, empty-save clears (the reset path), errors ride the page banner. - **ADR**: 0040 (0037–0039 already taken) documents identity-immutable / display-overlay decision. Merge is gated on the operator’\s explicit free-text go-ahead.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#111
No description provided.