Skip to content

feat(workbench): Knowledge Workbench — new React/TS frontend + supporting backend#189

Merged
KylinMountain merged 98 commits into
mainfrom
feat/workbench-adoption
Jul 20, 2026
Merged

feat(workbench): Knowledge Workbench — new React/TS frontend + supporting backend#189
KylinMountain merged 98 commits into
mainfrom
feat/workbench-adoption

Conversation

@KylinMountain

Copy link
Copy Markdown
Collaborator

What this is

Replaces the shipped plain-CSS/JSX Workbench (frontend/) with a React 18 + TypeScript + Vite + Tailwind + shadcn/Radix single-page app, wired to the real OpenKB REST API, with an Apple-design material/motion pass, light-default / dark-auto theming, and 中文 / English i18n. Adds the backend endpoints and runtime-config plumbing the new UI needs.

86 commits · 170 files · +21k / −4k. Squash-merge intended.

Frontend

  • Chat-first IA — Home launcher (recent sessions + grounded chat input with KB scope + slash-command generators /deck /skill /visualize), KB list with a real create-KB flow, KB detail (card-nav Overview: Index / Concepts / Entities / Summaries / Reports / Documents; opens index.md like a real wiki), per-KB settings gear.
  • Docked artifact panel — deck / graph / conversationally-generated output/**.html open in a sandboxed-iframe side panel (blob URL + bearer, never a token in a URL); chat shows compact "open" cards.
  • Interleaved chat trace — streamed narration + tool reads render as an ordered step trace (each read a click-through ✅ line) followed by the answer — live and on restore (the trace is persisted per turn, read-tool-only; sessions saved before this fall back to the flat text).
  • MarkdownView — fenced code, lazy Mermaid diagrams, [[wikilink|alias]], LaTeX math via KaTeX (bundled / self-contained), and --- rules.
  • Apple design — glass materials, critically-damped springs (motion), prefers-reduced-motion / -transparency, size-specific typography; a11y (Radix modal semantics for the settings sheet, labeled complementary region for the panel).
  • Settings — global-config editor (model / PageIndex threshold / wiki output language via a UN-official-languages combobox / global default credentials), an About tab, and theme + language toggles in the top-right chrome.
  • i18n — react-i18next, 8 lazily-loaded namespaces, 中文 default + English, plus a build guard (check-i18n.mjs) that fails on leftover UI CJK or zh/en key-set drift.
  • New deps pinned exactly (motion, mermaid, the i18next stack, katex).

Backend

  • entities added to GET /api/v1/list (+ get_kb_list).
  • New routers — GET /api/v1/graph/html (self-contained graph), GET /api/v1/output (path-guarded, output/-only HTML sink), GET/PATCH /api/v1/config (global defaults + credentials).
  • Runtime config unificationresolve_effective_config(kb_dir) layers defaults → ~/.config/openkb/global.yaml → KB .openkb/config.yaml for the global scalars, migrated across the load_config call-sites; RFC-7386 merge-patch with null-removal that preserves inheritance.
  • Chat — write-capable agent (emits an artifact SSE event on a write_file of output/**.html); per-turn trace persistence (ChatSession.assistant_traces), recording only read-tool calls so large generated file contents never hit the session JSON or the restore wire.
  • Global default credentials written to ~/.config/openkb/.env (0o600, atomic, under the global-config lock; the API key value is never returned by GET nor logged).

Testing

  • Backend — uv run pytest -q1175 passed; ruff check / ruff format clean.
  • Frontend — npm run build (tsc + vite) + the i18n:check guard green.
  • Manual browser verification across light/dark, both locales, and each surface (create-KB dialog, chat trace live + restore, KaTeX math + --- rules, settings credentials, artifact panel).
  • Each sub-project passed an opus whole-branch code review; findings were fixed and re-reviewed.

Notes

  • Configuration lives in files (global.yaml / per-KB config.yaml / .env), not startup input.
  • The local API is unauthenticated by default (opt-in bearer via OPENKB_API_TOKEN) — intended for local use.
  • Dependencies are pinned exactly (supply-chain caution).

https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

…rtifact preview

Migrate ChatInput, ChatSession, and ArtifactCard off hardcoded
bg-white/bg-blue-*/border-black/text-neutral-* colors onto the Apple
token layer (.glass/.glass-2, border-[hsl(var(--glass-border))],
bg-accent-brand, text-foreground/text-muted-foreground). Fixes the
shared ChatInput box + slash-command menu rendering white against the
dark shell (flagged in Task 4). Source chips gain active:scale-[0.97]
+ transition duration-fast ease-out-apple press feedback.

