Skip to content

feat(web): Profiles tab -- browse, search, create, edit, clone, delet… - #692

Open
sujoydc wants to merge 4 commits into
mainfrom
feat/510-profiles-ui
Open

feat(web): Profiles tab -- browse, search, create, edit, clone, delet…#692
sujoydc wants to merge 4 commits into
mainfrom
feat/510-profiles-ui

Conversation

@sujoydc

@sujoydc sujoydc commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Overview

Third and final PR for #510, building the web UI on top of the APIs from #575 (validation service, /validate, /schema) and #585 (create/update/delete/source endpoints). Adds a Profiles tab where you can browse and search installed profiles, create new ones from a scaffold template or from scratch, edit and clone existing ones, and delete local-store profiles — with the backend validator wired in front of every write.

AgentPanel is untouched; it remains the agent-launch picker. Closes #510 once merged.

What's in it

Navigation. A Profiles tab between Home and Agents, and the Home dashboard's Profiles stat card now navigates there. This renumbers the Alt+N shortcuts for the tabs after it (one-time shift; the code comment explains the ordering rationale).

List, search, detail. Master-detail layout. The catalog is fetched once on mount — no polling. Search delegates ranking to GET /agents/profiles/search with a 300 ms debounce; results render in server order (the client never re-sorts) and a monotonic token discards stale responses. The detail pane shows source, provider, model, tags, capabilities, and a warning when duplicated_in reports the name shadowed across directories.

Create. One modal, two entry points. From template: pick a scaffold template, fill a form generated from that template's own JSON-Schema, and watch a live preview rendered by POST /templates/preview (same 300 ms debounce; Create is gated while a render is in flight, and the preview shows the exact document that will be persisted, including the frontmatter name rewrite). From scratch: the form is generated from GET /agents/profiles/schema — primary fields visible, everything else behind an Advanced expander. Object-valued fields (mcpServers, codexConfig, …) use validated JSON editors rather than bespoke widgets. Frontmatter is emitted as JSON-valued YAML, so no YAML dependency is added. provider is a select fed by the live registry (uninstalled providers labelled but selectable, free-text fallback if the registry call fails); role is a datalist with the built-in roles plus free entry for settings.json custom roles; model stays free text deliberately — there is no registry of valid model IDs to validate against.

Edit, clone, delete. Edit opens a raw document editor over GET /agents/profiles/{name}/source and saves via PUT — deliberately not the schema form, because an edit must round-trip the exact stored bytes (env-var placeholders intact; one test pins a ${VAR} surviving the full load→edit→PUT cycle). Only local-store profiles get Edit/Delete; built-in, provider, and custom profiles are read-only and offer "Clone to customise", matching the backend's write model. Delete sits behind a type-the-name-to-confirm gate (an optional, additive confirmationText prop on the shared ConfirmModal; existing callers unchanged).

Validation. POST /agents/profiles/validate runs before every save, in both modals and both modes. Errors block client-side; warnings render but allow (and surface again via snackbar after the save). The findings panel renders the truncation contract from #585 precisely: at most 100 findings including one omission marker, exactly once, last, with its severity matching the omitted producer — an error-severity marker is explicitly flagged as hiding errors, and the marker text in any non-final position renders as an ordinary finding. Error findings also paint the specific form control they name (dotted path rooted at the frontmatter key, (root) required-property errors mapped by the quoted name), auto-expanding the Advanced section when the target is hidden. Clone validation runs on the exact rewritten document; one test asserts the validated bytes equal the POST body.

Hardening

