The knowledge base file list infinite scroll had a race condition:
- page++ executed before the condition check, causing page numbers
to be skipped when scroll events fired rapidly
- No loading guard meant concurrent API requests could overlap,
with later responses overwriting earlier ones
This caused documents to not load when scrolling to the bottom
of the file list, especially noticeable with 100+ files where
only the first page would reliably appear.
Changes:
- Add scrollLoading flag to prevent concurrent scroll-triggered fetches
- Move page++ inside the condition check (was unconditional before)
- Use resetPage() helper to consistently reset both page and loading state
- Change scroll trigger threshold from >= scrollHeight to >= scrollHeight - 10
for better trigger sensitivity
When a startup database migration fails (e.g. issue #1319: pg_trgm not
available so 000041 cannot build its trigram index), the application
intentionally keeps booting so operators can reach the UI to diagnose.
However, before this change the system info page silently dropped the
"DB Version" row because the value was empty:
- migration.go only cached the version after a successful m.Up(); the
error path returned early and left migrationVersionSet=false.
- system.go used CachedMigrationVersion's ok=false to skip emitting
db_version, and the JSON tag was already omitempty.
- SystemInfo.vue gated the entire row on v-if="systemInfo?.db_version".
The end result was the most useful diagnostic surface vanishing in the
exact failure mode that needs it most — Wiki ingest and KG features
would silently produce nothing with no UI hint.
Changes:
- migration.go: replace sync.Once-based setter with an RWMutex-guarded
state struct holding {version, dirty, err}. Every failure path now
calls captureMigrationFailure(m, err), which best-effort reads
m.Version() so the cached value still reflects the partial state.
- system.go (handler): always emit db_version (falling back to
"unknown" when no version could be read), append " (failed)" when an
error is recorded, and add db_migration_error to the response.
- swagger / client SDK: keep the API contract in sync with the new
response field.
- SystemInfo.vue: render the DB version row whenever either field is
present, show a "Migration failed" danger tag, and add a full-width
alert below the row carrying the error message plus two links:
1. View troubleshooting guide -> new docs/migration-troubleshooting.md
2. Report an issue -> github.com/Tencent/WeKnora/issues/new,
prefilled with the captured error and environment metadata.
- docs/migration-troubleshooting.md: new self-service guide covering
the common failure modes (missing extension, dirty state, privileges,
out of disk, schema drift) with concrete psql / make commands.
- i18n: add the new keys to zh-CN, en-US, ko-KR, ru-RU.
Refs #1319.
Co-authored-by: Cursor <cursoragent@cursor.com>
The runtime chat pipeline (internal/application/service/chat_pipeline/rerank.go:56)
already skips rerank when rerank_model_id is empty, so it should not be
required at save time.
The editor incorrectly required rerank_model_id whenever a RAG KB was
selected, even though the field is optional. This fix removes the
validation so users can save agents without configuring a rerank model.
Fixes#1252
Inject the frontend package.json version at build time via Vite and
render it as a dedicated "UI Version" row in System Info, alongside
the existing app (backend) version returned by /system/info. When the
two versions differ a warning tag is shown so users can tell at a
glance that their weknora-ui image is out of sync with weknora-app
(e.g. missing config fields for newer providers like SearXNG).
Rename the existing version row label from "System Version" to
"App Version" to distinguish it from the new UI version row in all
four locales (zh-CN, en-US, ko-KR, ru-RU).
Bump version to v0.5.2 across VERSION, Helm chart, frontend package
files, and cloud-image script comment. Update CHANGELOG and all four
language READMEs with v0.5.2 highlights (Wiki Mode at scale, MCP
human-in-the-loop approval, new LLM/vector/storage/search backends,
adaptive 3-tier chunking, global command palette, CLI preview, etc.).
Tool-calling rounds emit reasoning into the OpenAI-protocol
reasoning_content field rather than visible content, so AgentStep.Thought
is often empty in DB. The history reconstructor only read step.thought,
which made the historical step card silent for those rounds even though
the user saw the reasoning live.
Fall back to step.reasoning_content when step.thought is empty so
reload mirrors the live experience.
The Wiki browser exposes a page-link graph (references between Wiki pages)
while KB Settings → Knowledge Graph configures an LLM-extracted
entity-relationship graph. Both are labeled "图谱"/"Graph", which is easy
to confuse.
- Expand graphSettings.description to call out the distinction
- Add tabGraphTip tooltip on the Wiki "图谱" breadcrumb tab
- Extend wikiBrowser.graphNoData empty state with the same clarification
- Update zh-CN, en-US, ko-KR, ru-RU consistently
The "启用实体关系提取" switch in GraphSettings was silently dropped on
save: buildSubmitData only attached extract_config to the request when
both indexingStrategy.graphEnabled AND nodeExtractConfig.enabled were
true. When the graph indexing strategy was off (the default), the
toggle change never reached the backend and reloading the KB always
showed the switch as off.
Always emit extract_config based on the user's actual toggle state so
the value round-trips correctly through updateKBConfig regardless of
the indexing strategy selection.
To diagnose where latency lives between "user hits send" and "first
token appears" we need a single number that can be matched across
the browser console and the server log. Add correlated TTFB markers
keyed by X-Request-ID:
Frontend (streame.ts)
* Generate the X-Request-ID once and reuse it for logging.
* Log request:start when fetchEventSource is invoked, response:headers
when onopen fires, and response:first_answer the first time an SSE
payload with response_type === 'answer' arrives. Filtering by event
type avoids treating session_title / references / tool_call as the
"first token".
Backend (session handler)
* parseQARequest records the wall-clock entry time and logs TTFB:start
with the same X-Request-ID.
* qaRequestContext carries receivedAt through to the stream handler.
* AgentStreamHandler emits a one-shot TTFB:first_answer_chunk log the
first time it observes an AgentFinalAnswerData chunk, so the delta
against TTFB:start is the server's request-in → first-token-out
budget.
The frontend delta minus the backend delta is then attributable to
network + gin middleware, which were previously invisible.
Quick-answer (RAG) agents can now configure a separate chat model for
the query-understanding stage (rewrite + intent classification),
decoupling it from the main conversation model so users can route the
lightweight rewrite call to a cheaper / faster model.
- Add CustomAgentConfig.QueryUnderstandModelID and plumb it through
PipelineRequest and ChatManage.Clone.
- query_understand plugin prefers QueryUnderstandModelID on the
text-only path; falls back to ChatModelID if the configured model
cannot be resolved (with a warn log). Multimodal path is unchanged
to keep vision-capable model selection intact.
- AgentEditorModal exposes a ModelSelector under the existing Query
Rewrite block; empty means reuse the main chat model.
- Add i18n strings (queryUnderstandModel / placeholder / desc) for
zh-CN, en-US, ko-KR, ru-RU.
- Extend Go SDK AgentConfig with the new field.
The data-analysis pipeline stage is a retrieval-strategy concern (it only
runs after chunk search/rerank), so the toggle belongs alongside the other
retrieval knobs rather than under knowledge base settings.
Refs: https://github.com/Tencent/WeKnora/issues/1244
The legacy in-pipeline DuckDB SQL data-analysis stage used to run on every
quick-answer RAG request whose retrieved chunks included a CSV/Excel file,
adding one extra LLM round-trip (~3s) to generate a SQL query that most
plain Q&A users never wanted. There was no way to disable it.
Introduce a per-agent DataAnalysisEnabled flag (default off), wire it
through PipelineRequest, and gate the DATA_ANALYSIS stage on it. Surface
the toggle in the agent editor for quick-answer agents with at least one
knowledge base attached.
Refs: https://github.com/Tencent/WeKnora/issues/1244
Add three new optional filters to the document list under a knowledge base
detail page — parse status, source/channel, and updated time range — and
rework multi-select to no longer cause the card title to jitter on hover.
Backend
- Introduce types.KnowledgeListFilter to aggregate optional filter dimensions
(tag, keyword, file_type, parse_status, source, updated_from/to) and switch
ListPagedKnowledgeByKnowledgeBaseID (repository/service/interface) to accept
it instead of a growing positional parameter list.
- The ListKnowledge HTTP handler accepts new parse_status, source, start_time
and end_time query params; time params accept RFC3339, "YYYY-MM-DD HH:MM:SS"
and "YYYY-MM-DD". The repository routes source="manual"/"url" onto the type
column to stay consistent with file_type semantics; other source values match
the channel column.
- Update the four other callers (agent_service, initialization) to pass an
empty filter struct, preserving prior behavior.
Frontend
- Add three controls in the doc-filter-bar (status select, source select,
date-range picker with future-date disabled) wired through getKnowled /
listKnowledgeFiles into the new backend params.
- Replace the hover-triggered card checkbox with an explicit "批量管理" mode
(mirrors the session list UX): in card view the checkbox only renders while
batch mode is on, entered via the per-card "..." menu; the list view keeps
its leading checkbox column. Switching from list to grid auto-enables batch
mode when something is already selected, so the selection stays visible.
- DocumentBatchBar now stays open whenever batch mode or selection > 0, and
its "取消选择" button both clears the selection and exits batch mode.
API surface sync
- Regenerate Swagger artifacts (docs/docs.go / swagger.json / swagger.yaml).
- Update docs/api/knowledge.md with the new query parameters.
- Add backward-compatible ListKnowledgeWithFilter + KnowledgeListFilter to the
Go SDK; the existing ListKnowledge keeps its signature.
i18n
- New filter labels in zh-CN / en-US / ko-KR / ru-RU; reuse existing
menu.batchManage / batchManage.cancel for the multi-select strings.
Previously the custom agent editor hid the rerank model field when no
RAG-type knowledge base existed in the configured scope, but the
not-ready check and runtime hard-failed whenever knowledge_search was
in allowed_tools. Users with wiki-only or empty scopes saw "Rerank
Model required" warnings they could not resolve, and "All knowledge
bases" agents broke later if a RAG-type KB was added.
- Backend: when knowledge_search is enabled, fall back to the tenant
default rerank model (ConversationConfig.RerankModelID) before
erroring out, matching the built-in agent behaviour.
- Editor: always show the rerank field once a KB scope is selected;
only mark it required (red *) when the scope contains a RAG KB, with
a hint explaining the tenant-default fallback.
- Editor: only render rerank top_k / threshold sliders when a rerank
model is actually selected.
- Input field: drop the eager "missing rerank" not-ready reason; the
backend is now the single source of truth for rerank availability.
- i18n: add agent.editor.rerankModelOptionalHint across all locales.
Quick-answer agent mode retrieves purely through vector/keyword chunk
search and ships with no `allowed_tools`, so the existing capability
filter (which only reads from `allowed_tools`) let wiki-only KBs through
in every entry point. End result: users could @-mention, select, and
receive suggested questions from wiki-only KBs in quick-answer mode,
but the underlying retrieval always returned empty.
Treat "RAG-only" as an implicit property of `agent_mode = quick-answer`
and union it with the tool-derived filter. The same predicate is now
used everywhere the user can pick or be steered toward a KB:
Backend
- `tools.DeriveKBFilterForAgent` / `KBSatisfiesAgentRequirements`
layer the implicit quick-answer requirement on top of tool derivation.
- `ListKnowledgeBases`, `SearchKnowledge` (shared-agent `@file`),
`resolveKnowledgeBasesFromAgent` (chat runtime), `/search` IM command,
and `GetSuggestedQuestions` now all use the agent-mode-aware variant.
- `GetSuggestedQuestions` also skips the wiki-page fallback for
quick-answer agents to cover the `selected` / explicit-kb-ids paths
where a wiki-only KB could still slip through.
Frontend
- `deriveKbFilterForAgent` / `kbSatisfiesAgentRequirements` mirror the
Go helpers.
- `@` mention dropdown (`Input-field.vue`) uses the new helper.
- Agent editor's "specified KB" picker (`AgentEditorModal.vue`) grays
out wiki-only KBs for quick-answer agents with a tooltip, and the
pre-save warning fires for quick-answer mode too.
- i18n: add `agentEditor.agentType.kbMismatch.quickAnswer` across all
four locales.
- Updated .env.example to clarify SEARXNG_SECRET generation and added SSRF_WHITELIST_EXTRA for improved security.
- Modified docker-compose files to bind SearXNG to localhost by default and introduced a one-time initialization service to set up settings.yml correctly.
- Enhanced SearxngProvider with stricter URL validation, ensuring no query or fragment is present in the base URL.
- Added unit tests for SearXNG validation and date parsing to ensure robustness.
- Updated frontend WebSearchSettings to reflect changes in SearXNG instance URL handling.
This commit improves the security and usability of the SearXNG integration, addressing potential misconfigurations and enhancing the developer experience.
- Added gzip compression settings to improve loading times for static assets, significantly reducing the size of files served and enhancing performance for users, especially in low bandwidth regions.
Add an opt-in human approval gate so Agent runs pause before executing
MCP tools that operators flag as dangerous, surface an approval card in
the chat UI, and only resume after the user approves (optionally with
edited args) or rejects.
Backend
- New mcp_tool_approvals table + repo/service to mark per-tool approval
required (PG migration 000042 + sqlite init).
- approval.Gate coordinates RequestAndWait / Resolve with sync.Once
delivery, configurable timeout, and Redis Pub/Sub fan-out so multi-
replica deployments work without sticky sessions.
- MCPTool.Execute integrates the gate; uses a round-level ApprovalCtx
(without the per-tool 60s timeout) for the wait, and re-derives a
fresh 60s exec ctx after approval so CallTool keeps a full window.
- New SSE response types (tool_approval_required / _resolved) and
EventBus events plumb approval state to AgentStreamDisplay.
- REST: list/set per-tool approval flag, resolve pending approval.
- Configurable via agent.tool_approval_timeout_seconds (yaml) or
WEKNORA_AGENT_TOOL_APPROVAL_TIMEOUT env (accepts seconds or Go
duration).
Frontend
- MCP settings: per-tool "require approval" switch on the test panel.
- Chat: ToolApprovalCard renders the pause point with editable JSON
args, validation feedback, mm:ss countdown that turns warning/danger
near deadline, and a resolved state that retains context.
- i18n strings added for zh-CN / en-US / ko-KR / ru-RU.
Docs
- docs/zh/mcp-approval.md covering behavior, config, API, deployment
considerations (Redis cross-instance, restart limitations).
Fixes O(n²) write amplification during wiki ingest on large KBs. Previously
every ingest/retract op re-wrote the single `slug='log'` row end-to-end and
every batch re-wrote the entire `slug='index'` directory markdown. On a 40k-
doc KB the log row grew to tens of MB and the index row to several MB, so
each batch triggered giant TOAST updates that dominated ingest wall time.
Log: new `wiki_log_entries` event table (`id DESC` indexed per KB) replaces
the single TEXT row. Batch ingest now collects entries and flushes them
once per batch via `AppendBatch`. Each entry stores `pages_affected` as
JSONB `[{slug,title}]` so the UI can render real titles; custom Scan falls
back to legacy `[]string` so older rows still deserialize.
Index: `wiki_pages[slug=index].content` keeps only the LLM-generated intro
(a few KB). The directory is now served by a structured paginated API
(`GetIndexView`) that reads `slug/title/summary` per type with cursor
pagination, so the agent and the frontend only pull the slice they need.
`RebuildIndexPage` degrades to a no-op; agent `wiki_read_page('index')`
synthesizes a small top-K overview and points callers at `wiki_search`.
Ingest resilience: LLM calls wrap with 3-attempt exponential backoff on
transient errors (5xx/408/429, transport resets/timeouts). Summary/extract
failures now bubble up so the batch's failed-op requeue path runs instead
of silently dropping the doc.
Frontend: sidebar Index/Log entries switch to dedicated views. Index view
streams intro → Summary → Entity → Concept → Synthesis → Comparison via
IntersectionObserver (with a nextTick re-check so small KBs still load
every section). Log view uses cursor pagination; pages_affected renders
titles with slug tooltip.
- new migration: migrations/versioned/000040_wiki_log_entries.{up,down}.sql
- tests: log repo pagination + legacy Scan, ListByTypeLight windowing,
renderIndexOverviewForAgent output, isTransientLLMError classifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Dockerfile invoked corepack + pnpm install, but the repo only
ships package-lock.json (no pnpm-lock.yaml). On node:24-alpine
corepack resolves to pnpm v10, whose default --strict-dep-builds
rejects unbuilt postinstall scripts (esbuild, vue-demi,
@vue-office/pptx) and aborts with ERR_PNPM_IGNORED_BUILDS, breaking
docker compose build.
Switching to npm ci / npm run build aligns the image build with
CLAUDE.md and the local dev workflow that already uses npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm v11 no longer supports onlyBuiltDependencies in package.json and does not
accept install-time --allow-build flags. Move build-script approvals to
pnpm-workspace.yaml and copy it before install in Dockerfile so CI Docker builds
can apply allowBuilds during pnpm install.
Some CI runners still fail with ERR_PNPM_IGNORED_BUILDS even with package-level
configuration. Pass explicit allow-build flags to `pnpm install` in Dockerfile so
required postinstall scripts are always permitted during image builds.
pnpm 10+ refuses to run dependency build scripts by default and exits
with ERR_PNPM_IGNORED_BUILDS, causing `pnpm install` to fail in the
frontend Docker image. Whitelist the packages that legitimately need
their install scripts (esbuild native binary, @vue-office/pptx,
vue-demi) via `pnpm.onlyBuiltDependencies`.
?tab=wiki used to call listWikiPages({page_size: 500}) in a loop up to
50 times, loading ~25k pages into memory before rendering the sidebar
on knowledge bases with tens of thousands of pages. All rows were then
materialized as DOM nodes inside collapsible groups stacked vertically.
This commit reshapes the sidebar around per-type buckets that are
fetched on demand and rendered through a single virtual scroller:
* pagesByType holds one bucket per page_type (summary / entity /
concept / synthesis / comparison), each with its own items list,
pagination cursor, KB-wide total, and loading flag. Initial load
fires one 50-item request per type in parallel.
* Index and Log are fetched directly via getWikiIndex / getWikiLog
instead of scanning the full page list to find them.
* Groups are surfaced as a horizontal tab bar; switching tabs swaps
the RecycleScroller's items. Only one bucket is visible at a time,
so we never stack multiple virtual viewports.
* The scroller runs in page-mode with the outer .wiki-page-list as the
scrollable ancestor; a 1px sentinel after the list is watched by
IntersectionObserver to trigger the next page fetch. After each
batch lands we call scroller.updateVisibleItems(true) on the next
tick so appended rows render in order without requiring the user
to jiggle the scroll.
* Switching tabs resets scrollTop so the retained position of the
previous tab can't re-trigger the sentinel and cascade loads on
the new (shorter) list.
* Search enters a dedicated flat-result mode (searchResults ref);
clearing the query flips back to the bucketed view without
refetching the buckets the user already had open.
Adds vue-virtual-scroller dependency and imports its stylesheet.
Wikis with tens of thousands of pages used to crash the browser trying
to render the entire link graph at once (30MB+ JSON, 100k+ SVG elements).
This change moves the graph viewer from "fetch everything, render
everything" to "fetch a slice, expand on demand" with several matching
UX improvements.
Backend
* GET /wiki/graph now accepts `mode` (overview | ego), `center`, `depth`,
`types`, `limit` query params. Default is overview top-500 by
link_count, capped at 2000. Response includes a Meta object so the
frontend can render a truncation hint and drive the UI.
* Pure helper `computeGraphSubset` extracted for testability; six unit
tests cover overview truncation / type filter / ego BFS / missing
center error.
* WikiLintService passes Limit=0 (uncapped) so link integrity checks
still walk every page.
* Repo `Search` adds CASE-based relevance ranking (title 4, slug 3,
summary 2, content 1) so full-text results put the most obvious
matches first instead of whatever was updated most recently.
Frontend
* Graph viewer fetches overview on entry; ego pivot on double-click,
search, wiki-link click, URL ?slug=, global issues jump.
* Shift+click or hover ⊕ button blooms neighbors onto the current
canvas additively. Bloom tracks generations and LRU-evicts oldest
when total exceeds 1500 nodes; ego center / selected node / latest
anchor are always protected.
* "Grow frontier (N)" legend action expands every dashed-ring node in
one click (6-way concurrency) while skipping Index/Log super-nodes
that would otherwise dump the whole wiki onto the canvas.
* Node dashed expansion ring + drawer "X/Y neighbors shown" hint tell
users which nodes still have neighbors to load.
* Search dropdown uses remote full-text search (debounced, sequence-
numbered to drop stale responses) and falls back to the overview
top-500 snapshot when the input is empty.
* Type filters now round-trip to the server so top-N is always
computed from the user's active type set rather than hiding nodes
client-side and shrinking the view.
* Status card replaces the cramped "centered on X · N hops · M nodes"
line with a structured focus/overview summary, resolving slugs to
page titles.
* Help popover in the legend lists every canvas shortcut.
i18n entries added for zh-CN / en-US / ko-KR / ru-RU.
- Fix global `--td-brand-color-disabled` to use correct muted green instead of bright green
- Unify `variant="text"` buttons (refresh, download, check dimension) in model settings to inherit standard TDesign styles
- Adjust WeKnora Cloud credential status layout and change reconfigure button to outline
- Fix font size of external doc links in model settings hint blocks
- Increase font weight of KB chunking debug trigger for better visibility
- Add global .doc-link utility in theme.css (brand color text + link icon, underline on hover).
- Convert all external doc/console links across settings to use .doc-link with a trailing <t-icon name="link" />, matching the Ollama browse-library style.
- Switch primary section-toolbar 'Add X' buttons (Model/WebSearch/VectorStore/MCP/DataSource) to outline variant for a lighter, more consistent look.
- Drop redundant per-component CSS for builtin-hint-link, weknoracloud-hint-link, engine-doc-link, engine-link, hint-link, section-doc-link, ds-doc-link-inline, model-link, etc.
- Add global t-radio-button outline styles in theme.css (brand fill when checked).
- Align ModelEditor, share KB, datasource, agent/MCP settings with t-radio-button.
- Remove redundant :deep overrides in AgentEditorModal, KB editor, FAQEntryManager, etc.
- Use brand disabled tone for disabled+checked radio buttons so selection stays visible.
The wiki page drawer opened from the chat view rendered tables without
borders or padding because the global `.wiki-graph-drawer .wiki-reader-body`
style block was missing GFM table rules. Add the same table styles already
used by `WikiBrowser.vue` so tables show consistent borders, header
background, and hover states across both entry points.
Some LLMs (e.g. qwen3 series) emit blank lines like "\n\n" before tool
calls. The backend faithfully forwards these as thought_chunk events
because streamThinkingToEventBus only filters chunk.Content == "" and
treats whitespace as content. The frontend then merges them into a
thinking event whose content is non-empty in JS truthiness terms but
visually empty, producing a "思考" card with a chevron and no text.
Filter such events out at the end of buildFullEventList so neither the
intermediate-steps tree nor the displayEvents fallback path renders
them.
The natural-stop path on the backend (observe.go Case 1) emits the same
content twice: once as streaming thinking chunks (from
streamThinkingToEventBus's per-chunk callback) which the frontend merges
into a single thinking event, and once as one big EventAgentFinalAnswer.
The result is that the intermediate-steps tree contains a thinking card
whose content is identical to the answer card rendered below it.
Detect this duplication and hide the offending thinking event from the
tree:
- New utils `normaliseForComparison` and `thinkingEqualsAnswer` perform
a tolerant comparison: strip <think>…</think> blocks (which leak into
raw thinking chunks but not into the post-StripThinkBlocks answer),
strip final-answer wrappers, collapse whitespace, then exact-equal or
±2% length with matching head/tail.
- `hiddenThinkingEventIds` (replacing the previous single-id
`promotedThinkingEventId`) now collects two cases of duplicate
thinking: the legacy promotion fallback (no answer event present) and
the natural-stop content-match case.
The final_answer-tool path is unaffected because reasoning thinking
events ("我找到了…让我整理…") and the answer payload have different
content and don't match the equivalence check.
Many models (especially smaller or non-OpenAI-style ones) do not reliably
call the structured final_answer tool, even when the system prompt
explicitly requires it. Two visible symptoms in the agent UI:
1. The natural-stop path emits the answer as a final_answer event whose
content is wrapped in <answer>...</answer>, <final_answer>...</final_answer>,
or prefixed with "Final Answer:" / "最终答案:". These wrappers were
rendered verbatim into the answer card.
2. When no answer event arrives at all, the trailing thinking event was
surfaced via the legacy fallback but kept its collapsed "思考" card
styling instead of rendering as a proper answer.
This change adds a small unwrap helper and wires it through the answer
rendering path:
- New util `unwrapFinalAnswerWrappers` strips conservative outer wrappers
(XML-style answer tags, fenced ```final_answer code blocks, and leading
"Final Answer:" / "最终答案:" prefixes) only when they cover the entire
trimmed content, so user markdown that happens to contain similar text
is left alone.
- `renderAnswerContent` and `getActualContent` apply the unwrap before the
markdown renderer / clipboard copy, so users see and copy clean text.
- The legacy "promote trailing thinking to final answer" fallback now
synthesizes a virtual answer event so it renders with the answer card
UI (expanded markdown + copy/add toolbar) instead of the collapsed
thinking card. The promoted thinking event is hidden from the
intermediate-steps tree to avoid duplication.
The font picker in 常规设置 silently failed in three distinct ways:
1. Font family changes were invisible on macOS because 6 of the 7 options
(Inter / Roboto / Segoe UI / Cascadia Code / JetBrains Mono / Fira
Code) aren't installed by default and fell back to system SF. Swap
the picker to a platform-aware list — Mac / Windows / Linux each see
only the fonts their OS actually ships. The "third sans slot" now
always serves a serif face (Georgia / Times New Roman / DejaVu
Serif) so the difference is unambiguously visible instead of being
three near-identical sans families.
2. Font size flipped --app-font-scale on <html>, but that variable was
only consumed by a handful of TDesign tokens and the body reset, so
the ~1000+ hard-coded font-size: NNpx rules across the frontend
stayed put. Apply the multiplier via `zoom` on <html> so the whole
document scales, then fix the 15 `100vh` rules that would overflow
under zoom (html / body / #app are already height: 100%, so the
descendants can use 100% too).
3. The monospace picker had no visible effect even on a working stack
because `<t-input style="font-family: ...">` is forwarded to the
inner <input>, but TDesign applies `font: var(--td-font-body-medium)`
— a shorthand that resets font-family. Reach the real <input> /
<textarea> via `:deep()` instead of inline style.
Additional polish:
- Add a live preview row under each picker so users can see the effect
without hunting for a code block or API Info screen
- Rename "等宽字体 / Monospace Font" → "代码字体 / Code Font" to match
VSCode / GitHub / Notion convention; the old label was jargon
- Rewrite descriptions to explain the actual purpose and scope
The chunking-settings debug trigger and strategy picker had two UX
issues reported by real usage:
- The test button used theme=primary + variant=outline, a combo that
appears nowhere else in the project. A green-bordered green-text
pill in the corner of the section header read as a misplaced
promotional element rather than a secondary action.
- The strategy select's placeholder was "选择策略(默认使用经典递归分块)"
/ "Select strategy (defaults to classic recursive splitting)". Naming
the fallback strategy inside the placeholder made the empty select
look as if the user had already picked "classic recursive" — even
though the value is actually empty.
- Move the debug trigger from the section header into the strategy
row's right column, stacked below the select. Users meet the test
action exactly when they're deciding which strategy to use.
- Switch the trigger to a text-style button and override TDesign's
default hover/focus/active surfaces so the button reads as an inline
link (text colour shift only, no filled background block).
- Rename the trigger to "测试分块效果" / "Preview chunking" so it
describes what will be inspected rather than the input source.
- Drop the reference to the fallback strategy inside the placeholder.
"不填则按长度切分" / "splits by length if left empty" preserves the
original information about empty-value behaviour without looking
like a selected option.
- Remove the now-unused 80px right padding the section header reserved
to keep the old inline trigger clear of the modal close button.