Skip to content

(MOT-4353) feat(console,llm-router): standalone call cards + injectable llm-router config UI - #695

Merged
andersonleal merged 3 commits into
mainfrom
feat/console-call-cards
Aug 4, 2026
Merged

(MOT-4353) feat(console,llm-router): standalone call cards + injectable llm-router config UI#695
andersonleal merged 3 commits into
mainfrom
feat/console-call-cards

Conversation

@andersonleal

@andersonleal andersonleal commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two UX tracks, one branch — the console's autonomous-activity cards and llm-router's configuration screen.

Console chat cards

  • Every function call is its own card — the accordion group is gone; consecutive call-like cards stack tight (6px) while prose keeps the chat rhythm.
  • Collapsed headers say what happened: args preview (ƒ engine::functions::list (prefix: "c…)), failed verb on errors (gate denials stay verb-less — the function never ran), duration.
  • Trigger fires (⚡) and notify deliveries (🔔) share the card language: verb-first triggered … headers, TERMINAL / RAW JSON tabs, and a recovered REGISTRATION view (live harness rows first, else the transcript's own engine::register_trigger call — notifications correlate by the subscription id embedded in their entry id, order-independent for idle-session wakes).
  • CONDITIONS met chip on delivered fires — the harness evaluates gates before writing the fired record (trigger_deliver.rs), so a fire card existing proves the criteria passed.
  • RegisterTriggerView shows gating conditions as an ONLY IF section (the zod schema was silently stripping them).
  • Copy affordances on every JSON pane; assistant turn header renamed AGENT.

llm-router injectable configuration UI

  • New llm-router/ui/ per the injectable-console-ui SOP: config form registered over host.configForms, assets built by build.rs (esbuild; SKIP_UI_BUILD escape) and embedded via the shared iii-console-ui crate.
  • Plain-text api_key detection: a raw secret shows a warning ("stored verbatim in the configuration entry") with a one-click ${PROVIDER_API_KEY} fix chip; partial references warn too; pure env refs render unmasked with a green note, real keys stay masked.
  • Console Select for the default-provider and heuristic pickers (stale selections shown as (not connected)), stream budgets with human-readable echoes (= 5m, default · 32,000 tokens), system-prompt override flow that shows the provider-declared prompt and prefills it on override.

Testing

  • console/web: tsc, scoped biome, 457 component tests (17 new for the registration/notification correlation logic + conditions render).
  • llm-router: cargo fmt/clippy clean, 7 embed tests (asset paths, configForms + warning copy present in the built bundle, scoped CSS).
  • Verified live against real sessions via the console dev server (dock + route widths, keyboard-only paths).
  • External review round applied: denied≠failed, call-target preserved in row-backed registrations, order-independent notification correlation, wrapped fallback notifications.

Fixes MOT-4353

Summary by CodeRabbit

  • New Features

    • Added an embedded configuration interface for llm-router, including provider settings, credentials, routing preferences, stream budgets, and prompt customization.
    • Added support for displaying trigger registration details, delivery information, conditions, and structured notification data.
    • Added clearer trigger cards with argument previews and distinct denied-call states.
  • Improvements

    • Trigger messages now display individually with improved spacing and expandable details.
    • Configuration forms provide validation feedback, environment-variable guidance, and focused deep-link navigation.

… cards with terminal tabs

Every function call renders as its own card (accordion group removed);
consecutive call-like cards stack tight. Collapsed headers carry an args
preview; errored calls say 'failed' (gate denials stay verb-less).
Trigger-fired and notification rows share the card language: verb-first
headers, TERMINAL/RAW JSON tabs, registration recovery (harness rows ->
transcript register call, entry-id-exact notification correlation),
CONDITIONS met chip, copy on every JSON pane. RegisterTriggerView renders
gating conditions as an ONLY IF section. Assistant header renamed 'agent'.
…var key guidance

Console-injected configuration UI (docs/sops/injectable-console-ui.md):
per-provider credential cards with plain-text api_key detection steering
to ${ENV_VAR} references (one-click fix chip, partial-reference warning,
env refs unmasked / secrets masked), console Select for provider pickers,
stream budgets with human-readable echoes, and a system-prompt override
flow that shows the provider-declared prompt. Assets built by build.rs
(esbuild, SKIP_UI_BUILD escape) and embedded in the binary via the shared
iii-console-ui crate.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 4, 2026 8:53pm
workers-tech-spec Ready Ready Preview Aug 4, 2026 8:53pm

Request Review

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 54 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@andersonleal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b6d7d8c-8732-4d0e-b8b5-bb23da396f0d

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4a92e and d0b8de7.

📒 Files selected for processing (1)
  • console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx
📝 Walkthrough

Walkthrough

The PR adds structured trigger and notification cards with registration metadata and condition details. It removes grouped function-trigger rendering. It also adds an embedded, build-on-demand configuration UI for llm-router and registers it at runtime.

Changes

Chat trigger registration and rendering

Layer / File(s) Summary
Trigger registration conditions
console/web/src/components/chat/engine/parsers.ts, console/web/src/components/chat/engine/RegisterTriggerView.tsx, console/web/src/components/chat/engine/__tests__/parsers.test.ts
Registration requests now preserve optional conditions. The registration view renders condition identifiers and configuration values.
Registration resolution and message wiring
console/web/src/components/chat/MessageList.tsx, console/web/src/components/chat/ChatView.tsx, console/web/src/components/chat/trigger-registration.test.ts
The chat resolves registration data from trigger rows and call transcripts, correlates notifications, and passes metadata to individual messages.
Structured trigger cards
console/web/src/components/chat/Message.tsx, console/web/src/components/function-trigger/FunctionTriggerCard.tsx, console/web/src/components/chat/FunctionTriggerGroup.tsx, console/web/src/components/chat/FunctionTriggerGroup.stories.tsx
Notifications and trigger-fired messages now use expandable structured cards. Function trigger cards show argument previews and distinguish approval denials from failures. The grouped function-trigger component and stories were removed.

llm-router injectable console UI

Layer / File(s) Summary
Configuration form and frontend package
llm-router/ui/package.json, llm-router/ui/tsconfig.json, llm-router/ui/page.tsx, llm-router/ui/src/configuration/index.tsx, llm-router/ui/styles.css, llm-router/ui/build.mjs, pnpm-workspace.yaml
The new UI package provides provider, routing, credential, budget, prompt, validation, and environment-reference controls. It bundles JavaScript and scoped CSS assets.
Asset generation and embedding
llm-router/Cargo.toml, llm-router/build.rs, llm-router/src/ui.rs
The Rust build process tracks UI inputs, builds stale assets with pnpm, validates outputs, and embeds them into a ConsoleUi.
Runtime UI registration
llm-router/src/lib.rs, llm-router/src/main.rs
The crate exposes the UI module and registers the llm-router UI after router initialization.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: sergiofilhowz, ytallo, rohitg00

Poem

A rabbit checks each trigger card,
With tidy fields and clues.
It builds a form for router paths,
Then bundles CSS and views.
The console blooms in burrow-light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes both primary changes: standalone console call cards and the injectable llm-router configuration UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/console-call-cards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… column

1180px dialog around a max-w-3xl form was ~400px of dead margin; 880px
fits the column + padding, and narrow viewports keep the 100vw-2rem cap.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
console/web/src/components/chat/Message.tsx (2)

257-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the misplaced doc comment.

The comment at lines 257-263 describes NotificationTerminal, but it sits directly above the hasConditions doc comment. NotificationTerminal is defined at line 309. Move this block above that function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/Message.tsx` around lines 257 - 276, Move the
doc comment describing the friendly notification tab so it directly precedes the
NotificationTerminal function, leaving the hasConditions documentation
immediately above hasConditions. Do not alter either comment’s wording or the
functions’ behavior.

555-563: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The notify-versus-call rule is hand-coded in three places. deliveryOf already encodes it, and TriggerFiredTerminal uses it at Message.tsx line 375. The inline 'notify' and 'harness::send' comparisons can drift from the helper.

  • console/web/src/components/chat/Message.tsx#L555-L563: derive called and notified from deliveryOf(t.target) instead of comparing target strings.
  • console/web/src/components/chat/MessageList.tsx#L120-L124: use deliveryOf(t.target) to decide whether a fire is a notify delivery before it enters notifyFireByName.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/Message.tsx` around lines 555 - 563, Replace
the inline target-string checks in Message.tsx lines 555-563 with
deliveryOf(t.target) to derive called and notified, preserving the existing null
behavior. In MessageList.tsx lines 120-124, use deliveryOf(t.target) to
determine whether the fire is a notify delivery before adding it to
notifyFireByName.
console/web/src/components/chat/engine/RegisterTriggerView.tsx (2)

170-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

isScalar is defined twice with identical bodies. Both definitions are new in this PR, and both files already import from console/web/src/components/chat/engine/shared.tsx.

  • console/web/src/components/chat/engine/RegisterTriggerView.tsx#L170-L174: export isScalar from engine/shared.tsx and import it here.
  • console/web/src/components/chat/Message.tsx#L251-L255: delete the local copy and import isScalar from engine/shared.tsx, next to the existing FilterChip import.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/engine/RegisterTriggerView.tsx` around lines
170 - 174, The duplicated isScalar helper should be centralized in shared.tsx.
In console/web/src/components/chat/engine/RegisterTriggerView.tsx lines 170-174,
export isScalar from engine/shared.tsx and import it for use; in
console/web/src/components/chat/Message.tsx lines 251-255, remove the local
definition and import isScalar alongside FilterChip.

193-198: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the chippable predicate once.

Lines 194 and 197 repeat the same expression with a negation. A single predicate removes the risk of the two branches drifting apart.

♻️ Proposed refactor
-  const chippable = entries.filter(
-    ([, v]) => isScalar(v) || (Array.isArray(v) && v.every(isScalar)),
-  )
-  const rest = entries.filter(
-    ([, v]) => !(isScalar(v) || (Array.isArray(v) && v.every(isScalar))),
-  )
+  const isChippable = (v: unknown) =>
+    isScalar(v) || (Array.isArray(v) && v.every(isScalar))
+  const chippable = entries.filter(([, v]) => isChippable(v))
+  const rest = entries.filter(([, v]) => !isChippable(v))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/engine/RegisterTriggerView.tsx` around lines
193 - 198, Extract the repeated scalar-or-scalar-array condition from the
chippable and rest filters into a single named predicate near the entries
processing in RegisterTriggerView. Reuse that predicate directly for chippable
and with negation for rest, preserving the existing partitioning behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@console/web/src/components/chat/Message.tsx`:
- Line 191: The notification regex name group is too restrictive and rejects
names containing colons. In console/web/src/components/chat/Message.tsx:191-191
and console/web/src/components/chat/MessageList.tsx:158-158, update the name
capture to a lazy group so it captures through the last colon preceding the JSON
payload, preserving card rendering and notifyFireByName correlation.

In `@llm-router/build.rs`:
- Around line 132-153: Update subtree_older_than to validate the modification
time of root and every traversed child directory, not only surviving files.
Return false when any directory or file is newer than ceiling, so deletions that
update directory metadata invalidate dist_is_fresh and trigger the build.

In `@llm-router/ui/src/configuration/index.tsx`:
- Around line 150-159: Update the focusField handling in the useEffect to locate
the interactive control contained within the matching data-field wrapper, rather
than focusing the non-focusable wrapper itself. Preserve scrolling the matched
field into view, then focus the appropriate descendant control so keyboard focus
moves to the linked input or control.
- Around line 418-447: Associate the “system prompt” label with the conditional
textarea by giving the visible label an identifying value and assigning the same
value to the textarea’s id or equivalent labeling attribute. Update the elements
in the system-prompt section without changing the existing toggle or prompt
behavior.

---

Nitpick comments:
In `@console/web/src/components/chat/engine/RegisterTriggerView.tsx`:
- Around line 170-174: The duplicated isScalar helper should be centralized in
shared.tsx. In console/web/src/components/chat/engine/RegisterTriggerView.tsx
lines 170-174, export isScalar from engine/shared.tsx and import it for use; in
console/web/src/components/chat/Message.tsx lines 251-255, remove the local
definition and import isScalar alongside FilterChip.
- Around line 193-198: Extract the repeated scalar-or-scalar-array condition
from the chippable and rest filters into a single named predicate near the
entries processing in RegisterTriggerView. Reuse that predicate directly for
chippable and with negation for rest, preserving the existing partitioning
behavior.

In `@console/web/src/components/chat/Message.tsx`:
- Around line 257-276: Move the doc comment describing the friendly notification
tab so it directly precedes the NotificationTerminal function, leaving the
hasConditions documentation immediately above hasConditions. Do not alter either
comment’s wording or the functions’ behavior.
- Around line 555-563: Replace the inline target-string checks in Message.tsx
lines 555-563 with deliveryOf(t.target) to derive called and notified,
preserving the existing null behavior. In MessageList.tsx lines 120-124, use
deliveryOf(t.target) to determine whether the fire is a notify delivery before
adding it to notifyFireByName.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d49daae5-8a80-4c60-bf70-39b9f3181ea7

📥 Commits

Reviewing files that changed from the base of the PR and between cdb1d19 and 2f4a92e.

⛔ Files ignored due to path filters (2)
  • llm-router/Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/FunctionTriggerGroup.stories.tsx
  • console/web/src/components/chat/FunctionTriggerGroup.tsx
  • console/web/src/components/chat/Message.tsx
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/engine/RegisterTriggerView.tsx
  • console/web/src/components/chat/engine/__tests__/parsers.test.ts
  • console/web/src/components/chat/engine/parsers.ts
  • console/web/src/components/chat/trigger-registration.test.ts
  • console/web/src/components/function-trigger/FunctionTriggerCard.tsx
  • llm-router/Cargo.toml
  • llm-router/build.rs
  • llm-router/src/lib.rs
  • llm-router/src/main.rs
  • llm-router/src/ui.rs
  • llm-router/ui/build.mjs
  • llm-router/ui/package.json
  • llm-router/ui/page.tsx
  • llm-router/ui/src/configuration/index.tsx
  • llm-router/ui/styles.css
  • llm-router/ui/tsconfig.json
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (2)
  • console/web/src/components/chat/FunctionTriggerGroup.tsx
  • console/web/src/components/chat/FunctionTriggerGroup.stories.tsx

export function parseNotification(
content: string,
): { name: string; payload: Record<string, unknown> } | null {
const m = /^\[notification\]\s*([^:]+):\s*(\{[\s\S]*\})\s*$/.exec(content)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The notification-name pattern rejects names that contain a colon. Both files use ([^:]+) for the name group, so a name such as db::exec never matches and the notification loses both its card rendering and its registration correlation.

  • console/web/src/components/chat/Message.tsx#L191-L191: change the name group to a lazy (.+?) so it binds to the last : { before the JSON payload.
  • console/web/src/components/chat/MessageList.tsx#L158-L158: apply the same lazy group so notifyFireByName lookups use the full name.
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 191-191: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

📍 Affects 2 files
  • console/web/src/components/chat/Message.tsx#L191-L191 (this comment)
  • console/web/src/components/chat/MessageList.tsx#L158-L158
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/Message.tsx` at line 191, The notification
regex name group is too restrictive and rejects names containing colons. In
console/web/src/components/chat/Message.tsx:191-191 and
console/web/src/components/chat/MessageList.tsx:158-158, update the name capture
to a lazy group so it captures through the last colon preceding the JSON
payload, preserving card rendering and notifyFireByName correlation.

Comment thread llm-router/build.rs
Comment on lines +132 to +153
fn subtree_older_than(root: &Path, ceiling: SystemTime) -> bool {
let Ok(read) = std::fs::read_dir(root) else {
return false;
};
for entry in read.flatten() {
let path = entry.path();
let Ok(meta) = entry.metadata() else {
return false;
};
if meta.is_dir() {
if !subtree_older_than(&path, ceiling) {
return false;
}
continue;
}
let Ok(m) = meta.modified() else {
return false;
};
if m > ceiling {
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate generated assets after source deletion.

subtree_older_than checks only surviving files. It does not check the root or child-directory modification times. If ui/src/configuration/index.tsx is deleted, dist_is_fresh can retain the old bundle instead of running pnpm build.

Proposed fix
 fn subtree_older_than(root: &Path, ceiling: SystemTime) -> bool {
+    let Ok(root_mtime) = root.metadata().and_then(|m| m.modified()) else {
+        return false;
+    };
+    if root_mtime > ceiling {
+        return false;
+    }
     let Ok(read) = std::fs::read_dir(root) else {
         return false;
     };
-    for entry in read.flatten() {
+    for entry in read {
+        let Ok(entry) = entry else {
+            return false;
+        };
         let path = entry.path();
         let Ok(meta) = entry.metadata() else {
             return false;
         };
+        let Ok(m) = meta.modified() else {
+            return false;
+        };
+        if m > ceiling {
+            return false;
+        }
         if meta.is_dir() {
             if !subtree_older_than(&path, ceiling) {
                 return false;
             }
             continue;
         }
-        let Ok(m) = meta.modified() else {
-            return false;
-        };
-        if m > ceiling {
-            return false;
-        }
     }
     true
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn subtree_older_than(root: &Path, ceiling: SystemTime) -> bool {
let Ok(read) = std::fs::read_dir(root) else {
return false;
};
for entry in read.flatten() {
let path = entry.path();
let Ok(meta) = entry.metadata() else {
return false;
};
if meta.is_dir() {
if !subtree_older_than(&path, ceiling) {
return false;
}
continue;
}
let Ok(m) = meta.modified() else {
return false;
};
if m > ceiling {
return false;
}
}
fn subtree_older_than(root: &Path, ceiling: SystemTime) -> bool {
let Ok(root_mtime) = root.metadata().and_then(|m| m.modified()) else {
return false;
};
if root_mtime > ceiling {
return false;
}
let Ok(read) = std::fs::read_dir(root) else {
return false;
};
for entry in read {
let Ok(entry) = entry else {
return false;
};
let path = entry.path();
let Ok(meta) = entry.metadata() else {
return false;
};
let Ok(m) = meta.modified() else {
return false;
};
if m > ceiling {
return false;
}
if meta.is_dir() {
if !subtree_older_than(&path, ceiling) {
return false;
}
continue;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm-router/build.rs` around lines 132 - 153, Update subtree_older_than to
validate the modification time of root and every traversed child directory, not
only surviving files. Return false when any directory or file is newer than
ceiling, so deletions that update directory metadata invalidate dist_is_fresh
and trigger the build.

Comment on lines +150 to +159
const rootRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const field = props.focusField?.[0]
if (!field || !rootRef.current) return
const el = rootRef.current.querySelector<HTMLElement>(
`[data-field="${CSS.escape(field)}"]`,
)
el?.scrollIntoView({ block: 'center' })
el?.focus()
}, [props.focusField])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Focus an interactive control for focusField.

querySelector() returns the data-field wrapper. The current wrappers are a non-focusable <div> or <section>. The deep link scrolls, but .focus() leaves keyboard focus at the previous control.

Proposed fix
-    const el = rootRef.current.querySelector<HTMLElement>(
+    const fieldRoot = rootRef.current.querySelector<HTMLElement>(
       `[data-field="${CSS.escape(field)}"]`,
     )
-    el?.scrollIntoView({ block: 'center' })
-    el?.focus()
+    fieldRoot?.scrollIntoView({ block: 'center' })
+    fieldRoot
+      ?.querySelector<HTMLElement>(
+        'input, textarea, button, [role="combobox"], [tabindex]:not([tabindex="-1"])',
+      )
+      ?.focus()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rootRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const field = props.focusField?.[0]
if (!field || !rootRef.current) return
const el = rootRef.current.querySelector<HTMLElement>(
`[data-field="${CSS.escape(field)}"]`,
)
el?.scrollIntoView({ block: 'center' })
el?.focus()
}, [props.focusField])
const rootRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const field = props.focusField?.[0]
if (!field || !rootRef.current) return
const fieldRoot = rootRef.current.querySelector<HTMLElement>(
`[data-field="${CSS.escape(field)}"]`,
)
fieldRoot?.scrollIntoView({ block: 'center' })
fieldRoot
?.querySelector<HTMLElement>(
'input, textarea, button, [role="combobox"], [tabindex]:not([tabindex="-1"])',
)
?.focus()
}, [props.focusField])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm-router/ui/src/configuration/index.tsx` around lines 150 - 159, Update the
focusField handling in the useEffect to locate the interactive control contained
within the matching data-field wrapper, rather than focusing the non-focusable
wrapper itself. Preserve scrolling the matched field into view, then focus the
appropriate descendant control so keyboard focus moves to the linked input or
control.

Comment on lines +418 to +447
<div className="llmr-cfg-prompt-head">
<span className="llmr-cfg-label">system prompt</span>
{typeof systemPrompt === 'string' ? (
<button
type="button"
className="llmr-cfg-toggle"
onClick={() => set('system_prompt', undefined)}
>
use provider default
</button>
) : (
<button
type="button"
className="llmr-cfg-toggle"
// Prefill with the provider's own prompt: an override usually
// starts as an edit of it, not a blank page.
onClick={() => set('system_prompt', promptDefault ?? '')}
>
override
</button>
)}
</div>
{typeof systemPrompt === 'string' ? (
<textarea
className="llmr-cfg-input llmr-cfg-textarea"
value={systemPrompt}
rows={4}
placeholder="override the provider-declared identity prompt"
onChange={(e) => set('system_prompt', e.target.value)}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Associate the system-prompt text area with its label.

The visible system prompt <span> is not a label. The <textarea> has no programmatic name for assistive technology.

Proposed fix
-        <span className="llmr-cfg-label">system prompt</span>
+        <label
+          className="llmr-cfg-label"
+          htmlFor={`llmr-${id}-system-prompt`}
+        >
+          system prompt
+        </label>
@@
         <textarea
+          id={`llmr-${id}-system-prompt`}
           className="llmr-cfg-input llmr-cfg-textarea"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="llmr-cfg-prompt-head">
<span className="llmr-cfg-label">system prompt</span>
{typeof systemPrompt === 'string' ? (
<button
type="button"
className="llmr-cfg-toggle"
onClick={() => set('system_prompt', undefined)}
>
use provider default
</button>
) : (
<button
type="button"
className="llmr-cfg-toggle"
// Prefill with the provider's own prompt: an override usually
// starts as an edit of it, not a blank page.
onClick={() => set('system_prompt', promptDefault ?? '')}
>
override
</button>
)}
</div>
{typeof systemPrompt === 'string' ? (
<textarea
className="llmr-cfg-input llmr-cfg-textarea"
value={systemPrompt}
rows={4}
placeholder="override the provider-declared identity prompt"
onChange={(e) => set('system_prompt', e.target.value)}
/>
<div className="llmr-cfg-prompt-head">
<label
className="llmr-cfg-label"
htmlFor={`llmr-${id}-system-prompt`}
>
system prompt
</label>
{typeof systemPrompt === 'string' ? (
<button
type="button"
className="llmr-cfg-toggle"
onClick={() => set('system_prompt', undefined)}
>
use provider default
</button>
) : (
<button
type="button"
className="llmr-cfg-toggle"
// Prefill with the provider's own prompt: an override usually
// starts as an edit of it, not a blank page.
onClick={() => set('system_prompt', promptDefault ?? '')}
>
override
</button>
)}
</div>
{typeof systemPrompt === 'string' ? (
<textarea
id={`llmr-${id}-system-prompt`}
className="llmr-cfg-input llmr-cfg-textarea"
value={systemPrompt}
rows={4}
placeholder="override the provider-declared identity prompt"
onChange={(e) => set('system_prompt', e.target.value)}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm-router/ui/src/configuration/index.tsx` around lines 418 - 447, Associate
the “system prompt” label with the conditional textarea by giving the visible
label an identifying value and assigning the same value to the textarea’s id or
equivalent labeling attribute. Update the elements in the system-prompt section
without changing the existing toggle or prompt behavior.

@andersonleal
andersonleal merged commit e9bda14 into main Aug 4, 2026
20 checks passed
@andersonleal
andersonleal deleted the feat/console-call-cards branch August 4, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant