@tiptap/markdown only parses markdown for initial content / insertContent,
never for the clipboard — so pasting block markdown (headings, lists,
tables, quotes, fences) landed as literal text while only StarterKit's
inline mark paste-rules fired, and `_`/`__` formatted on paste even though
load keeps them literal. Add a MarkdownPaste extension that routes
plain-text pastes through the same faithful markdown layer as load, so the
two are consistent. Rich sources keep ProseMirror's own fidelity: an
in-editor copy (data-pm-slice) and web/Office HTML (block tags) fall
through to the default path, and pastes inside code stay literal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the 10 round-trip editor files (markdown-editor, its extensions, the
marked layer, inline-scan, tag/variable highlights, view-mode toggle, and
their tests) from components/shared/ into components/shared/markdown-editor/.
Consumers now import from the folder path; internal cross-references stay
relative. The read-only markdown viewer (markdown.tsx) stays put — it is a
react-markdown renderer, not part of the editor. Corpus test walks up one
more level to reach the backend prompt fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tool prompts (and agents without a human template) rendered a lone
"System Prompt" tab, which looks unbalanced. Always render both tabs and
disable the Human tab when the prompt has no human template
(hasHumanPrompt = agent && hasHuman), so it greys out at 50% opacity
instead of vanishing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same light-mode collapse as the view toggle: an unconditional
bg-background track + bg-card active render as white-on-white in light
(background == card == popover), hiding the track. Scope both to dark
(dark:bg-background, dark:data-[state=active]:bg-card) so light falls back
to the default bg-muted track + bg-background active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dark-only restyle set an unconditional bg-background track + bg-card
active, which collapse to the same white in light mode (background ==
card == popover), so the segmented track vanished. Scope the raised
treatment to dark (dark:bg-background track, dark:data-[state=active]:bg-card)
and let light fall back to the default bg-muted track + bg-background
active. Same fix on the Theme picker. Also silence the hover/focus
highlight on the View row so it reads as a control host, not an action —
matching the Theme row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the "View" row to the bottom of each actions dropdown — directly
above Delete when a Delete row exists, otherwise last. Swap the segment
icons to SquareMenu (rich editor) and Type (raw source), and give the
segmented control a dark-friendly treatment (bg-background track with a
bg-card active segment, matching the System/Human prompt tabs) so the
active segment reads as raised on dark. Apply the same treatment to the
Theme picker in the sidebar for consistency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the ad-hoc per-surface view-mode enums (`code`/`plain`,
`visual`/`plain`) and dropdown-item toggles with one shared
`EditorViewMode = 'raw' | 'rich'` type and an `EditorViewModeToggle`
segmented control (matching the Theme picker in main-sidebar). Applied
across all three editing surfaces: prompts, templates, and knowledge.
Rich is the default everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marked's url/autolink tokenizers wrapped a bare https://…, <url> or email in
[text](text) on save — a content-changing rewrite of prompt/template/knowledge
prose. Neutralize both (same pattern as the html/tag/del/emphasis tokenizers);
explicit [link](url) and  still work via the untouched link
tokenizer. Pinned by byte-identity tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Image toolbar passed window.prompt output straight to setImage; unlike the
link path, the Image extension does not validate the protocol, so a
javascript:/data:text/html src was stored. Not active XSS (the viewer strips it),
but a stored-content hygiene gap. Reject non-http(s)/non-image-data URLs before
setImage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The corpus fidelity check compared word Sets, so a count drop / duplication /
reorder passed as long as each distinct word survived once. Assert the sorted
word multiset instead (the real guard the reviews flagged as degraded). Keep the
existing <=2-save convergence check — some templates canonicalize over two saves,
so save1 === save2 would be stricter than the codebase's contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The promptInfo-sync effect unconditionally reset BOTH the System and Human forms
on every promptInfo identity change, and neither useForm set keepDirtyValues. A
Save refetches settingsPrompts → promptInfo gets a new identity → the effect fires
→ the inactive tab's unsaved edits were silently wiped. This is the exact M3
data-loss class already fixed in template.tsx/knowledge-form.tsx, left unfixed in
this sibling. Add resetOptions.keepDirtyValues to both forms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marked's emphasis tokenizer fires on `_`, so `__init__`/`_word_` parsed to
strong/em marks and re-serialized as `**init**`/`*word*` — the underscores were
gone by serialize time (a serialize-side escape can't recover them). Two
external reviews reproduced this at DB level on real knowledge content. Neutralize
the `_` case of marked's emStrong tokenizer (defer `*`/`**` to the default, which
the toolbar emits) so Python dunders, snake_case and `_`-wrapped prose round-trip
verbatim; `*`/`**` emphasis is unaffected. Pinned by byte-identity tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Post-review comment hygiene: drop the editor-highlight-regex test header that
restated the describe/it names; drop the diffStyles "Tailwind vars" comment (the
satisfies type + name already convey it, and it mislabeled the one hard-coded
green); trim the knowledge-form performSave comment to its load-bearing half (the
backend trims/normalizes). The (M1) test-name tag was dropped with the
single-tilde test rename.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marked's GFM del tokenizer accepts a single ~text~ as strikethrough, so prose
like "from ~5~ to ~10~" parsed into <del> and re-serialized as ~~5~~ —
corrupting ranges/paths in prompt/template/knowledge content. Neutralize the
single-tilde case in createFaithfulMarked (defer real ~~ to the default
tokenizer, drop a lone ~…~ to literal text), symmetric with the html/tag
neutralization already there. Double-tilde ~~strike~~ (the toolbar feature) is
unaffected.
Pinned by byte-identity tests for ~x~ pairs; the 366+39 corpus still converges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The M3 unsaved-changes guard trapped the user after a successful CREATE: a new
template's form stays dirty (performSave reset only on the update branch) and
handleSubmit navigated to the list without telling the guard the navigation was
intentional, so useBlocker intercepted it and popped the "Unsaved changes"
dialog right after the template was saved. Move handleSubmit below the guard and
call guard.skipNextBlock() before navigate (mirroring knowledge-form).
Live-verified side-by-side: before — create stuck on /new with the dialog;
after — create lands on the list, no dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop comments that restate the code or narrate a test assertion: the value-sync
intro (the skip-echo block below already explains it), the two PM descendants
return-value notes, the marked-cast type trivia, the 6-line aria-label essay,
the corpus-test harness/per-assertion narration (the it() title already lists
the checks), and two table/nesting assertion labels. Tightened the knowledge-form
"server's canonical document" note to its load-bearing line.
Kept the genuinely load-bearing ones the pass also flagged: the performSave
navigation/cycle warning (removing it invites reintroducing the guard cycle) and
the test "sanity" assertions (they prove the split/hard-break case is exercised).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop two-panel resizable shell (outer flex, ResizablePanelGroup, left
45/30 panel + scroll + Card, GripVertical handle, right 55/30 panel) was
copy-pasted byte-identical across knowledge-form-layout, template,
settings-prompt and settings-provider. Extract a presentational
DetailTwoPanelLayout({left, right, rightClassName?}); the only per-page variance
is the right pane's inner className, exposed as an optional prop. Net -44 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Plain/Visual DropdownMenuItem sat inside the `canShowActions` (!isNew)
block, and the dropdown trigger itself was gated the same way, so a brand-new
knowledge doc was locked to the visual editor until first save — inconsistent
with templates, where the toggle is outside that gate. Move the toggle out of
`canShowActions` (Rename/Delete still require a saved doc) and show the trigger
whenever a toggle handler is supplied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diffStyles is a ~40-key constant (only literals + CSS var strings, no closure
over props/state) that sat in the component body. Lift it to module scope so it
is defined once, with a `satisfies` clause for the ReactDiffViewer styles type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page reset({text,title}) in an effect on every templateData change. Since
UpdateFlowTemplate returns the flowTemplateFragment, an inline title rename
normalizes the Apollo cache → the FlowTemplate query re-emits → the effect
refired → reset() clobbered the user's unsaved editor body. Switch to the
knowledge-form pattern: reactive `values` + resetOptions.keepDirtyValues, which
re-syncs server changes while preserving in-flight edits; delete the manual
reset-in-effect. Also add useUnsavedChangesGuard + UnsavedChangesDialog (the
page was the only detail page without navigate-away protection) and switch
mode onChange→onTouched (no full-schema zod on every editor keystroke).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
variableUseRegex built `{{[^{}]*?\.Name\b[^{}]*?}}` with two lazy spans; on an
unclosed `{{` with many `.Name` anchors it backtracked O(n²) (measured 25KB 49ms
/ 100KB 782ms / 250KB 4.9s — a multi-second main-thread freeze from one
variable-panel click). The panel COUNT path already extracted `{{…}}` blocks
linearly via VARIABLE_RE then probed each; apply the same block-first shape to
the two CYCLE consumers (findVariableOccurrences for the editor, the plain-mode
textarea cycle in settings-prompt) via shared findVariableUseRanges/variableProbe.
Same inputs now 0.16/0.21/0.48ms (~10000x at 250KB). Also escape the interpolated
variable name (a `.` in a name would otherwise match any char). Drop the dead
variableUseRegex; re-add InlineMatch.text so the doc scan can probe per block.
Pinned by a linear-time regression test on the pathological input + an escape
test; de-stales the editor-highlight-regex test header (scan is per-textblock
since 69f86ae, not per-text-node).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
faithfulEscape backslash-escaped every ~, but in GFM only ~~ is strikethrough —
a lone ~ is literal. Escaping it injected a stray \ into prompt prose (e.g.
~10%/~30% in generator.tmpl + refiner.tmpl), and since the editor's getMarkdown
output is the Go text/template stored and sent to the LLM, the \ shipped to the
model. Escape ~ only in runs of 2+ (a balanced ~~strike~~ is handled by the
Strike mark upstream and never reaches this path); backtick/backslash unchanged.
Pinned by byte-identity tests for lone tildes (the corpus test asserts only
word-multiset + convergence, so \~ slipped through).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Boolean presence flags use the has* prefix per the project naming convention.
showToolbar (MarkdownEditor) had no external callers; showLabel had one
(knowledge-form-layout) — renamed both together to stay consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-ups:
- knowledge-form.tsx: drop the DirtyFlags comment (restates the type) and the
"backend also wipes subtypes" parenthetical (defends a redundant path).
- markdown-editor-extensions.test.ts: drop the roundTrip narration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-ups on markdown-editor.tsx:
- drop onChangeRef/onBlurRef + their sync effect: tiptap invokes onBlur/onUpdate
via its own live options ref (`mostRecentOptions.current = options` every
render, @tiptap/react 3.27.1), so the closures already see the latest props —
call onChange/onBlur directly.
- memoize createMarkdownExtensions(placeholder) so it isn't rebuilt per render.
- trim three paragraph-length ref comments + the resetUndoHistory tail to their
load-bearing kernel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
collectInlineMatches derived `to = from + match.length`, which undershoots
when a non-text inline node (a hard break from Shift+Enter) sits inside a
{{...}} or <tag> token: the highlight decoration and the cycle/select then
land one char short (off by the node's size). Read `to` from the per-character
position map instead. View-only — getMarkdown() was already byte-identical.
The module comment asserted the opposite (false) invariant; corrected. Test
inserts a hard break inside a token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A {{.Var}} (or <tag>) split across text nodes by a mark — e.g. a user
styles one brace — was missed by the per-text-node scan: the cycle then
inserted a duplicate while the panel still counted it used, and the
highlight silently dropped on the fragment.
- new collectInlineMatches (editor-inline-scan.ts) scans each textblock's
concatenated inline text and maps offsets back to doc positions, so a
split token reunites in one block string.
- VariableHighlight, TagHighlight and findVariableOccurrences all use it.
- share one variableUseRegex + VARIABLE_RE between the editor and
settings-prompt (countVariableUses + the plain-mode cycle), replacing the
hand-synced duplicate regexes.
- test: a brace-styled {{.Foo}} is now found (was 0 before).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
insertAtCursor dispatched scrollIntoView before view.focus() — the reverse
of cycleToVariable — so the first post-load variable insert (fired from a
button outside the editor, before it is focused) would not scroll the
inserted text into view. Reorder to match cycleToVariable.
Also drop the unused getEditor() and focus() from MarkdownEditorHandle:
the only callers are cycleToVariable + insertAtCursor (verified by grep).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Available variables" panel could insert {{.X}} but, in the rich
(tiptap) editor, could not jump to an existing use the way the plain
textarea already did — the used-highlight + go-to-next were plain-only,
which read as a regression.
- cycleToVariable on MarkdownEditorHandle: finds {{.var}} occurrences in
the doc and selects + scrolls to the next one; returns false when there
are none so the caller inserts instead (cycle-or-insert contract).
- findVariableOccurrences (editor-variable-highlight.ts): the doc scan,
position-mapped to match the VariableHighlight decoration.
- panel isUsed = count > 0 in BOTH modes; handleVariableClick cycles when
the variable is used, inserts when not.
- focus before scrollIntoView: ProseMirror no-ops scrollToSelection on an
unfocused view, so the first post-load click would otherwise not scroll
(looked like a lost/double click).
- perf: the value-sync effect re-serialized the whole ~24KB doc twice per
keystroke only to no-op on self-echo; early-return when value is our own
last-emitted output (mount + real external form.reset still sync).
- tests: findVariableOccurrences span/word-boundary/unused cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tiptap/markdown pins marked ^17 while our direct dep (report-pdf + the
editor's parser instance) is ^18, so two marked copies were installed and
the marked config needed an `as never` to bridge the version skew.
Add `overrides: { marked: ^18.0.5 }` so @tiptap/markdown resolves onto v18
too — one copy. Behaviourally a no-op for the editor (it already passed a
v18 Marked instance to the MarkdownManager; only @tiptap/markdown's unused
default + types change, and the lexer API is identical 17↔18). Narrow the
createMarkdownLayer cast from `as never` to a typed cast now that the
versions match.
Verified on the forced v18: tsc, 747 vitest, lint, production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-dimension review found no bugs, leaks, or security issues; these are
the confirmed robustness + comment nits:
- resetUndoHistory: match the history plugin by PluginKey identity (a fresh
history() shares prosemirror-history's module-level singleton key) instead
of sniffing the undocumented stringified `history$` name — so an upstream
change fails loudly instead of silently turning the reset into a no-op.
- MarkdownTable: narrow `pipeEscaping as never` to
`as Parameters<typeof renderTableToMarkdown>[1]` so a future signature
change is caught at compile time rather than swallowed.
- Trim restating/duplicated comments in markdown-editor.tsx and
editor-markdown.ts; kept the genuine framework gotchas (reconfigure trap,
onBeforeCreate timing, setContent contentType, pipe re-parse).
tsc, lint, 747 vitest green; verified history() spec.key is a singleton.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the unified rich editor's markdown engine: drop the community
tiptap-markdown (markdown-it parse + prosemirror-markdown serialize) and
our 5 prosemirror-internal monkey-patch extensions for the official
@tiptap/markdown (marked-based MarkdownManager). The consumer moves to the
new API (editor.getMarkdown() / contentType:'markdown' / markdown.parse).
editor-markdown.ts adds three small, supported-API customizations:
- a private HTML-neutralized marked instance so literal <xml-tags> survive
(marked otherwise swallows real-HTML-element names like <input>);
- FaithfulMarkdownText overrides MarkdownManager.encodeTextForMarkdown to
drop entity-encoding and over-escaping of literal punctuation;
- MarkdownTable wraps renderTableToMarkdown to escape cell pipes (#7884).
Verified: tsc, 747 vitest (rewritten extension + 39-prompt corpus tests),
lint, build; live on the dev stand — knowledge tables render and resize,
prompt <tags> stay literal (124 tag + 116 variable highlights, no
entity-encoding), console clean.
Known accepted bug, pinned by a test (marked parser, 0 corpus impact):
a code block nested ordered-list > bullet-sublist > code is dropped on
parse. Repro recorded for an upstream report.
Folds in the in-progress unified-editor work it depends on: knowledge
Plain/Visual toggle, settings-prompt and template editor wiring, the
CodeMirror removal, and the editor table/tag/variable styles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
templates/{id} now mirrors knowledges/{id} and settings/prompts/{id}: a left
ResizablePanel (intro + title + the Presets panel, in the spot where the prompt
page shows "Available variables") and a right panel holding the Code/Plain
editor that fills the space. Save moves to the header (FormSubmitButton
form="template-form"); the right-side Presets sidebar/Sheet, the PanelRight
toggle, the in-input save button, and the Enter-to-submit handler are removed.
Live-verified on the stand: 2-panel render (create + edit), preset apply,
create->DB->reload byte-fidelity (incl. the blank line, reconstructed from
.cm-line), the Code editor filling the right panel, delete. tsc/eslint/build
+ 652 frontend tests green; console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- providers: prove a stale user-provider row does not knock out a valid USER
sibling (TestGetProviders_StaleUserRowSpansValidSibling — ollama builds keyless
so it survives beside a skipped minimax; side-by-side verified).
- settings-provider: cover the create-form ?type=/?id= guards (disabled/unknown
type and clone-of-disabled bounce to the list; an enabled type renders). These
had zero coverage — a swap/drop-return regression would have shipped green.
- knowledge-form: cover performSave's server-document reset branch (untouched
fields reflect the returned document under keepDirtyValues), and the useBlocker
"Save and leave" path via a real data router (proceeds the blocked nav, does
NOT honor a CREATE redirect). Swap the negative no-navigate assertion's
setTimeout(0) flush for a deterministic Save-disabled anchor.
Each test mutation-verified to fail on the reverted production code; 652 frontend
tests + go test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From a strict re-review of the recent commits (no blocker/high/security found):
- providers.go GetProviders: the skip covers ANY unbuildable saved provider, not
only a disabled type — drop the misleading "of unavailable type" wording and
lower the line to Debug (it re-fires on every providers fetch; WithError keeps
the reason).
- settings-prompt countVariableUses: drop the redundant seed-in-map side effect;
the component already falls back to `?? 0` for unused variables, so the loop's
own `?? 0` is the only seed needed.
- settings-providers create menu: render a disabled "No available provider types"
placeholder instead of a silently-empty menu (loading / no-keys / failed-query
states), with a test covering it.
Counts live-verified unchanged (AgentType 1->3 on the stand); go test + 646
frontend tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to b6d1036. Cloning an existing provider whose type is now disabled
(?id=) would have produced another dead provider — apply the same enabled-check
on the clone path. Adds a Vitest covering the create menu's enabled-only filter
(exports SettingsProvidersHeader for the render test; eslint sort-modules then
reorders it above the page component — declaration hoisting, no runtime change).
Live-verified: clone of the disabled minimax (?id=3) redirects to the list;
clone of bedrock (?id=2) still opens with a "(Copy)" name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A user could create a provider of a type whose API key isn't configured: it
saved fine and showed in settings, but was dead for flow creation with no
signal (and used to break the whole providers query — see 08a24c9). Two layers:
- the "Create provider" menu now only offers types whose key is set
(settingsProviders.enabled, already fetched by the page), and
- the create form bounces a hand-typed ?type= that is unknown or disabled to
the list, closing the direct-URL bypass of the filtered menu.
Frontend-only. Live-verified on the docker stand: minimax/custom drop from the
menu (11 -> 9); ?type=minimax and ?type=garbage123 redirect; ?type=anthropic
still opens the form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The knowledges feature had zero coverage while 01fa02d reworked its save/guard
logic. Add Vitest tests for the exported pure mappers (create/update inputs —
including the dirty-gated "" vs undefined distinction — and the zod
docType->subtype superRefine) and the component wiring (create -> navigate to
redirect, update -> no navigate, save-disabled-until-dirty, scoped
anonymize-disabled). The useBlocker dialog path stays covered by manual/live
testing (it needs a data router the component tests intentionally stub out).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A saved user provider whose type is no longer enabled (e.g. its API key was
removed) made GetProviders return an error for the ENTIRE `providers` query —
one stale row blocked all flow creation in the UI ("No available providers").
Skip and log such rows, mirroring how startup already tolerates disabled
default providers. Pre-existing robustness gap, not introduced by this branch.
Verified live on the docker stand: the `providers` query went from a hard
error to returning all 10 enabled providers (the stale minimax row skipped),
and flow creation works again. Adds a side-by-side regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Available variables" badges recomputed a per-variable `.match` over
the whole template on every keystroke (O(variables × length)); fold them
into a single pass over the `{{ … }}` blocks. Tighten the action regex to
`[^{}]` so an unclosed `{{` cannot drive quadratic backtracking — closes
the self-DoS flagged by the security review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the function-behavior restatement; keep the actionable invariant (navigation
stays with the caller, else the guard↔onSaveFromDialog↔performSave cycle returns).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From .cursor/rules/CODE-REVIEW-prompt-provider-commits.md (settings-prompt, code-editor):
- rename abbreviations currentIdx->currentIndex, pos->position
- drop the cycle-ternary restatement comment
- gate the green "go to occurrence" badge affordance to plain view (code view only inserts)
- move code-editor's byte-faithful note into an actionable JSDoc warning on the component
Cross-component uniformity: collapse the four scoped-useWatch wrappers (DiffContent,
VariablesPanelContainer, KnowledgeFormHeader, DeleteProviderDialog) to one consistent
"Don't hoist this useWatch..." warning, dropping the duplicated narration + provenance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The provider `name` field was watched at the top of SettingsProvider but only
fed the delete dialog's itemName, so every keystroke re-rendered the whole form
tree (Accordion + 12 agent configs + resizable panels). Move the watch into a
small DeleteProviderDialog wrapper that subscribes on its own. Measured: typing
10 chars now re-renders SettingsProvider 2x (the one-time isDirty flip) instead
of once per keystroke; the per-keystroke re-renders land on the tiny dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Code/Plain toggle in the ⋯ menu swaps the autosize textarea for the
lazy-loaded CodeMirror CodeEditor, mirroring the prompt-template editor, so
flow templates with {{VAR}} placeholders edit byte-faithfully (no markdown
normalization). The toggle is available for new templates too; the editor
nests in the existing InputGroup shell so the in-input save button is shared
across both modes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing in the knowledge markdown editor re-rendered the whole KnowledgeForm on
every keystroke: form.watch('content') — used only to toggle the anonymize
button's disabled state — subscribed the top-level component, dragging the form
body, layout, and metadata fields through a re-render per character. Measured on
a multi-KB document: max input processing 58ms -> 21ms, keystrokes over 25ms
20 -> 0, long tasks 1 -> 0.
- Move the content subscription into a small module-scope KnowledgeFormHeader
wrapper (scoped useWatch), so only the header reacts to typing; the form body,
layout, and editor stay put. Mirrors the existing pattern in
knowledge-form-controls.tsx.
- Removing the watch let the React Compiler optimize KnowledgeForm, which then
flagged the skipNextBlockRef latest-ref (react-hooks/immutability). Rather than
suppress it, untangle the performSave<->guard cycle the ref existed to break:
performSave now returns the result and the *caller* owns navigation. The form
Save button (defined after the guard) does the CREATE redirect via the stable
skipNextBlock; the unsaved-changes dialog's "Save and leave" saves and lets the
guard proceed the navigation the user initiated. No eslint-disable, no memo.
Behavior note: on CREATE, the Save button lands on the new document; the
"Save and leave" dialog now lands on the destination the user was navigating to
(it previously raced between the two). Verified live on the local stack:
create/update via button and dialog, delete on a dirty form, anonymize toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SettingsPrompt called form.watch('template') on both forms at the top level, which
subscribes the whole ~1000-line component to re-render on every keystroke (header,
tabs, panels, meta, all four dialogs). Move the live-template reads into small
useWatch children so only they re-render:
- VariablesPanelContainer wraps the variables panel (needs the live ×N counts).
- DiffContent wraps the diff viewer — live while the dialog is open, and Radix
unmounts it (and its subscription) when closed.
- handleValidate and the variable-insert callback read form.getValues() at call
time instead of the watched vars; the diff dialog's derived currentTemplate is
dropped.
Mirrors the existing knowledge-form-controls.tsx pattern. Verified with render-count
instrumentation that the parent no longer renders per keystroke (only the scoped
children do), and that the live count, variable insert/cycle, validate, diff (live
while open), reset, code-editor insert, and dual-tab dirty flows are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prompt templates are Go templates with markdown tables, <xml-tags>, and
significant whitespace — content a markdown (Tiptap) editor normalizes and
corrupts on round-trip. Add a CodeMirror-based view instead, toggled from the
prompt page actions menu, that edits the raw template verbatim.
- New shared CodeEditor (CodeMirror 6 + markdown highlighting, line wrapping,
theme-aware), lazy-loaded so its chunk only loads when code view is opened.
- "Code editor" / "Plain text" toggle in the prompt actions menu.
- Available-variables click inserts {{.Var}} into the code editor at the caret.
- Fix a pre-existing tsc error in the variable-cycling code
(noUncheckedIndexedAccess on the matched-occurrence lookup).
Round-trip verified byte-exact end to end: original -> code edit -> plain, and
save -> DB -> reload, all identical to source (tables, tags, whitespace intact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking an in-use variable in "Available variables" jumped only to the first
occurrence. Now it cycles: from the occurrence the caret is on it advances to
the next (wrapping past the last), else jumps to the first at/after the caret.
Each badge also shows a ×N count when the variable is used more than once.
Verified live (vite dev → docker backend) on the adviser prompt: a ×7 variable
steps through all 7 occurrences in order and wraps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FormModelComboboxItem manually glued an Input and a dropdown Button by
stripping adjacent borders/corners and juggling z-index on focus/hover.
Replace that with the project's InputGroup primitive (single border, ghost
trigger, native group focus-ring) — matching InputPassword and the ~20 other
input-group usages. Also gives the icon-only trigger an accessible name.
Verified live (vite dev against the docker backend): input editable, dropdown
opens via the asChild trigger, option/price autofill intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
anthropic, bedrock, and gemini also carry per-agent prices in config.yml but
were not covered. Adding them confirms no current drift and guards them going
forward. bedrock's loaders take a *config.Config, so they're wrapped with an
empty config to read the embedded catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>