Wrap the deck preview panel in motion AnimatePresence + a critically-
damped spring (bounce:0, 0.24s), degrading to an opacity-only cross-fade
under prefers-reduced-motion via useReducedMotion. The sandboxed iframe
(sandbox=allow-scripts, src={previewUrl} blob URL) and its blob-URL
revocation lifecycle are unchanged; the spring only wraps the panel.

Slash-command logic, streaming, citations, and session handling are
untouched. Semantic deck/skill/graph accent colors are preserved.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
KbDetail kept light-only semantic status colors (bg-*-50 / text-*-500 /
text-*-600) with no dark: pair, so red error cards and emerald/red status
pills rendered as bright light patches in dark mode. Add dark: variants
only (no light-mode, layout, or logic changes), matching the app-wide
pattern from ChatSession.tsx (red) and ArtifactCard.tsx StatusBadge
(emerald): dark:bg-red-500/10 dark:border-red-500/25 dark:text-red-400 and
dark:bg-emerald-500/10 dark:text-emerald-400.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
Previously all streamed delta text (planning narration + the answer) was
concatenated into one blob and tool reads showed only as separate chips.

- chat.ts: fold SSE events into an ordered TurnStep[] preserving arrival order
  (text segments interleaved with tool steps); tool_call marks the prior read
  done; final reconciles the trailing text with the authoritative answer. The
  flat `answer`/`sources`/`reading` fields are retained for restore/consumers.
- ChatSession.tsx: render steps in order — narration via MarkdownView, each read
  as a status line (spinner -> green check), page reads stay click-through.
- drop `/compile` from the slash palette (runTurn never had a compile branch;
  recompile lives on the KB gear) and remove its now-unused icon import + keys.
  Chat is for Q&A + generation; KB-lifecycle actions live on the KB surface.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…guage picker

Settings global config surface:
- backend: GET/PATCH /api/v1/config now also read/write global default credentials
  (LLM_API_KEY / OPENAI_API_BASE) in ~/.config/openkb/.env, mirroring the per-KB
  .env write (0o600, atomic, RFC-7386 null-removal, key value never returned/
  logged), under the existing global-config lock. GlobalConfigResponse gains
  openai_api_base + has_api_key; GlobalConfigPatchRequest gains api_key
  (SecretStr) + openai_api_base. +6 tests.
- Settings.tsx: add a Default credentials section (masked key, set/unset hint,
  deferred clear, API base) folded into the merge-patch save; remove the dead
  brand/privacy footer card + its i18n key.

Wiki output language now offers the six official UN languages as a datalist
combobox (still free-text, so e.g. 日本語 works) in both the global Settings
and the per-KB gear. Also a one-line LanguageToggle doc-comment fix.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…p the trace

Revisiting a saved session collapsed the interleaved trace back to one text
block, because only the flat answer was persisted. Now the ordered trace
(narration text + tool reads) is stored per turn and rebuilt on restore.

Backend:
- ChatSession gains `assistant_traces` (parallel to assistant_texts, 1:1 by
  index); to_dict/load_session round-trip it; record_turn accepts a `trace` and
  back-fills empty traces for older turns so a pre-existing session that gets a
  new turn stays index-aligned. Old files without the key load as [] (no crash).
- iter_chat_turn_events accumulates the trace from delta/tool_call events (same
  order the frontend folds live) and persists it via record_turn.
- ChatSessionLoadResponse returns assistant_traces (new ChatTraceStep model).

Frontend:
- loadSession exposes assistant_traces; ChatSession restore rebuilds steps via
  stepsFromTrace (tool steps re-derived through the same read-only whitelist),
  falling back to a single text step for trace-less (old / CLI) turns.
- fix a latent live bug: on `final`, do NOT overwrite the trailing text step
  with final.answer (which is the full narration+answer concatenation) — that
  duplicated narration once tool reads split the text; keep the streamed trace
  and only inject the answer when nothing streamed.

Backward compatible: only sessions created after this change carry a trace;
older ones render their saved text as before.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
Review follow-up. The trace recorded every tool_call, including write_file,
whose `arguments` carries the entire generated file `content`. That bloated the
session JSON and was shipped to the browser on restore only to be dropped by
the frontend's read-only whitelist. Gate recording to _TRACE_READ_TOOLS
(read_file / get_page_content) — the only tools the UI renders — which also
fixes a live-vs-restore text-grouping divergence a dropped write_file caused.
Plus a tidy-up of the languages.ts doc comment (drop raw CJK).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…n MarkdownView

