fix(ai-settings): real model dropdown + Ollama auto-pick installed model#55
Conversation
The model picker used an `<input list>` + `<datalist>` combo, which WebView2 filters by the current input value — when ai.model didn't substring-match an installed tag, the dropdown appeared empty and unclickable. Replace with a Radix Select populated from the live catalog merged with installed Ollama tags, keeping the text input as a fallback for custom ids. Also fix test-connection always failing on first Ollama use: the static default model `qwen3-coder` is rarely installed, so /api/chat returned 404. On provider switch and on URL change, probe Ollama and repoint ai.model to the best installed tag via pickBestModel before any test runs. Auto-fetch the provider catalog on switch (when no key is required or one is configured) so the dropdown is populated without needing to click "Fetch models" first.
…render <em> in hint The Trans component only registered <code/>, so the <em> tag in the model hint leaked through as escaped entities. Adding em to the components map renders it as italics again. The Select and the text input were two siblings of the .daisu-field flex row, which split the row into three columns and squeezed the label into one-word- per-line. Wrap them together in a constrained flex column so the right side behaves as a single field cell.
…l modal The Radix Dialog blocked the whole IDE every time the agent asked to run a tool, which broke the user's reading flow and obscured the conversation that triggered the request. Move the permission UI inline into the agent panel, anchored just above the composer, so the request sits next to the chat it relates to and the user can still read messages while deciding. PermissionModal.tsx now hosts two exports: the original PermissionModal (listener only — registers the Tauri event handler, returns null) and a new PermissionInline that reads the same store and renders the styled card.
Slow local models (qwen2.5-coder:1.5b on first load, large context windows) can keep generating server-side after the HTTP connection drops, so the backend Cancelled event arrives seconds late or not at all. Cancel now flips isStreaming/runId locally and marks the pending message as cancelled before calling cancelRun, so the user is never stuck staring at a non-responsive Stop button. The Tauri call is best-effort — if the run already finished, the swallow is harmless.
…-as-text tool calls Two bugs the chat panel surfaced when running against Ollama with qwen2.5-coder:1.5b: 1. The StreamPayload enum carried snake_case fields (run_id, conversation_id, message_id) because serde's enum-level rename_all only renames variants, not their fields. The frontend listener typed everything as camelCase, so payload.runId was undefined for every event. The runId-mismatch filter then silently dropped every delta after the first one and the UI sat on the pending placeholder forever even though tokens were streaming. Add rename_all_fields = camelCase so variant fields get renamed too. 2. Small Ollama models (1.5b/0.5b families) ignore the wire-level tools field and instead emit a JSON object describing the tool call inside a fenced code block. The agent loop saw no tool_calls, treated the turn as terminal, and the user got the raw JSON instead of an action. Parse fenced JSON bodies after the stream finishes and synthesise the same UI events the provider would have, validating the tool name against the registry so we never invent dispatches the user didn't ask for.
…system prompt Three fixes that surfaced testing the agent against small Ollama models: - Implement write_file (Prompt tier, 1MiB cap, creates parent dirs). - Drop the StubTool registrations for grep/find_files/git_status/git_diff/ delete_file/run_command. Advertising tools whose dispatch returns 'not yet implemented' just teaches small models to keep picking them. Wave 2 will register real impls; until then the model only sees the working subset (read_file, list_dir, write_file, propose_edit). - ToolRegistry::descriptors() now derives from the actually-registered tools instead of the global registry() literal, so callers can never advertise a name the dispatcher can't resolve. agent_send_message picks up tool defs the same way. - Inject a default system prompt when the frontend doesn't send one. Anchors the model to the workspace, says 'don't call tools for greetings', and gives a one-line guide for each tool plus path rules so 3b-class models stop passing '/' or absolute paths.
Default 5-minute idle unload made every turn after a pause pay a 2-6s model-reload tax for 7B-class models. Set keep_alive on every /api/chat request so the daemon's env var can't undercut us. Tune Options for code chats: num_ctx 8192 (up from default 2048 which silently truncates the system prompt + tool defs + history), num_batch 512 (~30% prefill speedup on RTX-class GPUs), top_k 40, top_p 0.9, repeat_penalty 1.05. Backed by the May 2026 research pass.
Bundles four related changes that share a single file:
1. Hoist history load out of iteration loop — read SQLite once before the
while iteration loop and mutate the in-memory buffer in lock-step with
persistence. Eliminates ~50ms x N redundant SQLite reads per tool chase.
2. Fallback parser robustness — extend extract_fallback_tool_calls to handle:
- bare unfenced JSON (balanced-brace scan respecting strings)
- Qwen3-coder <tool_call>{...}</tool_call> XML tag
- Llama 3.1 <|python_tag|>{...}<|eom_id|> shape
Returns FallbackParse { calls, cleaned_text } so the chat UI can render
the prose without the raw payload. New StreamPayload::ReplaceText event
tells the listener to swap the pending content with the cleaned text.
Eight unit tests covering each shape, dedup, brace-in-string, unknown
tools, and text-cleaning.
3. ChatMode (Auto / Chat / Agent / Plan) wired through SendMessageRequest:
- Chat strips tools entirely + tool_choice=None.
- Agent forces full catalogue.
- Plan keeps only read-only tools (read_file, list_dir).
- Auto preserves the conversational-opener heuristic. ToolChoice::None
is also set for Auto on conversational openers so the model keeps
tool awareness for follow-ups (Ollama still strips because it ignores
tool_choice).
4. Dual-variant default system prompt — long form (~550 tok) for cloud
providers, short imperative form (~210 tok) for Ollama / LM Studio.
Per-mode addenda appended at the prompt tail where small models attend
most strongly. Path rules + tool guide + few-shot included in long
variant only.
Stubs (grep / git_status / find_files / run_command / delete_file) stay
off the wire; ToolRegistry::descriptors() now derives from registered
tools so we never advertise names the dispatcher can't resolve.
- agentStore.chatMode persisted in localStorage with setChatMode action. - ChatPanel composer gets a 4-button mode picker (Auto / Chat / Agent / Plan) above the textarea with tooltips describing each. - ModelInlinePicker in panel header so users can swap provider+model without opening Settings. Reuses listProviders + listProviderModels + probeOllama; auto-fetches the catalog when a key is configured. - StreamPayload typed with new replaceText variant; listener swaps the pending message body wholesale when the backend strips a tool-call payload from the streamed text. - Daisu Nocturne-styled CSS for the new mode row + permission inline + composer-side inline picker. - en/ja i18n keys for mode labels + tooltips + provider/model picker labels.
i18n: switched es.json from rioplatense voseo (vos / abri / usa / elegi / queres / reindexa / ejecuta / cambia / confia / volve) to neutral Spanish (tu / abre / usa / elige / quieres / reindexa / ejecuta / cambia / confia / vuelve). 17 strings normalized. docs: README gains a Tuning local models (Ollama) section covering the OLLAMA_FLASH_ATTENTION=1 + OLLAMA_KV_CACHE_TYPE=q8_0 env vars and a recommended-models table per VRAM tier (3B / 8B / 16B / 20B+). Daisu already sets keep_alive 30m and num_ctx 8192 on every request so users don't need to touch those.
…rcion Hardens every tool dispatch against the most common LLM mistakes: 1. JSON-Schema validation. Compile each tool's input_schema once at registry build via jsonschema-rs. Validate args before execute; surface the JSON-pointer path of every violation so the model can self-correct next turn. Skip silently when a schema fails to compile. 2. Strict-ready schemas. Every in-tree tool now declares additionalProperties: false and lists every property in required. Prerequisites for OpenAI + Anthropic strict modes (grammar-constrained sampling at the API layer). 3. Tool-name normalization. Models emit read_file, readFile, read-file, tool.read_file, functions::read_file. Strip namespace prefix, split on case boundaries, lowercase, snake_case before registry lookup. 4. Windows path traps. resolve_within rejects verbatim namespace, device namespace, UNC paths, and reserved DOS device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9). Plus coerce_to_relative converts workspace-absolute paths to relative and forward-slash to platform sep on Windows.
…epair
Wave-1 provider plumbing for cache + strict mode:
1. ToolDef gains a strict bool. Anthropic and OpenAI tag tools with
strict: true so the API runs grammar-constrained sampling at the
token level. Gemini / Ollama / LM Studio ignore the flag silently.
The runtime defaults strict to true for in-tree tools and false
for MCP-injected tools whose schemas come from third parties.
2. TokenUsage carries cache_read_tokens + cache_creation_tokens. OpenAI
parses input_tokens_details.cached_tokens (50% discount on the
cached prefix, automatic above 1024 tokens). Anthropic parses both
cache_read_input_tokens + cache_creation_input_tokens. Gemini /
Ollama / LM Studio default to 0.
3. Anthropic prompt caching with the Continue.dev optimized strategy:
one breakpoint each on system, last tool def, last 2 user/tool
messages (4 total, the API max). Skipped under the per-model min
(Sonnet 4.6 at 2048, Opus 4.7 + Haiku 4.5 at 4096) so we never eat
a write that gets silently rejected.
4. Lightweight repair for truncated tool args on Anthropic streams.
When max_tokens cuts the JSON mid-string, balance the open quote
then the brackets/braces. Falls back to {} with a warning on the
stream when repair fails. Same path emits a recovery warning when
it succeeds, so partial argument data still reaches the model.
Two bundled changes that share the runtime file:
1. Imperative error feedback. Tool errors used to land in the model as
a JSON envelope ({"error": ...}). Small models attend to imperative
prose far more reliably than to blobs, so dispatch failures now
ship as TOOL_ERROR (tool): msg with a contextual HINT and the
instruction Try again with corrected arguments OR explain in plain
text. Permission denials surface as TOOL_DENIED with a hard rule
not to retry. repair_hint_for picks tool-specific hints from the
error text (escapes workspace, no such file, is a directory,
propose_edit no match, write_file too large).
2. dedupe_file_reads pass before each provider call. When the same
read_file(path) or list_dir(path) appears multiple times in
history, every Tool result before the latest is replaced with
a small NOTE deduplicated marker. Cuts tool-heavy conversation
history 30-60% on average without losing semantics — the latest
result is always preserved. Cline pattern; same file-context
tracker shape.
3. Tool-name normalization helper mirrored from dispatcher so the
fallback parser (extract_fallback_tool_calls) also coerces
readFile / read-file / functions.read_file before registry lookup.
Two new modules under daisu-agent::memory: - tokens: o200k_base BPE estimator wrapping tiktoken-rs. Used as a universal proxy across providers (Anthropic + Gemini are also BPE, drift ~10% — well inside our budgeting margin). The provider's official count_tokens endpoints add 100-300ms per call so they stay reserved for offline cost reports. count() / count_message() / count_messages() share a Lazy CoreBPE. - window: token-budget sliding window. Drops oldest until total tokens fit 75% of the model context, but always preserves the first 2 messages (anchors the original task) and cascades the drop to tool_use/tool_result pairs (Anthropic, OpenAI and Gemini all reject orphan tool_results). context_window_for() maps Claude/ GPT-5/Gemini/Ollama to their per-family ceilings.
read_file gains optional offset (0-indexed) + limit (default 2000, hard
cap 2000) args. Output is now an envelope:
{path, bytes, total_lines, shown_lines: [start, end], truncated, contents}
plus a hint string when truncated. Lines longer than 2000 chars are cut
mid-line so a minified bundle can't drown the model. Mirrors Claude
Code's read tool conventions.
list_dir caps at 200 entries with truncated:true + total + hint when
exceeded. Entries sorted dirs-first then alphabetical so the model gets
a stable scan-friendly order. The hint nudges the model to narrow the
path or use targeted search instead of paginating forever.
Two compaction passes added to the agent loop: 1. Sliding window: after dedupe_file_reads, slide the message buffer to 75% of context_window_for(model) before sending to the provider. tool_use/tool_result pairing and the first 2 anchor messages stay intact; the rest gets dropped from the head when over budget. 2. Per-turn cache: a HashMap<(name, args_canonical_json), ToolResult> wrapping the dispatch loop. Two parallel read_file calls with identical args in a single assistant turn now hit disk once. Cache resets between turns so tool results stay fresh on the next loop.
Honour the cross-tool AGENTS.md convergence (Codex, Cursor, Zed, Aider, RooCode, Goose, ~20 others) plus Claude Code's CLAUDE.md fallback. Reads the first present file from this order, trimmed, capped at 32 KiB: .daisu/AGENTS.md → AGENTS.md → CLAUDE.md → .cursorrules Appended at the system-prompt tail (after Daisu defaults + mode addendum) so small local models attend to it most strongly. Full rules system with frontmatter + globs + watcher + UI lands in M5; this minimal pass already gives users a one-file escape hatch without forking the agent.
Three new components feed the upgraded ToolBlockView:
- ToolStatusBadge: pending/running/done/errored/denied pill with
three-dot pulse, spinner, check, X, prohibit. role=status +
aria-live=polite so screen readers announce state transitions.
- StreamingJson: renders tool args buffer with a blinking caret while
the model streams partial_json deltas. JSON.parse on done for pretty
indentation; falls back to raw on parse failure (truncated stream).
- ToolResultRenderer: per-tool dispatcher.
- read_file: header with basename + lines + bytes + truncated pill,
code body with auto-collapse when >20 lines, hint surfaced when
output is paginated, range label for offset+limit visibility.
- list_dir: tree grid with Phosphor Folder/File icons, dir/file/total
counts, hint when capped at 200.
- write_file: minimal confirmation card with bytes written.
- errored: red panel with error / denied text extracted from envelope.
- default: pretty-printed JSON dump.
ChatPanel.tsx integrates the new tool components: - ToolBlockView replaces inline rendering: status pill, latency footer (completedAt - startedAt), one-line argument summary in the header (path / pattern / command), auto-expand while running and on errored results, auto-collapse on success. User toggle sticks once clicked. - StreamingJson + ToolResultRenderer wired into the body so result envelopes (read_file pagination, list_dir tree, write_file confirm) render natively instead of as raw JSON. - agent-messages container now declares role=log + aria-live=polite + aria-relevant=additions so screen readers announce tool progress and result additions. - agentStore.ToolBlock gains startedAt / argsAt / completedAt; the stream listener stamps timestamps on toolUseStart / toolUseDone / toolResult so latency is computable client-side. - styles.css: tool-badge with status colours + caret-blink + spin keyframes, streaming JSON panel, tool-result tree grid + code body with collapse toggle, error variant, hint + range labels. - i18n: tool status labels, truncated/show-all, and per-tool meta formatters for en / es (neutral) / ja.
|
@copilot review this entire pr |
Here's a full review of the PR. Overall the direction is right and the core fix (Radix 🔴 Bugs1. Hardcoded Spanish string 2. Comment/code mismatch in 3. Potential re-render loop in 🟡 Inconsistencies / Design Issues4. 5. Permission UX regression: 6. 7. 🔵 Minor / Style8. 9. IIFE inside JSX for the model dropdown 10. 11. |
Replace hardcoded "Cancelado" warning in agentStore with the i18n key
chat.cancelled (en/es/ja). Without this every user — English or Japanese —
saw a Spanish word the moment they hit Cancel on a streaming run.
Drop the truncated-hint paragraph from ReadFileResult and ListDirResult.
The Rust tools generate the hint as English prose for the model
("Showed X of Y lines. Re-call with offset/limit to see the rest."), it
isn't a user-facing string. The truncated pill plus the localized
"Showing lines X–Y of Z" label already convey the same information in
the user's locale.
…dden
The inline permission prompt only renders inside ChatPanel, so a
permission request that arrived while the chat panel was hidden, in
config mode, or in focus mode would queue forever in the store with no
visible UI — every agent run waiting on permission silently stalled.
Track an inlineMounted flag in permissionStore that PermissionInline
sets on mount and clears on unmount. PermissionModal at the app root
now renders a centered Dialog with the same prompt body whenever a
request is pending and the inline prompt is not mounted, so the user
always sees the prompt regardless of which view they're in.
Drop the duplicate "Deny + edit" button — it called the exact same
decide("deny") path as the plain Deny button. The variant can come
back when there's actual edit-prompt UI behind it.
Drop ai.model from the AiSettings probe useEffect dependency list. The probe is only meaningful when the provider or base URL changes; reading the current model via a ref avoids one /api/tags HTTP call per keystroke in the model field. Wrap fetchModels in useCallback so the auto-fetch effect can list it as a dependency cleanly without disabling react-hooks/exhaustive-deps. Extract autoPickInstalledOllamaModel in lib/ollama-detect that probes and falls back to pickBestModel in one round trip. AiSettings.handleSelect and ModelInlinePicker.handleProviderChange now both go through it, so switching to Ollama from the inline header picker also lands on the best installed tag — without this the inline picker reproduced the same 404 the original PR was meant to fix. Lift the in-render IIFE that builds the model dropdown into a ModelDropdown sub-component for readability.
…nding Move the canonical ChatMode union into lib/agent.ts (lower layer) and re-export from agentStore. Both files used to declare the same "auto" | "chat" | "agent" | "plan" union inline; one source of truth keeps SendMessageOptions and the store in lockstep. Drop "pending" from ToolStatus. ToolBlockView never assigned it — args streaming is "running", awaiting dispatch is also "running", so the pulse code path was dead and the only effect of keeping it was a phantom branch in the badge mapping. Tidy the badge mapping in ToolBlockView while there: collapse the running/done branches into one and rewrite the stale "auto-collapse 400ms" comment that promised a timer the code never had.
…ollbars User report: opening a file tab, jumping to the Home (Inicio) tab, then clicking the file tab again left the editor permanently black until the window was resized. Root cause was the conditional render in EditorArea — clicking Home set activeTabId to null, which switched the JSX from Editor to WelcomeScreen and disposed the Monaco instance. The remount on the way back raced through theme registration / layout / model swap and frequently landed on a 0×0 viewport that automaticLayout didn't recover from. Tracked in microsoft/monaco-editor#2057, #2294, #4306. Switch to the canonical multi-tab pattern: Editor stays mounted, the WelcomeScreen overlays on top via absolute positioning when no file is active, and the underlying Editor host gets visibility:hidden so it keeps a measurable layout. Editor also now forces a layout() call in the next frame whenever the active tab transitions back to a real file, so any residual viewport drift is corrected before the next paint. While there: surface scrollbars permanently when content overflows (scrollbar.vertical/horizontal: "visible" + matching slider sizes). Default Monaco fades the scrollbar after a couple of seconds, so users who clicked elsewhere in the IDE lost their overflow indicator. Sourced from microsoft/monaco-editor IEditorScrollbarOptions docs + issue #1686.
User reported a slight lag when moving the mouse fast over the window. The biggest contributor on Tauri 2 / WebView2 is the data-tauri-drag-region attribute: every mousemove over a drag area hits the window plugin's internal_on_mousemove IPC for cursor updates. With a flex-1 spacer covering most of the title bar, that flood is hundreds of IPC calls per second of motion. tauri-apps/tauri#8770, #10767, #12597 track the flood. Even with the PR-side throttling that landed in Tauri 2.x, eliminating the attribute on a wide drag surface is still a net win. Replace the spacer's data-tauri-drag-region attribute with manual onMouseDown -> startDragging() and onDoubleClick -> toggleMaximize() handlers. The OS still gets the drag intent (Aero Snap, edge maximize), but the per-frame IPC chatter goes away — only mousedown / dblclick fire across the bridge now.
Two regressions from the previous editor commit: 1. The "always-mounted" Editor was rendered with absolute positioning inside a flex parent. The .daisu-editor-host class already declares flex: 1 + position: relative, so the override interacted badly and left a translucent overlay on top of Monaco. 2. alwaysConsumeMouseWheel was flipped to false. The official Monaco TypeDoc is clear: that option is purely a propagation switch (calls preventDefault + stopPropagation on wheel). With it false, hovering the editor without focus lets the wheel event bubble to the nearest scrollable ancestor — so after clicking the file explorer, scrolling over Monaco did nothing until you clicked into it. VS Code, Cursor and every other Monaco-based IDE keep the default true. Refs: microsoft/monaco-editor#69, #4599, suren-atoyan/monaco-react#262. Switch EditorArea to a guarded display:none mount: the Editor lives inside the standard daisu-editor-host (no absolute positioning) and is only created the first time a real file becomes active. After that it stays mounted across Home tab visits via display:none — no remount, no canvas churn, no overlay artefacts. Editor.tsx already calls editor.layout() in a rAF whenever activeTabId flips back, so the display:none → block transition repaints cleanly. Drop alwaysConsumeMouseWheel from the scrollbar config so it falls back to Monaco's true default and scrolling works on hover.
…onaco Reported: after clicking the sidebar/explorer, hovering the editor and turning the scroll wheel did nothing — the user had to click into Monaco first. Every other editor (VS Code, Cursor, Sublime, Zed) scrolls on hover without needing focus. Root cause is WebView2-specific. Win32 delivers WM_MOUSEWHEEL to the window with focus, not the window under the cursor. Chromium itself routes wheel by hit-test, but only after the message reaches it. Inside WebView2 the input-routing layer between the host HWND and Chromium re-introduces the Win32 focus dependency, so a wheel event over Monaco gets dropped while focus lives on the sidebar. VS Code doesn't show the bug because Electron embeds Chromium directly with its own message pump. Tracked upstream: - MicrosoftEdge/WebView2Feedback#829 - MicrosoftEdge/WebView2Feedback#3769 - microsoft/microsoft-ui-xaml#2931 - tauri-apps/wry#616 Workaround: install a passive pointerenter handler on Monaco's DOM node that calls editor.focus() — but only when the previously focused element isn't an editable input/textarea/contenteditable region. That preserves typing focus in the chat composer, settings forms and permission prompts while letting the wheel reach Monaco the moment the mouse crosses into the editor. The skip-when-already-inside check avoids the secondary nuisance of re-focusing scrolling to the cursor whenever the mouse re-enters the viewport.
The previous "always-visible scrollbars" change set the right Monaco options but the daisu-nocturne theme registered slider colours at ~10x lower alpha than VS Code's defaults (0.039 / 0.078 / 0.20 vs 0.4 / 0.7 / 0.4). Result: the scrollbar was forced visible but functionally invisible against the dark canvas — users still couldn't tell where they were in a long file. Bring slider alphas in line with VS Code Dark+ while keeping the kintsugi-gold tint on the active state (cited from microsoft/vscode miscColors.ts). Also bump verticalScrollbarSize from 12 to 14 to match VS Code's EditorScrollbar defaults, and pin mouseWheelScrollSensitivity / fastScrollSensitivity / mouseWheelZoom to their documented defaults so a future Monaco bump can't silently drift the scroll feel.
Two more sources of mousemove jank surfaced by deeper research: 1. core:window:default in capabilities/default.json brings in the internal mousemove IPC handler that fires across the entire webview, not only inside data-tauri-drag-region elements (tauri-apps/tauri#8770). The previous TitleBar fix removed the drag-region attribute but the capability kept the handler armed. Replace the bundle with the explicit allow-* permissions Daisu actually uses (start-dragging, start-resize-dragging, minimize, toggle-maximize, close) — same surface, no flood. 2. WebView2's Edge mouse-gesture recognizer intercepts every move to detect gestures we don't use. Disable via additionalBrowserArgs (--disable-features=msEdgeMouseGestureSupported, msEdgeMouseGestureDefaultEnabled). While there, opt into --enable-zero-copy and --enable-gpu-rasterization so paint work stays on the GPU instead of the CPU compositor. Refs: tauri-apps/tauri#7692, MicrosoftEdge/WebView2Feedback#5072, #1469.
Three fixes triggered by a DevTools HAR + Performance trace.
LSP status chip was polling lsp_servers_status every 3 s. The HAR
showed 274 IPC calls in 9 minutes (~30/min) of idle time, eating Tauri
thread budget for transitions the lsp://server-ready and
lsp://workspace-opened listeners already deliver. Drop to 15 s — only
matters as a fallback when the backend exits without firing any event.
DevTools also flagged CLS 0.44 ("poor"), 86 layout shifts in the worst
cluster. The dominant sources were:
- Cursor "Ln X, Col Y" + tab counters in the status bar; default
fonts have variable digit widths so siblings slid every keystroke.
Added font-variant-numeric: tabular-nums on .daisu-statusbar.
- Editor breadcrumb appearing/disappearing as activeTabId flipped
between Home and a real file. Pinned min-height + tabular-nums.
- Pending assistant message growing line-by-line as deltas streamed.
Reserve ~3 lines of min-height + contain: layout on the message
body so the scroll viewport doesn't reflow per delta.
…ures Three zero-risk memory wins: 1. xterm scrollback 5000 -> 2000 lines per terminal. Each line carries a parsed cell buffer; halving the cap saves ~3 MB per open terminal without affecting realistic re-read windows for build/test output. 2. Disable Monaco's unicodeHighlight scan (ambiguous characters, invisible chars, non-basic ASCII). The feature targets security review of pasted prose for homoglyph/RTL trojans — useful in code review tooling, irrelevant for everyday editing. The decoder caches and per-line scans were a measurable share of editor memory. 3. Extend additionalBrowserArgs to disable WebView2 Edge features Daisu never uses: Translate (page translation), AutofillServerCommunication (no remote forms), OptimizationHints (ML UX nudges), MediaRouter (cast to TV), InterestFeedContentSuggestions (news feed), CalculateNativeWinOcclusion (browser-tab occlusion detection — a Tauri window is always foreground when visible).
Three deferral wins surfaced by parallel research on Tauri 2 + Monaco IDE optimization (refs: tauri-apps/awesome-tauri, microsoft/monaco- editor#1681 + #1987 + #318, MicrosoftEdge/WebView2Feedback memory target spec, Sidenai/sidex): 1. Code-split SettingsModal, CommandPalette, SymbolSearchPalette, LspWorkspaceSymbolPalette, FileSymbolPalette, PermissionModal and InlineEditOverlay behind React.lazy() + a single Suspense boundary. None are on the first-paint path; their Radix Dialog/Popover code, icon subsets and store hookups stay out of the main bundle until the user opens them. 2. Discord RPC handshake now waits for requestIdleCallback (with an 8s timeout fallback, and a 5s setTimeout for environments without ric). The previous 1.5s setTimeout still fired during the busy first-paint window where Monaco workers and LSP transport spawn — pushing Discord behind idle frees the main thread for those. 3. Disable Monaco's bracketPairColorization (was on, default in standalone Monaco is off). The bracket-pair tree grows per-model and is purely cosmetic; LSP diagnostics + tokenization render the same without it.
Quality Checks workflow on the PR was failing under Rust 1.95 because the toolchain rolled in a few new -D-warnings lints that the existing code tripped. None of them were behavioural bugs — all are style / modernisation hints — but they block merge. daisu-agent - memory/tokens.rs: once_cell::sync::Lazy → std::sync::LazyLock (clippy::non_std_lazy_statics). - memory/window.rs: scope cast lints onto the single u32→f32 multiplication used to compute the sliding-window target. Range stays inside f32's exact-integer band so the precision warning is noise. - provider/anthropic.rs: rewrap the cache-token estimator with map_or, mark EnvelopeUsage's Anthropic-mirroring field names intentional (they're wire-format names, can't rename without breaking serde), and flatten the nested match arm in the truncated-tool-args path into an if-let. - tools/dispatcher.rs: hoist the reserved-DOS-name list to a module const + reformat the multi-line doc comment so the second bullet/continuation are properly indented. daisu-app - commands/agent.rs: backtick OpenAI / camelCase / PascalCase / None mentions in doc comments, lift the conversational-opener action-prefix list to a module const, switch a couple of map(...).unwrap_or(...) calls to map_or, drop a String to_string no-op, replace a needless_range_loop with iter_mut().skip, swap content = replacement.clone() for clone_into, lift a spawn_blocking-await out of a match scrutinee, and drop a .clone() on a Copy ToolChoice. daisu-lsp - tests/manager_smoke.rs: ResolutionPublic::Found is a struct variant since the M4 LSP serde fix; the test pattern still used the old tuple form.
The fallback parser strips fenced/bare JSON tool calls out of the
streamed assistant text and replaces them with empty regions. The
post-processing pass kept a single blank line between surrounding
prose, so a message like
Sure! Here's the file:
```json
{...}
```
Let me know!
cleaned to
Sure! Here's the file:
Let me know!
instead of the contiguous paragraph the cleans_text_around_consumed_block
test expects. The CI failure under Rust 1.95 was the first run that
actually executed the test — earlier toolchains failed at clippy, which
ran before cargo test, so the regression went unnoticed.
Tighten collapse_blank_runs so it skips blank lines entirely while
joining non-blank lines with single newlines.
Summary
<input list>+<datalist>with a RadixSelectpopulated from live catalog merged with installed Ollama tags. WebView2 filtered the datalist by the input value, so the dropdown looked empty and unclickable whenai.modeldidn't substring-match an installed tag./api/tagsand repointai.modelto the best installed tag viapickBestModel. The static defaultqwen3-coderis rarely installed, so/api/chatreturned 404 on test-connection.Test plan
pnpm typecheckpnpm test --run AiSettings(4 passed)