Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8c6dc84
Reshuffle Roles model UI: inline list control + Model section, fix sa…
ajonkisz Jun 20, 2026
9e7ff59
Merge roles model UI implementation
ajonkisz Jun 20, 2026
7aa9057
test(roles): cover model reshuffle list/detail + save-hang regression
ajonkisz Jun 20, 2026
81d5842
Merge roles model UI tests
ajonkisz Jun 20, 2026
61950de
test(roles): add browser E2E for model reshuffle (detail layout, save…
ajonkisz Jun 20, 2026
e3c7e28
Merge roles model browser E2E
ajonkisz Jun 20, 2026
58e19a6
test: click explicit role edit action in marketplace E2E
ajonkisz Jun 20, 2026
1b1bd31
docs: document Roles model UI reshuffle and save-hang fix
ajonkisz Jun 20, 2026
7f04b2a
Merge roles model UI documentation
ajonkisz Jun 20, 2026
3ae3cb2
Merge remote-tracking branch 'origin/master' into goal/roles-model-re…
ajonkisz Jun 20, 2026
b05be8e
Merge remote-tracking branch 'origin/master' into goal/roles-model-re…
ajonkisz Jun 20, 2026
1fe1416
Add role model/thinking source metadata to /api/roles
ajonkisz Jun 20, 2026
a607990
Merge server role model metadata
ajonkisz Jun 20, 2026
e24f82b
test(roles): pin polished list model-control contract (source badges,…
ajonkisz Jun 20, 2026
abadf54
Merge roles model UI tests
ajonkisz Jun 20, 2026
abfb510
Compact, source-aware Roles list model/thinking controls
ajonkisz Jun 20, 2026
90c82d5
Merge remote-tracking branches 'origin/goal/a0f95537/coder-4516' and …
ajonkisz Jun 20, 2026
3e42dd1
Align Roles list controls with server metadata + test hook contract
ajonkisz Jun 20, 2026
ce7ff92
Merge compact roles list controls
ajonkisz Jun 20, 2026
d9325dc
Fix Roles model-control inheritance + precedence findings
ajonkisz Jun 20, 2026
f67ac7d
Merge branch 'goal/a0f95537/coder-6ac8' into goal/polish-roles-m-a0f9…
ajonkisz Jun 20, 2026
c68ce4b
Fix role model-source winner precedence + inline save race
ajonkisz Jun 20, 2026
ca913c9
Merge branch 'goal/a0f95537/coder-178c' into goal/polish-roles-m-a0f9…
ajonkisz Jun 20, 2026
b35e897
test(lifecycle-hub): stabilize provider dispatch flake under load
ajonkisz Jun 20, 2026
c77569c
Merge branch 'goal/a0f95537/coder-2c13' into goal/polish-roles-m-a0f9…
ajonkisz Jun 20, 2026
c158164
fix(roles): stop render-time autosave of inherited model/thinking + c…
ajonkisz Jun 20, 2026
c7dfae2
Merge branch 'goal/a0f95537/coder-260d' into goal/polish-roles-m-a0f9…
ajonkisz Jun 20, 2026
b45681c
docs(roles): document Roles list inline model controls and modelResol…
ajonkisz Jun 20, 2026
f653f85
Merge branch 'goal/a0f95537/docs-writer-84ac' into goal/polish-roles-…
ajonkisz Jun 20, 2026
17b60f8
Merge child goal branch goal/polish-roles-m-a0f95537 into goal/roles-…
ajonkisz Jun 20, 2026
0e24a19
fix(roles): make list thinking Select compact-by-construction to stop…
ajonkisz Jun 20, 2026
f4cf986
Merge compact Roles control overflow fix
ajonkisz Jun 20, 2026
43fb81c
design: Roles list model/thinking row polish mockup
ajonkisz Jun 20, 2026
160bd6f
Merge Roles model row polish mockup
ajonkisz Jun 20, 2026
21b449d
feat(roles): readable Option A model row (flat source caption, in-box…
ajonkisz Jun 20, 2026
bdebc8f
Merge readable Roles model row redesign
ajonkisz Jun 20, 2026
3a8999d
polish(roles): self-explanatory inherited model captions + clear rese…
ajonkisz Jun 20, 2026
06854d5
Merge Roles source label clarification
ajonkisz Jun 20, 2026
670e925
polish roles inline model defaults
ajonkisz Jun 20, 2026
0b6211e
Merge inline Roles default labels
ajonkisz Jun 20, 2026
75366b7
Polish roles inline model controls
ajonkisz Jun 21, 2026
6842fed
Merge Roles thinking width and override cue polish
ajonkisz Jun 21, 2026
19e9529
polish roles row width allocation
ajonkisz Jun 21, 2026
7f492fc
keep roles list compact at 880px
ajonkisz Jun 21, 2026
ac2d64e
Merge origin/master into roles model branch
ajonkisz Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
678 changes: 678 additions & 0 deletions design/roles-model-row-polish-options.html

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions docs/design/per-role-model-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,3 +589,121 @@ model under a different id" class of bug.
| `tests/manual-integration/role-model-override.test.ts` | Manual binding-verification test. |
| `AGENTS.md` | Add a Recipes bullet: **"Per-role model override"** → role yaml `model:`/`thinkingLevel:`; bound at session start by `tryAutoSelectModel` / `tryApplyDefaultThinkingLevel` (`session-manager.ts:2814`); reviewer/QA path in `verification-harness.ts` 3 sites. |
| `docs/internals.md` | Brief note under "Per-project config" or a new "Per-role model" subsection cross-linking this design doc. |

---

## 8. Roles Model UI Reshuffle & Save-Hang Fix

To make per-role model and thinking level overrides visible at a glance and editable in one place, the Roles UI was reshuffled (not redesigning the underlying model) to introduce list-row inline model controls, a relocated detail-page Model section, and a bug-fix for a save-button race condition.

### 8.1 Roles List View Inline Controls & Heuristic

The Roles list page (`src/app/role-manager-page.ts`) now renders an inline model + thinking level control in the middle of each list row by reusing the existing `renderModelRow` component.

* **Auto-Save on Change:** Interacting with the inline control (selecting a model, changing thinking level, or clearing an override) immediately persists the change to the backend via `updateRole` (scoped to the current project). A transient `"Saved"` confirmation badge briefly flashes next to the role name on success.
* **Propagation Stopping:** Events (`click`, `keydown`) inside the inline control container call `stopPropagation` to prevent triggering the outer list-row's click handler (which navigates to the role editor).
* **Overridden State:** When a role override is active, the model picker displays with an filled accent style, the thinking dropdown is fully interactive, and both clear (`×`) and Test (flask) buttons are exposed.
* **Inherited State Heuristic:** When no role override exists, the list row displays a resolved, greyed-out effective default model label formatted as `<resolved-model> · default` with the thinking dropdown disabled and clear/Test buttons hidden. Since inherited default resolution is contextual at session-start (reviewer sessions use review defaults, others use session defaults), the list UI uses a deterministic display-only heuristic:
* **Review Default Allowlist (`default.reviewModel`):** `architect`, `code-reviewer`, `reviewer`, `security-reviewer`, `spec-auditor`, and `qa-tester` display the review-model default.
* **Session Default (`default.sessionModel`):** All other roles (including custom roles) display the session-model default.

### 8.2 Role Detail Editor Relocation

In the role detail editor (`renderRoleEditor`):

* **Removed Tab:** The dedicated "Model" tab is removed; the tab bar now exposes only "Prompt" and "Tool Access".
* **Static Section:** Model and thinking controls are presented as a static section placed between the **Accessory** selector and the tab bar, styled identically to the Accessory selector.
* **Draft-Based Saves:** Unlike the list page, modifications within the detail-page Model section do not auto-save. They are draft-based and only committed when the user clicks the main **Save** button.
* **Read-Only Inspector:** The read-only inspector (`renderRoleInspector` used in the goal-proposal modal) is updated to reflect the new vertical layout, rendering the Model section as read-only.

### 8.3 Save-Hang Bug Fix

* **Root Cause:** Inside the `handleSave` function of `role-manager-page.ts`, the successful PUT path calls `showEdit(updated)` -> `setHashRoute("role-edit", name)`. If the user saved changes while already on that role's edit page, `window.location.hash` was already current. Consequently, `setHashRoute` was a no-op that did not trigger a `hashchange` event. Because rendering was only bound to route/hash transitions, the DOM was never refreshed; the Save button remained stuck showing "Saving…" even though `saving` was set to `false` in memory.
* **Fix:** Explicitly call `renderApp()` in the success path of `handleSave` (after `showEdit`/`showList`) so that the DOM is guaranteed to re-render and return the button to the idle "Save" state regardless of whether the hash changed.

### 8.4 Test Coverage

* **UI Fixture Tests (`tests/ui-fixtures/settings-admin-fixture.spec.ts`):**
* Asserts inherited list rows display the correct default model label according to the allowance heuristic (e.g., `coder` shows session default, `reviewer` shows review default).
* Asserts inline model selection auto-saves, flips row state to override, reveals clear/Test buttons, does not trigger edit page navigation, and survives page reloads.
* Asserts clearing an override reverts the row back to the inherited display state and clears backend fields.
* Asserts the detail page vertical section ordering (Accessory -> Model section -> Tab bar) and draft-based saving.
* Asserts the Save button returns to idle "Save" after saving (regression check for the save-hang).
* **Browser E2E (`tests/e2e/ui/roles-model-reshuffle.spec.ts`):**
* Real browser-driven spec verifying section layout ordering, tab bar composition (exposing only Prompt and Tool Access), and the Save button transition lifecycle under direct text changes on a real gateway.
* **Marketplace E2E (`tests/e2e/ui/marketplace.spec.ts`):**
* Adjusted row-edit navigation. Because list rows now host interactive inline model controls, the test specifically clicks the explicit Pencil Edit action (`button[title="Edit"]`) in the row instead of clicking anywhere on the row.

---

## 9. Polished Roles Model Row Controls & Source Hierarchy (Release 0.14)

### 9.1 Server-Side Metadata and Cascade Resolution (`modelResolution`)

The server exposes granular field-level model/thinking resolution metadata via `resolveRoleModelResolution(roleName, projectId)` in `src/server/agent/config-cascade.ts`. This goes beyond flat role definitions to provide the client with the precise source, origin, and editability of each field.

#### Source Hierarchy
The resolution engine walks the cascade (highest priority to lowest):
1. **Direct Role Override:** User-defined override at the current scope (Project or Server-wide).
2. **Inherited Role Override:** An override defined at an ancestor project layer, the server-wide layer, or via built-in / market-pack defaults.
3. **Effective Defaults:** If no role layer provides a value, it falls back to global preferences (`default.sessionModel` or `default.reviewModel` based on role type).

#### Heuristic for Default Models
Since inherited model resolution is contextual at session-start, the UI uses a deterministic display-only heuristic to determine the fallback default:
* **Review Defaults (`default.reviewModel`):** Applied to review-centric roles (`architect`, `code-reviewer`, `reviewer`, `security-reviewer`, `spec-auditor`, and `qa-tester`).
* **Session Defaults (`default.sessionModel`):** Applied to all other roles, including custom-created roles.
* **Auto Default:** Rendered when no global defaults are configured (the AI gateway auto-selects a model).

#### Metadata Fields
GET `/api/roles` and GET `/api/roles/:name` attach a `modelResolution` object mapping `model` and `thinkingLevel` to `RoleFieldSource` descriptors:
* `value`: The resolved model string (e.g. `anthropic/claude-opus-4-8`) or thinking level.
* `source`: `"role"` (current scope), `"inherited-role"` (inherited scope), or `"default"` (fallback).
* `origin`: `"project"`, `"server"`, or `"builtin"`.
* `originPackName`: The name of the source market-pack, if applicable.
* `editable`: Boolean indicating if editing is permitted. Gated by pack-managed status (pack roles are read-only) or whether a higher-precedence winner (like a global server user-override) shadows this scope, preventing confusing no-ops.
* `sourceLabel`: Clean human-friendly label (e.g., `Project`, `Server`, `Built-in`, `Inherited project`, or market pack name).

---

### 9.2 Main Roles List Row Behavior & Visual Polish

The list row inline controls inside `role-manager-page.ts` are designed to be extremely compact, source-aware, and aligned.

#### Inline Selector Layout
* **Two-Line Layout:** Line 1 hosts the interactive selectors (reusing a compact variant wrapper of `renderModelRow` with contained chevrons). Line 2 houses the compact, monochromatic source badges below the selectors, aligning with the layout without overflowing boundaries or wasting vertical space.
* **Instant Auto-Save:** Interacting with a selector immediately persists the change to the backend via `updateRole` (scoped to the current project context). On success, a transient green `"Saved"` flash badge briefly renders next to the role label.
* **Rapid Save Coalescing (Queue & Lock):** To guard against race conditions—such as a user changing model and immediately changing thinking level before the network fetch completes—a per-role pending edit map (`pendingInlineEdits`) and lock set (`inlineSaveActive`) implement a sequential save queue. Edits are merged and written sequentially, preventing stale-state overwrites.
* **Independent Field Overrides:** Model and thinking can be overridden and cleared independently. A role can have a thinking-only override with no visible model override. Clearing the model override via the `×` button preserves any custom thinking level (falling back to the default/inherited model) and vice-versa.
* **Affordance Precision:** Clear and reset buttons are only rendered when there is an active current-scope override (`source === "role"`). If the field is inherited or default, no reset button is shown, and the dropdown shows `(use default)`, preventing confusing no-ops.
* **Event Isolation:** Select container events call `stopPropagation()` to prevent triggering the parent list-row click action, ensuring users do not accidentally navigate to the details editor when adjusting models.
* **Market Pack / Above-Current-Scope Read-Only:** Read-only roles render static effective labels plus a badge like `Managed by pack <packName>` or `Read-only`, completely preventing inline modification.

---

### 9.3 Detail Editor Behavior & Section Relocation

The role detail page (`renderRoleEditor`):
* **Tab Simplification:** The separate "Model" tab has been removed; only the "Prompt" and "Tool Access" tabs remain.
* **Vertical Section Ordering:** Model and thinking controls are now laid out as a static vertical section positioned between the **Accessory** grid and the tab bar.
* **Draft-Based Saves:** Unlike the list row's immediate auto-saves, edits in the detail editor Model section are strictly draft-based. They are only committed to the backend when the user clicks the main **Save** button in the header.

---

### 9.4 Visual QA Evidence & Checklist

The following visual assertions have been verified in the development environment and are pinned by automated tests (`tests/e2e/ui/roles-model-reshuffle.spec.ts` and `tests/ui-fixtures/settings-admin-fixture.spec.ts`).

#### Visual Verification Checklist
1. **Compact Rows:** Selectors and source badges are aligned, compact, and fit neatly inside the list rows. No overflow or layout brokenness.
2. **Source Badges Visible:** Labels like `Role override` (green), `Inherited role override · Server` (blue/grey), or `Session default` (grey) render correctly on line 2.
3. **Model & Thinking Always Visible:** The current effective labels (e.g. `claude-opus-4-8 · default` or overridden names) are legible.
4. **Chevrons Contained:** Dropdown arrows are perfectly positioned inside the controls.
5. **Responsive Action Buttons:** The Pencil (Edit) and Trash (Delete) buttons on the extreme right of the row remain fully reachable and clickable even at narrow viewport widths.
6. **Save-Hang Guard:** Detail editor's Save button correctly reverts to the idle "Save" state (not stuck on "Saving...") after a successful save.

#### Visual Screenshots
Screenshots captured during visual validation can be found in:
* `.bobbit/tmp/roles-list-inherited.png` — Default and inherited role rows with greyed-out default indicators and disablement.
* `.bobbit/tmp/roles-list-overridden.png` — Active role overrides displaying interactive selectors and clear (`×`) / Test (flask) buttons.
* `.bobbit/tmp/roles-detail-layout.png` — Polished detail editor vertical structure with the static Model section.

56 changes: 54 additions & 2 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1432,9 +1432,61 @@ This is what makes "my `code-reviewer` role always runs on opus" work without ch

**Naming model is explicitly unaffected** - `default.namingModel` and `pickFallbackAigwNamingModel` still drive title generation regardless of role.

### UI
### UI & API Metadata

The role-manager page (`src/app/role-manager-page.ts`) has a third tab next to **Prompt** and **Tool Access**, labelled **Model**. It reuses the model picker and thinking dropdown components from the settings page, with a leading "(use default)" option that maps to the empty string → omitted from YAML. The standard origin badge / Customize / Revert flow operates on the whole role record, so touching either field flips builtin→overridden and Revert clears them along with any other overrides.
The Roles model and thinking configuration integrates directly across both list and detail views, driven by server-resolved metadata.

#### 1. Server-Side API Metadata (`modelResolution`)

To enable accurate, source-aware rendering of effective values and inheritance, the `/api/roles` endpoints (both GET list and GET single role) return additive `modelResolution` metadata. This is computed by `resolveRoleModelResolution(roleName, projectId)` in `src/server/agent/config-cascade.ts`.

It provides a granular, field-level audit of both `model` and `thinkingLevel`:

```ts
export interface RoleModelResolution {
model: RoleFieldSource;
thinkingLevel: RoleFieldSource;
}

export interface RoleFieldSource {
/** Resolved field value when a role layer supplies it; omitted for `default` */
value?: string;
source: "role" | "inherited-role" | "default";
/** Cascade layer the value came from (omitted for `default`) */
origin?: "project" | "server" | "builtin";
/** Market-pack name when the value comes from a pack-defined role; null otherwise */
originPackName?: string | null;
/** False only for pack-managed/read-only roles or when shadowed above current scope; true otherwise */
editable: boolean;
/** Short human label for the source ("Project", "Server", "Built-in", or pack name) */
sourceLabel: string;
}
```

The metadata ensures that:
* **Shadowing is respected:** If a higher-precedence winner (e.g. a global server-user override) shadows the current project scope, the server reports the field as non-editable (`editable: false`). The UI displays it without misleading clear/reset or edit affordances that would secretly no-op.
* **Source labels are accurate:** Clear distinction between direct local overrides (`source: "role"`), inherited layers (`source: "inherited-role"`), or system-wide fallbacks (`source: "default"`).

#### 2. Roles List View Inline Controls

The main Roles page (`src/app/role-manager-page.ts`) renders compact, two-line model + thinking level inline controls in the middle of each list row by reusing a compact layout variant of the `renderModelRow` component:

* **Two-Line Layout:** Line 1 displays the interactive model selector and thinking level dropdown. Line 2 displays compact, monochromatic source badges (e.g., `Model: [Session default] Thinking: [Session default]` or `[Inherited role override · Server]`).
* **Auto-Save on Change:** Interacting with the inline control immediately persists the changes to the backend. On success, a transient green `"Saved"` confirmation badge briefly flashes next to the role name.
* **Coalesced Saves (Race Guard):** Rapid sequential edits (such as picking a model and immediately changing thinking before the network refetch completes) are queued via `pendingInlineEdits` and handled sequentially in a serialized save loop (`scheduleInlineSave`). This prevents a newer change from reading stale data from the un-refetched row object and clobbering the previous field with empty strings.
* **Independent Overrides & Model Clears:** Model and thinking level can be overridden independently. Clicking the clear (`×`) button next to the model selector removes only the model override, preserving any custom thinking override (shown as a `thinking-override` state), and vice-versa.
* **No Misleading Reset Affordances:** Clearing and reset buttons are only exposed for active current-scope overrides (`source === "role"`). Inherited or default values display the placeholder label `(use default)` and have no clear buttons, avoiding confusing no-ops.
* **Propagation Stopping:** Dropdown click/keydown events are stopped via `stopPropagation` so that interacting with the selectors does not trigger the outer row's click-to-edit navigation.
* **Read-Only/Pack Rows:** Roles installed from a Market Pack (or non-editable due to scope shadow rules) render a static effective-value summary (Model + Thinking labels) alongside a badge like `Managed by pack <name>` or `Read-only`, completely disabling inline editing.

#### 3. Role Detail Editor Relocation

In the role detail editor (`renderRoleEditor`):

* **Removed Tab:** The dedicated "Model" tab is removed; the tab bar now exposes only "Prompt" and "Tool Access".
* **Static Section:** Model and thinking controls are presented as a static section placed vertically between the **Accessory** selector and the tab bar, styled identically to the Accessory selector.
* **Draft-Based Saves:** Unlike the list page's immediate auto-saves, modifications within the detail-page Model section do not auto-save. They are draft-based and are only committed when the user clicks the main **Save** button in the top navigation bar.
* **Read-Only Inspector:** The read-only inspector (`renderRoleInspector` used in the goal-proposal modal) mimics this vertical layout, rendering the Model section as read-only.

---

Expand Down
4 changes: 2 additions & 2 deletions docs/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,9 @@ See [docs/cache-hit-rate.md](cache-hit-rate.md) for full formula and null semant

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/roles` | List all roles |
| `GET` | `/api/roles` | List all roles. Response roles include the resolved `modelResolution` field-level source and inheritance metadata. |
| `POST` | `/api/roles` | Create a role (`{ name, label, promptTemplate, toolPolicies?, allowedTools?, accessory?, model?, thinkingLevel? }`). `toolPolicies` is the source of truth for tool access; `allowedTools` is accepted for backward compatibility and merged as `always-allow` entries. `model` is `"<provider>/<modelId>"` format and overrides `default.sessionModel` / `default.reviewModel` for sessions of this role. `thinkingLevel` is one of `"off" | "minimal" | "low" | "medium" | "high" | "xhigh"` (clamped to the role's model capability at write time when `model` is set; see [docs/thinking-levels.md](thinking-levels.md)). |
| `GET` | `/api/roles/:name` | Get a role (includes `toolPolicies` and derived `allowedTools`) |
| `GET` | `/api/roles/:name` | Get a role (includes `toolPolicies`, derived `allowedTools`, and resolved `modelResolution` field-level source and inheritance metadata) |
| `PUT` | `/api/roles/:name` | Update a role. Accepts `toolPolicies` (Record of tool/group name → policy), `model` (`"<provider>/<modelId>"`), and `thinkingLevel` (`"off" | "minimal" | "low" | "medium" | "high" | "xhigh"`, clamped to the role's model capability when `model` is set). Policy values are validated against: `always-allow`, `ask-once`, `always-ask`, `never-ask`, `never`. Malformed `model` strings are rejected with 400. |
| `DELETE` | `/api/roles/:name` | Delete a role |

Expand Down
Loading