Skip to content

feat: configuration and storybook#204

Merged
sergiofilhowz merged 2 commits into
mainfrom
feat/configuration-ui
Jun 1, 2026
Merged

feat: configuration and storybook#204
sergiofilhowz merged 2 commits into
mainfrom
feat/configuration-ui

Conversation

@sergiofilhowz

@sergiofilhowz sergiofilhowz commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Integrated Storybook for component docs, interactive examples, and the Playground harness.
    • Added a Workers configuration area with a schema-driven editor (master/detail, save/reset, validation).
    • Environment-variable templating support in configuration inputs (inline ${VAR} placeholders).
  • Documentation

    • Updated guides and READMEs to reflect Storybook-first workflows and Playground changes.
  • Chores

    • Migrated in-app Examples/Playground surfaces into Storybook stories.
    • Removed the build-time Playground flag in favor of dev-mode detection; added Storybook scripts.

@vercel

vercel Bot commented May 29, 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, Comment Jun 1, 2026 11:34am

Request Review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a49260a0-a9b6-4a9d-89a2-454d2fb97f11

📥 Commits

Reviewing files that changed from the base of the PR and between c973b20 and 45abaec.

📒 Files selected for processing (1)
  • console/web/src/components/ui/Select.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • console/web/src/components/ui/Select.tsx

📝 Walkthrough

Walkthrough

Moves Playground/Examples into Storybook and removes VITE_PLAYGROUND. Adds Storybook config, many stories and fixtures. Implements a tabbed Configuration UI with Console and Workers tabs, a schema-driven Workers editor (fields, env-lexical input, DnD, validation), API/hooks, utils, tests, and related docs/CSS updates.

Changes

Storybook migration and Playground flag removal