Answers that contain LaTeX (\[ … \] block, \( … \) inline) and `---` rules
showed the raw delimiters/dashes as literal text. Add math + horizontal-rule
support to the hand-rolled MarkdownView: block/inline math render via KaTeX
(bundled + self-contained, no CDN; malformed TeX falls back to a code box), and
a `---`/`***`/`___` line renders an <hr>. Adds katex (pinned exact, matching the
mermaid/motion convention) + @types/katex.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
The About tab linked a "Website openkb.ai" that does not exist yet; remove the
link entry and its i18n key (both locales). The Globe icon stays (still used by
the Vectify AI company link).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
… / junk)

GET /api/v1/page serves pages verbatim, so every concept/entity/summary page's
OKF `--- … ---` frontmatter leaked into the reader as junk metadata lines — and,
now that MarkdownView renders `---` as an <hr>, as a horizontal rule at the very
top. Strip a leading frontmatter block before rendering (reader only; chat
answers carry no frontmatter and are untouched). Second-review follow-up.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
Comment thread tests/test_cli.py Fixed
Comment thread tests/test_config.py Fixed
Comment thread openkb/api_models.py

# Single source of truth for the writable config keys (derived from the model
# above so the two never drift).
_KB_CONFIG_WRITABLE_KEYS = set(_KbConfigWritable.model_fields)
- credential .env: consolidate the KB and global read-modify-write into one
  _merge_patch_env helper that strips a leading `export ` before parsing (so an
  `export LLM_API_KEY=…` line is actually cleared/rotated, not leaked); reject
  api_key/openai_api_base values containing newlines (400) so a value can't
  inject extra KEY=VALUE lines python-dotenv would parse.
- resolve_effective_config: coerce a non-dict config.yaml/global.yaml to {} so a
  malformed (esp. global) YAML degrades to defaults instead of AttributeError-ing
  KB-wide on every command's hot path.
- PATCH /api/v1/config: run the blocking global-config flock via run_in_threadpool
  so a contended lock no longer stalls the async event loop.
+4 tests.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…ack (review)

- write_kb_file now uses atomic_write_text (temp + os.replace) instead of a plain
  write_text, so a REST /chat-triggered wiki/output write can't be observed torn
  by a concurrent lint/recompile (and it goes through locks.py per the invariant).
- iter_chat_turn_events: inject the final answer into the persisted trace unless a
  text step with NON-whitespace content exists, so a whitespace-only delta no
  longer makes a restored turn render empty.
- iter_agent_response_events: derive the tool-call correlation id via a shared
  helper using the SDK's call_id-or-id fallback on BOTH the call and output sides,
  so LiteLLM/ChatCompletions-routed writes still fire the output artifact card.
+8 tests.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
- foldSseEvent final: inject the answer unless a non-whitespace text step exists
  (whitespace-only delta no longer hides the answer); export markToolStepsDone and
  run it in runTurn's catch/finally so a stream error settles the tool spinner.
- App: wrap the /chat/:id route in a keyed ChatSessionRoute so a route-param change
  remounts and re-runs restore (no more stale previous session on nav).
- ChatInput: guard Enter with !isComposing (CJK/IME users no longer submit
  mid-composition); use || so an empty kbId shows the placeholder, not blank.
- Settings: reject a negative pageindex_threshold, matching the per-KB gear.
- KbDetail: stripFrontmatter only strips a real YAML-mapping frontmatter block, so
  a page whose body legitimately opens with a --- rule keeps its first content.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…rkdownView

The hand-rolled renderer dropped several GFM features vs the old react-markdown.
Add [text](url) links (URL-scheme-guarded), *italic*/_italic_, ~~strikethrough~~,
and real <ol> ordered lists — with load-bearing alternation order ([[wikilink]]
before link, **bold** before italic) so existing tokens still parse. GFM tables
and code syntax highlighting remain out (they need react-markdown re-adoption).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…config

- write_wiki_file now uses atomic_write_text (its sibling write_kb_file already
  did), so a REST /chat-triggered exploration write can't be observed torn.
- _setup_llm_key resolves `model` via resolve_effective_config (DEFAULT ->
  global.yaml -> KB) instead of KB-only load_config, so a global-default model
  drives provider extraction; drop the now-unused load_config import. +1 test.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…tables