The branch was reviewed adversarially before opening, with empirical probes against the frontmatter helpers; everything found was fixed here rather than left for review:

  • rewriteFrontmatterName uses replacement functions, not strings — a replacement string interprets $-patterns, which corrupted names containing $&/$' and mangled documents whose frontmatter legally contains such text.
  • The frontmatter regexes accept CRLF, so cloning a Windows-authored profile can't silently no-op the name rewrite and then fail the server's name-match check.
  • Backdrop clicks are inert while a save is in flight (Cancel already was).
  • The authoring calls (validate/preview/create/replace) use a 30 s timeout instead of the 10 s default, so a slow round-trip doesn't render as a phantom validation failure.
  • A failing template-schema fetch surfaces its error instead of setting it invisibly behind a schema-gated render.

Tests

63 new UI tests across four files (12 panel, 25 create modal, 15 editor, 11 findings renderer); full web suite 240/240, tsc clean, production build clean. The areas #585's review focused on get the same treatment here: debounce coalescing is asserted by call count, the search-order test uses three rows whose ranked order differs from both alphabetical and catalog order so a re-sort can't pass by coincidence, and the truncation rendering is tested with full 100-finding payloads including marker-severity and marker-position cases.

Not in scope (per #510)

Launching agents from the Profiles tab, workflow/team composition, profile rename semantics, and full JSON-Schema modelling of every object-valued field (JSON editors suffice). One known follow-up: making the template live preview directly editable (with dirty-state semantics so form edits don't clobber manual edits) — deferred to keep this PR reviewable; happy to file an issue.

@haofeif haofeif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the very thorough write-up — the self-review section made this much faster to audit, and every hardening claim in it held up under execution (details below).

Reviewed at 0948572a (merge-base b6a0520b). Web-only: no Python file changes.

Author claims I reproduced

Claim Result
63 new UI tests Exact. merge-base 177 passed, PR 240 passed -> +63
Full web suite 240/240 Exact. Test Files 16 passed, Tests 240 passed
tsc clean, production build clean Confirmed. npm run build exit 0
No pre-existing regressions Confirmed. merge-base 177/177 green; 0 tests regressed
rewriteFrontmatterName handles $-patterns Confirmed for $&, $', $`, $1, $$, both in the typed name and pre-existing in the frontmatter
CRLF documents rewrite rather than silently no-op Confirmed
Editor loads unresolved /source, never the resolved parse Confirmed, and genuinely pinned (mutation below)
Truncation contract rendered exactly Confirmed against live validator output
Search order never re-sorted Confirmed — the fixture is genuinely discriminating
Backdrop inert while saving Confirmed in both modals
ConfirmModal.confirmationText additive Confirmed — gated on !== undefined; no existing caller passes it

Truncation contract, end-to-end

Rather than trust the copied constant, I drove the real validator to truncation:

300 non-string capabilities: total=100 markers=1 marker_idx=[99] last_sev=error
   last_message='Additional validation findings omitted.'
   EXACT-MATCH-UI-CONSTANT=True

100 including the marker, exactly one marker, last position, error severity because errors were dropped, and byte-identical to ValidationFindings.OMISSION_MESSAGE. isOmissionMarker requires index === all.length - 1, so the marker text in a non-final position does render as an ordinary finding, as documented.

The secret-leak path is really pinned

I mutated getProfileSource to drop /source (so the editor would read the resolved profile):

Tests  9 failed | 6 passed (15)

Nine editor tests go red. That behaviour is enforced, not just asserted in prose.

Things I suspected and cleared

  • Portaled CustomSelect menu breaking existing callers. z-[80] clears the z-[60] maximum elsewhere, and the two backdrop closers (AgentPanel:518, FlowsPanel:300) are element-level onClicks, not document-level handlers, so a click on the portaled menu never reaches them. The pre-existing memory-graph test opens the menu and clicks an option, so this is covered by tests that predate the PR.
  • Mixed line endings from a CRLF clone. rewriteFrontmatterName does emit a mixed-ending document (---\n ... \r\n ... ---\r\n). I fed it to the real backend parser and it round-trips cleanly (meta={'name': 'newname', 'description': 'd'}), as does ${VAR} inside a JSON-valued field. Not a defect — noting it only so it doesn't get re-litigated.
  • Route ordering. All 14 URLs match api/main.py, and every literal path is declared before /agents/profiles/{name}.
  • deleteProfile against a 204. fetchJSON's empty-body branch already covers it.

What blocks

Two defects, same root cause: a debounced effect whose early return does not invalidate the in-flight request token. The monotonic tokens are correct for the case they were written for (a newer request superseding an older one) but not for "the reason to want this response disappeared".

The first one silently persists the wrong document, so I'm marking this changes-requested — everything else here is in good shape, and both fixes are one-liners I verified.

Both fixes together keep 240 passed and tsc/build clean.

// Debounced live preview: one render request per quiet burst of config edits.
useEffect(() => {
if (previewTimer.current) clearTimeout(previewTimer.current)
if (!template || !templateSchema) return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] A stale template preview can be persisted under a different template

In template mode the create POST body is the preview state (ProfileCreateModal.tsx:471), and Create is gated on preview !== null && !previewLoading (:462) precisely so a mid-debounce render can't be persisted. That gate can be defeated.

Selecting a different template runs setTemplateSchema(null) synchronously (:316). The preview effect then re-runs and hits this early return — which does not bump previewSeq and does not clear preview. When the previous template's in-flight render lands, seq === previewSeq.current still holds, so it calls setPreview(oldContent) and setPreviewLoading(false).

Reproduced (template A's preview released after switching to template B, B's schema still loading):

PROBE selected template shows: aws/stepfunction
PROBE preview pane present: false | contains TEMPLATE-A-BODY: undefined
PROBE create button disabled: false
PROBE POSTed body contains TEMPLATE-A-BODY: true

This is silent rather than merely wrong-looking: the whole Live preview block is gated on (templateSchema || previewError) (:593), so during this window the user sees only "Loading template schema…" (:570) — nothing on screen shows template A's content — while Create is enabled and armed with it. Clicking it writes a profile whose body came from the template the user just navigated away from, under the name they chose, with no error.

Fix — invalidate the token and drop the orphaned render:

    if (previewTimer.current) clearTimeout(previewTimer.current)
    if (!template || !templateSchema) {
      // The reason to want the in-flight render just disappeared: discard it
      // so it cannot land as this template's preview (and be persisted).
      previewSeq.current++
      setPreview(null)
      return
    }

Verified: PROBE create button disabled: true, no stale POST, and the full suite stays at 240 passed. (Leaving previewLoading true here is correct — the next schema load re-arms it.)

useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current)
const q = query.trim()
if (q === '') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Clearing the search box restores stale results once the in-flight response lands

Same shape as the P1, without the write. Clearing the box takes this early return after setResults(null), but never bumps searchSeq, so an already-dispatched search still satisfies seq === searchSeq.current at :283 and calls setResults(r). Since rows = results ?? catalog (:299), the list drops back to the filtered set.

Reproduced (response released after the box is cleared):

PROBE search requests in flight: 1
PROBE rows after clear (before response lands): 6
PROBE searchbox value: ""
PROBE rows after stale response lands: 1 ["mid-agentkir"]

The user is left looking at a filtered list with an empty search box and no indication why — including the duplicated_in shadowing warnings that only the catalog rows carry. It is recoverable (type a character and clear again, with nothing in flight), which is why this is a P2 and not a P1.

Fix:

    if (q === '') {
      searchSeq.current++
      setResults(null)
      setSearchError(null)
      setSearching(false)
      return
    }

Verified: the probe then reports all 6 catalog rows, and the full suite stays at 240 passed.

}

/** Extract the frontmatter `name:` value from a rendered document, if any. */
export function extractFrontmatterName(content: string): string | null {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nit] extractFrontmatterName can match a name: line in the markdown body

[\s\S]*? isn't bounded to the end of the frontmatter block, so when the frontmatter has no name: the scan continues into the document body:

EXTRACT no-fm-name -> "decoy"     // '---\ndescription: d\n---\nbody\nname: decoy\n'
EXTRACT normal     -> "real"
EXTRACT quoted     -> "real"
EXTRACT crlf       -> "real"

Impact today is limited to pre-filling the profile-name box (:343, only when !nameTouched.current), and every shipped template carries a frontmatter name, so I don't think it's reachable — hence nit. Bounding the scan to the block would close it, e.g. match the frontmatter with /^---\r?\n([\s\S]*?)\r?\n---/ first and search name: within that captured group, which is what rewriteFrontmatterName already does correctly.

@gutosantos82 gutosantos82 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR Review: #692 — feat(web): Profiles tab — browse, search, create, edit, clone, delete profiles

Summary

Adds the Profiles tab to the CAO Web UI (browse/search/create/edit/clone/delete over the #575/#585 APIs) — the final piece of #510. The implementation and its 63 new tests are high quality: we independently reproduced the 240/240 suite, clean tsc/build, and every hardening claim in the PR body. However, maintainer @haofeif has an open CHANGES_REQUESTED at this exact head for a data-integrity race (stale template preview silently persisted), and our review found the root cause is systemic: previewSeq is bumped only when a request is issued, never when the preview is invalidated, so the flagged one-liner alone leaves two more paths to the same silent persistence. Recommendation: fix the token-invalidation class as a whole (all clear sites), not just the two flagged lines, and land regression tests with it.

Blocking (must fix before merge)

  • [correctness][verification] web/src/components/ProfileCreateModal.tsx:286-306 (open-reset effect) and :311-314 (template-deselect branch) — 🆕 Both setPreview(null) without bumping previewSeq.current (the reset also leaves previewTimer and previewLoading untouched). The modal stays mounted across close (open prop), so: preview in flight → close → reopen → the stale response still satisfies seq === previewSeq.current, re-lands setPreview(staleContent) + setProfileName(extractFrontmatterName(stale)), and canCreate (which checks only preview !== null && !previewLoading, not template) enables Create — persisting a body from a template that isn't even selected, with the preview pane not rendered. This is the same silent-persistence class as the maintainer's P1 but a distinct trigger his line-333 fix does not cover. Fix: invalidate the token at every clear site — bump previewSeq.current, clear previewTimer, reset previewLoading in the reset effect and the deselect branch (or bump the token on every preview-effect run/teardown).

Important (should fix)

  • [consistency][correctness] ProfileCreateModal.tsx:309-327 — 🆕 getTemplateSchema is the one fetch in the modal with no staleness guard (no seq token, no cancelled flag, no abort) — unlike the search, preview, and ProfileDetail.getProfile effects beside it. Fast A→B template switch with out-of-order resolution leaves templateSchema = A's schema and config = A's seeded defaults while template = B: the form renders A's fields under a B selection and the debounced preview renders B with A's config. Same fix norm as the blocking item: token or cancelled flag keyed on the selected template.
  • [tests] web/src/test/ — 🆕 The monotonic seq-guard machinery the PR headlines has zero test coverage: no test creates two overlapping in-flight requests with reordered resolution, so the seq !== ...current discard branch is never taken (all search/preview mocks resolve synchronously). The P1/P2 fixes should ship with gate-promise regression tests — (a) search: type, let the request go in flight, clear the box, then resolve → assert results stay null; (b) preview: template A render in flight, switch to B, resolve A → assert no A content in pane and Create does not POST A's body. Otherwise the one-liner fixes land undefended.
  • [tests] web/src/components/CustomSelect.tsx — 🆕 The shared-component rewrite (portal to document.body, fixed positioning, flip-up, scroll/resize-to-close, portal-aware outside-click, new invalid prop) is essentially untested — tests only pick options by testid, and the invalid red-boundary path on a select is never asserted (the two red-boundary tests hit text/number inputs). This component backs Flows and Agents too; a regression in scroll-close/flip would be invisible to the suite.
  • [correctness] ProfileCreateModal.tsx (SchemaField object branch) — 🆕 Object/JSON textareas are uncontrolled (defaultValue) and always receive value=undefined (object fields render only in Advanced, which passes undefined for object types). Collapse and reopen Advanced (or trigger the error-driven auto-expand) and the textarea remounts empty while jsonDrafts still holds — and persists — the typed JSON. Display/state desync: user sees an empty field, the value saves anyway; retyping clobbers the draft. Make the textarea controlled from the draft string.
  • [conventions] web/README.md — 🆕 The canonical frontend doc (linked from docs/web-ui.md and CODEBASE.md) enumerates every page, supporting component, and file in three places; this PR adds a page (ProfilesPanel) + three components (ProfileCreateModal, ProfileEditorModal, ValidationFindings) and updates none of them. Per the repo's documentation-maintenance rule, update all three enumerations in this PR.
  • [conventions] CHANGELOG.md — 🆕 No [Unreleased] → Added entry for the new Profiles tab. The #510 arc's API work is already recorded in the changelog; this user-facing UI piece should be too.

Nits (optional)

  • [correctness] ProfileCreateModal/ProfileEditorModal save paths — a transport error/timeout on the pre-save validateProfile (a UX gate; the write route re-validates authoritatively) hard-blocks the save behind a generic "Validation failed". Consider letting transport failures fall through to the server-side validation while still blocking on real 400s.
  • [consistency] web/src/api.ts:174, 484-495TemplateConfigValidation + validateTemplateConfig() are defined but never referenced anywhere in web/src (the UI uses previewTemplate instead). Wire it or drop it.
  • [consistency] web/src/api.ts:474-476 — the "category/name travels as two path segments, slash must NOT be encoded" comment sits above getProfileSchema (fixed path, no template arg) but describes getTemplateSchema below it. Move it.
  • [tests] App.tsx Alt+N renumbering — the flagged-as-risky shortcut shift has no keyboard test (only DOM order is asserted); one test that Alt+2 opens Profiles and Alt+3 opens Agents would pin it.
  • [tests] ConfirmModal typed reset-on-open and the delete/detail error paths (DELETE rejection → snackbar + row retained; detail-pane getProfile failure) are untested.
  • [conventions] docs/web-ui.md — the Features paragraph doesn't mention profile management; one clause would do (component detail stays in web/README.md).
  • [conventions] commit subject — 77 chars, over the ~72 norm; move the verb enumeration to the body.
  • [conventions] ProfilesPanel.tsx — the {/* Master list */} comment: "master-detail" is a standard pattern name, but the standalone "Master" could read as "List pane" per the project's inclusive-language convention.

Tests

63 new tests across 4 files, counts exact, and assertion quality is deliberately anti-tautological (ordering fixtures defeat both alphabetical and catalog-order coincidences; $&/$'/CRLF adversarial cases assert the actual old bugs; the byte round-trip pins ${VAR} through edit→PUT; the clone test asserts validated bytes equal the POST body; optimistic delete is proven independent of the refetch). The systematic gap is concurrency: the stale-response discard branch is never exercised anywhere, which is precisely where all the known and new race findings live — see the Important items for the two regression tests that should accompany the fixes. Secondary gaps: the CustomSelect rewrite, Alt+N mapping, ConfirmModal reset, and a few error paths.

Verification

Independent dynamic verification at this head (clean checkout):

  • ✓ VERIFIED — web suite: 16 files, 240/240 passed (after working around the known npm optional-deps issue for rolldown's native binding, unrelated to the PR).
  • ✓ VERIFIED — npm run build (tsc + vite) exit 0; standalone tsc exit 0; only the advisory 823 kB chunk-size notice.
  • ✓ VERIFIED — maintainer P1 defect chain end-to-end by code reading (early return at :333 leaves token valid → stale setPreviewcanCreate enables → createProfile persists the stale body), plus the two additional un-flagged instances of the same root cause (open-reset effect, template-deselect branch) now in Blocking.
  • ✓ VERIFIED — maintainer P2 at ProfilesPanel.tsx:270-276 as described.
  • ✓ VERIFIED — the extractFrontmatterName nit reproduced empirically (body name: returned when frontmatter lacks one).
  • ✓ Audited the remaining async paths in the diff: ProfileEditorModal's unguarded load is not a practical defect (conditional mount discards stale state); ProfilesPanel detail effect, MemoryGraphView, and useEventFollow use correct guard patterns.

Verdict

Request changes — maintainer @haofeif's CHANGES_REQUESTED stands at this exact head with the P1 data-integrity blocker unaddressed, and the fix should cover the whole token-invalidation class (the two additional preview-clear sites and the unguarded getTemplateSchema fetch), with regression tests, not just the two flagged lines. Everything else about the PR is in strong shape; once the race class is closed and the doc/CHANGELOG drift is patched, this is a clean approve.

Review findings on #692: the monotonic seq tokens guarding the debounced
search and preview fetches were bumped only when a new request was
issued, never when the reason for the in-flight request disappeared.
Every clear path therefore left the old token valid, letting a late
response re-land silently:

- template switch (preview effect early return): the previous template's
  render re-armed Create with its body while the pane showed only
  'Loading template schema...' (P1, silent wrong-document persistence)
- modal close/reopen and template deselect: same class, distinct triggers
- search-box clear: stale results restored under an empty box (P2)

Also adds the staleness token getTemplateSchema was missing (fast A->B
switch with out-of-order resolution left A's schema under B's selection).

Regression tests use gated promises to genuinely reorder resolutions --
the seq-discard branch was previously never exercised by any test. All
four new tests fail against the unfixed components (mutation-verified).
Suite: 244/244, tsc clean, build clean.
Review items on #692:

- Object/JSON textareas are controlled from the draft string: an
  uncontrolled defaultValue remounted EMPTY when Advanced collapsed and
  reopened while the draft silently persisted into the POST -- the user
  saw a blank field but the typed JSON still saved
- extractFrontmatterName matches the frontmatter block first and scans
  name: within it, so a 'name:' line in the markdown body can no longer
  pre-fill the profile-name box (same bounding rewriteFrontmatterName
  already used)
- CustomSelect gains its own suite (portal placement, portal-aware
  outside-click, flip-up/down positioning, scroll- and resize-to-close,
  invalid red boundary, disabled options). Writing it exposed a real
  defect: the resize path reused the scroll handler, whose contains()
  check throws on a resize event's window target, so the menu never
  closed on resize -- fixed with an instanceof Node guard
- web/README.md (Profiles page section, supporting-components table,
  project tree), CHANGELOG.md Unreleased/Added, and docs/web-ui.md
  Features updated per the repo documentation-maintenance rule

Suite: 255/255, tsc clean, build clean.
Remaining #692 review nits:

- drop validateTemplateConfig + TemplateConfigValidation (defined but
  never referenced; the UI uses previewTemplate) and move the
  two-path-segment encoding comment to getTemplateSchema, the method it
  describes
- pre-save validate transport failures fall through to the authoritative
  server-side validation instead of hard-blocking behind a phantom
  'Validation failed'; real 4xx findings still block (both modals)
- rename the 'Master list' comment to 'List pane' per the inclusive
  naming convention
- tests: Alt+2/Alt+3 keyboard mapping pinned beyond DOM order,
  ConfirmModal typed-confirmation reset on reopen, DELETE failure
  (error snackbar + row retained), detail load failure (list survives,
  reselect works), and the validate transport fall-through

Suite: 260/260, tsc clean, build clean.
@sujoydc

sujoydc commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

PR #692 — reviewer reply drafts

All fixes are in three new commits (no force-push): 2806cfd (blockers),
e070c3b (important items), 33d6477 (nits). Suite is now 260/260, tsc and
build clean.


Reply 1 — haofeif's P1 thread (ProfileCreateModal.tsx:333, stale preview persisted)

Fixed in 2806cfd, and you were right that the root cause was the token only
being bumped when a new request is issued. Rather than patching just this
early return, I closed the class: every site that clears the preview now
invalidates the token (the early return you flagged, plus the open-reset
effect and the template-deselect branch, which had the same latent path via
the modal staying mounted across close/reopen). getTemplateSchema also
gained a staleness token — it was the one fetch in the modal with no guard,
so a fast A→B switch with out-of-order resolution could leave A's schema
under B's selection.

Your probe scenario is now a regression test: template A's render released
only after switching to B (B's schema held in-flight), asserting no A content
surfaces, Create stays disabled, and no POST can carry A's body. All four new
race tests use gated promises to genuinely reorder resolutions, and all four
fail against the unfixed components.


Reply 2 — haofeif's P2 thread (ProfilesPanel.tsx:272, stale search restored)

Fixed in 2806cfd with exactly your snippet (searchSeq.current++ in the
clear branch). Regression test holds the search response in flight, clears
the box, then releases it — the full catalog must stay, with the box empty
and no error left behind.


Reply 3 — haofeif's nit thread (extractFrontmatterName body match)

Fixed in e070c3b using the bounding you suggested: match the frontmatter
block first, then scan name: within the captured group — same shape
rewriteFrontmatterName already used. Your decoy document is now a test
case (body name: decoy with no frontmatter name → null), plus a guard that
the normal case still resolves.


Reply 4 — top-level reply to haofeif's review

Thanks for the depth here — reproducing every claim in the PR body and then
finding the one class the self-review missed is exactly the review this
needed. Both defects are fixed in 2806cfd, and since the root cause was
systemic (token bumped only on request issue, never on invalidation), the fix
covers every clear site plus the unguarded template-schema fetch, not just
the two flagged lines. Details and regression tests in the inline replies.

Follow-up commits e070c3b and 33d6477 address the rest of the review
feedback (JSON-draft display desync, bounded name scan, CustomSelect test
suite, docs/CHANGELOG sync, dead API surface, error-path tests). One thing I
can't fix without rewriting pushed history: the 77-char subject on the
original commit — happy to have the squash-merge message shortened at merge
time.


Reply 5 — top-level reply to gutosantos82's review

Thanks — the systemic framing was the right call. All three blocking sites
are fixed as a class in 2806cfd: invalidatePreview() (token bump + timer
clear + loading reset) runs at the open-reset effect, the deselect branch,
and the preview effect's early return, and getTemplateSchema now carries
its own staleness token. The two regression tests you specified are in
(gated promises, reordered resolution, discard branch actually taken), plus
a close/reopen test and an out-of-order schema test — all four fail against
the unfixed components.

On the important items (e070c3b): the JSON textareas are now controlled from
the draft string (your display/state desync repro is a test), and CustomSelect
has its own 9-test suite — which promptly caught a real bug: the resize path
reused the scroll handler, whose contains() throws on a resize event's
window target, so the menu never actually closed on resize. Fixed with an
instanceof Node guard. README (all three enumerations), CHANGELOG, and
docs/web-ui.md are synced.

Nits (33d6477): dead validateTemplateConfig dropped, encoding comment
moved to the method it describes, "List pane" rename, validate transport
failures now fall through to the authoritative server-side validation (real
4xx still blocks), and tests for Alt+2/Alt+3, ConfirmModal reset-on-open,
DELETE failure, and detail load failure. The commit-subject nit I'll leave
to the squash-merge message, since the original commit is already pushed.

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.

[Feat] Web UI: profile management surface — search, create, edit, delete, validate

3 participants