Layer / File(s) Summary
Build/workflows/env cleanup and backend selection
.github/workflows/*, console/build.rs, console/web/src/lib/backend/index.ts, console/web/src/vite-env.d.ts
Remove VITE_PLAYGROUND overrides from CI/build and switch backend selection to import.meta.env.DEV.
Storybook configuration and fonts
console/web/.storybook/*, console/web/package.json, console/web/biome.json
Add Storybook main/preview/head, decorators, fonts, package scripts, and dev dependencies.
Remove in-app Examples/Playground and slim routing
console/web/src/App.tsx, console/web/src/pages/Examples/*, console/web/src/stories/playground/*
Delete in-app Examples/Playground pages, simplify routing to Traces/Configuration, and provide Storybook harness/stories.
Playground and README documentation updates
console/web/PLAYGROUND.md, console/web/README.md, console/web/docs/*
Document Storybook-first Playground flow and update README/docs to reflect new verification and scenario locations.
Component/design stories and conversions
console/web/src/components/*/*.stories.tsx, console/web/src/stories/design/*, console/web/src/components/ui/Select.tsx
Add many component stories and convert design docs to CSF modules; update Select to support undefined/clearable values used in stories.
Design tokens and CSS/lint/test tweaks
console/web/DESIGN.md, console/web/src/index.css, console/web/src/pages/Traces/*, tests
Update dark ink tokens, scope react-flow overrides, add workers-scoped lexical styles, and minor lint/test comment adjustments.
Chat filter returns and sandbox formatting
console/web/src/components/chat/*ListView.tsx, console/web/src/components/chat/sandbox/*
Return chip arrays directly from RequestFilters and reformat sandbox parser/tests without changing behavior.

Workers configuration editor and routing

Layer / File(s) Summary
Hash routing and Configuration shell
console/web/src/hooks/use-hash-route.ts, console/web/src/pages/Configuration/index.tsx
Add configuration sub-routing, useConfigurationRoute, and a tabbed Configuration shell with unsaved-changes guard.
ConsoleSettingsTab and provider/permission plumbing
console/web/src/pages/Configuration/tabs/ConsoleSettingsTab.tsx, providers/permissions files
Add a Console settings tab for theme, permission default mode and allowlist, provider rows, and keyboard navigation.
Workers tab list, editor, SaveBar and guard
console/web/src/pages/Configuration/tabs/WorkersTab/*
Implement master-detail Workers editor: WorkersList, WorkerEditor (SchemaForm-backed), SaveBar, dirty tracking, and navigation guard.
Configuration API and TanStack Query hooks
console/web/src/pages/Configuration/tabs/WorkersTab/api.ts, .../hooks.ts
Add typed configuration bus wrappers and TanStack Query hooks/mutation with cache invalidation.
SchemaForm core and fields
console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/*
Add FieldDispatch, FieldShell, SchemaForm, and many field components (ArrayField, DictionaryField, EnumField, StringField, NumberField, BooleanField, OneOfField, NullableField, ObjectSection) plus RowChrome.
Env-lexical input for templates
.../env-lexical/*
Add EnvLexicalInput, EnvPlaceholderNode, transform plugin, parsing/serialization helpers and tests for ${VAR} templates.
Path helpers, $ref resolver, and tests
.../schema-form/path*, .../ref-resolver*
Add immutable path get/set/unset, JSON Pointer helpers, a Draft-07 $ref resolver with helpers, and comprehensive tests.
Reorder utility and drag-reorder hook
.../schema-form/reorder*, .../useDragReorder.ts
Add non-mutating moveItem, useDragReorder hook with keyboard and drag handlers, and tests.
Workers fixtures and stories
console/web/src/stories/fixtures/*, .../WorkersTab/*.stories.tsx
Add story fixtures for worker messages and schema examples, plus SchemaForm and WorkersTab stories and decorators.
Server error parsing and dirty-state helpers
.../WorkersTab/errors.ts, .../WorkersTab/dirty*
Add parseSetError to normalize server validation errors and jsonEqual/isDirty helpers with tests used by save flow.
Workers-tab scoped CSS
console/web/src/index.css
Add .workers-tab scoped styles for env-lexical single-line editor and placeholder overlay.

Sequence Diagram(s)

sequenceDiagram
  participant UI as WorkerEditor
  participant Mutation as useSetConfiguration
  participant Bus as configuration::set
  UI->>Mutation: save(id, value)
  Mutation->>Bus: send payload
  Bus-->>Mutation: Ok or SCHEMA_INVALID
  Mutation-->>UI: invalidate queries or return parsed errors
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

  • iii-hq/workers#141 — Earlier changes introducing or using VITE_PLAYGROUND; this PR removes that flag and migrates behavior.
  • iii-hq/workers#137 — Related updates to documentation guidelines / skill-bundle guidance overlapped with the docs overhaul here.
  • iii-hq/workers#186 — Also modifies hash routing and view selection; touches similar routing code.

Suggested reviewers

  • andersonleal

Poem

I hop through stories, bright and quick,
From flags to flows with a tidy click.
Schema burrows, fields in rows,
Pills for env where config grows.
Storybook lights the fullscreen stream —
Hooray! The editor’s baking the dream. 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/configuration-ui

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 13 skipped (no docs/).

Layer Result
structure
vale
ai
render

Note

17 stale rendered artifact(s) detected on main, unrelated to this PR. This PR is fine; the drift was already there. A maintainer should open a chore PR to re-render these.

  • shell/README.md
  • shell/skill.md
  • shell/skills/chmod.md
  • shell/skills/exec.md
  • shell/skills/exec_bg.md
  • shell/skills/grep.md
  • shell/skills/kill.md
  • shell/skills/list.md
  • shell/skills/ls.md
  • shell/skills/mkdir.md
  • shell/skills/mv.md
  • shell/skills/read.md
  • …and 5 more (see the workflow logs)

@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: 6

🧹 Nitpick comments (1)
DOCUMENTATION_GUIDELINES.md (1)

21-22: 💤 Low value

Consider clarifying the section count.

The phrase "four body sections" is accurate if it refers only to the level-2 subsections (When to Use, Boundaries, Functions, Reactive triggers), but readers might count the level-1 overview heading and become confused.

✨ Optional rewording for clarity
-Every worker `SKILL.md` has YAML frontmatter and four body sections (the last
-one only when the worker exposes a trigger type).
+Every worker `SKILL.md` has YAML frontmatter, a level-1 overview heading, and 
+up to four level-2 subsections (the last one only when the worker exposes a 
+trigger type).
🤖 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 `@DOCUMENTATION_GUIDELINES.md` around lines 21 - 22, Reword the ambiguous
sentence "Every worker `SKILL.md` has YAML frontmatter and four body sections
(the last one only when the worker exposes a trigger type)." to explicitly state
that the count refers to level-2 subsections (e.g., "Every worker `SKILL.md`
begins with YAML frontmatter followed by four level-2 body subsections — When to
Use, Boundaries, Functions, and Reactive triggers — with the last subsection
included only when the worker exposes a trigger type.") so readers cannot
mistake the overview heading for an additional section.
🤖 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/ui/Select.tsx`:
- Line 128: The span in the Select component uses a bare boolean attribute
aria-hidden (in the <span aria-hidden className="shrink-0 text-ink-faint">)
which is inconsistent with other SVGs; update that element in Select.tsx to use
an explicit value aria-hidden="true" so assistive tech behavior is clear and
consistent with other icons in the component.

In `@console/web/src/hooks/use-hash-route.ts`:
- Around line 56-72: In configRouteFromHash, wrap the decodeURIComponent(second)
call in a try/catch inside the branch where first === 'workers' so malformed
percent-encoding won’t throw; if decodeURIComponent throws (URIError) fall back
to a safe value (e.g., null or the original raw segment) for workerId and
optionally log the error, ensuring the function still returns { tab: 'workers',
workerId: ... } rather than propagating the exception.

In `@console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx`:
- Around line 44-53: The auto-selection logic currently only triggers when
selectedId is falsy, leaving the pane empty if the URL contains a stale
selectedId not present in entries; update the useEffect in WorkersTab to also
check whether the current selectedId exists in entries and, if it does not and
entries.length > 0, call onSelect(entries[0].id). In other words, inside the
existing useEffect (which references selectedId, entries, onSelect) change the
early-return logic to: if (entries.length === 0) return; if (!selectedId ||
!entries.find(e => e.id === selectedId)) onSelect(entries[0].id); keeping the
same dependency array.

In `@console/web/src/pages/Configuration/tabs/WorkersTab/SaveBar.tsx`:
- Line 47: The save confirmation is never visible because the shouldShow boolean
(const shouldShow = dirty || inFlight || status.kind === 'error') omits
status.kind === 'saved'; update shouldShow in SaveBar.tsx to include saved
(e.g., const shouldShow = dirty || inFlight || status.kind === 'error' ||
status.kind === 'saved') so the bar remains visible when a save just succeeded,
or alternatively adjust WorkerEditor.tsx where status is set to 'saved' to delay
resetting to 'idle' (add a short timeout or clear-on-next-edit) so the transient
saved state persists long enough to be displayed; reference the shouldShow
variable and the status.kind checks when making the change.

In
`@console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/DictionaryField.tsx`:
- Around line 100-122: commitRows currently loses values when a row's key is
renamed because it only looks up obj[r.key]; capture the prior per-row value by
recording a map from each existing row's id to its current value before calling
setRows (e.g. build prevValues: Map(row.id -> (obj[row.key] ??
schemaDefault(valueSchema))) using the existing rows array), then when you build
next inside commitRows use prevValues.get(r.id) first, falling back to
obj[r.key] and then schemaDefault(valueSchema) (references: commitRows,
DictionaryRow.id, rows, obj, schemaDefault, valueSchema, lastSyncedKeysRef).

In
`@console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/EnumField.tsx`:
- Around line 35-38: The code treats only undefined as the unset state so
clearing the field (which sets null in onClear) becomes encodeEnumValue(null)
and shows "j:null" instead of the placeholder; update the currentKey logic (the
variable named currentKey used with encodeEnumValue and the onClear behavior) to
treat both null and undefined as unset (i.e., map value === undefined || value
=== null to undefined) so the trigger shows emptyLabel/placeholder when cleared
— assume null is not a valid enum member.

---

Nitpick comments:
In `@DOCUMENTATION_GUIDELINES.md`:
- Around line 21-22: Reword the ambiguous sentence "Every worker `SKILL.md` has
YAML frontmatter and four body sections (the last one only when the worker
exposes a trigger type)." to explicitly state that the count refers to level-2
subsections (e.g., "Every worker `SKILL.md` begins with YAML frontmatter
followed by four level-2 body subsections — When to Use, Boundaries, Functions,
and Reactive triggers — with the last subsection included only when the worker
exposes a trigger type.") so readers cannot mistake the overview heading for an
additional section.
🪄 Autofix (Beta)

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

Run ID: a0f24938-1d2b-4a41-8e92-13628c519d95

📥 Commits

Reviewing files that changed from the base of the PR and between 4c02d40 and c973b20.

⛔ Files ignored due to path filters (1)
  • console/web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (129)
  • .github/workflows/_rust-binary.yml
  • .github/workflows/ci.yml
  • .gitignore
  • DOCUMENTATION_GUIDELINES.md
  • console/build.rs
  • console/web/.gitignore
  • console/web/.storybook/main.ts
  • console/web/.storybook/preview-head.html
  • console/web/.storybook/preview.tsx
  • console/web/DESIGN.md
  • console/web/PLAYGROUND.md
  • console/web/README.md
  • console/web/biome.json
  • console/web/docs/custom-function-components.md
  • console/web/package.json
  • console/web/src/App.tsx
  • console/web/src/components/chat/ChatDock.tsx
  • console/web/src/components/chat/Composer.stories.tsx
  • console/web/src/components/chat/FunctionCallGroup.stories.tsx
  • console/web/src/components/chat/FunctionCallMessage.stories.tsx
  • console/web/src/components/chat/Message.stories.tsx
  • console/web/src/components/chat/ThoughtMessage.stories.tsx
  • console/web/src/components/chat/directory/SkillsViews.tsx
  • console/web/src/components/chat/engine/FunctionsListView.tsx
  • console/web/src/components/chat/engine/RegisteredTriggersListView.tsx
  • console/web/src/components/chat/engine/TriggersListView.tsx
  • console/web/src/components/chat/engine/WorkersListView.tsx
  • console/web/src/components/chat/sandbox/__tests__/parsers.test.ts
  • console/web/src/components/chat/sandbox/index.tsx
  • console/web/src/components/chat/sandbox/parsers.ts
  • console/web/src/components/permissions/DefaultPermissionModePicker.tsx
  • console/web/src/components/permissions/FunctionAllowlistTree.tsx
  • console/web/src/components/providers/ProviderSettingsDialog.tsx
  • console/web/src/components/ui/Button.stories.tsx
  • console/web/src/components/ui/Caret.stories.tsx
  • console/web/src/components/ui/ModeToggle.stories.tsx
  • console/web/src/components/ui/ModeToggle.tsx
  • console/web/src/components/ui/Prompt.stories.tsx
  • console/web/src/components/ui/Select.stories.tsx
  • console/web/src/components/ui/Select.tsx
  • console/web/src/components/ui/StatusDot.stories.tsx
  • console/web/src/components/ui/Wordmark.stories.tsx
  • console/web/src/hooks/use-approval-settings.ts
  • console/web/src/hooks/use-hash-route.ts
  • console/web/src/index.css
  • console/web/src/lib/backend/index.ts
  • console/web/src/lib/backend/pending-approvals-store.test.ts
  • console/web/src/pages/Configuration/index.tsx
  • console/web/src/pages/Configuration/tabs/ConsoleSettingsTab.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/EmptyState.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/SaveBar.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/WorkerEditor.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/WorkersList.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/WorkersTab.stories.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/api.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/dirty.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/dirty.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/errors.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/hooks.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ArrayField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/BooleanField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/DictionaryField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/EnumField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/FieldDispatch.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/FieldShell.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/NullableField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/NumberField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ObjectSection.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/OneOfField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/RowChrome.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/SchemaForm.stories.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/SchemaForm.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/StringField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/env-lexical/EnvLexicalInput.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/env-lexical/EnvPlaceholderNode.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/env-lexical/EnvPlaceholderTransformPlugin.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/env-lexical/env-template.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/env-lexical/env-template.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/path.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/path.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ref-resolver.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ref-resolver.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/reorder.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/reorder.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/useDragReorder.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/typography.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/useUnsavedGuard.ts
  • console/web/src/pages/Examples/Section.tsx
  • console/web/src/pages/Examples/index.tsx
  • console/web/src/pages/Examples/sections/composer-variants.tsx
  • console/web/src/pages/Examples/sections/custom-function-views.tsx
  • console/web/src/pages/Examples/sections/loading-states.tsx
  • console/web/src/pages/Examples/sections/message-variants.tsx
  • console/web/src/pages/Examples/sections/primitives.tsx
  • console/web/src/pages/Examples/sections/worker-fixtures.ts
  • console/web/src/pages/Playground/ScenarioPicker.tsx
  • console/web/src/pages/Traces/components/WaterfallChart.tsx
  • console/web/src/stories/decorators.tsx
  • console/web/src/stories/design/Colors.stories.tsx
  • console/web/src/stories/design/Loading.stories.tsx
  • console/web/src/stories/design/Typography.stories.tsx
  • console/web/src/stories/fixtures/directory-fixtures.ts
  • console/web/src/stories/fixtures/engine-fixtures.ts
  • console/web/src/stories/fixtures/sandbox-fixtures.ts
  • console/web/src/stories/fixtures/web-fixtures.ts
  • console/web/src/stories/fixtures/worker-fixtures.ts
  • console/web/src/stories/playground/Agent.stories.tsx
  • console/web/src/stories/playground/EventLog.tsx
  • console/web/src/stories/playground/FailureModes.stories.tsx
  • console/web/src/stories/playground/HappyPaths.stories.tsx
  • console/web/src/stories/playground/Markdown.stories.tsx
  • console/web/src/stories/playground/Timing.stories.tsx
  • console/web/src/stories/playground/harness.tsx
  • console/web/src/stories/playground/scenario-story.tsx
  • console/web/src/stories/playground/scenarios/abort-mid-thought.ts
  • console/web/src/stories/playground/scenarios/error-on-fcall.ts
  • console/web/src/stories/playground/scenarios/fast-tokens.ts
  • console/web/src/stories/playground/scenarios/happy-agent.ts
  • console/web/src/stories/playground/scenarios/happy-ask.ts
  • console/web/src/stories/playground/scenarios/happy-plan.ts
  • console/web/src/stories/playground/scenarios/helpers.ts
  • console/web/src/stories/playground/scenarios/index.ts
  • console/web/src/stories/playground/scenarios/long-markdown.ts
  • console/web/src/stories/playground/scenarios/markdown-stress.ts
  • console/web/src/stories/playground/scenarios/multi-function-agent.ts
  • console/web/src/stories/playground/scenarios/pending-approval.ts
  • console/web/src/stories/playground/scenarios/slow-tokens.ts
  • console/web/src/vite-env.d.ts
💤 Files with no reviewable changes (13)
  • console/web/src/components/chat/ChatDock.tsx
  • console/web/src/pages/Examples/Section.tsx
  • console/web/src/pages/Examples/index.tsx
  • console/web/src/vite-env.d.ts
  • console/web/src/pages/Examples/sections/composer-variants.tsx
  • console/web/src/pages/Examples/sections/message-variants.tsx
  • console/web/src/pages/Examples/sections/worker-fixtures.ts
  • console/web/src/pages/Examples/sections/loading-states.tsx
  • console/web/src/pages/Playground/ScenarioPicker.tsx
  • console/web/src/pages/Examples/sections/custom-function-views.tsx
  • console/web/src/pages/Examples/sections/primitives.tsx
  • .github/workflows/ci.yml
  • .gitignore

</SelectPrimitive.Value>
<SelectPrimitive.Icon asChild>
<span aria-hidden className="text-ink-faint">
<span aria-hidden className="shrink-0 text-ink-faint">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit value to aria-hidden.

The <span> wrapper uses aria-hidden without an explicit value, while the nested SVG and other SVGs in the component correctly use aria-hidden="true". For consistency and clarity, add the explicit value.

♿ Proposed fix
-        <SelectPrimitive.Icon asChild>
-          <span aria-hidden className="shrink-0 text-ink-faint">
+        <SelectPrimitive.Icon asChild>
+          <span aria-hidden="true" className="shrink-0 text-ink-faint">
📝 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
<span aria-hidden className="shrink-0 text-ink-faint">
<span aria-hidden="true" className="shrink-0 text-ink-faint">
🤖 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/ui/Select.tsx` at line 128, The span in the Select
component uses a bare boolean attribute aria-hidden (in the <span aria-hidden
className="shrink-0 text-ink-faint">) which is inconsistent with other SVGs;
update that element in Select.tsx to use an explicit value aria-hidden="true" so
assistive tech behavior is clear and consistent with other icons in the
component.

Comment on lines +56 to +72
function configRouteFromHash(hash: string): ConfigurationRoute {
// Treat bare `#/configuration` and any non-configuration hash as the
// default `console` tab. Legacy `#/providers` is funneled here too.
if (!hash.startsWith('#/configuration/')) {
return { tab: 'console', workerId: null }
}
const rest = hash.slice('#/configuration/'.length)
const segments = rest.split('/').filter(Boolean)
const [first, second] = segments
if (first === 'workers') {
return {
tab: 'workers',
workerId: second ? decodeURIComponent(second) : null,
}
}
return { tab: 'console', workerId: null }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap decodeURIComponent in error handling.

Line 68 calls decodeURIComponent(second) without catching potential URIError from malformed percent-encoding. If an operator manually edits the hash to something like #/configuration/workers/%E0%A4%A, route parsing will throw and break navigation.

🛡️ Proposed fix to add try-catch
 function configRouteFromHash(hash: string): ConfigurationRoute {
   // Treat bare `#/configuration` and any non-configuration hash as the
   // default `console` tab. Legacy `#/providers` is funneled here too.
   if (!hash.startsWith('`#/configuration/`')) {
     return { tab: 'console', workerId: null }
   }
   const rest = hash.slice('`#/configuration/`'.length)
   const segments = rest.split('/').filter(Boolean)
   const [first, second] = segments
   if (first === 'workers') {
+    let workerId: string | null = null
+    if (second) {
+      try {
+        workerId = decodeURIComponent(second)
+      } catch {
+        // Malformed encoding — treat as no selection
+        workerId = null
+      }
+    }
     return {
       tab: 'workers',
-      workerId: second ? decodeURIComponent(second) : null,
+      workerId,
     }
   }
   return { tab: 'console', workerId: null }
 }
📝 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
function configRouteFromHash(hash: string): ConfigurationRoute {
// Treat bare `#/configuration` and any non-configuration hash as the
// default `console` tab. Legacy `#/providers` is funneled here too.
if (!hash.startsWith('#/configuration/')) {
return { tab: 'console', workerId: null }
}
const rest = hash.slice('#/configuration/'.length)
const segments = rest.split('/').filter(Boolean)
const [first, second] = segments
if (first === 'workers') {
return {
tab: 'workers',
workerId: second ? decodeURIComponent(second) : null,
}
}
return { tab: 'console', workerId: null }
}
function configRouteFromHash(hash: string): ConfigurationRoute {
// Treat bare `#/configuration` and any non-configuration hash as the
// default `console` tab. Legacy `#/providers` is funneled here too.
if (!hash.startsWith('`#/configuration/`')) {
return { tab: 'console', workerId: null }
}
const rest = hash.slice('`#/configuration/`'.length)
const segments = rest.split('/').filter(Boolean)
const [first, second] = segments
if (first === 'workers') {
let workerId: string | null = null
if (second) {
try {
workerId = decodeURIComponent(second)
} catch {
// Malformed encoding — treat as no selection
workerId = null
}
}
return {
tab: 'workers',
workerId,
}
}
return { tab: 'console', workerId: null }
}
🤖 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/hooks/use-hash-route.ts` around lines 56 - 72, In
configRouteFromHash, wrap the decodeURIComponent(second) call in a try/catch
inside the branch where first === 'workers' so malformed percent-encoding won’t
throw; if decodeURIComponent throws (URIError) fall back to a safe value (e.g.,
null or the original raw segment) for workerId and optionally log the error,
ensuring the function still returns { tab: 'workers', workerId: ... } rather
than propagating the exception.

Comment on lines +44 to +53
useEffect(() => {
if (selectedId) return
if (entries.length === 0) return
onSelect(entries[0].id)
}, [selectedId, entries, onSelect])

const selectedEntry =
selectedId != null
? (entries.find((e) => e.id === selectedId) ?? null)
: null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'WorkerEditor.tsx' --exec sed -n '1,200p' {}

Repository: iii-hq/workers

Length of output: 7109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WorkersTab/index.tsx (around lines 30-80) ==="
FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '1,140p' | sed -n '30,120p'
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection (rest of component) ==="
# locate WorkerEditorEmptySelection definition lines
# Use rg for the component name and then print around it.
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . | head -n 1)"
echo "Using: $WORKER_FILE"
rg -n "function WorkerEditorEmptySelection|export function WorkerEditorEmptySelection" "$WORKER_FILE" || true
# Print from match line -20 to +120
LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n1 | cut -d: -f1 || true)"
if [ -n "$LINE" ]; then
  start=$((LINE-20))
  end=$((LINE+200))
  nl -ba "$WORKER_FILE" | sed -n "${start},${end}p"
else
  echo "Could not find WorkerEditorEmptySelection export function."
fi

Repository: iii-hq/workers

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WorkersTab/index.tsx (around lines 30-120) ==="
FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '1,220p' | sed -n '30,160p'
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection (rest of component) ==="
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs/WorkersTab . 2>/dev/null | head -n 1 || true)"
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs . 2>/dev/null | head -n 1 || true)"
fi
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . | head -n 1)"
fi
echo "Using: $WORKER_FILE"
rg -n "WorkerEditorEmptySelection" "$WORKER_FILE" || true
LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n1 | cut -d: -f1 || true)"
if [ -n "$LINE" ]; then
  start=$((LINE-30))
  end=$((LINE+260))
  nl -ba "$WORKER_FILE" | sed -n "${start},${end}p"
else
  echo "Could not find WorkerEditorEmptySelection export function."
fi

Repository: iii-hq/workers

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WorkersTab/index.tsx (around lines 30-160) ==="
FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '30,160p'
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection (around its definition) ==="
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs/WorkersTab . 2>/dev/null | head -n 1 || true)"
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs . 2>/dev/null | head -n 1 || true)"
fi
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . | head -n 1)"
fi
echo "Using: $WORKER_FILE"
LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n 1 | cut -d: -f1 || true)"
if [ -n "$LINE" ]; then
  start=$((LINE-40))
  end=$((LINE+260))
  nl -ba "$WORKER_FILE" | sed -n "${start},${end}p"
else
  echo "Could not find WorkerEditorEmptySelection export function."
fi

Repository: iii-hq/workers

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WorkersTab/index.tsx (around lines 30-160) ==="
FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '30,160p'
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection (around its definition) ==="
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs/WorkersTab . 2>/dev/null | head -n 1 || true)"
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs . 2>/dev/null | head -n 1 || true)"
fi
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . | head -n 1)"
fi
echo "Using: $WORKER_FILE"
LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n 1 | cut -d: -f1 || true)"
if [ -n "$LINE" ]; then
  start=$((LINE-40))
  end=$((LINE+260))
  nl -ba "$WORKER_FILE" | sed -n "${start},${end}p"
else
  echo "Could not find WorkerEditorEmptySelection export function."
fi

Repository: iii-hq/workers

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WorkersTab/index.tsx (around lines 30-160) ==="
FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '30,160p'
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection (around its definition) ==="
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs/WorkersTab . 2>/dev/null | head -n 1 || true)"
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs . 2>/dev/null | head -n 1 || true)"
fi
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . | head -n 1)"
fi
echo "Using: $WORKER_FILE"
LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n 1 | cut -d: -f1 || true)"
if [ -n "$LINE" ]; then
  start=$((LINE-40))
  end=$((LINE+260))
  nl -ba "$WORKER_FILE" | sed -n "${start},${end}p"
else
  echo "Could not find WorkerEditorEmptySelection export function."
fi

Repository: iii-hq/workers

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="console/web/src/pages/Configuration/tabs/WorkersTab/index.tsx"
echo "=== WorkersTab/index.tsx (lines 1-200) ==="
if [ -f "$FILE" ]; then
  awk 'NR>=1 && NR<=200 {printf "%5d\t%s\n", NR, $0}' "$FILE"
else
  echo "Missing $FILE"
fi

echo
echo "=== WorkerEditorEmptySelection definition (find file + show component) ==="
WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs/WorkersTab . 2>/dev/null | head -n 1 || true)"
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' console/web/src/pages/Configuration/tabs . 2>/dev/null | head -n 1 || true)"
fi
if [ -z "$WORKER_FILE" ]; then
  WORKER_FILE="$(fd -t f 'WorkerEditor.tsx' . 2>/dev/null | head -n 1 || true)"
fi

echo "Using: ${WORKER_FILE:-<not found>}"
if [ -z "$WORKER_FILE" ]; then
  exit 0
fi

LINE="$(rg -n "export function WorkerEditorEmptySelection" "$WORKER_FILE" | head -n 1 | cut -d: -f1 || true)"
if [ -z "$LINE" ]; then
  exit 0
fi

start=$((LINE-40))
end=$((LINE+260))
echo "=== $WORKER_FILE (around definition: $start-$end) ==="
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%5d\t%s\n", NR, $0}' "$WORKER_FILE"

Repository: iii-hq/workers

Length of output: 5566


Stale selectedId (not present in entries) leaves the Workers right pane empty with no recovery

WorkersTab only auto-selects when selectedId is falsy; if the URL carries an id that isn’t in entries, selectedEntry becomes null and the UI renders WorkerEditorEmptySelection (which only shows the empty state—no redirect/reselect). Add a guard to auto-select the first entry when selectedId doesn’t match any loaded item (while keeping the existing entries.length === 0 load guard):

🐛 Proposed guard
   useEffect(() => {
-    if (selectedId) return
     if (entries.length === 0) return
+    if (selectedId && entries.some((e) => e.id === selectedId)) return
     onSelect(entries[0].id)
-  }, [selectedId, entries, onSelect])
+  }, [selectedId, entries, onSelect])
🤖 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/pages/Configuration/tabs/WorkersTab/index.tsx` around lines
44 - 53, The auto-selection logic currently only triggers when selectedId is
falsy, leaving the pane empty if the URL contains a stale selectedId not present
in entries; update the useEffect in WorkersTab to also check whether the current
selectedId exists in entries and, if it does not and entries.length > 0, call
onSelect(entries[0].id). In other words, inside the existing useEffect (which
references selectedId, entries, onSelect) change the early-return logic to: if
(entries.length === 0) return; if (!selectedId || !entries.find(e => e.id ===
selectedId)) onSelect(entries[0].id); keeping the same dependency array.

saveDisabled,
}: SaveBarProps) {
const inFlight = status.kind === 'saving'
const shouldShow = dirty || inFlight || status.kind === 'error'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The "saved" status display is unreachable.

The visibility condition on Line 47 does not include status.kind === 'saved', so the bar hides when status is 'saved' and dirty is false. Meanwhile, the "saved" message (Lines 96-98) only renders when status.kind === 'saved' && !dirty, which means:

  1. When the bar is visible (dirty || inFlight || error), the saved state never displays because dirty is true.
  2. When status.kind === 'saved' && !dirty, the bar is hidden entirely.

Looking at WorkerEditor.tsx, the 'saved' status is transient—it's set on Line 99 after mutation success, but then immediately reset to 'idle' when the refetch completes and triggers the effect on Lines 50-54. Users never see the "saved" confirmation.

The comment on Lines 26-28 suggests the bar should show "just succeeded", but the current logic prevents this.

💡 Proposed fix to show "saved" feedback

Option 1: Include 'saved' in the visibility condition:

- const shouldShow = dirty || inFlight || status.kind === 'error'
+ const shouldShow = dirty || inFlight || status.kind === 'error' || status.kind === 'saved'

Option 2: In WorkerEditor.tsx, delay the status reset to 'idle' so 'saved' persists briefly:

  useEffect(() => {
    if (valueQuery.data === undefined) return
    setDraft(valueQuery.data)
-   setStatus((cur) => (cur.kind === 'saving' ? cur : { kind: 'idle' }))
+   setStatus((cur) => (cur.kind === 'saving' || cur.kind === 'saved' ? cur : { kind: 'idle' }))
  }, [valueQuery.data])

Then add a timeout to clear 'saved' after a few seconds, or let the next edit clear it (which already happens on Line 80).

Also applies to: 96-98

🤖 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/pages/Configuration/tabs/WorkersTab/SaveBar.tsx` at line 47,
The save confirmation is never visible because the shouldShow boolean (const
shouldShow = dirty || inFlight || status.kind === 'error') omits status.kind ===
'saved'; update shouldShow in SaveBar.tsx to include saved (e.g., const
shouldShow = dirty || inFlight || status.kind === 'error' || status.kind ===
'saved') so the bar remains visible when a save just succeeded, or alternatively
adjust WorkerEditor.tsx where status is set to 'saved' to delay resetting to
'idle' (add a short timeout or clear-on-next-edit) so the transient saved state
persists long enough to be displayed; reference the shouldShow variable and the
status.kind checks when making the change.

Comment on lines +100 to +122
function commitRows(nextRows: DictionaryRow[]) {
setRows(nextRows)
const counts = new Map<string, number>()
for (const r of nextRows) {
if (r.key.length === 0) continue
counts.set(r.key, (counts.get(r.key) ?? 0) + 1)
}
const allUnique = [...counts.values()].every((c) => c === 1)
const allNonEmpty = nextRows.every((r) => r.key.length > 0)
if (allUnique && allNonEmpty) {
const next: { [key: string]: JsonValue } = {}
for (const r of nextRows) {
next[r.key] = obj[r.key] ?? schemaDefault(valueSchema)
}
onChange(next)
// Order-independent: reordering keeps the same key set, so external
// sync won't fight a drag.
lastSyncedKeysRef.current = nextRows
.map((r) => r.key)
.sort()
.join('\u0000')
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Renaming a key silently drops its value.

commitRows rebuilds the object keyed by each row's current key and reads obj[r.key]. When a key is renamed, the new key doesn't exist in obj, so the value falls through to schemaDefault(valueSchema) and the previously-entered value is lost on the first keystroke of the rename. For an env-style editor where the value (e.g. ${VAR}) is the important part, this is meaningful data loss.

Carry the prior value across a rename by mapping the previous key through the stable row id:

🐛 Proposed fix to preserve value on rename
 function commitRows(nextRows: DictionaryRow[]) {
   setRows(nextRows)
   const counts = new Map<string, number>()
   for (const r of nextRows) {
     if (r.key.length === 0) continue
     counts.set(r.key, (counts.get(r.key) ?? 0) + 1)
   }
   const allUnique = [...counts.values()].every((c) => c === 1)
   const allNonEmpty = nextRows.every((r) => r.key.length > 0)
   if (allUnique && allNonEmpty) {
+    const prevKeyById = new Map(rows.map((r) => [r.id, r.key]))
     const next: { [key: string]: JsonValue } = {}
     for (const r of nextRows) {
-      next[r.key] = obj[r.key] ?? schemaDefault(valueSchema)
+      const prevKey = prevKeyById.get(r.id)
+      const carried = prevKey !== undefined ? obj[prevKey] : undefined
+      next[r.key] = carried ?? obj[r.key] ?? schemaDefault(valueSchema)
     }
     onChange(next)
📝 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
function commitRows(nextRows: DictionaryRow[]) {
setRows(nextRows)
const counts = new Map<string, number>()
for (const r of nextRows) {
if (r.key.length === 0) continue
counts.set(r.key, (counts.get(r.key) ?? 0) + 1)
}
const allUnique = [...counts.values()].every((c) => c === 1)
const allNonEmpty = nextRows.every((r) => r.key.length > 0)
if (allUnique && allNonEmpty) {
const next: { [key: string]: JsonValue } = {}
for (const r of nextRows) {
next[r.key] = obj[r.key] ?? schemaDefault(valueSchema)
}
onChange(next)
// Order-independent: reordering keeps the same key set, so external
// sync won't fight a drag.
lastSyncedKeysRef.current = nextRows
.map((r) => r.key)
.sort()
.join('\u0000')
}
}
function commitRows(nextRows: DictionaryRow[]) {
setRows(nextRows)
const counts = new Map<string, number>()
for (const r of nextRows) {
if (r.key.length === 0) continue
counts.set(r.key, (counts.get(r.key) ?? 0) + 1)
}
const allUnique = [...counts.values()].every((c) => c === 1)
const allNonEmpty = nextRows.every((r) => r.key.length > 0)
if (allUnique && allNonEmpty) {
const prevKeyById = new Map(rows.map((r) => [r.id, r.key]))
const next: { [key: string]: JsonValue } = {}
for (const r of nextRows) {
const prevKey = prevKeyById.get(r.id)
const carried = prevKey !== undefined ? obj[prevKey] : undefined
next[r.key] = carried ?? obj[r.key] ?? schemaDefault(valueSchema)
}
onChange(next)
// Order-independent: reordering keeps the same key set, so external
// sync won't fight a drag.
lastSyncedKeysRef.current = nextRows
.map((r) => r.key)
.sort()
.join('\u0000')
}
}
🤖 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/pages/Configuration/tabs/WorkersTab/schema-form/DictionaryField.tsx`
around lines 100 - 122, commitRows currently loses values when a row's key is
renamed because it only looks up obj[r.key]; capture the prior per-row value by
recording a map from each existing row's id to its current value before calling
setRows (e.g. build prevValues: Map(row.id -> (obj[row.key] ??
schemaDefault(valueSchema))) using the existing rows array), then when you build
next inside commitRows use prevValues.get(r.id) first, falling back to
obj[r.key] and then schemaDefault(valueSchema) (references: commitRows,
DictionaryRow.id, rows, obj, schemaDefault, valueSchema, lastSyncedKeysRef).

Comment on lines +35 to +38
// Leave the key `undefined` when the value is unset so the trigger shows
// the placeholder instead of stringifying `undefined` into `j:undefined`.
const currentKey =
value === undefined ? undefined : encodeEnumValue(value as JsonValue)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

null is not treated as the unset state.

onClear commits null as the canonical empty value (Line 60), but currentKey only maps undefined to the placeholder. A cleared field therefore produces encodeEnumValue(null)j:null, which matches no option, so the trigger renders a mismatched value instead of the emptyLabel/placeholder. Treat null as unset as well (assuming null isn't itself an enum member):

🐛 Proposed fix
   const currentKey =
-    value === undefined ? undefined : encodeEnumValue(value as JsonValue)
+    value === undefined || value === null
+      ? undefined
+      : encodeEnumValue(value as JsonValue)
📝 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
// Leave the key `undefined` when the value is unset so the trigger shows
// the placeholder instead of stringifying `undefined` into `j:undefined`.
const currentKey =
value === undefined ? undefined : encodeEnumValue(value as JsonValue)
// Leave the key `undefined` when the value is unset so the trigger shows
// the placeholder instead of stringifying `undefined` into `j:undefined`.
const currentKey =
value === undefined || value === null
? undefined
: encodeEnumValue(value as JsonValue)
🤖 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/pages/Configuration/tabs/WorkersTab/schema-form/EnumField.tsx`
around lines 35 - 38, The code treats only undefined as the unset state so
clearing the field (which sets null in onClear) becomes encodeEnumValue(null)
and shows "j:null" instead of the placeholder; update the currentKey logic (the
variable named currentKey used with encodeEnumValue and the onClear behavior) to
treat both null and undefined as unset (i.e., map value === undefined || value
=== null to undefined) so the trigger shows emptyLabel/placeholder when cleared
— assume null is not a valid enum member.

@sergiofilhowz sergiofilhowz merged commit d27671d into main Jun 1, 2026
11 checks passed
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