Re-establishes capabilities the plain-CSS app had and closes review gaps:
- Stream cancellation: apiStream/streamChat/streamQuery/runRecompile take an
  optional AbortSignal; ChatSession shows a Stop button while running and aborts
  the in-flight turn on unmount (no wasted tokens, clean settle — abort ≠ error);
  KbSettingsSheet aborts a recompile on close.
- Documents: per-row delete (POST /api/v1/remove, confirm) + streaming upload
  with per-file progress (SSE /api/v1/add) replacing the single spinner.
- Sidebar KB list refreshes on an `openkb:reload-kbs` event dispatched after a
  create, so a new KB appears without a full reload.
- MarkdownView renders GFM tables (aligned, scrollable, inline-formatted cells).
- client.ts warns once when the API base is cross-origin plain-http (token leak).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…very

- kb_root_dir() precedence: env OPENKB_KB_ROOT > global.yaml `kb_root` > default
  <config>/kbs; GET/PATCH /api/v1/config expose/set `kb_root` (+ `kb_root_env_pinned`).
- POST /api/v1/init accepts an optional absolute `path`; a custom-path KB is
  registered (register_kb_alias) so it still resolves by name (resolve_init_kb_dir;
  relative path -> 400).
- KB discovery (moved to api_kbs.py to keep api.py/api_helpers.py < 800) now unions
  kb_root children with registered known_kbs/kb_aliases outside the root, deduped by
  resolved path; each /api/v1/kbs item gains `path`.
+15 tests.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
…-root setting

- CreateKbDialog shows a live "Will be created at <root>/<name>" preview (fetched
  from /api/v1/kbs root) and an optional Custom path field that overrides it.
- Settings › General adds a Knowledge base root field (top-level kb_root merge-patch;
  empty -> null/default) with a note when it's pinned by OPENKB_KB_ROOT.
- KbList cards show each KB's path (now that KBs can live outside the root).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
Union KB discovery surfaced registered KBs outside kb_root (e.g. a CJK-named
`笔记` at ~/Documents/笔记), but they couldn't be opened: validate_kb_name only
allowed ASCII so a name-based endpoint 400'd on "笔记", and resolve_kb_alias only
checked kb_aliases + the kb_root fallback, never known_kbs.

- KB_NAME_RE relaxed to `[\w-]+` (Unicode letters/digits/_/-, still no spaces,
  dots, or path separators) so a Chinese-named KB is a valid, traversal-safe name.
- resolve_kb_alias now matches a known_kbs path by its directory basename (after
  aliases, before the root fallback), consistent with the KB list — so a
  registered out-of-root KB is addressable by the name the UI shows.
+2 tests.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
The i18n build guard (check-i18n.mjs) flagged raw CJK inside comments in
KbDetail.tsx, failing `npm run build`. Its comment-stripper is a naive
state machine with no regex-literal handling, so a regex/quote earlier in
the file desyncs it into "string" state; from there `{/* ... */}` JSX
comments are read as string content and their CJK leaks past the strip.

All flagged lines are comments (no real UI string carries CJK in this
file), so converting the comments to English resolves the build failure
with zero functional risk and matches the codebase norm (English code
comments; CJK only in locale JSON).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
@KylinMountain
KylinMountain merged commit 4b8c0db into main Jul 20, 2026
4 checks passed
KylinMountain added a commit that referenced this pull request Jul 20, 2026
Bring in #189 (Knowledge Workbench frontend) and #191 (openai==2.44.0 pin)
so the lazy-import converter branch is current with main before review.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
KylinMountain pushed a commit that referenced this pull request Jul 20, 2026
Re-implements the setup wizard natively in the new (#189) React/TS frontend,
using its conventions — react-i18next (common namespace, en+zh parity),
shadcn Dialog/Button, and the config/kb API clients.

Shown when the global config reports no LLM key (has_api_key=false):
1. Welcome.
2. Connect your model — model + API key + optional custom base URL, saved via
   patchGlobalConfig (RFC 7386 merge-patch; the key is only sent when typed and
   is never returned by the API).
3. Create your first knowledge base — createKb, then navigates to it.

"Skip for now" dismisses it. Wired in App.tsx via a getGlobalConfig() check on
mount. Build passes (i18n:check key-parity + no-hardcoded-CJK guard, tsc -b,
vite build); verified rendering steps 1–2 via headless Chromium against a
freshly-built frontend served by openkb-api.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
KylinMountain pushed a commit that referenced this pull request Jul 20, 2026
The #189 frontend gates desktop-only UI (the in-app TitleBar, offset chrome
cluster) on a `window.__OPENKB_DESKTOP__` flag. Set it from the shell so those
features light up when the UI runs inside Tauri.

