The generative oracle shipped green over the escape-tokenizer bug: its atoms
held only backslash-before-letter (which survives), and both oracles are too
weak for a first-load loss — survives/includes never ran on the missing class,
and convergence passes because the drop happens once then stabilizes. Add
backslash-before-punctuation atoms to the generative pool and a direct
byte-exact source-fidelity assertion for the escape class (regex/glob/UNC/
line-leading star), which is strictly stronger than survives+converges.
Fails on the pre-fix escape-active parser, passes on the neutralized one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createFaithfulMarked neutralized marked's del/emStrong/html/tag/autolink/url
tokenizers but left `escape` active, so marked dropped the backslash before any
CommonMark punctuation on the first load — corrupting regex `\.`, globs `\*`,
UNC paths `\\`, escaped `\[`/`\|` in real knowledge docs. A serialize-side
re-escape cannot recover a byte already gone from the doc model.
Neutralize `escape` too, so a source `\.` stays literal `\.`. With no escape
decoded on parse, the serialize side must emit text verbatim rather than
re-escaping (which would resurface as a literal backslash on the next load), so
encodeTextForMarkdown collapses to identity — load and save stay byte-symmetric.
Verified over the 405-sample corpus (39 templates + 366 knowledge): backslash-
punct loss 5 samples -> 0, byte-identical 104 -> 106, word-loss unchanged; 15
adversarial inline cases and table pipe cells round-trip clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exports resetUndoHistory and drives it with a synthetic plugin that advances
view.state during the reconfiguring updateState — reproducing the exact
"Applying a mismatched transaction" RangeError in jsdom. Fails on the pre-fix
newState.tr, passes on the live-view.state.tr fix, so a future revert can't
silently reintroduce the production-only crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resetUndoHistory dispatched newState.tr after view.updateState(newState).
updateState can synchronously advance view.state (a plugin view or tiptap's
deferred initial-content transaction firing during reconfigure), leaving the
stale tr's before-doc mismatched against the current doc — ProseMirror then
throws "Applying a mismatched transaction". This crashed the editor detail
page for ANY document containing a list, but only in the production build
(React StrictMode's dev double-mount masked the timing). Build the wake-up
transaction from the live view.state instead, whose before-doc is the current
doc by construction.
Live side-by-side verified on the docker prod build: the same list document
that hit the error boundary now renders; full torture doc keeps all atoms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a seeded generative round-trip test (300 random docs combining every
content atom — dunders, {{.vars}}, <tags>, regex/path backslashes, HTML
entities, C++ — across paragraph/heading/list/quote/inline-code contexts):
asserts convergence and that no atom is dropped, catching combination-only
regressions the per-atom byte tests can't. Also correct the ordered>bullet>code
pinned-bug comment: marked keeps the code token; the drop is in
@tiptap/markdown's list reconstruction, not the marked parser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tiptap/markdown runs a module-scope decodeHtmlEntities on every text token
during parse, so a pentest doc that stores literal `<script>` (teaching
the encoded form) loaded as raw `<script>`, and `AT&T` as `AT&T`. Its
`.lexer()`/`.inlineTokens()` path never routes through marked's walkTokens or
hooks, so add a private-instance inline extension that emits each `&` as its
own text token pre-encoded to `&`; the decode then nets back to the
original byte, keeping the entity literal. Code tokens are untouched (decode
skips them) and literal `<tags>` still pass through unencoded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three StarterKit defaults corrupted content or diverged from the load path:
- Underline (`underline: false`): its `++text++` markdown tokenizer collapsed
the space in `C++ then C++` on load, and Ctrl+U emitted non-standard `++`.
No underline semantics in our content, so drop it outright.
- Bold/Italic underscore input+paste rules: typing `__init__`/`_word_` created
emphasis (`**init**`/`*word*`) while the same text loaded stays literal (the
marked layer neutralizes `_`), breaking identifiers on the typing path only
(input rules don't run on paste/load). Extend StarterKit to drop the
underscore rules (regex mentions `_`), keeping the `*`/`**` rules.
- Link autolink/linkOnPaste (`false`): a typed/pasted bare URL now stays
literal, matching load; explicit [text](url) and the toolbar still work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
faithfulEscape backslash-escaped every `\`, so a save doubled regex classes
(\d -> \\d) and Windows paths (C:\Users -> C:\\Users) — 210/366 knowledge
docs carry such content. marked only treats `\` as an escape when it
precedes a CommonMark-escapable punctuation char; a `\` before a
letter/digit/space is literal. Escape a `\` only in that punctuation case,
leaving content backslashes verbatim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the paste decision into a pure `shouldParseMarkdownOnPaste` and
test all branches (plain text parses; empty, in-editor copy, block-tag
HTML, and code-context defer; styled-inline HTML still parses). Plus
integration over the real handler: pasted block markdown becomes rich
blocks, `_`/`__` stay literal like load, and rich block HTML defers to
ProseMirror.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The folder move nested the corpus test one level deeper, so its relative
walk to backend/pkg/templates/prompts needs one more `..`. The fix landed
in the working tree (tests were green locally) but missed the move commit's
staging, so HEAD carried the pre-move path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@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>