The window is now created in setup() via WebviewWindowBuilder with an
initialization_script, instead of declaratively in tauri.conf.json — an init
script runs before page scripts on every navigation, so the flag is present on
the splash and (critically) after we navigate the WebView to the sidecar's
served UI. tauri.conf.json's windows[] is emptied accordingly.

Verified under xvfb (WebKitGTK): with the sidecar serving the new frontend,
the Tauri window renders the desktop TitleBar ("OpenKB Studio — Home", traffic
lights, Local badge) — confirming the flag reaches the page. cargo build clean.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
KylinMountain pushed a commit that referenced this pull request Jul 20, 2026
Re-implements the setup wizard natively in the new (#189) React/TS frontend,
using its conventions — react-i18next (common namespace, en+zh parity),
shadcn Dialog/Button, and the config/kb API clients.

Shown when the global config reports no LLM key (has_api_key=false):
1. Welcome.
2. Connect your model — model + API key + optional custom base URL, saved via
   patchGlobalConfig (RFC 7386 merge-patch; the key is only sent when typed and
   is never returned by the API).
3. Create your first knowledge base — createKb, then navigates to it.

"Skip for now" dismisses it. Wired in App.tsx via a getGlobalConfig() check on
mount. Build passes (i18n:check key-parity + no-hardcoded-CJK guard, tsc -b,
vite build); verified rendering steps 1–2 via headless Chromium against a
freshly-built frontend served by openkb-api.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
KylinMountain pushed a commit that referenced this pull request Jul 20, 2026
The #189 frontend gates desktop-only UI (the in-app TitleBar, offset chrome
cluster) on a `window.__OPENKB_DESKTOP__` flag. Set it from the shell so those
features light up when the UI runs inside Tauri.

The window is now created in setup() via WebviewWindowBuilder with an
initialization_script, instead of declaratively in tauri.conf.json — an init
script runs before page scripts on every navigation, so the flag is present on
the splash and (critically) after we navigate the WebView to the sidecar's
served UI. tauri.conf.json's windows[] is emptied accordingly.

Verified under xvfb (WebKitGTK): with the sidecar serving the new frontend,
the Tauri window renders the desktop TitleBar ("OpenKB Studio — Home", traffic
lights, Local badge) — confirming the flag reaches the page. cargo build clean.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
KylinMountain added a commit that referenced this pull request Jul 20, 2026
Re-implements the setup wizard natively in the new (#189) React/TS frontend,
using its conventions — react-i18next (common namespace, en+zh parity),
shadcn Dialog/Button, and the config/kb API clients.

Shown when the global config reports no LLM key (has_api_key=false):
1. Welcome.
2. Connect your model — model + API key + optional custom base URL, saved via
   patchGlobalConfig (RFC 7386 merge-patch; the key is only sent when typed and
   is never returned by the API).
3. Create your first knowledge base — createKb, then navigates to it.

"Skip for now" dismisses it. Wired in App.tsx via a getGlobalConfig() check on
mount. Build passes (i18n:check key-parity + no-hardcoded-CJK guard, tsc -b,
vite build); verified rendering steps 1–2 via headless Chromium against a
freshly-built frontend served by openkb-api.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
KylinMountain added a commit that referenced this pull request Jul 20, 2026
The #189 frontend gates desktop-only UI (the in-app TitleBar, offset chrome
cluster) on a `window.__OPENKB_DESKTOP__` flag. Set it from the shell so those
features light up when the UI runs inside Tauri.

The window is now created in setup() via WebviewWindowBuilder with an
initialization_script, instead of declaratively in tauri.conf.json — an init
script runs before page scripts on every navigation, so the flag is present on
the splash and (critically) after we navigate the WebView to the sidecar's
served UI. tauri.conf.json's windows[] is emptied accordingly.

Verified under xvfb (WebKitGTK): with the sidecar serving the new frontend,
the Tauri window renders the desktop TitleBar ("OpenKB Studio — Home", traffic
lights, Local badge) — confirming the flag reaches the page. cargo build clean.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
@KylinMountain
KylinMountain deleted the feat/workbench-adoption branch July 20, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant