KKTerm Architecture
July 13, 2026 · View on GitHub
Overview
KKTerm is a cross-platform desktop workspace for local terminals, SSH sessions, SFTP/FTP file browsing, URL workspaces, and remote desktop. The architecture prioritizes startup speed, local-first privacy, testable Rust source files, and a UI that can evolve toward GPU-accelerated terminal rendering without blocking the first usable prototype.
Platform Shape
- Desktop shell: Tauri v2.
- Core runtime: Rust.
- Frontend: React, TypeScript, Vite.
- Styling: Tailwind with strict CSS variable tokens.
- UI primitives: Radix UI or Ariakit.
- Icons: reicon-react, with lucide-react kept only for fallback glyphs behind
src/lib/reicon.tsx. - State: Zustand or TanStack Store.
- Storage: SQLite for non-secret local data.
- Secrets: selected credential backend. Windows and macOS default to the OS keystore; Linux uses the encrypted SQLite store.
Windows, macOS, and Linux are supported desktop release targets. Platform-specific behavior should stay isolated behind explicit capability checks so each build can preserve native OS behavior without regressing the shared architecture.
Every layout adjustment must be evaluated for all three desktop rendering targets: Windows WebView2, macOS WKWebView, and Linux WebKitGTK. Prefer standards-based shared CSS and layouts that behave consistently across all three engines. When identical behavior is not practical and a documented tradeoff is required, use this product priority order: Windows first, macOS second, Linux third. That priority decides among unavoidable tradeoffs; it does not permit silently breaking a lower-priority target. Isolate necessary differences behind explicit platform or capability checks, document the reason, and verify each available target in its real Tauri runtime.
Major Source Areas
App Shell
Owns Tauri setup, window lifecycle, command registration, menus, native dialogs, logging setup, and platform-specific capabilities.
The main app window close path stays native and free of frontend hooks. Do not add onCloseRequested, tauri://close-requested, JS-side close listeners, or app-owned close-confirmation flows on the title-bar close button — those have repeatedly broken the Windows native close button under Tauri v2. Window and layout state should be persisted during ordinary resize/move/settings flows, not during app close.
The single sanctioned exception is the Windows minimize-to-tray diversion in app_tray.rs: a native, synchronous Rust-side WindowEvent::CloseRequested arm inside the existing on_window_event handler. It calls api.prevent_close() and hides the window only when minimize-to-tray is enabled on Windows; when disabled, or on macOS/Linux, the close request is untouched and quits natively. The handler does no async work, and the tray "Exit" item (app.exit(0)) bypasses CloseRequested so a guaranteed quit path always exists. On macOS, the menu bar status item uses a monochrome template icon so AppKit can tint it consistently with the menu bar.
Automatic database backups follow the same rule: they run during startup or explicit Settings actions, never during app-window close. Startup backup files are full KKTerm settings ZIP snapshots. The visible Settings Export button creates category-aware .kkbackup files, while the visible Settings Import button accepts both category-aware .kkbackup files and full KKTerm settings ZIP snapshots.
Command Boundary
Provides a typed command wrapper between React and Rust. The frontend should not manually string-build backend calls. Commands should return structured results and structured errors.
Debug-only observability should use local debug logs, console output, or the diagnostics bundle path rather than visible in-app status indicators. Visible diagnostic UI belongs only in deliberate product features or explicit user-requested debugging surfaces, not as ad hoc instrumentation for a temporary debugging build.
Do not add command-based polling without explicit user consent. In particular, do not add recurring frontend timers that invoke Tauri commands to shell out to OS tools, inspect process state, or probe helper status just to keep an icon fresh. Prefer settings-derived UI, event updates from user-initiated actions, existing low-overhead telemetry loops, or a documented Watchdog when the user has explicitly chosen monitoring behavior.
AI Assistant context is also a command-boundary concern. Any frontend page context, backend tool output, or debug-only assistant payload must be designed as a compact projection, not a raw dump of local app state. Passive context sent on every request should contain only labels, ids, summaries, counts, small metadata, and current UI state needed for the assistant to decide what to do next. Full source code, large schemas, terminal/file contents, screenshots, data URLs, and other high-token or sensitive payloads must be attached only through explicit user actions or specific read tools with clear scope. When adding a new assistant context surface, add a test or manual check that the serialized prompt/tool result does not contain raw source fields, secret values, or avoidable bulky catalogs.
Frontend Settings
src/modules/settings/SettingsPage.tsx owns the Settings shell — the header, sidebar nav, and section routing. Each settings section is a separate page component under src/modules/settings/, owning its own draft state, save/reset handlers, and helper controls:
src/modules/settings/GeneralSettings.tsx— Language (i18n) selector, Activity Rail module visibility and drag-to-reorder controls, Auto Backup toggle and last-backup status, Windows auto-start and software update checks, Windows-only minimize-to-tray toggle, DirectX screen capture toggle, Status Bar visibility and monitor controls, the Debug group (Advanced Debugging, RDP session stability), Settings data export/import actions (openingSelectiveExportDialog.tsx/SelectiveImportDialog.tsxfrom the plain Import/Export buttons; Import also handles full ZIP backup restore), database/log folder openers, and Reset All Settings.src/modules/settings/ShortcutsSettings.tsx— the Workspace Module keyboard-shortcut section (Settings sidebar, listed above Proxy; tutorial targetsettings.shortcuts). Rows come from the catalog insrc/modules/workspace/keymap.ts, which owns the action ids, scopes (workspaceTab actions vs. focused-terminalPane actions), default bindings (Ctrl+Shift-based so they never shadow shellCtrl+letterinput; some actions ship unbound), and the event→action / conflict-detection matchers. Overrides persist inGeneralSettings.workspaceShortcuts(action-id → binding, null = explicitly unbound). The window-level capture listener for Tab actions lives insrc/modules/workspace/WorkspaceCanvas.tsx; terminal-scope actions are matched inside the focused Pane's xtermattachCustomKeyEventHandlerinsrc/modules/workspace/connections/terminal/TerminalWorkspace.tsx. Remote Desktop Panes are excluded so keys still reach the remote host.src/modules/settings/ProxySettings.tsx— the global app Proxy section (Use system settings / No Proxy / Manual HTTP/HTTPS/SOCKS5). The Proxy choice is the single source of truth for app/web network routing: it is resolved bysrc-tauri/src/net/proxy.rsand applied to everyreqwestclient (app updates, AI providers, MCP, Install Helper, favicon/currency, Copilot, web search, IT Ops webhooks) and to the URL WebView2 (resolveUrlProxyinsrc/modules/workspace/connections/webview/urlProxy.ts). "Use system settings" is reqwest's default OS-proxy detection (Windows WinINET, macOS CFNetwork, Linux env vars); a manual SOCKS5 value also routes SSH/SFTP and Telnet sessions, while HTTP/HTTPS values apply to web/app traffic only.- The main window always uses the custom React-painted title bar. Persisted/imported appearance settings must not restore native window decorations.
src/modules/settings/AppearanceSettings.tsx— App UI font family, layout reset, Color Scheme selection and preview, including tutorial targetsettings.appearance.colorScheme.src/modules/settings/DashboardSettings.tsx— Dashboard-wide preferences: default landing view, widget network-tools permission, and the active script widget cap. Per-view grid density is owned by the view row and edited from the Dashboard topbar in edit mode only.src/modules/settings/WorkspaceSettings.tsx— Workspace display preferences, including whether the top Tab Strip is hidden in favor of Child Connection Tabs under parent Connections in the Connection Tree, whether saved Connections require double-click to open from that tree, and the connected Connection rail shortcut toggle.src/modules/settings/FileExplorerSettings.tsx— File Explorer preferences, including whether local files open with the OS default app or KKTerm's inline Document/light editor and the platform-aware shell/admin preference used by the local-pane terminal action. Custom shell definitions remain owned by Terminal settings.src/modules/settings/DontSleepSettings.tsx— Don't Sleep (keep-awake) behavior preferences.src/modules/settings/InstallerSettings.tsx— Install Helper preferences, currently the automatic update-check interval.src/modules/settings/CredentialsSettings.tsx— credential backend selection, general stored-credential metadata/delete actions, and placement of the shared editable website-data records.src/modules/settings/AiSettings.tsx— AI provider kind, dynamic provider fields, provider-specific model selector, custom model ID input, API key, OpenAI/Anthropic CLI backend toggles, output language, and insecure TLS provider toggle. Provider addition rules live indocs/AI_PROVIDERS.md.src/modules/settings/SshSettings.tsx— SSH defaults, SSH terminal buffer behavior, SSH OSC 52 clipboard policy, managed VcXsrv launcher defaults for local X11 windows, and SFTP transfer defaults summary.src/modules/settings/TerminalSettings.tsx— Local terminal font, size, line height, scrollback, cursor, default shell, reusable custom shell profiles, and local terminal toggles. Do not put SSH-only terminal behavior here; SSH terminal behavior belongs inSshSettings.tsx.src/lib/fontCatalog.ts— Shared platform-aware App UI/terminal font recommendations plus subscribed custom/system font state. Both font pickers consume this catalog so one explicit refresh rescans the app fonts folder and installed OS fonts, then updates both without scanning during initial render.src/modules/settings/UrlSettings.tsx— URL Connection security, global data shard and user-agent defaults, saved website password/input-data placement, and URL data shard management. The proxy default moved to Settings → Proxy; per-Connection URL proxy overrides still win over the global value.src/modules/settings/UrlCredentialManager.tsx— shared saved website data rows, selector/value editor dialog, and delete confirmation used by both URL and Credentials Settings. The editor reads only the selected URL password throughread_url_credential_password; non-password fields remain in SQLite, and their persistedmaskedflag is presentation metadata only. Keep both Settings surfaces on this shared component so their controls and secret/non-secret boundaries remain identical.src/modules/settings/RdpSettings.tsx— Planned RDP quality defaults summary.src/modules/settings/VncSettings.tsx— Planned VNC quality defaults summary.src/modules/settings/AboutSettings.tsx— Product info, version, open-source component tables.src/modules/settings/shared.tsx— ReusableSettingsSummaryandPlannedSettingsGridcomponents.src/modules/settings/aboutData.ts— Static product metadata and open-source component groups.
src/App.tsx only routes to Settings; the persisted-settings bootstrap into the workspace store lives in src/lib/settings.ts as a single useBootstrapSettings() hook so new persisted settings can be added in one place. The stable secret owner id for the AI API key (AI_PROVIDER_SECRET_OWNER_ID) is also defined in src/lib/settings.ts so SettingsPage and bootstrap share one constant. New Settings sections should stay in the settings source area unless they become large enough to justify a sub-directory under src/modules/settings/.
Add/edit Connection dialogs that expose a "Use Settings defaults" / default-options mode must treat that mode as a group-level contract. While it is on, every field in the group displays the current Settings default, is disabled, and submits the displayed non-secret default into the Connection request for database persistence. Turning it off makes the same fields editable and saves explicit per-Connection values. For SSH, this group includes ProxyJump, SOCKS proxy address, SOCKS proxy credentials, and tmux management; do not wire the defaults toggle to only one of those fields. The SSH SOCKS proxy default now comes from the global app proxy (Settings → Proxy), not a Settings → SSH field: while the defaults toggle is on the SOCKS fields are blank and the Connection falls back to the global proxy at launch (resolve_ssh_socks_proxy applies the global SOCKS5 endpoint when the per-Connection value is blank and ProxyJump is unset); turning the toggle off stores an explicit per-Connection SOCKS endpoint that wins over the global value. SOCKS proxy passwords are secrets: per-Connection overrides store their password under the Connection id rather than in SQLite.
Settings sections use one shared visual grammar. A top-level page is a single settings-card settings-section; related controls inside that page are grouped with fieldset elements using settings-subsection settings-fieldset, with a translated legend that sits in the border. This fieldset treatment is the canonical group-box style for General, Appearance, AI Assistant, SSH, Terminal, URL, RDP, VNC, and future Settings sections. Avoid one-off nested cards, heading-only group boxes, or custom bordered panels inside Settings.
Related Settings rows should stay in the same grouped card whenever they share a subsection. Do not split one-topic controls into standalone one-row cards unless the control is conceptually separate or needs a distinct action area; prefer consecutive row blocks inside the same fieldset so Settings CSS can merge them into one card divided by row rules. Platform-specific Settings subsections should be hidden when the current platform has no available rows.
Controls should communicate state consistently. Editable Settings text boxes and selects should use the editable surface styling in the default color scheme; disabled and readonly controls stay muted so they read as unavailable. Keep provider model choice as a real <select> and custom model/deployment IDs in a separate input. Toggle rows inside Settings groups should use the existing settings-toggle-list and settings-toggle-row structure. Delete buttons inside Settings pages should be icon-only red trash can buttons, centered in their row, with translated accessible labels and no visible "Delete" text.
Technical text inputs should opt out of OS spelling assistance through src/lib/inputBehavior.ts (technicalInputProps: autoCorrect="off", autoCapitalize="off", spellCheck={false}). Use this for usernames, passwords, hosts, IP addresses, URLs, local/remote paths, serial lines, commands, cron expressions, encoded text, and source-code fields. The policy is frontend/WebView-level and is intended to apply on both Windows WebView2 and macOS WebKit; platform IME candidate windows and keyboard-level suggestions can still appear outside KKTerm's DOM control. Do not apply the technical policy to prose surfaces such as Notes or ordinary assistant chat where spellcheck may help.
Global Settings data actions live in General → Settings data. Backup, Import, database folder opening, and Reset All Settings belong there; feature pages should not grow their own global reset buttons. Destructive Settings actions must use app-owned translated dialogs, never browser-native window.alert, window.confirm, or window.prompt. Reset All Settings resets persisted Settings pages to defaults, closes open Sessions, resets saved layouts, and removes the saved AI API key, but it must not delete saved Connections.
Internationalization
The i18n layer lives in src/i18n/ and uses i18next with react-i18next.
src/i18n/config.tsowns the i18next instance, language detection (localStoragekeykkterm.language), dynamic locale chunk loading, theswitchLanguage()API, and theensureI18nReady()startup guard.src/i18n/useT.tsprovides a typeduseT()hook with full key autocompletion from the English locale shape.src/i18n/locales/en.jsonis the source-of-truth translation file (19 namespaces, ~3,500 keys) and defines the canonical namespace/key order for every locale. English is bundled with the app; the 13 other locales (fr,it,de,es,es-MX,pt-BR,zh-TW,zh-CN,ja,ko,th,id,vi) load on demand via dynamicimport()and are automatically code-split by Vite.- Settings → General → Language exposes a dropdown that calls
switchLanguage(), which hot-swaps the locale bundle and persists the choice. - All user-visible strings must go through
t()oruseTranslation(). Hardcoded English text in JSX is forbidden. New or changed keys go intoen.jsonfirst and must get a matching one-key file underdocs/localization_todo/in the same change, even when an AI session adds best-effort translated values. Keep all 13 other locale files structurally aligned in the same relative key order; direct translations count as complete only after an intentional localization pass translates every non-English locale, preserves placeholders, follows regional terminology rules, verifies the result, and deletes the matching todo file. Renamed or removed keys must be updated in every file and indocs/localization_todo/. Runnpm run i18n:normalizeafter broad locale edits andnpm run i18n:checkbefore finishing; the check rejects missing, redundant, and misordered keys. Pure helper functions that cannot use React hooks importi18nextfromsrc/i18n/config.tsand calli18next.t(key). - Prefer context-specific keys over reusing one key for the same English word. English frequently collapses meanings that other languages distinguish — e.g. "Play" is one word for starting media, running something, and a theatrical play, but each sense translates differently. Add a separate key per usage context (the meaning, not the spelling, decides) even when the English label is identical, and give it a name that encodes the context (
workspace.startSession, not a sharedcommon.play). Reuse a key only when the meaning is genuinely the same everywhere it appears; do not deduplicate keys merely because their English text matches. The same caution applies to single words whose grammatical form shifts with context (gender, plural, formal/informal address). - Related regional locales must not borrow terminology from each other. Cross-locale translation bleed is forbidden even when scripts, spelling, or many words overlap:
zh-CNandzh-TW,es-ESandes-MX, andpt-PTandpt-BRmust use their own script, spelling, and regional terminology. zh-TW is a critical case: it must use Taiwan computing terminology, never Mainland Chinese terms (even in traditional characters). Seedocs/manual/16-localization.mdfor the full forbidden→required term mapping table (e.g. 連線 not 連接, 終端機 not 終端, 儲存 not 保存, 預設 not 默認, 資料 not 數據, 伺服器 not 服務器, 客戶端 not 用戶端, 使用者 not 用戶, 程式 not 程序, 螢幕 not 屏幕, 萬用字元 not 通配符, 介面 not 接口). Never copy fromzh-CN.jsonand convert characters — the vocabulary itself differs. - Keep interpolation placeholders translation-safe. Use named i18next placeholders (
{{count}},{{host}}) so translators can reorder them — word order and grammar differ per language, and a placeholder that is fixed in the middle of an English sentence may need to move to the front or end in another locale. Keep one complete sentence per key; never assemble a sentence by concatenating several keys or by gluing fragments around a variable, because the surrounding words are then un-reorderable. Use stable, descriptive placeholder names, list every placeholder in the matchingdocs/localization_todo/entry, and verify each{{…}}token survives byte-for-byte in every locale — translators sometimes translate, localize, drop, or duplicate the token, which silently breaks substitution. Prefer i18next plural/context features over hand-built English-shaped string math.
Storage
Owns current SQLite schema initialization and repositories for:
- connection tree nodes
- saved connections
- settings
- UI layout
- recent sessions
- non-secret AI provider metadata
- non-secret SSH tmux launch preferences
- non-secret SSH port forwarding mappings
- dashboard views, widget instances, and AI Created Widget definitions (see
docs/DASHBOARD.md)
Plaintext secrets are never stored in SQLite. When the encrypted SQLite backend is selected, SQLite stores only encrypted secret rows plus KDF/cipher metadata.
Secrets
Owns secret storage behind a backend abstraction:
- Windows Credential Manager / DPAPI path
- macOS Keychain
- password-encrypted SQLite store, unlocked with
KKTERM_SECRET_STORE_PASSWORD - Linux Secret Service / KWallet/libsecret later
- External password-manager backends such as Bitwarden, KeePassXC, and 1Password later
Secrets include passwords, SSH passphrases, and AI API keys.
Reusable Connection password metadata lives in SQLite in connection_password_credentials; plaintext password bytes still live only in the configured secret backend under the credential id with secret kind connectionPassword. SSH, Telnet, RDP, VNC, and FTP Connections may point at one of these credential ids via password_credential_id, while legacy per-Connection passwords stored under the Connection id remain valid. Add/Edit Connection surfaces same-type saved password choices by metadata only; it must not read secret values into React.
Connection Model
Represents all openable resources as saved connections. Current connection types:
- Local Terminal
- SSH Terminal
- Telnet Terminal
- Serial Terminal
- URL
- RDP
- VNC
- FTP/FTPS
- File Explorer (
localFiles) - Document (
fileView)
SFTP is a related workspace surface opened from an SSH Connection, not a standalone saved Connection type. FTP/FTPS is a standalone Connection type that routes through the same file-browser workspace with FTP command adapters. File Explorer (localFiles) is a standalone Connection type that routes through the same file-browser workspace (SftpWorkspace) as a single-pane local browser driven by local filesystem commands: listing reuses list_local_directory, item details use local_path_properties, and it has no remote host, network Session, connection-status indicator, or transfer-activity footer. The shared local pane exposes an ephemeral local-terminal action rooted at its current directory; in dual-pane SFTP/FTP surfaces the action is local-side only and never appears on the remote pane. Normal launches create a runtime-only app-owned terminal popup at the Workspace Canvas level, outside the workspace Tab list and without Activity Rail Session registration. Closing the popup unmounts the terminal and ends its PTY. Non-elevated Windows apps use the bounded launch_elevated_terminal command for the three administrator variants and pass the local directory as the external process working directory.
Document (fileView) is a standalone Connection type that opens one local file in a universal viewer / light editor surface (FileViewerWorkspace, tab/pane kind fileViewer), with no remote host or network Session. The target file path is stored in the local_startup_directory column (reused as the file path). A per-Connection file_view_open_external flag opens the target through the operating system default app instead of creating an in-app Tab. The backend exposes three bounded, spawn_blocking file-read commands in src-tauri/src/file_viewer.rs — probe_file_view (size, mtime, text heuristic, magic-byte signature), read_file_view_text (capped UTF-8 head or tail), and read_file_view_bytes (capped base64 chunk) — every read is size-capped so a huge file cannot exhaust memory. The frontend fileViewerModel routes the file to a viewer mode (text/code via CodeMirror, Markdown via marked+DOMPurify, table via papaparse, JSON, image, a dedicated Log mode with selectable parser signatures, level coloring/filter/ANSI/follow-tail, PDF via the dependency gate, an explicit unsupported-choice page for unsupported document containers, and a Hex fallback) from extension plus the probe; the user can switch modes from the viewer toolbar. PDF and image/SVG modes expose a viewer-options hamburger with Background, reusing the shared Dashboard/File Explorer background picker and storing the choice per durable Document Connection in the existing Connection background payload so it survives app launch. All in-app viewer libraries were already bundled, so those modes add no new runtime dependency. The text/code mode is also a light editor (Phase 3): CodeMirror provides undo/redo and a Ctrl/Cmd+S save, and write_file_view performs an atomic, conflict-checked save — it writes a sibling temp file, copies the original's permissions, then renames it over the target (atomic on the same filesystem), refusing the write with a FILE_VIEW_CONFLICT error when the on-disk mtime changed since load (unless the user confirms an overwrite). Editing is gated by isEditableText to whole, cleanly-decoded UTF-8 text; truncated or non-UTF-8 (lossy) loads stay read-only so a save can never lose or corrupt data.
Some file types render through an external dependency downloaded on demand rather than bundled (Phase 2). PDF is the first: the poppler Install Helper recipe (kind githubRelease) provides pdftocairo/pdfinfo, and file_viewer.rs resolves the binary from the Install Helper-managed install dir (installer::detect::github_release_install_dir) or PATH. file_view_pdf_status reports availability and render_pdf_view rasterizes one page to a PNG. The frontend fileViewerDependencies map links the pdf viewer kind to the poppler recipe; PdfDependencyGate checks status and, when missing, installs the dependency in-context via the Install Helper (installRecipeAndWait, Windows) or asks the user to provide it on PATH (other platforms), then mounts PdfViewer. New dependency-backed file types follow the same pattern: add a catalog recipe, a fileViewerDependencies entry, a backend resolve/render path, and a gate.
Every saved Connection (and ConnectionFolder) belongs to exactly one Workspace via a workspace_id column (schema v20). A Workspace is a named, isolated container of Connections; the seeded permanent Default Workspace has the stable id default. The Activity Rail hosts a Workspace switcher (src/app/ActivityRail.tsx) with a New Workspace wizard (src/modules/workspace/NewWorkspaceDialog.tsx); creating a Workspace can copy-import Connections from other Workspaces. The frontend's active Workspace (activeWorkspaceId, persisted in localStorage) is passed to list_connection_tree and threaded onto create_connection / create_connection_folder; switching it re-scopes Workspace navigation and never closes open Sessions. New frontend Tabs remember the active Workspace id that opened them. When the top Tab Strip is enabled, it shows only Tabs owned by the active Workspace and Workspace switching activates the first open Tab in the destination Workspace, or clears the active Tab when none are open there. The hidden Tabs from other Workspaces remain mounted so their live Sessions stay attached. Workspace CRUD is exposed through the list_workspaces / create_workspace / rename_workspace / delete_workspace / reorder_workspaces Tauri commands; list_connection_tree without a workspaceId still returns the full cross-Workspace tree for export, import validation, and AI context.
SSH Connections may store a non-secret useTmuxSessions preference. This value describes how future terminal Sessions should launch; it does not represent a live remote process.
RDP Sessions are Windows-native child controls hosted through the Microsoft RDP ActiveX COM control in mstscax.dll. The Rust backend creates and drives the control from Tauri commands while the frontend owns Tab/workspace placement. VNC Sessions use the Rust vnc-rs client for RFB handshakes, password auth, framebuffer updates, CopyRect handling, and pointer/key input; the frontend renders RGBA framebuffer rectangles into a canvas in src/modules/workspace/connections/remote-desktop/RemoteDesktopWorkspace.tsx.
RDP ActiveX is a native child HWND, not a DOM element. It can draw above React dialogs and overlays regardless of CSS z-index; this is the Win32/WPF "airspace" limitation, not a stacking-context bug. Simple command menus avoid this problem by using Tauri native context menus through src/lib/nativeContextMenu.ts, so Quick Connect, Add Connection, Connection Tree context menus, Activity Rail Connection context menus, Tab context menus, and screenshot toolbar menus do not participate in DOM overlay parking. When a real DOM overlay must appear above an active RDP view, src/modules/workspace/nativeOverlay.ts detects whether a registered overlay actually intersects the RDP host rectangle. Only then does src/modules/workspace/connections/remote-desktop/RemoteDesktopWorkspace.tsx capture the current RDP host rectangle through the screenshot command, render that transient bitmap inside the workspace, and hide/park the ActiveX HWND until the overlay is gone. This preserves the user's visual context for dialogs and Region selection without blanking the active workspace for ordinary menus.
App-owned blocking dialogs opened from contained panes or panels must mount at the app-window level through src/app/DialogPortal.tsx (or an equivalent createPortal(..., document.body) call when local state makes a shared wrapper awkward). Do not render .dialog-backdrop.connection-dialog-backdrop directly under .connection-sidebar, .assistant-panel, terminal Panes, or other layout-contained surfaces: those parents use containment, overflow, resizing, or native child surfaces that can make a fixed-looking dialog size itself to the pane and get cut off. Any dialog UI adjustment must check the full z-order of parent dialogs, child dialogs, popovers, native overlay suppression, and shared backdrop classes so newly opened dialog surfaces cannot hide behind their opener. Pane-local popovers and command menus may stay inline when they are deliberately anchored to a button, cursor, or row. tests/dialog-portal-policy.test.mjs guards the known contained-pane blocking dialogs and should be extended whenever a new pane-owned full-window dialog is added.
Overlay golden rule — every transient popup floats above every dialog backdrop, always. This is the same stacking-context trap as the contained-pane dialogs above, and it bites any overlay rendered inside a positioned ancestor that owns its own z-index. A high z-index on the overlay does not rescue it: CSS resolves it only within the nearest ancestor stacking context, so a child of .status-bar (position:relative; z-index:80) can never paint above a dialog backdrop (z-index:130+) no matter how large its own value is. The fix is structural, not numeric — portal the overlay to document.body so its z-index is resolved at the document root. The bottom Status Bar transient notice popup (showStatusBarNotice → StatusNoticePopup, anchored by .status-bar-notice-area, src/modules/workspace/StatusBar.tsx) follows this: it is wrapped in DialogPortal precisely so it escapes the .status-bar stacking context and stays on top of Settings and every other dialog. When adding a new always-on-top surface (notice, banner, toast-like popup), mount it through the app-window portal and keep .status-bar-notice-area's z-index above every blocking backdrop; tests/dialog-portal-policy.test.mjs guards both the portal and the z-order.
Dialog dismissal controls are intentionally single-path. If a popup dialog footer contains a bottom-right dismiss action such as Cancel, Skip, Later, or Close, do not also render a title-bar close X. If no footer dismiss action exists, render the close X with the shared connection-dialog-close header class (or the existing MCP close button class) so the control is absolutely anchored to the dialog header's top right and the header receives enough right padding to avoid overlapping the title.
Do not turn overlay detection back into a document-wide boolean. RDP is the only workspace family that uses the screenshot-backed parking workaround. URL WebView2 Sessions use stable top-level WebviewWindow overlays owned by the main window; the frontend positions them over the Pane rect, hides them when inactive or covered by registered blocking overlays, and repushes bounds on DOM resize/scroll plus native tauri://move/tauri://resize. Do not reintroduce Tauri's unstable child-webview API for URL Connections: that API changed the main app WebView2 construction path from normal window content to a child HWND, which made terminal keyboard routing unreliable after Alt+Tab/app switch and minimize/restore. xterm could still report DOM focus while WM_KEYDOWN never reached the terminal until the user clicked back into the Pane. Ordinary menus should rely on native menu popups or the app's normal stacking/layout. If another native-surface overlay bug is proven in the real Tauri runtime, document the exact failure mode and add a narrow surface-specific selector or an explicit owning-component suppression path rather than reusing RDP parking.
When KKTerm itself runs inside a remote session (launched over mstsc on an RDP host), ending and resuming the mstsc connection tears down the host's display/GPU device underneath WebView2. WebView2's GPU process loses its DirectComposition device and the renderer hangs on reconnect — the native heartbeat thread keeps logging while the frontend heartbeat ages out (see src-tauri/src/debug_heartbeat.rs). Because the --disable-gpu workaround did not stop every such hang, each heartbeat line also records mainThreadPongAgeMs (a run_on_main_thread round-trip that ages out only when the native UI/event-loop thread is itself blocked — so it separates a WebView2 renderer hang from a native UI-thread stall), remoteSession (SM_REMOTESESSION, marking the mstsc connect/disconnect transition without registering for WM_WTSSESSION_CHANGE), gdiObjects/userObjects (this process's GDI/USER handle counts, to catch the documented WebView2 GDI region-handle leak around RDP redraws), and lastScaleFactor/scaleFactorAgeMs (from WindowEvent::ScaleFactorChanged, to correlate a DPI change on reconnect with the freeze). These fields exist to localize the next hang instead of guessing at its cause. To survive this, the main window is created in Rust in setup (not in src-tauri/tauri.conf.json, whose app.windows is intentionally empty) so it can pass webview::REMOTE_SESSION_WEBVIEW2_ARGS (--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection,CalculateNativeWinOcclusion --disable-gpu) through WebviewWindowBuilder::additional_browser_args. URL overlay WebviewWindows are created through the same stable builder and receive the same args when the main app WebView does. When a URL proxy is also active, the overlay builder appends --proxy-server to those explicit WebView2 arguments because Wry does not merge generated proxy arguments after custom browser arguments have been supplied. The flags force software compositing (no GPU device to lose) and disable native-window occlusion throttling for the main app WebView. The env var WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS cannot be used here: wry always sets WebView2's AdditionalBrowserArguments itself, which makes the runtime ignore that env var, and setting the option replaces wry's defaults — so wry's defaults are re-included in the constant. The flags are applied when the user enables the General → Debug "RDP session stability" setting (rdp_webview_stability) or when is_remote_session() (GetSystemMetrics(SM_REMOTESESSION)) detects a remote session, so local installs keep full GPU acceleration. Because the args are fixed at window creation, the setting takes effect on the next launch. This is a WebView2 runtime workaround and is unrelated to the RDP ActiveX parking path above.
Native context menu icons are not SVGs passed directly to Tauri. Menu builders pass existing image assets, such as Connection PNGs or URL favicon data URLs, as iconSrc; command-only glyphs use app-owned line-icon-style SVG strings from src/lib/nativeMenuIcons.ts as iconSvg. src/lib/nativeContextMenu.ts rasterizes image sources to 16px PNG bytes with an offscreen canvas, creates Tauri Images through Image.fromBytes, caches the result, and creates explicit Tauri IconMenuItems for icon-bearing entries. On macOS, command-only iconSvg entries must resolve through macosTemplateNativeIconsBySvg to a Tauri NativeIcon template image so AppKit tints icons with the native menu text color in normal, disabled, and selected states; add an alternative native template icon there when no exact symbol exists, and keep tests/native-context-menu-tauri-shape.test.mjs covering every nativeMenuIcons entry. Keep Tauri's image-png Cargo feature enabled; without the image feature, Windows native menu icons may render as text-only even though the menu itself opens. This keeps native menus visually aligned with the existing DOM tree/rail icons without adding one-off PNG assets or reintroducing DOM overlays.
If a future design truly needs a live overlay over RDP without parking the ActiveX host, the practical options are outside normal React layering: render that overlay as a native popup/owned HWND, clip or reshape the RDP child HWND around the overlay region, or replace the ActiveX path with a renderer that draws into KKTerm's own canvas/texture stack. Native popup HWNDs match how many Win32 desktop apps keep menus above embedded controls, but they add DPI, focus, accessibility, styling, and coordinate-sync complexity. Replacing the RDP renderer would avoid the airspace issue but moves KKTerm into owning RDP protocol/rendering concerns such as authentication, clipboard, display resize, and device redirection.
RDP sizing has an important diagnostic trap: gray left/right gutters or a visible resize after switching Tabs can be caused by frontend workspace layout changes, not by the RDP transport itself. In particular, global chrome such as the right AI Assistant panel must keep one workspace-wide width/collapsed state; it must not load per-Connection panel layout on Tab activation. Per-Connection assistant panel state changes the workspace width during Tab switching, which then forces the native RDP ActiveX HWND to resize and can look like an RDP display-sync bug. When investigating RDP gutters, first verify that remote-desktop-workspace bounds and app chrome widths stay identical before and after Tab activation.
RDP sizing separates presentation scaling from remote display renegotiation. The default settings.rdpRemoteResolutionAutomatic mode is a live client-area mode: it seeds DesktopWidth/DesktopHeight before Connect, normally keeps ActiveX SmartSizing disabled, forwards the WebView-reported pixel scale, and tracks the Pane's physical pixel size so the remote OS renders at the right scale and pointer coordinates stay 1:1. Use the frontend window.devicePixelRatio for RDP overlay coordinate conversion instead of only the native window scale factor, because Windows text-size accessibility scaling can affect WebView CSS-to-physical pixels without changing the monitor DPI reported by Tauri. Microsoft documents 200 physical pixels as the minimum DesktopWidth and DesktopHeight; when either native Pane dimension falls below that limit, KKTerm requests the 200-pixel minimum and temporarily enables SmartSizing so the ActiveX surface remains fitted to the smaller Pane instead of overflowing adjacent Panes. Fixed resolution modes and the internal smartSizing mode are presentation-fit modes: they keep SmartSizing enabled and resize/reveal the native child control in place without changing the remote desktop size. This mirrors RDCMan's split between Scale (local SmartSizing) and Reconnect (remote desktop resized to the client area).
The RDP backend commands are not interchangeable, because they differ in whether they leave the ActiveX HWND on-screen. For Pane-tracking resolution modes such as settings.rdpRemoteResolutionAutomatic, sync_rdp_display_size (and the shared stage_rdp helper) deliberately moves the control off-screen to HIDDEN_RDP_POSITION while it prepares the initial display; it is a staging primitive, and it only works because the normal connect/reveal flow always follows it with a set_rdp_visibility { visible: true } (show_rdp) that brings the control back on-screen. It must respect the cached-size gate and must not force a remote resize when the startup DesktopWidth/DesktopHeight already match the Pane. update_rdp_bounds (show_and_resize_rdp) is the on-screen path: it shows the control at the requested rectangle, re-applies SmartSizing, and only re-applies the remote desktop size when the selected resolution mode tracks the Pane. Therefore any code that needs to re-apply visible RDP bounds while the session must stay visible - for example the post-connect settle passes - must go through update_rdp_bounds, never sync_rdp_display_size. Calling a staging resize path without an immediate follow-up reveal parks the desktop off-screen and leaves the pane blank even though the toolbar reads "Connected" (the regression fixed in PRs #180/#181). When a tracking-mode remote resize must run even though the cached desktop size is unchanged, pass update_rdp_bounds { force: true }; the cached-size gate in should_resize_remote_desktop otherwise skips the redundant resize, and that gate compares both pixel dimensions and the DPI scale factors so a scale-only correction still re-applies. Dynamic remote resize uses UpdateSessionDisplaySettings. Do not silently fall back to ActiveX Reconnect(width, height) in the default lifecycle: RDCMan exposes reconnect as a separate resize strategy, and GNOME Remote Desktop/Ubuntu 24.04 can disconnect when Reconnect is invoked during the establishing/login window.
Bounds changes emit paired rdp.geometry.frontend and rdp.geometry.native records to ui.debug.log, keyed by Session id, so DOM, DPI conversion, outer ATL host, inner ActiveX object, SmartSizing, and remote desktop dimensions can be compared without logging Connection hostnames or usernames. In an embedded Panorama Pane, the frontend rectangle must be intersected with the owning .embedded-workspace-pane before it crosses the native command boundary; native RDP windows correctly following an overflowing descendant DOM box would otherwise cover adjacent Connections.
Connection Tree
Owns root-level saved Connections, optional folders, subfolders, search/filter, drag/drop ordering, rename/delete/duplicate, quick connect, optional Child Connection Tabs, and open-session status badges.
Current implementation note: a Connection may have no folder and live directly in the root of the tree. Folders may contain Connections and subfolders. Status badges are derived from active frontend workspace Sessions. Durable Connections load as idle and do not persist live session state in SQLite.
The persisted Workspace setting doubleClickOpensConnection is off by default: saved Connection rows open on single click. When enabled, a saved Connection row opens only from the row double-click handler. Renaming saved Connection rows remains available through the context-menu connections.rename action.
Child Connection Tabs are frontend workspace state, not nested durable Connections. They are shown as italic rows under a parent Connection when settings.hideTopTabButtons is enabled. A Child Connection Tab stores the Workspace id, child row id, parent Connection id, user-facing child name, optional icon/background override, optional terminal opacity/background override, optional tmux session id, optional last terminal working directory, and, for File Explorer-opened Document children, the local file path in local workspace storage (kkterm.workspace.childConnections.v1). Startup does not connect these children: selecting a child row creates the live Session lazily, and selecting the parent Connection opens all of its active Workspace's Child Connection Tabs together in a generated split layout. When the parent has no Child Connection Tabs yet, selecting the parent creates the first Child Connection Tab and shows that child layout instead of opening the parent as a standalone Session. File Explorer inline-opened files become Document child Panes under the parent File Explorer Connection so they do not create standalone connected Activity Rail items when the top Tab Strip is hidden. The tree only renders child rows and open child-tab locations whose stored Workspace id matches the active Workspace; legacy rows without a stored Workspace id belong to the Default Workspace. Reopening or reselecting a live parent child-layout Tab must preserve the last focused child Pane when that Pane is still present, clear any single-Pane maximize state, and allow terminal input focus to return to that Pane.
For tmux-enabled SSH Connections, per-Pane tmux session names are generated and remembered in the frontend workspace layer so split Panes can resume independently. New Pane names are drawn directly from the active locale's ai.tmuxSessionLabels pool, so the actual remote tmux session name is localized rather than an English slug with a translated display label. The locale pools do not need to map one-to-one with English. The frontend stores these Pane names under kkterm.tmuxSessions.<connectionId> so the same Connection can reopen its previous Pane-to-tmux mapping. Stored Pane ids are reused as literal remote tmux names when they are tmux-safe (no whitespace, colon, semicolon, or control characters). The durable Connection stores only the launch preference and non-user-facing namespace fields; those fields are not the active Pane tmux session id.
Backend Command Runtime Boundaries
UI liveness invariant: no Tauri command may block the frontend/native UI thread for Install Helper or Session work. Any operation that can touch the filesystem, SQLite, the Windows Registry, child processes, service control, sockets, or the network must run through a background worker (tauri::async_runtime::spawn_blocking, run_blocking_command, or an explicit worker thread that reports progress by event). A foreground wait is allowed only for a deliberately requested, documented exception where the user explicitly wants KKTerm to wait.
Tauri commands that need synchronous native integrations, OS process calls, network control loops, or helper functions that internally create and block_on a Tokio runtime must cross that boundary with run_blocking_command/tauri::async_runtime::spawn_blocking before calling the helper. Async command handlers must not directly call code that starts a nested runtime or blocks the current Tokio worker; doing so can panic with "Cannot start a runtime from within a runtime" and can also starve unrelated Connection types. This invariant applies across all live Session families: local terminal helpers, SSH/tmux/AI context inspection, SFTP transfers, URL external-open stubs, RDP ActiveX operations, and VNC RFB operations. When adding a command for any Connection or Session type, choose one of these shapes explicitly: pure async all the way down, synchronous command with no nested async runtime, or async command that immediately moves the blocking/nested-runtime work into run_blocking_command.
Connection startup must keep the frontend event loop and Tauri command runtime available even when a remote host is down or black-holed. DNS lookup, TCP connect, SSH host-key inspection, authentication, protocol handshakes, initial directory listings, and similar preflight work must be bounded by an explicit timeout and must not run in a synchronous Tauri command handler. SSH/SFTP host-key inspection is part of Session startup for this purpose, not a harmless metadata read. Future Connection types should return from the command boundary through a pure async future or a blocking worker while the Tab renders an in-progress state; do not wait on network availability from React render/effect code, a sync command, or a main-thread native callback. RDP is the narrow exception that creates and controls the ActiveX host on the main thread, but new RDP work must still avoid synchronous waits for remote network readiness on that thread.
Terminal Session
Owns local PTY lifecycle, SSH terminal channel lifecycle, Telnet TCP lifecycle, Serial port lifecycle, input/output streams, resize events, tab integration, split pane integration, and terminal compatibility behavior.
Lifecycle invariant: switching the active workspace Tab must not disconnect, close, or recreate a local terminal Session, SSH terminal Session, or SFTP Session. Open Tab surfaces stay mounted while inactive so their live Sessions remain attached. Explicit tab close from the tab strip is the user-owned teardown action for the Session or Sessions presented by that Tab.
Local-shell environment: a spawned local shell must see the same environment a user would get by launching that shell directly — including user-defined variables (e.g. ANTHROPIC_BASE_URL set via setx). On macOS/Linux this is the inherited process environment plus KKTerm's explicit overrides. On Windows, KKTerm does not inherit its own (long-lived, possibly stale, WebView2-polluted) process environment; sanitize_windows_local_environment rebuilds the block from the registry via CreateEnvironmentBlock so user/system variables flow through and stay current after setx. KKTerm's overrides (TERM/COLORTERM, the OneDrive-aware PSModulePath redirection, and managed/CLI-account variables) are layered on top; the legacy curated allowlist remains only as a fallback if that API fails. psmux launches must forward refreshed PATH and managed/CLI-account variables with new-session -e so panes created under an existing psmux server do not inherit the server's stale environment.
When an SSH Connection has tmux enabled, each terminal Pane starts by attaching to or creating its generated tmux session with tmux new-session -A -s <name>. Native russh sessions and system ssh fallback sessions use the same remote startup behavior. If tmux is not installed on the remote host, KKTerm starts a normal interactive shell instead and the Pane toolbar must suppress tmux controls for that live shell. When tmux negotiation succeeds, the Pane toolbar shows the tmux session id and can list or close remote tmux sessions without logging terminal contents.
Native SSH terminal Sessions do not set an app-side inactivity timeout; quiet and unfocused Sessions should remain connected. If a tmux-enabled native SSH terminal channel unexpectedly closes after startup, the SSH runtime attempts a small bounded silent reattach to the same Pane tmux session id. This is recovery for a broken transport, not a replacement for normal Session ownership: explicit Tab close still tears down the frontend Session, and non-tmux shells are not auto-restarted because that would create a fresh remote shell rather than resume existing live state.
Terminal Engine
Owns terminal parsing/state and exposes a renderer-neutral model. Evaluate alacritty_terminal first.
The engine boundary should make rendering swappable by separating:
- terminal state/input
- glyph atlas/shaping
- scrollback
- selection/copy
- cursor rendering
- dirty-region updates
Terminal Renderer
Milestone A may use the fastest reliable terminal view that can prove session lifecycle and product UX. Milestone B introduces or replaces it with WGPU rendering. The renderer must be behind an internal interface from the start.
The current Milestone A renderer is xterm.js with the @xterm/addon-webgl GPU glyph renderer attached opportunistically inside XtermTerminalRenderer.open (src/modules/workspace/connections/terminal/renderer.ts). The addon is loaded after Terminal.open(element) because it needs the host element to mount its WebGL2 canvas. If the WebglAddon constructor throws (no WebGL2, blocklisted driver, headless RDP) or loadAddon rejects it, the renderer silently stays on the xterm DOM renderer. When the GPU context is later evicted (sleep/wake, GPU reset, driver crash), WebglAddon.onContextLoss fires; the renderer disposes the addon and xterm transparently falls back to the DOM renderer for subsequent frames. The renderer is not recreated and the Session is not torn down. Font-family changes follow the same live-Session rule: mounted local/SSH renderers update fontFamily, coordinate texture-atlas refresh across every live renderer, redraw, and refit; the explicit kkterm:custom-fonts-loaded event repeats that refresh when asynchronous custom faces finish after document.fonts.ready. xterm WebGL renderers with matching font/theme/DPR share one glyph atlas, so never clear and redraw only one terminal: clear every live renderer first, then redraw every renderer in a second pass, or hidden sibling Tabs retain stale texture coordinates and render corrupted text when shown. Atlas refresh diagnostics are written as terminal.font_atlas_refresh records and Tab activation snapshots as terminal.font_atlas_state records in ui.debug.log. This keeps the renderer abstraction unchanged while removing CPU pressure on heavy output (build logs, journalctl -f, multi-pane Tabs).
Terminal Connection backgrounds use the same shared background picker datasource as Dashboard Views: src/modules/dashboard/edit/SharedBackgroundPopover.tsx consumes BACKGROUND_PRESETS, DYNAMIC_BACKGROUNDS, media import/load helpers, fit options, and dim controls for dashboard.changeBackground, terminal.background, Document viewer backgrounds, and IT Ops drill-view backgrounds. Supported custom media is validated as PNG/JPEG/WebP/GIF/BMP/SVG images or MP4/WebM/MOV/M4V/OGV videos. Future background presets, dynamic backgrounds, media modes, supported extensions, fit options, or picker layout changes must flow through that shared datasource/component so the Dashboard, terminal, Document viewer, and IT Ops lists stay identical. New terminal Connections default to 50% transparency (terminalOpacity 50). Settings - SSH and Settings - Terminal expose settings.defaultTransparency and settings.randomDynamicBackgroundOnCreate; SSH settings apply to SSH terminal Connections, and Terminal settings apply to local/Telnet/Serial terminal Connections. The random dynamic toggle is creation-only: new terminal Connections, top-strip new Tabs, and Child Connection Tabs may receive an initial dynamic background, while existing Connections/Tabs are unchanged. By default, a terminal Connection Tab paints one background behind the terminal workspace content area. Child Connection Tabs persist terminal font size, opacity, and background separately from their parent Connection. The persisted Workspace setting separateSplitTerminalBackgrounds is off by default; when enabled and a terminal Tab has multiple terminal Panes, each Pane may store a separate terminalBackground in the serialized terminal layout. Missing or invalid persisted settings normalize through the Settings/storage validation boundary, clearing back to the default shared-background mode.
SSH Transport
Owns in-process SSH connections, host key verification, authentication, terminal channels, resize propagation, idle behavior, bounded tmux reattach behavior, optional system ssh fallback/debug, native SOCKS5 proxy dialing for terminals/tmux/SFTP/key-transfer (no external tools; mutually exclusive with ProxyJump), and noninteractive remote tmux management commands. The SOCKS5 endpoint is the per-Connection override when set, otherwise the global app proxy (Settings → Proxy) when it is a manual SOCKS5 proxy — applied in resolve_ssh_socks_proxy and never when ProxyJump is configured. Telnet (telnet.rs) dials the same global SOCKS5 endpoint when one is configured.
SOCKS support is for true SOCKS5 endpoints. Do not add protocol guessing that treats the SSH SOCKS field as an HTTP CONNECT proxy listener; keep HTTP proxy and ProxyJump semantics separate unless a new product design explicitly introduces them. A proven proxy edge case is ssh.debug.log showing connection.socks.connect_ok, followed by repeated connection.disconnected.error entries with error: "early eof", while PuTTY or OpenSSH reproduces the same drop through the same SOCKS server. Treat that as a proxy/TCP-path close surfaced through the SSH transport. Preserve the bounded tmux reattach behavior and diagnostic logging, but do not hide persistent proxy-side disconnects with peculiar app-specific retry loops.
Evaluate russh first. Evaluate ssh2 if russh does not meet v0.1 needs. Legacy SHA-1-era SSH key exchange support is opt-in only: settings.sshOldProtocols supplies the global default, and each SSH Connection may override it through the single connections.sshOldProtocols switch. The switch enables the complete supported legacy compatibility set; do not expose individual legacy-algorithm choices. Keep modern algorithms as the default and append legacy KEX only for trusted older hosts.
SFTP
Owns SFTP sessions launched from SSH Connections, local/remote listing, multi-select upload/download by button or drag/drop, create folder, inline rename, delete, refresh, scoped context menu actions, remote properties, chmod/chown updates, transfer queue, progress, cancellation, finished-history clearing, overwrite conflict prompts, overwrite-all queue handling, and "open terminal here."
SFTP vs SSH invariant — pane.kind is authoritative for the rendered surface; do not infer it from connection.type. An SFTP browser Pane intentionally carries an ssh-typed Connection: SFTP is launched from an SSH Connection (openSftpBrowser), and an sftp-protocol FTP Connection is normalized to an ssh shape at Pane creation via sftpBrowserConnectionFromFtpConnection. Because the same connection.type === "ssh" backs both an SSH terminal and an SFTP browser, anything that reconstructs a Pane from a Connection must read the stored pane.kind first. In particular, terminal layout serialization persists pane.kind (serializeLayout) and hydration must honor it (buildPaneFromStoredLayoutPane in src/store.ts): deriving the kind from connection.type alone silently rebuilds a saved/docked SFTP Pane as an SSH terminal on the next launch. The same applies to file-browser Panes for localFiles and plain ftp.
Screenshot Capture
Owns explicit user-triggered screenshot capture for active workspace surfaces. Terminal Panes expose the screenshot action in the Pane toolbar; URL, SFTP, RDP, and VNC workspaces expose it in the top workspace toolbar. The frontend owns the native command menu and Region selection overlay, then calls the typed Tauri command with a client-area rectangle.
The standalone screenshot gallery page has been removed. Screenshot capture (Region and Entire Window/Panel to clipboard or AI Assistant context) remains available through terminal Pane toolbars and workspace top toolbars. The Rust backend screenshot capture code in src-tauri/src/screenshot.rs and src-tauri/src/storage.rs is retained for the in-context capture path.
On Windows, the Rust backend translates the requested rectangle into physical screen coordinates and uses DXGI Desktop Duplication when settings.useDirectxScreenCapture is enabled. The DXGI path is used only when the requested rectangle fits a single physical output; unsupported cases fall back to the existing GDI capture path so native child surfaces such as WebView2 and the RDP ActiveX host remain capturable. Captures can be written directly to the system clipboard as image data, or encoded as a transient PNG data URL and attached to the AI Assistant context through an explicit Send to AI Assistant action. Screenshot capture does not persist image files and does not log terminal contents.
The same screenshot path is also used internally for RDP overlay suppression. Before hiding the ActiveX HWND for a registered DOM overlay such as a dialog or Region overlay, the frontend captures the RDP host rectangle and displays the resulting transient bitmap under the DOM overlay. That internal capture is not persisted and is distinct from user-requested clipboard or AI Assistant captures.
SSH Config Importer
Parses SSH config and creates draft connections. It should preserve supported directives and visibly report unsupported directives.
Current implementation note: the importer supports Host, HostName, User, Port, IdentityFile, and ProxyJump. It creates drafts only for concrete Host aliases, then resolves supported values through every matching exact, *, ?, and negated (!) Host pattern in file order. Resolution follows OpenSSH's first-obtained-value-wins rule, so broad defaults normally belong after specific blocks; supported directives before the first Host apply globally. Wildcard-only patterns contribute inherited values but never create placeholder Connections. Unsupported global or host-scoped directives are reported with line numbers through the typed Tauri command. The Add SSH Connection dialog exposes connections.importSshConfig as the footer's left auxiliary action; it first reads the platform default ~/.ssh/config / %USERPROFILE%\.ssh\config when present, otherwise opens a file picker, then applies the first importable Host draft to the dialog.
Connection Batch Importer
Owns the cross-format Connection import flow surfaced as the Import tile in the New Connection wizard (src/modules/workspace/connections/ConnectionSidebar.tsx) and rendered as the unified source-switching dialog src/modules/workspace/connections/ImportDialog.tsx. The dialog keeps file import, browser bookmarks, and network scan in one sheet, then funnels every source into the same editable preview list before persisting.
Backend lives in src-tauri/src/import.rs and is exposed through two typed Tauri commands:
parseImportFile— reads a user-picked file from disk, dispatches by extension, conventional~/.ssh/config/%USERPROFILE%\.ssh\configfilename, or content sniff, and returns a list ofImportedConnectionDraftrows plus parser warnings. Supported formats: CSV/TSV/text (column-aware via header row when present), OpenSSH config (reuses the wildcard-aware SSH Config Importer and preservesIdentityFile/ProxyJump), RDCMan.rdg(XML, preserves folder hierarchy asfolderPath), MobaXterm.mxtsessions(INI bookmarks with protocol id mapping), and PuTTY.reg(Windows Registry export ofSoftware\SimonTatham\PuTTY\Sessions). Each draft carriesname,host,user, optionalport,connectionType, and optionalfolderPathfor nested folder reconstruction; SSH-config drafts may additionally carrykeyPathandproxyJumpthrough the editable preview into Connection creation.scanNetworkForConnections— performs a light TCP port probe over a single host, hyphen range, or CIDR, capped atMAX_SCAN_HOSTS = 1024per call withSCAN_CONCURRENCY = 64and a 500 ms per-port timeout. Each open port becomes a draft whoseconnectionTypeis mapped from the well-known port (22 → SSH, 23 → Telnet, 3389 → RDP). Progress events are emitted on the app handle so the dialog can render an in-flight status without blocking the UI thread.
The frontend preview list lets the user toggle individual rows, edit name/host/port/type/user, bulk-fill or overwrite usernames across the current selection, optionally bulk-set passwords for the current selection (stored only after import via the selected credential backend), and choose the destination Workspace. Imported rows land at that Workspace root unless the source provides nested folderPath data; RDCMan, MobaXterm, and bookmark imports preserve those nested folder paths by reconstructing ConnectionFolders inside the chosen Workspace.
Secrets boundary: imported passwords are not persisted into SQLite alongside the draft. They are routed through the existing Connection secret owner, mirroring the rest of the connection storage model. Drafts without a typed password store no secret and behave like any other Connection without saved credentials. Bulk-clearing a password leaves no credential backend entry for that imported Connection.
Out-of-scope vs. SSH config import: this importer does not parse ~/.ssh/config. SSH config remains the responsibility of the SSH Config Importer described above; the two flows share no parser state.
Dashboard
The Dashboard Module is a built-in Activity Rail destination that presents a 12-column drag-and-drop widget grid. Users select widgets from a catalog, customize their visual preset/accent/icon/title per instance, and arrange them freely; the AI Assistant can read the active dashboard and create, modify, or remove widgets through atomic Tauri commands.
The full architecture lives in docs/DASHBOARD.md. Summary of the durable shape:
- Two widget kinds.
builtIn(TypeScript components insrc/modules/dashboard/widgets/, registered insrc/modules/dashboard/registry/builtInRegistry.ts) andscript(JavaScript executed inside an isolatediframe srcdochost with declarednetwork/pollSecondspermissions). Built-in widgets ship with the app;scriptwidgets are AI Created Widgets in v1 and stored indashboard_custom_widgets. - Three visual presets.
panel,ambient,hero. Each preset is a CSS chrome wrapper that reads the widget's--w-accent/--w-accent-softvariables; presets do not encode their own palette. Ambient supports optional frosted-glass background and hides its title bar by default. - Per-instance customization. Each widget instance carries
preset,accent_name(palette key or strict#RRGGBBcustom color),icon_name(curated Reicon-compatible whitelist), and optionalcustom_title. - Layout uses
react-grid-layoutwithWidthProvider, 12 columns,compactType: 'vertical', drag handles restricted to preset headers, debounced batched layout writes throughdashboard_apply_layout. Per-viewgrid_density(compact/default/roomy) is edited from the topbar in edit mode only. - Persistence uses three SQLite tables:
dashboard_views,dashboard_widget_instances,dashboard_custom_widgets. View delete cascades to instances; AI Created Widget delete cascades through Rust (conditional FK enforced manually). Fresh installs seed a Default view containing one App Launcher widget. - AI Assistant integration exposes every
dashboard_*Tauri command as a registered assistant tool with a JSON schema gated by the existing approval flow. For visible widget creation, the assistant should usedashboard_create_widget, which validates a structured body, creates the AI Created Widget, and places an instance on the active view in one step; successful mutating dashboard tools emit a backenddashboard-changedevent that reloads the Dashboard store so the newly mounted widget appears without app restart. The Dashboard page-context payload sent throughonAssistantContextChangeincludes a compact snapshot of the active view, its instances, and known AI Created Widgets so the AI can detect duplicates and offer edit/place/create-new choices without reading full widget source. This payload must stay metadata-only: nobodyJson, nosettingsSchemaJson, no per-instance settings values, no full widget library catalog, and no generated source. Full AI Created Widget source is exposed only through the scopeddashboard_read_widget_sourcetool after the assistant has selected one widget id from metadata for checking or editing. Widget source and schema payloads are UTF-8 JSON strings; assistant tool descriptions explicitly require preserving non-English text as Unicode when creating or updating widgets. Successful Dashboard create/update tool results return redacted metadata for the affected widget/instance rather than echoing source back into the model context. The assistant must choose preset, accent, icon, and grid size deliberately from widget purpose and KKTerm's quiet desktop UI style; these presentation fields are not decorative random choices. - Theming. Dashboard chrome consumes existing app CSS variables only; per-widget accent is independent of the active color scheme so a purple widget stays purple across schemes.
- Script widget host is a fault-isolation boundary, not a security boundary. KKTerm is MIT and single-user; the iframe exists so a typo in one widget cannot crash the dashboard and so future Tauri-command exposure is a deliberate per-handler postMessage decision.
- Settings → Dashboard carries cross-widget app preferences (confirm-before-remove, default landing view).
App Launcher is rendered as a builtIn widget hosted by Dashboard; its data model and management UI remain in src/modules/dashboard/widgets/builtin/app-launcher/. App Launcher is intentionally not a peer Activity Rail entry.
Current built-in widgets are App Launcher, Connection, Notes, AI Coding Usage, PC Info, Network Tools, Generators, and Converters; each ships as a TypeScript Body component registered in src/modules/dashboard/registry/builtInRegistry.ts. Dashboard AI Created Widgets remain the extension point for user-specific script widgets.
When docs/ARCHITECTURE.md and docs/DASHBOARD.md conflict on Dashboard-internal concerns, docs/DASHBOARD.md wins.
IT Ops
The IT Ops Module is a built-in Activity Rail destination for site operations: Site topology over existing Connections, Batch Runs over Sites via SSH/WinRM/PsExec transports, and durable trigger → condition → action Automations that evolve the in-memory Watchdog into saveable rules. Definitions persist in SQLite and re-arm on launch; live run/poll state stays in-memory, and KKTerm installs no background service. The accepted design and its trade-offs live in docs/ADR/0011-it-ops-module.md; the durable architecture and normative Module-internal UI contract live in docs/ITOPS.md; Site / Server Room / Rack terminology lives in docs/SITE.md and CONTEXT.md. When docs/ARCHITECTURE.md and docs/ITOPS.md conflict on IT-Ops-internal concerns, docs/ITOPS.md wins.
Install Helper
The Install Helper Module is a built-in Activity Rail destination (rail icon Package, grouped with the other built-in Module buttons near the top of the rail) that manages a curated catalog of Windows developer tools — git, node, python, docker, AI coding tools, design tools, and so on. Users see categorized section groupboxes; installed tools stay in their category, updateable tools highlight the latest-version value and expose Update, and unavailable tools expose Install. Updateability is based on version precedence, not raw string inequality, so equivalent trailing-zero build segments such as 26.01 and 26.01.00.0 do not surface as updates. Pin excludes a tool from "Update all".
The catalog itself is a JSON file embedded into the KKTerm binary at compile time via include_str!("../../../installer/catalog.v1.json"). Updates to the catalog ship with each KKTerm release. There is no network fetch, no on-disk cache, and no signature verification — the trust anchor is the app binary itself (eventually backed by Windows code-signing of the KKTerm installer). The design rationale and what it supersedes from the earlier remote-signed approach lives in docs/ADR/0008-installer-helper-bundled-catalog.md (see also the superseded docs/ADR/0007-installer-helper-remote-catalog.md).
Summary of the durable shape:
- Nine recipe providers, all pure data.
winget,chocolatey,npm,uvPip,downloadInstaller,githubRelease,windowsFeature,wslDistro,bundle. Nocustom, no script strings — a tool that does not fit one of these structured shapes does not ship in the catalog. - Bundles are composite recipes whose detection is the AND of their steps' detections. Dependency edges (
needs) point to bundle ids, never to steps inside. - Dependency resolution is frontend-side and lazy: when the user clicks Install, the transitive
needsgraph is walked, already-installed prerequisites are skipped, and a confirm dialog lists what will actually run. During queued installs, the dialog follows the currently running package; if a prerequisite or target fails, the queue stops and the stepper stays open on that package with its stdout/stderr logs. Cycle detection runs at catalog load in the Rust loader. - Scoped winget installs default to per-user. The backend normalizes missing
scopetouserfor winget recipes that declare thescopeoption, so direct installs and bundle prerequisite steps both pass--scope userunless the user explicitly selectsmachine. Winget recipes still run through the elevated streaming wrapper when the user selects machine scope or the curated recipe is known to require/self-trigger administrator rights (for example Docker Desktop, nvm-windows, Git for Windows, PowerShell, and other system MSIs); this keeps KKTerm itself non-elevated while surfacing the normal UAC consent prompt instead of lettingwingetfail with an administrator-required exit code. Winget recipes that also declaredownloadProviderexposeinstaller.options.provider; choosing the download provider skips winget-specific options and downloads the cataloged installer URL instead. Curated winget recipes may also declarechocolateyProvider; choosing Chocolatey requires thechocolateyrecipe, then runschocofor install/update/latest/uninstall. If a Chocolatey-backed row is already installed throughchoco, later management preferschocoeven when the primary catalog provider is winget. Thewingetprerequisite itself downloads Microsoft Desktop App Installer frommicrosoft/winget-cliand, before staging that MSIX bundle, installs the Desktop VCLibs and Microsoft.UI.Xaml 2.8 AppX framework dependencies required on Store-less/LTSC-style Windows installs. - Winget CLI utilities ensure PATH. NSSM, ripgrep, jq, fzf, uv, and FFmpeg add
%LOCALAPPDATA%\Microsoft\WinGet\Linksto the persisted user PATH after install so new shells and follow-up helpers can resolve their command shims. Bundle follow-up commands, manager-backed runtime detection, managed app version probes, and managed web UI launchers refresh the persisted Windows user and machine environment, including PATH, manager-specific variables such asNVM_HOME/NVM_SYMLINK, and existing Git for Windows command directories, before spawning. - Reverse-DAG check on uninstall lists installed dependents that would be left without a prerequisite before proceeding.
- WSL reboot gating sets a session-only flag when base WSL is installed this session; Docker Desktop and WSL distro shortcuts are disabled with an explanatory hint until KKTerm restarts.
- UAC handling is honest — "Update all" warns up front with the estimated prompt count. We do not try to suppress or batch UAC.
- Cancellation kills the child process for an in-flight install via a shared
AtomicBoolflag. For "Update all", cancel stops the queue but not the in-flight tool. Partial installs are not rolled back. - No UI-thread blocking. Install Helper command handlers must treat catalog parsing, registry/cache reads, detection, latest-version lookups, SQLite state reads/writes, managed web UI process launch/stop/status, managed terminal launch, and NSSM service registration/removal as background work. Streaming operations return after starting their worker and send progress over
installer://progress. - Persistence is split: SQLite table
installer_tool_state(tool_id PK, pinned, latest_version_seen, last_check_at)(schema version 17) holds per-tool prefs and the latest-version cache; the catalog itself is compile-time-embedded and needs no on-disk cache;%LOCALAPPDATA%\KKTerm\installer\bin\<tool_id>\holds installedgithubRelease-provider tools, including winget recipes installed through a GitHub-releasedownloadProvider; Windows Registry keyHKCU\Software\Ryan Tsai\KKTerm\InstallerDetectionCacheholds non-portable per-tool detection snapshots with last-checked timestamps; the active detection sweep and in-flight queue remain in-memory only. - Managed server and agent apps live under
%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\. n8n, Flowise, and OpenClaw install as local npm projects, Excalidraw installs as a local React/Vite host, BentoPDF and OpenFlowKit install from upstream source archives into app-local static web builds, Open WebUI, Langflow, and Hermes AI Agent install into app-local uv/pip virtual environments, and Ollama installs into an app-localappdirectory where supported by the Windows installer. Runtime data stays under each app-localdatadirectory where the upstream tool supports it: n8n setsN8N_USER_FOLDER, Flowise setsDATABASE_PATHplus local secret/log/blob storage paths, Open WebUI setsDATA_DIR, Langflow setsLANGFLOW_CONFIG_DIR, and Ollama setsOLLAMA_MODELS. Browser-only app state such as Excalidraw libraries remains browser site storage for the localhost origin, not an NSSM service data directory. - Winget detection uses a local Add/Remove Programs registry snapshot plus catalog detection aliases (
registryKeys,displayNames,displayNamePrefixes) instead of shelling out towinget list;wingetremains the default install/update provider. Detection results include the provider that found the installed tool so installed dialogs and update/uninstall confirmations describe the actual management path, not only the catalog primary provider. Latest-version checks prefer the structured Microsoftwinget-pkgsmanifest repository and fall back towinget showwhen that lookup is unavailable. Chocolatey alternate-provider detection uses localchoco list --local-only --exact --limit-output; when that reports a package as installed, latest checks usechoco search --exact --limit-output, updates usechoco upgrade, and uninstall useschoco uninstall. - Download providers are narrow vendor shortcuts for tools that publish canonical installer URLs or portable GitHub release archives alongside package-manager metadata. Download-installer providers fetch the declared URL to a temp file and launch it with the OS installer UI; detection still uses local Add/Remove Programs aliases, so later update checks cannot prove the vendor installer was the original provider. GitHub-release download providers fetch the latest matching release asset, extract zip archives under
%LOCALAPPDATA%\KKTerm\installer\bin\<tool_id>\, and may add a cataloged nested executable directory to PATH. Once KKTerm detects its GitHub-release marker for a winget recipe's download provider, latest-version checks and updates keep using that GitHub-release provider instead of switching back to winget. A winget recipe may declare adownloadProviderfallback only when the vendor has a canonical install artifact; otherwise the recipe remains winget-only. - Progress streaming is via the Tauri event channel
installer://progresscarrying a discriminatedProgressEventunion (detectStarted,detectResult,detectFinished,checkStarted,checkResult,checkFinished,step,stdout,stderr,progress,completed,failed,cancelled). The frontend reduces these into per-tool detection, log, current-step, and ratio state in a Zustand store. - Managed web UI affordances are allowlisted per tool. Installed n8n, Flowise, Open WebUI, Langflow, Excalidraw, BentoPDF, OpenFlowKit, and Ollama expose Run/Stop/Open web UI actions with app-local commands and localhost URLs. BentoPDF prefers
localhost:3022and OpenFlowKit preferslocalhost:3023, but their app-local servers record the actual free port if the preferred port is already occupied, and Open web UI uses the recorded URL. After install/update, these managed web UI apps start automatically in normal run mode. Normal Run starts the app process hidden rather than opening a terminal window. Stop is also allowlisted per tool port (or the recorded dynamic port) and, when a registered service exists, routes through the service helper before cleaning up the listening managed process. Arbitrary catalog commands are not accepted. - Managed terminal launch affordances are allowlisted per tool. Installed Hermes AI Agent and OpenClaw expose Run and Add to Workspace actions that launch or save local PowerShell Connections with fixed app-local activation/alias setup and upstream setup hints. Arbitrary catalog commands are not accepted.
- Managed Windows service helpers are allowlisted per tool and backed by NSSM. Installed n8n, Flowise, Open WebUI, Langflow, Excalidraw, BentoPDF, OpenFlowKit, and Ollama expose an explicit Register as service action that first stops the normal localhost run for that tool when KKTerm has a recorded managed port, then registers and starts a
KKTerm-*service with the same managed app-local command and environment, writes NSSM stdout/stderr to%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\logs\, and sets startup to automatic. BentoPDF and OpenFlowKit keep the same dynamic-port fallback under the service path instead of killing an unrelated process that already owns the preferred port. Register as service installs thenssmprerequisite on demand when the user clicks the service action, so managed server app installs do not pull NSSM unless service registration is requested. The helper may trigger UAC; arbitrary catalog commands are not accepted. - AI Assistant integration is deferred. No installer Tauri command is exposed to the assistant in v1.
- Tutorial-capable. Targets:
app.activityRailInstaller(rail),installer.updateAll(header button),installer.toolOptions(per-row options form).
Source layout:
src-tauri/src/installer/— Rust backend:schema.rs(Recipe / Provider / Catalog with validate + cycle check),catalog.rs(compile-timeinclude_str!loader),cache.rs(Windows Registry detection cache),detect.rs,install.rs,uninstall.rs,latest_version.rs,state.rs(SQLite),events.rs,commands.rs(Tauri commands +InstallerRuntimestate).src/modules/installer/— frontend:InstallerPage.tsx,ToolRow.tsx,InstallerConfirmDialog.tsx,state.ts(Zustand),dag.ts(pure DAG helpers),types.ts,installer.css.installer/catalog.v1.json— the catalog source, embedded into the Rust binary at compile time.
When docs/ARCHITECTURE.md and docs/ADR/0008-installer-helper-bundled-catalog.md conflict on installer concerns, the ADR wins.
AI Assistant
Owns provider adapters, prompt construction, command proposal, approval flow, assistant tool registration, command execution handoff, and output capture.
Assistant input construction follows a minimum-context rule:
- Passive page context: metadata only. Settings exposes active section/control keys/tutorial targets. Dashboard exposes active View metadata, Widget Instance placement, AI Created Widget title/summary/category/body metadata/settings metadata, unhealthy instance ids/errors, compact visual context, and compact library key/global hints. Do not include source code, full schemas, settings values, or full catalogs in passive context.
- Explicit user attachments: selected terminal output, terminal buffers, screenshots, pasted images, and files are sent only after the user attaches or sends them. Keep caps/truncation at the request boundary and label every source.
- Read tools: full or sensitive payloads are available through narrow tools only when the task requires them.
dashboard_read_widget_sourceis the pattern: first identify a widget from compact metadata, then read exactly that widget's source before updating/checking it. - Tool results: avoid replaying large tool arguments or full source in successful results. Return ids, summaries, metadata, validation status, and next-action hints. Error results may include enough detail for self-correction but should still avoid unrelated state.
- Debug logs: assistant, MCP, Install Helper, URL Connection, RDP, SSH, SFTP, Telnet, and heartbeat debug logging is local and sensitive. Release builds write full
aiassistant.debug.log,mcp.debug.log,installer.helper.debug.log,url.connection.debug.log,rdp.debug.log,ssh.debug.log,sftp.debug.log,telnet.debug.log, andkkterm-heartbeat.debug.logonly when Settings → General → Debug → Advanced Debugging is enabled. Built-in MCP debug logging redacts terminal input/buffers, Dashboard widget source/body JSON, and secret-looking argument fields before writing. URL Connection debug logging records URL Session lifecycle, frontend DOM bounds, clipping, requested overlay bounds, native coordinate conversion, and WebView2 overlay window/client rectangles; it may include URL hostnames. RDP debug logging records non-secret Connection details such as host, username, port, selected options, bounds, ActiveX control creation, display sync, macOS IronRDP TCP/security/TLS/CredSSP and clipboard-handshake stages, active-loop read/write failures, and command errors, and defensively redacts password-like, secret-like, token-like, and credential-like fields before writing. Clipboard diagnostics record only format IDs and byte/character counts, never clipboard text. SSH debug logging records native SSH lifecycle, SOCKS proxy routing metadata with credentials stripped, keepalive configuration, channel open/close events, and terminal input/output byte counts without terminal contents. SFTP debug logging records SFTP browser startup stages, session operation waits, transfer start/completion/error summaries, upload open/write/shutdown timeouts, and partial-upload cleanup without file contents or credential values. Telnet debug logging records Connection metadata, protocol option names and numeric codes, negotiation decisions, terminal-type and window-size events, lifecycle errors, and input/output byte counts without terminal contents or credential values. Treat these files as user-private diagnostic data.
v0.1 providers:
- OpenAI-compatible endpoint with BYO API key.
- Anthropic and OpenAI can optionally delegate assistant turns to local Claude Code CLI / Codex CLI backends when the matching Settings toggle is enabled. These modes prefer an Agent Client Protocol stdio adapter for the selected vendor CLI and fall back to documented one-shot CLI command mode if ACP is unavailable. They use the vendor CLI's own cached authentication, disable the API key entry, and are intentionally not full parity with KKTerm's native HTTP/tool-calling loop.
- Claude Code CLI path.
- Codex CLI path.
The AI source area must enforce permission-bounded execution. Prompt mode is the default tool permission mode; mutating tools return a structured permissionRequired result instead of running. Allow All is an explicit user setting that lets enabled tools run automatically. CLI integrations should be constrained to suggest-only/ask-before-execute where possible.
Assistant tools are registered in src-tauri/src/ai.rs with JSON schemas and are filtered by persisted assistantTools settings. Long Dashboard widget prompt contracts live in src-tauri/src/ai/prompt_contracts.rs; keep prompt-contract wording there so provider streaming, tool execution, and schema plumbing stay easier to navigate. The widget-authoring contracts are appended to the dashboard_create_widget tool description only — never also duplicated into the system prompt — so each contract is sent once per request. Tool families currently include general utility tools, Dashboard tools, saved Connection tools, live Session tools, network admin tools, watchdog tools, the app-owned Tutorial overlay tool, the read-only mcp_list_tools listing for configured remote MCP servers, the display-only update_plan tool (publishes the model's work plan as a PlanUpdate stream event; chip-silent and never gated by approval), and the assistant_memory_* tools. Assistant memory persists short durable operator notes in the SQLite assistant_memories table scoped to global or the active connection:<id>; the global notes plus the active Connection's notes are injected as background knowledge at the start of each turn, and secrets are never stored there. Saved Connection tools operate on durable SQLite Connection data and may list, create, open, update, or delete saved Connections. Live Session tools operate on currently mounted workspace surfaces only: terminal Panes expose visible buffer reads and input writes; RDP/VNC Sessions expose screenshots, text injection, named keypresses, and mouse clicks; SFTP/FTP browser Sessions expose listing plus create-folder, rename, and delete operations. For UI help, the assistant should answer first and offer to navigate; it should call the Tutorial tool only after the user asks to be shown or accepts the offer. The Tutorial tool may only target frontend-owned data-tutorial-id anchors listed in current page context or known targets documented in its schema; it may navigate to known app pages/Settings sections before highlighting, but it does not accept arbitrary CSS selectors. Every tutorial-capable target needs the anchor, a navigation entry in src/app/tutorialNavigationModel.ts, matching tutorial_highlight metadata in src-tauri/src/ai.rs, and manual grep hints. npm run check enforces this mapping so future UI additions do not strand the AI Assistant without a navigable target. Do not put live Session state into the durable Connection model to make an assistant tool easier to call.
Live Session tools cross the Rust/frontend boundary through AssistantLiveToolBridge. Rust emits an assistant-live-tool-request event and waits for the frontend to complete it through complete_assistant_live_tool_request; the wait is per-tool (capture-style tools such as remote-desktop screenshots, terminal buffer reads, and SFTP listings get a wider timeout than the default). The frontend handles the request in src/ai/assistantLiveTools.ts (dispatched from src/ai/AssistantPanel.tsx) and resolves concrete targets through src/modules/workspace/paneRegistry.ts. This bridge is intentional because terminal renderers, RDP/VNC host geometry, and SFTP/FTP browser state are frontend-mounted Session surfaces. Backend Tauri commands remain the authority for native operations such as RDP key/mouse injection, VNC input events, screenshots, and SFTP/FTP filesystem actions.
RDP and VNC interactions use different transport paths behind one assistant tool family. RDP text/key/mouse operations go through native Tauri commands that target the RDP ActiveX Session; VNC text/key/mouse operations go through the existing VNC key and pointer event commands. RDP/VNC screenshots use the same transient screenshot capture path as explicit Send to AI Assistant actions. Screenshot results and terminal buffer reads are returned to the model as tool results for the active request; they are not logged by default.
OpenAI-compatible providers share one orchestrator. run_agent_loop in src-tauri/src/ai/openai_provider.rs owns the agent loop for both API styles, streaming and non-streaming: the sub-turn cap, tool execution with the approval flow, the consecutive-tool-error abort, user cancellation, stream events, and the final forced answer once the cap is reached. The genuine wire differences — request body shape, SSE-vs-JSON reply parsing, and transcript bookkeeping — are isolated in the AgentTransport enum (Chat for DeepSeek and other Chat Completions-style providers, Responses for OpenAI/Azure/LiteLLM-style providers); streaming versus non-streaming is just whether a Channel is present. This replaced four near-duplicate loops that had drifted, so tool-calling or streaming changes now live in one place — but still verify both transports. A final streamed assistant turn must contain visible assistant content before emitting Done; otherwise the UI can show completed tool work with an empty assistant message. The streaming Tauri command must also return the final AgentRunResponse as the authoritative completed assistant message, and the frontend must reconcile the in-flight bubble from that return value after run_ai_agent_streaming resolves. Channel deltas are for live progress, not the only source of truth for the final text. The backend regression tests in src-tauri/src/ai.rs cover this with streamed_final_answer_requires_visible_content for the Chat Completions path, deepseek_tool_turn_serializes_reasoning_content_and_tool_result for DeepSeek-style thinking/tool context, and responses_stream_parser_uses_done_text_and_function_call_id for the Responses path. On the frontend, stream deltas must update the in-flight assistant message snapshot synchronously before React state scheduling; do not hide snapshot mutation inside a setMessages updater. tests/assistant-stream-message.test.mjs covers preserving tool status and recovering final content from the returned response. Assistant stream console diagnostics use the [kkterm-ai] prefix, must compile/run only in debug builds, and should stay metadata-only: provider/model, subturn, finish reason, event type, tool id/name, and content/reasoning/tool-result lengths, not raw prompt text or tool output.
Assistant stream events are a typed Rust-to-React contract. AiStreamEvent must serialize both variant tags and struct fields in frontend camelCase (toolId, toolName), not Rust snake_case (tool_id, tool_name). A mismatch here records malformed tool calls and can crash the Assistant work panel while rendering active tool labels, blanking the app even though Dashboard widgets are not involved. Fix this at the stream serialization/normalization boundary and keep tests/assistant-stream-message.test.mjs plus the Rust ai_stream_tool_events_use_frontend_field_names regression in place. Do not respond to this symptom by adding a Dashboard widget error boundary: Assistant panel crashes sit outside the Dashboard widget tree and that boundary cannot catch them.
Debug builds write raw AI Assistant interaction records to aiassistant.debug.log in the same directory as kkterm.log. Release builds write the same full log only when the user enables Settings → General → Debug → Advanced Debugging. This JSONL log is for local troubleshooting only and is intentionally more verbose than [kkterm-ai] console diagnostics: it records assistant run start/end, raw provider request/response bodies, raw SSE stream chunks, emitted stream events, tool calls/results, permission-required blocks, live Session bridge requests/completions/results, GitHub Copilot SDK prompt/response/error, and Dashboard widget creation checkpoints. Debug builds write built-in and remote MCP request/response records to mcp.debug.log beside kkterm.log; release builds write the same MCP log when Advanced Debugging is enabled. Built-in MCP debug records redact terminal send input, terminal buffer reads, Dashboard widget source/body JSON, and secret-looking argument fields before writing. Debug builds also write Install Helper command, detection, latest-version, install, uninstall, process output, cache, and progress-event records to installer.helper.debug.log; release builds write that log when Advanced Debugging is enabled. Debug builds write URL Connection frontend/native geometry and WebView2 overlay lifecycle records to url.connection.debug.log; release builds write that log when Advanced Debugging is enabled. Debug builds write RDP startup, ActiveX control creation, display-size sync, and main-thread command timing records to rdp.debug.log; release builds write that log when Advanced Debugging is enabled, with password-like, secret-like, token-like, and credential-like fields redacted. Debug builds write native SSH lifecycle records to ssh.debug.log, SFTP startup/transfer records to sftp.debug.log, and Telnet lifecycle/protocol records to telnet.debug.log; release builds write those logs when Advanced Debugging is enabled. These transport logs omit terminal contents, file contents, and credential values. kkterm-heartbeat.debug.log records frontend liveness timing and native window/tray event timing; debug builds write it automatically, while release builds start the heartbeat only when Advanced Debugging is enabled and stop it when Advanced Debugging is disabled. Enabling Advanced Debugging writes a small advanced_debugging.enabled marker to the JSONL debug logs so the active release code path is visible before the next subsystem event. Use these logs to distinguish model decisions from KKTerm harness, permission, Dashboard validation, MCP transport, widget persistence, Install Helper provider/process failures, URL WebView2 overlay geometry failures, RDP ActiveX/display-sync failures, SSH/SOCKS transport stalls, SFTP upload/listing stalls, Telnet negotiation failures, and frontend/native liveness stalls. Do not add ad hoc visible in-app indicators for them, and treat the files as sensitive because remote hostnames, usernames, and file paths remain diagnostic metadata even when terminal contents, file contents, and credentials are omitted. API keys and credential backend secret values should not be logged deliberately, but users must still review the files before sharing them. When diagnosing normal production issues, prefer compact logs and explicit error surfacing before asking users to enable Advanced Debugging.
Screenshot context is user-attached and transient. The Assistant sends it through OpenAI-compatible multimodal chat content only when the user submits a prompt with that screenshot context still attached.
Extension creation is currently an Assistant draft mode, not a general extension runtime. The frontend can mark a chat request as extensionCreation, and the backend prompt builder adds guardrails requiring reviewable designs, manifests, permission requests, and source files only. KKTerm must not claim that generated extension code was installed, enabled, executed, loaded, written to disk, or verified unless a future explicit approval flow and extension platform provide that behavior. The platform shape is defined in docs/ADR/0005-extension-platform-architecture.md.
Watchdog
Owns AI-driven, session-scoped, multi-instance monitors that pair a sensor loop with an AI actor loop. A Watchdog watches one target against a predicate and, when the condition is met, can notify and run an AI intervention. Watchdogs are runtime state, not durable data: the registry is in-memory only and nothing persists across app restart. A Watchdog is not a Connection, Session, or Module.
Backend and frontend split the loop. Rust (src-tauri/src/watchdog/) owns the registry, polling task, predicate evaluation, sustained-window tracking, stop-condition arbitration, and event emission; the frontend (src/watchdog/) owns the Watchdog Status Bar indicator, the detail panel, and the AI intervention sub-turn (model inference and tool calls), reporting results back through the watchdog_record_intervention command. All backend events flow over a single watchdog://event channel so the frontend has one subscription point, mirroring net::commands::EVENT_CHANNEL. The Tauri command surface is watchdog_create, watchdog_list, watchdog_cancel, watchdog_get_report, and watchdog_record_intervention; the registry (WatchdogRegistry) and SSH-silence SessionActivityTracker are managed Tauri state.
Target kinds are mock, performanceCounter, sshSessionOutputSilence, ping, and tcpReachable. The registry enforces hard limits so a runaway AI cannot exhaust resources: at most 16 concurrent Watchdogs, poll intervals bounded between 500ms and 1 hour, and a per-Watchdog tick ring capped at 200 samples. Anything needing a longer cadence is a scheduled job, not a Watchdog.
Extensions
Owns user-installed extension manifests, permissions, install/update lifecycle, isolated storage, and runtime boundaries. Extension execution is deferred until this platform exists in code. The accepted direction is manifest-first, permissioned, user-mediated, and isolated from secrets and raw live session contents by default.
Diagnostics
Owns structured local logs, diagnostics bundle creation, and redaction rules. No telemetry or automatic crash upload in v0.1.
Updates
Owns update discovery and installation for packaged desktop builds. Windows uses the current release-metadata installer flow: metadata lookup, matching NSIS installer asset plus .sha256, user confirmation, checksum verification, app exit, and detached installer handoff. macOS and Linux use Tauri's signed updater flow with latest.json: macOS publishes a single universal (Intel + Apple Silicon) .app.tar.gz updater bundle plus .sig, registered under both darwin-aarch64 and darwin-x86_64, and Linux publishes a linux-x86_64 AppImage plus .sig. All paths remain user-mediated.
Release distribution is fronted by https://kkterm.ryantsai.com. A Cloudflare Worker streams /releases/* from the private kkterm-releases R2 bucket; immutable assets use versioned paths and releases/latest.json is published only after its referenced files. Windows checks this app-owned manifest first and uses the GitHub Releases API as fallback; installer and checksum downloads retry the same versioned asset on the other trusted host when the selected host fails. macOS and Linux list the Cloudflare manifest before the GitHub-hosted manifest in their Tauri updater endpoints. GitHub Releases remains the canonical release record and fallback; distribution-host changes must preserve Windows SHA-256 and Tauri updater-signature verification.
Update checks may contact the configured GitHub Releases/update metadata endpoint. This network request is part of the updater flow and must be described clearly in Settings as distinct from telemetry. KKTerm must not add analytics or crash upload as part of update checking.
Installation is user-mediated. Settings owns manual update checks and update preferences, while app chrome may show a lightweight update-available notification after a successful check. The first updater supports normal forward updates only. Rollback, downgrade, preview channels, managed update servers, and silent installs are deferred.
Data Boundaries
SQLite contains local, non-secret data only. The selected credential backend contains secrets. Terminal contents should not be logged by default. Diagnostics bundles must avoid secrets and terminal output unless the user explicitly includes selected content. Any future encrypted SQLite vault must be treated as a separate secret backend, not as ordinary settings storage.
Frontend Layout
The primary UI is a dense desktop workspace:
- left activity rail with Workspace, Dashboard, Install Helper, IT Ops (default hidden), and Settings entries
- left connection tree with root Connections and optional nested folders (inside the Workspace Module)
- main Module content area (each Module owns its layout: Workspace has tabs/panes, Dashboard has widget grid, etc.)
- terminal split panes inside terminal tabs
- tmux session tags and management popovers inside SSH terminal Pane toolbars
- screenshot Region and Entire Window/Panel actions, shown in terminal Pane toolbars for terminal Sessions and top workspace toolbars for non-terminal surfaces
- SFTP dual-pane view
- right AI Assistant panel
- settings
Default visual direction: quiet productivity light chrome with dark terminal panes.
The shared UI design language — color tokens, reusable dialog primitives in src/app/ui/dialog/ (Sheet, Field, Switch, ConnTile, etc.), the ConfirmSheet confirmation template, host-platform button order, and the SFTP symmetric dual-pane file-browser pattern — is documented in docs/DESIGN_LANGUAGE.md. The Default and Dark color schemes are the canonical reference appearance; new dialogs/sheets/settings/file-browser surfaces build from those primitives and read tokens from src/styles/colorSchemes.css so they theme across all schemes.
Dashboard motion is centralized in src/modules/dashboard/motion.tsx using motion/react. Dashboard surfaces should use those wrappers for entry, exit, hover, and layout transitions instead of local one-off animation props. Motion may animate opacity, transforms, scale, and layout only; colors, borders, shadows, and backgrounds must remain owned by CSS classes and the color-scheme variables in src/styles/colorSchemes.css. The Dashboard motion root uses user reduced-motion preferences, so new Dashboard animation should stay inside that root unless there is a product reason to opt out.
The Dashboard Module supports multiple views, each a durable SQLite-backed row in dashboard_views carrying its own widget instances and grid_density. The first view is named "Default" and is seeded on first run with one App Launcher widget. Views do not create backend Sessions or saved Connections. Dashboard supplies the shared right AI Assistant panel with an active page-context summary for the current view, including widget instances and known AI Created Widgets as compact metadata only; Settings supplies the same shared Assistant panel with the active Settings section and visible control keys from src/modules/settings/settingsAssistantContext.ts. Keep page context separate from terminal selected-output context in the assistant request model. App Launcher belongs to Dashboard as a widget: users add the widget to a view, then add local app, shortcut, script, or file entries inside it. Each launcher entry should render as only an icon and text label in the widget surface; editing, removal, administrator launch, alternate-user launch, and other management actions must stay in an app-owned right-click context menu. See docs/DASHBOARD.md for the durable Dashboard architecture, including the two widget kinds, the three visual presets, drag-and-drop layout, and the AI-Assistant tool surface.
The Activity Rail uses icons with delayed app-owned hover labels for built-in Modules and connected Connection shortcuts. Rail labels are rendered through the shared RailTooltip helper in src/app/RailTooltip.tsx; do not add native title tooltips because they can appear beside the app tooltip in Tauri/WebView2. Rail tooltips use the same light native-style bordered popup treatment; in the Windows Tauri runtime the shared helper mirrors the label through a Win32 topmost tooltip so labels can appear over HWND-backed RDP ActiveX surfaces. Connected Connection shortcuts show an insertion separator while being reordered with pointer drag. Non-Workspace Module pages must stay inset from the 48px rail and below the rail stacking layer so rail hover/focus tooltips keep working when those pages are active. Settings → General owns the unified visibility and drag-to-reorder controls for Workspace, Dashboard, Install Helper, IT Ops, and Don't Sleep; Workspace, Dashboard, Install Helper, and Don't Sleep default on while IT Ops defaults off. Connected Connection shortcuts have their separate Workspace setting, and Settings remains the bottom destination. App Launcher must not be reintroduced as a peer rail entry.
KKTerm does not include a global command palette in the current product scope; navigation and workflow entry points should stay visible in the Dashboard/connection tree, tab workspace, SFTP toolbar/context actions, assistant panel, and Settings.
The main workspace treats Tabs as frontend containers over live Sessions. Selecting another Tab changes visibility and focus only; it must not run backend Session close commands. Closing a Tab via the tab-strip close control removes that container and tears down the live Session resources it owns.
Terminal Pane focus is part of the Workspace visibility contract. When the main Tauri window loses and regains native focus, the focused visible terminal Pane must restore input focus to xterm's hidden textarea unless an app-owned editable surface such as terminal search, a dialog field, or the AI Assistant composer deliberately owns focus. Do not rely only on browser window.focus for this path in the desktop runtime; native WindowEvent::Focused changes can be the reliable signal after switching away to another Windows app and back. The terminal restore path should prefer pane-local xterm focus plus the current WebView2 MoveFocus wrapper (focusCurrentWebview()), using native focus events as signals rather than trying to raise the main frame or focus WebView2 content HWNDs directly. A single visible SSH terminal Tab returning from Chrome or another foreground app must still accept keyboard input without requiring a click. If ui.debug.log shows the first post-reactivation input is pointerdown instead of keydown while the terminal textarea already appears focused, suspect native child-window focus routing first. Do not add another native terminal focus command for Alt+Tab/app switch or minimize/restore until the exact HWND-level failure is proven in the real Tauri desktop runtime.
Workspace chrome layout is global state. Connection-specific live context may change assistant copy, selected terminal context, or prompt construction, but should not change the left/right panel widths or collapsed state when the active Tab changes. Native HWND-backed surfaces such as WebView2 and RDP depend on stable host bounds; changing chrome dimensions as a side effect of Tab activation creates visible native resize artifacts.
Frontend Source Map
src/App.tsx is intentionally a small shell now. It composes page routing, Settings routing, global Workspace chrome, the app-wide Status Bar, and startup/bootstrap hooks. Workspace surfaces, Connection UI, Activity Rail internals, and app-shell effects live in feature or app-shell source areas so terminal, SFTP, URL, RDP/VNC, assistant, connection-tree, and Activity Rail work can proceed independently without repeatedly touching the root shell.
src/App.tsx— rootAppcomposition, Workspace/Settings routing, Workspace chrome assembly, app-wide Status Bar placement, settings reset handoff, and startup hook wiring.src/App.css— global CSS entrypoint and cascade-order manifest. Keep@import "tailwindcss"first, then import shared token/base CSS, then per-feature CSS.src/styles/colorSchemes.css— global font faces,:rootdesign tokens, and[data-color-scheme="..."]CSS custom property blocks consumed by feature CSS files.src/styles/base.css— resets and shared CSS primitives used across feature areas: icon/text buttons, dialog backdrops, command menus, form basics, and shared context-menu surfaces.src/app/app.css— app shell, Activity Rail, rail tooltips, panel resize chrome, and Tutorial overlay styling.src/app/ModuleHeader.tsxandsrc/app/moduleHeader.css— shared compact Module-header structure, title typography, identity icon tiles, dividers, and spacing. Top-level Modules compose their local navigation and actions inside this template; Settings reusesModuleIconTilefor matching Module representations.src/app/ActivityRail.tsx— Activity Rail rendering, connected Connection shortcuts, pinned Connection actions, native rail Connection context menu, connected Connection drag ordering, and Don't Sleep control.src/app/RailTooltip.tsx— shared app-owned Activity Rail tooltip surface, including the Windows native tooltip bridge that can draw over HWND-backed RDP ActiveX surfaces; use this instead of browser-nativetitletooltips for rail icon labels.src/app/workspaceChromeLayout.tsx— global Workspace chrome panel widths/collapse state, panel resize handles, layout reset, and localStorage persistence for the Connection panel and AI Assistant Panel.src/app/appShellEffects.ts— app-shell effects for frontend launch timing, host usage polling, global context-menu suppression, and app-shell CSS variables/color scheme.src/modules/workspace/connections/ConnectionSidebar.tsx— connection tree orchestration: search, drag/drop, CRUD command handlers, folder rows, native Quick Connect/Add Connection/tree/Tab context menus, Connection dialog request assembly, and modal wiring.src/modules/workspace/connections/connection-dialog/— Connection dialog field surfaces split by Connection type (LocalConnectionFields,SshConnectionFields,TelnetConnectionFields,SerialConnectionFields,UrlConnectionFields,RdpConnectionFields,VncConnectionFields,FtpConnectionFields) plus shared password controls. Keep per-type form field names here aligned withConnectionSidebar.tsxrequest parsing.src/modules/workspace/connections/ConnectionMenus.tsx— browser-preview fallback Quick Connect and Add Connection menu surfaces.src/modules/workspace/connections/ConnectionGlyph.tsx— shared Connection glyph, type glyph, and Connection subtitle helpers used by rows, menus, and dialogs.src/modules/workspace/connections/connectionSidebarState.ts— sidebar-local persistence/event helpers for recent Connections, stored-secret masking, and connection-tree invalidation.src/modules/workspace/connections/treeUtils.ts— pure connection tree transforms, filtering, flattening, folder counts, and live status projection.src/modules/workspace/connections/utils.tsx— connection labels/icons, default ports, Quick Connect runtime ids, local shell options, and SSH host-key confirmation helpers shared by terminal/SFTP.src/modules/workspace/connections/connections.css— Connection sidebar, Quick Connect/Add Connection browser-preview menus, Connection dialog, import dialog, and connection-tree CSS.src/modules/workspace/WorkspaceCanvas.tsx—TabStripandWorkspaceCanvas, including active Tab dispatch to terminal, SFTP, URL, and remote desktop surfaces.src/modules/workspace/paneRegistry.ts— in-memory registry for mounted Pane/Session affordances that must stay frontend-owned, including terminal renderers, input writers, RDP text senders, remote desktop assistant controllers, and SFTP/FTP file-browser assistant controllers.src/modules/workspace/ScreenshotMenu.tsx— native screenshot command menu, Region overlay, screenshot-to-clipboard, and screenshot-to-AI handoff.src/modules/workspace/StatusBar.tsx— bottom app-wide Status Bar. It is available across current and future Modules and pages whensettings.statusBarVisibleis enabled. The left segment always renders low-frequency host usage metrics as plain icon-plus-value items with fixed-width numeric columns so the row does not shift as values change. The center segment is the universal notification popup surface driven bystatusBarNotice/showStatusBarNotice. Every Module, page, dialog, and background operation must route transient information, success confirmations, warnings, and errors through this action with the matchingtone; do not render transient outcome messages inline or introduce another toast, banner, or status system. Determinate work usesshowStatusBarProgress. Success notices auto-dismiss after 2 seconds, info and warning notices auto-dismiss after 5 seconds, and error notices stay until dismissed; auto-dismiss notices do not render a countdown bar. CPU, RAM, downstream transfer rate, and upstream transfer rate come from low-overhead direct Windows APIs and are not polled while the Status Bar is hidden. The X server indicator is settings-derived fromsettings.xServerManaged; it must not poll process state. It should not grow back into a debug timing strip; detailed performance measurements belong in diagnostics, scripts, or explicit release-measurement workflows.src/modules/workspace/nativeOverlay.ts— geometry-based overlay suppression detection for RDP ActiveX and URL WebView2 overlay windows. Update this only when a new DOM dialog or non-menu overlay is proven in the real Tauri runtime to intersect a native surface. Keep the detector surface-aware, not document-wide. Simple command menus should usesrc/lib/nativeContextMenu.tsinstead of joining this selector list. Dashboard-owned popovers should prefer an owning-component suppression prop over adding global WebView2 overlay selectors.src/modules/workspace/workspace.css— tab strip, workspace canvas, screenshot menu/Region overlay, and bottom Status Bar CSS.src/app/TutorialOverlay.tsx— app-owned guided-help overlay used by the AI Assistant Tutorial tool. It resolves constraineddata-tutorial-idanchors, scrolls the target into view, dims the rest of the app, places a short balloon, and dismisses on the next user interaction. Because it can intersect RDP, it participates insrc/modules/workspace/nativeOverlay.tsfor RDP ActiveX parking only.src/modules/dashboard/DashboardPage.tsx— Dashboard shell: topbar, view pills, edit-mode toggle, per-view grid density control. HostsDashboardCanvas.src/modules/dashboard/dashboard.css— Dashboard page, widget-grid, widget chrome, and Dashboard-specific widget CSS. It is imported throughsrc/App.css, not fromDashboardPage.tsx, so global CSS order stays centralized.src/modules/dashboard/schema.ts— TypeScript validator for AI Created script widget body JSON before browser-preview persistence and render.src/modules/dashboard/state/dashboardStore.ts— Zustand store: views, widget instances, AI Created Widgets,activeViewId,editMode, AI-Assistant read projection. The single source of truth for in-memory Dashboard state.src/modules/dashboard/state/persistence.ts— typed Tauri command wrappers for everydashboard_*command.src/modules/dashboard/registry/builtInRegistry.ts— built-in widget registry; the one place to add a new built-in widget along with its default preset/accent/icon/size and Body component.src/modules/dashboard/registry/presetRegistry.tsx— three preset chrome components (Panel, Ambient, Hero). Presets read--w-accent/--w-accent-softset byWidgetFrame.src/modules/dashboard/registry/palette.ts— accent palette table and Reicon-compatible icon validation helpers used by the customize popover and the catalog.src/modules/dashboard/view/DashboardCanvas.tsx—react-grid-layouthost. Owns drag/resize wiring and the debounced batched layout-save pipeline.src/modules/dashboard/view/WidgetFrame.tsx— preset chrome wrapper plus edit-mode controls (remove, customize). Sets inline accent CSS variables.src/modules/dashboard/view/WidgetBody.tsx— dispatcher:builtIn→ registry lookup;script→ScriptWidgetHost.src/modules/dashboard/widgets/— one body file per built-in widget (AppLauncherBody.tsx,ConnectionWidgetBody.tsx,NotesBody.tsx,AiCodingUsageBody.tsx,PcInfoBody.tsx,NetworkToolsBody.tsx,GeneratorToolsBody.tsx,ConvertersBody.tsx). Each delegates to its implementation folder undersrc/modules/dashboard/widgets/builtin/.src/modules/dashboard/widgets/builtin/app-launcher/app-launcher.css— App Launcher widget, list/details/tile layouts, and launcher dialogs/menus.src/modules/dashboard/widgets/builtin/ai-coding-usage/ai-coding-usage.css— AI Coding Usage Dashboard widget CSS; its compact Status Bar presentation stays withsrc/modules/workspace/workspace.css.src/modules/dashboard/script/ScriptWidgetHost.tsx—iframe srcdochost forscript-kind widgets. Wires the postMessage bridge and applies declared permissions through CSP.src/modules/dashboard/script/permissions.ts— script-kind capability validation shared with the Rust validator surface.src/modules/dashboard/edit/CatalogOverlay.tsx— "Add widget" modal with search, Built-in/AI Created source tabs, and thumbnail cards.src/modules/dashboard/edit/CustomizePopover.tsx— per-instance editor: preset row, accent palette, icon picker, title input, collapsible Advanced section per kind.src/modules/dashboard/edit/SharedBackgroundPopover.tsx— shared background picker datasource and UI for Dashboard Views, terminal Connection backgrounds, Document viewer backgrounds, and IT Ops drill-view backgrounds. Keep background preset/dynamic/media/extension/fit/dim additions here or in the registries it consumes so every surface shows the same list.src/modules/dashboard/motion.tsx— centralized motion wrappers for Dashboard surfaces; per the motion rule above.src/modules/workspace/connections/terminal/TerminalWorkspace.tsx— terminal workspace, split layout view, pane host, tmux session tag/popover, terminal context menu, SSH tmux inspection helpers.src/modules/workspace/connections/terminal/renderer.ts— renderer abstraction and xterm/WebGL renderer implementation.src/modules/workspace/connections/terminal/terminal.css— terminal workspace, split panes, xterm host, tmux controls, terminal toolbar, and terminal-local menu affordances.src/modules/workspace/connections/sftp/SftpWorkspace.tsx— SFTP dual-pane browser, file panes, transfers, overwrite conflicts, context menu, properties popup.src/modules/workspace/connections/sftp/sftp.css— SFTP/FTP workspace, file panes, transfer queue, conflict dialogs, properties popover, and SFTP-specific context menu CSS.src/modules/git/— Git Browser overlay opened from local terminal and File Explorer surfaces. Owns repository detection hooks, commit graph rendering, working-tree staging/commit UI, Git-specific dialogs, andgit.css; it calls typed command wrappers ingitCommands.tsand reports transient outcomes through the Status Bar. Its advanced side-by-side diff renderer (DiffSideBySide) lives insrc/modules/compare/and is shared with the File Compare overlay.src/modules/compare/— File Compare overlay opened from the File Explorer and SFTP/FTP browser right-click menu ("Select Left File for Compare" / "Compare to"). Owns the app-global left-file selection model (compareTypes.ts, store slicecompareLeft/compareView), the sharedDiffSideBySidetext renderer (also used by the Git Browser), the image difference-heatmap view, the dual hex view, andcompare.css. Remote files are staged to a temp dir (create_compare_temp_dir+downloadPath) so two local paths can be diffed; text diffs use thegit_diff_no_indexcommand.src/modules/workspace/connections/webview/WebViewWorkspace.tsx— URL Connection workspace shell, stable overlayWebviewWindowbounds/visibility/focus lifecycle, toolbar navigation affordances, credentials, downloads, screenshots, and disabled runtime placeholder.src/modules/workspace/connections/webview/webview.css— URL workspace toolbar (File Explorer Apple-esque chrome), connection icon, address bar, status, and placeholder CSS.src/modules/workspace/connections/remote-desktop/RemoteDesktopWorkspace.tsx— RDP/VNC workspace host, RDP ActiveX visibility/bounds synchronization, RDP-only snapshot/parking for intersecting DOM overlays, and VNC canvas framebuffer/input handling.src/modules/workspace/connections/remote-desktop/remote-desktop.css— RDP/VNC workspace shell, placeholder, VNC canvas, and RDP suppression snapshot CSS.src/ai/AssistantPanel.tsx— AI Assistant chat orchestration: prompt submission, streaming reconciliation, attachment capture, extension/dashboard intent UI, terminal send handoff, tool permission mode UI, and dispatch of live Session tool requests. Pure logic is extracted into the sibling modules below; keep new long-lived rendering helpers and stateless helpers out of this file unless they need direct access to Assistant Panel state.src/ai/assistantChatThreads.ts— chat-thread persistence and normalization: SQLite record round-trips, the legacylocalStoragemigration, newest-first ordering, and the defensive normalizers that keep malformed persisted threads/messages out of the UI.src/ai/assistantComposer.ts— composer-side pure helpers: intent inference and prompt wrapping, chat-message and run-manifest construction, attachment capture (image compression, file reading), and stream-event debug logging.src/ai/assistantLiveTools.ts— frontend live-tool dispatcher (runAssistantLiveTool): thesession_*,quick_command_*, screenshot, andtutorial_highlighthandlers that read live state from the workspace/dashboard stores and pane registry. Panel-owned capabilities (navigation, tutorial overlay, translations) are injected throughAssistantLiveToolDeps, so the dispatcher is testable without mounting the panel.src/ai/AssistantMessageView.tsx— one chat message: bubble, attachments, markdown body, work panel, copy/expand actions, image preview, and the inline secret-entry cards (which store secrets locally without exposing them to the model).src/ai/AssistantMarkdownContent.tsx— assistant markdown rendering, code-block copy/send controls, and safe external-link handoff.src/ai/AssistantWorkPanel.tsx— assistant work/timeline disclosure for reasoning content, skill invocations, tool-call progress, the model-published work plan (update_plan), and waiting phrases.src/ai/AssistantToolApprovalCards.tsx— prompt-mode tool approval cards and allow/allow-for-session/deny action UI, including the risk-reason warning block for command-bearing tool calls flagged by the backend heuristic.src/ai/streamMessage.ts— pure stream-event reducer that foldsAiStreamEvents (content/reasoning deltas, tool-call lifecycle, plan updates) into the in-flight assistant message snapshot.src/ai/assistantToolLabels.ts— assistant tool-name normalization plus running/completed tool label mapping.src/ai/assistantScreenshotRegion.ts— screenshot Region geometry helpers and capture-surface wait timing used by the Assistant Panel.src/ai/assistantTypes.ts— shared Assistant Panel data shapes, includingAssistantPageContext, chat messages, attachment records, and pending tool approval records.src/ai/providers.ts— frontend provider registry barrel and provider validation.src/ai/providerRegistry/— one provider definition per file plus shared registry types. Provider model suggestions live here, not in Settings. The Settings model picker must render these suggestions as a real provider-specific select so users can see every known model; freeform model or deployment IDs belong in the separate custom model ID input. Seedocs/AI_PROVIDERS.mdbefore adding or changing provider structure.src/ai/assistant.css— AI Assistant panel, markdown renderer, composer, attachment previews, permission menus, tool approval cards, and assistant work timeline CSS.src/modules/settings/SettingsPage.tsx— Settings shell with sidebar nav and section routing.src/modules/settings/settingsAssistantContext.ts— Settings-to-AI context projection: active section, visible i18n control keys, and allowed tutorial targets.src/modules/settings/WorkspaceSettings.tsx— Workspace display preferences, including the top Tab Strip / Child Connection Tab presentation toggle.src/modules/settings/UrlSettings.tsx— URL Connection security, global data shard and user-agent defaults, saved website password/input-data placement, and URL data shard management. The proxy default moved to Settings → Proxy; per-Connection URL proxy overrides still win over the global value.src/modules/settings/UrlCredentialManager.tsx— shared saved website data rows, compact editor dialog, and delete confirmation used by both URL and Credentials Settings. Keep those surfaces on this shared component so their controls and secret/non-secret boundaries remain identical.src/modules/settings/shared.tsx— SharedSettingsSummaryandPlannedSettingsGridfor settings pages.src/modules/settings/aboutData.ts— Product metadata and open-source component groups.src/modules/settings/settings.css— Settings shell, section fieldsets, form grids, Settings dialogs, MCP server management, assistant-skill settings, and Settings-owned import/export surfaces.src/manual/manual.css— in-app manual viewer layout and markdown CSS.src/lib/clipboard.ts— shared clipboard read/write fallback helpers.src-tauri/src/native_tooltip.rs— Win32 topmost tracking-tooltip bridge used only byRailTooltipso Activity Rail labels can layer above RDP ActiveX child windows without adding overlay parking or frontend close-path hooks.src/lib/nativeContextMenu.ts— shared Tauri native context-menu adapter for simple text command menus, including image/data URL-to-PNG-byte icon rasterization, macOS command-glyph mapping to TauriNativeIcontemplate images, withsrc/lib/nativeContextMenuModel.tscontaining testable menu normalization.src/lib/nativeMenuIcons.ts— app-owned line-icon-style SVG strings used by native context menuiconSvgentries for command-only actions; every entry needs a macOS template mapping insrc/lib/nativeContextMenu.ts, and Connection menus should prefer existing Connection image assets viaiconSrc.src/i18n/config.ts— i18next instance, language detection, dynamic locale loading,switchLanguage(),ensureI18nReady().src/i18n/useT.ts— typed translation hook with key autocompletion.src/i18n/locales/en.json— English source-of-truth; 13 additional locale files under the same directory.src/modules/workspace/connections/terminal/QuickCommandBar.tsx— terminal Quick Command Bar, per-Connection manager dialog, From Library add flow, and custom command editor.src/modules/workspace/connections/terminal/quickCommandLibrary.ts— curated Quick Command library entries and executable snippets.
New feature code should land in the owning source area above. New feature CSS should live beside that source area and be imported from src/App.css in a deliberate cascade order. Shared selectors such as .status-bar, .terminal-menu, .dialog-backdrop, .icon-button, form basics, and generic context-menu rules belong in src/styles/base.css unless they are truly area-specific. Keep src/App.tsx limited to app chrome and cross-cutting bootstrap. Workspace state, settings I/O, layout serialization, terminal rendering, pane input routing, and the Tauri command boundary remain separated under src/store.ts, src/lib/, src/modules/workspace/, src/modules/workspace/connections/terminal/, and src/lib/tauri.ts.
Backend Source Map
Rust backend modules live flat under src-tauri/src/, with multi-file features grouped into subdirectories. lib.rs wires the Tauri builder: it registers commands, manages shared state (registries, managers, trackers), and owns the app-shell/native integration glue. The generate_handler! registry is annotated with domain section headers (Connections, Settings, SFTP, RDP, Dashboard, Installer, Watchdog, …); pure custom-font and dashboard-background media helpers live in media.rs. New backend work should land in the owning module below rather than growing lib.rs.
User-writable media (custom fonts and dashboard/background-picker media) resolves to a writable location that differs per platform, and the asset-protocol scope in src-tauri/tauri.conf.json must list every variant. The shared resolver is media_root() in media.rs; custom_fonts_folder()/backgrounds_folder() just append the subdirectory. Only Windows keeps media next to the executable (current_exe()/..): the installer is a per-user (currentUser) install under %LOCALAPPDATA%, so that directory is writable, and $RESOURCE resolves to it in the scope glob (prefer $RESOURCE over $EXE on Windows — $EXE can fail to resolve there even though std::env::current_exe() locates the same folder). On macOS (read-only, code-signed .app) and Linux (read-only AppImage squashfs, or root-owned /usr/bin for .deb/.rpm) the executable directory is not writable, so media_root() returns app_data_dir() instead, matched by the $APPDATA/<dir>/**/* scopes (~/Library/Application Support/<id> on macOS, ~/.local/share/<id> on Linux). Because the path is platform-conditional, the resolver needs the AppHandle — thread it through the media commands (list_custom_fonts, load_custom_font_data, dashboard_import_background_image, dashboard_load_background_image) rather than resolving from current_exe() alone. Keep the trailing /**/* so files directly inside the folder are included. Custom TTF/OTF metadata supplies family, weight, italic style, and monospacing; the frontend groups available faces by internal family and registers each face with matching FontFace descriptors. Families remain usable when only some faces exist, while Normal, Mono, and Propo variants stay separate because their internal family names differ. App UI may use any custom family, but Terminal exposes and retains only families whose metadata reports fixed-width cells; proportional custom terminal settings normalize back to the terminal default because xterm cannot safely lay them out. WOFF/WOFF2 files retain filename-based fallback metadata when ttf-parser cannot decode their containers. When a custom font is selected but not rendered, inspect the generated asset.localhost request first; a 403 or zero-byte response is a scope/configuration failure before FontFace decoding or CSS family matching.
The two largest backend modules are split into directory modules rather than single files. ai/ and storage/ keep their public type/impl surface intact (additional impl Storage blocks attach to the same type) while spreading the implementation across cohesive submodules — see the ai.rs and storage.rs entries below. Their inline #[cfg(test)] suites live in ai/tests.rs and storage/tests.rs.
Connections, Sessions, and transports:
sessions.rs— central Session manager: spawns and tracks PTY, SSH, Telnet, Serial, and X-server Sessions; emits terminal output; owns SSH port forwarding and tmux session tracking.windows_local_pty.rs— Windows ConPTY abstraction: process spawn with attribute lists, handle inheritance, resize, and reader/writer I/O for local terminals.ssh.rs— native SSH terminal overrussh: key/password/agent auth, host-key verification, tmux resume, configurable buffering.ssh_config.rs— parses~/.ssh/configinto Connection drafts (host, user, port, key path, ProxyJump).ssh_keys.rs— generates Ed25519 key pairs viassh-keygen, copies public keys to remote hosts, and secures private-key permissions.x_server.rs— launches the VcXsrv X11 server when needed for SSH X forwarding and returns the display number.sftp.rs— SFTP client: directory listing, upload/download with progress events, transfer cancellation, and Windows-drive virtual paths.git.rs— Git Browser backend: shells out to the systemgitbinary on background workers for repository detection, overview/log/status/diff reads, and user-initiated mutations such as stage, commit, checkout, branch, tag, merge, cherry-pick, revert, stash, fetch, pull, and push. It relies on the user's Git config, credential manager, and SSH agent rather than storing Git credentials in KKTerm.telnet.rs— stateful Telnet client with interactive option negotiation, XTERM/VT100 terminal-type fallback, NAWS resize propagation, NVT input encoding, login-prompt detection, and optional automatic credential submission.serial.rs— native serial terminal with configurable speed and DTR/RTS, emitting raw bytes to the Session.ftp.rs— FTP/FTPS client with async transfers, listing parse, progress events, and configurable TLS/connection modes.rdp.rs— RDP client over the WindowsMsRdpClientActiveX control: connect, keyboard/mouse input, clipboard, Ctrl+Alt+Del, scaling, and the staged-bounds visibility lifecycle.vnc.rs— VNC/RFB client: pixel-format/encoding negotiation, mouse/keyboard input, clipboard paste, shared and view-only modes.webview.rs— URL Connection backend: stable owned overlayWebviewWindowcreation, global/per-Connection URL user-agent override for browser-allowlisted appliance sites, per-Session HTTP CONNECT/SOCKS5 proxy application, proxy-keyed Windows WebView2 data directories (required because different Environment arguments cannot share one user-data folder), bounds/visibility/focus, navigation, downloads, new-window handling, certificate bypass and macOS invalid-certificate Status Bar events (including first-WKWebView delegate refresh), credential-agent injection, shared WebView2 stability args, and the main WebView clipboard-permission helper.import.rs— parses Connection import formats (RDCMan XML, MobaXterm, PuTTY registry, CSV/TSV) into Connection drafts with warnings.
AI, Dashboard, Installer, and Network tools:
ai.rs— assistant runtime: tool registration and execution, approval gating, the live-tool bridge, and shared logging macros. See the AI Assistant area for the full contract.ai/holds the decomposition: provider adapters (providers/), prompt contracts (prompt_contracts.rs), the OpenAI-compatible chat/Responses implementation (openai_provider.rs), SSE streaming and the HTTP client (streaming.rs), ACP/CLI agent backends and discovery (cli_backend.rs), the email tool transports (email.rs), web search/scrape (web_search.rs), and the test suite (tests.rs).ai_coding_usage.rs— tracks and syncs coding-CLI usage/quota (Codex, Claude Code) and persists auth state.github_copilot.rs— GitHub Copilot OAuth device-flow sign-in and token polling.mcp.rs— remote MCP HTTP client: server CRUD, schema caching, tool calls, and credential-backend-stored auth headers.mcp_bridge.rs— local MCP bridge: a Windows named-pipe server that dispatches external JSON-RPCtools/callrequests to in-process assistant tools.assistant_skills.rs— loads assistant skill directories from disk, validates their YAML, and tracks enabled/disabled state.dashboard_commands.rs— Tauri commands for Dashboard View/Widget lifecycle (create, update, remove, load state) plus user-facing widget export/import (export_dashboard_widgets/import_dashboard_widgets/import_dashboard_widgets_json) to portable.kkwidgetJSON.dashboard_storage.rs— SQLite-backed Dashboard persistence: Views, Widget Instances, AI Created Widgets, backgrounds, and grid settings.dashboard_validation.rs— validates Widget presets, accents, icons, grid bounds, script body JSON, and background types against fixed enums.dashboard_ids.rs— generates monotonic Dashboard element ids.app_launcher.rs— App Launcher Widget backend: launches apps/folders with normal/admin/different-user modes and prepares entry metadata (icon, extension, size).net/— network admin tools exposed to Dashboard script widgets: DNS lookup, TCP reachability, interfaces, Wake-on-LAN, WHOIS, ping, and port scan, with a cancellable stream registry over a singlenet::commands::EVENT_CHANNEL.
Watchdog, secrets, storage, and diagnostics:
watchdog/— AI Watchdog backend (registry, polling, predicate evaluation, targets, SSH session-activity tracking); see the Watchdog area.secrets.rs— secret storage abstraction and platform backend selection (Connection passwords, API keys, MCP auth, widget secrets).selective_export.rs— category-aware (.kkbackup) export/import:export_selective_database,inspect_selective_database,import_selective_database. A generic, metadata-driven row engine copies chosen segments (connections, workspaces, dashboards, settings, MCP servers) with per-segment skip/add(merge)/replace and foreign-key remap; opt-in passwords are passphrase-encrypted intosecrets.enc(Argon2id + AES-256-GCM, reusing thesecrets/sqlite_store.rsenvelope). See ADR 0010. Distinct from the whole-database snapshot commands instorage.rs, which pack/replace the entire SQLite file for startup backup/recovery and the Settings Import full-ZIP restore path.storage.rs— SQLite schema, migrations, validation, and coreStoragelifecycle. The largeimpl Storageis split by entity understorage/:connections.rs(connection records, folders, duplicate/move, and credential metadata),settings.rs(all get/update settings accessors), with the test suite instorage/tests.rs. Additionalimpl Storageblocks in these files attach to the sameStoragetype, so callers are unaffected.diagnostics.rs— builds local-only diagnostic bundles (logs, performance snapshots, manifest) while excluding secrets, the database, and terminal output.logging.rs— initializes local log files and the Advanced-Debugging-gated subsystem debug logs.debug_heartbeat.rs— polls main-thread and frontend liveness, logging stalls for freeze diagnostics.performance.rs— host CPU/RAM/network and app uptime/working-set counters feeding the Status Bar metrics.
App shell, window, and OS integration:
app_tray.rs— system tray icon, context menu (recent Connections, Don't Sleep, exit), and minimize-to-tray routing.app_updates.rs— downloads, SHA256-validates, and installs app updates (currently gated; see the Updates area).auto_start.rs— toggles WindowsRun-key auto-start on login.power.rs— Don't Sleep mode viaSetThreadExecutionStateplus shutdown-block registration; exposes theDontSleepManagerstate.window_effects.rs— Windows 11 rounded corners, custom-title-bar window styling, andTAURI_DRAG_RESIZE_WINDOWno-activate focus neutralization for the Windows custom-titlebar helper.window_state.rs— persists/restores main-window size, maximized state, and multi-monitor bounds.native_tooltip.rs— Win32 topmost tracking-tooltip bridge used byRailTooltipto layer over RDP ActiveX surfaces.favicon.rs— fetches and parses HTML for favicon links and returns validated base64 icon data URLs for URL Connections.screenshot.rs— captures screen rectangles (DirectX or GDI) to clipboard or base64 JPEG for assistant context.manual.rs— bundled manual chapter metadata (slug, order, filename, title).bin/— thekkterm-clibinary: an external stdio MCP client that bridges to themcp_bridgenamed pipe.
Color Scheme CSS Variables
Color schemes are defined in src/styles/colorSchemes.css as CSS custom property blocks keyed by [data-color-scheme="scheme-name"]. The :root block defines the "default" scheme. src/App.css imports this file before shared selectors so all feature CSS files can consume the same design tokens. Each scheme must define every variable in the list below. When adding a new scheme, also update:
src/types.ts—ColorSchemeunion typesrc/modules/settings/ThemeSchemeGrid.tsx—SCHEME_ORDER(label key) andSCHEME_TOKENS(mini-app preview tokens)src/i18n/locales/en.json— display label undersettings.scheme*src-tauri/src/storage.rs—validate_appearance_settings()whitelistdocs/localization_todo/— add a per-key<namespace>.<keyPath>.mdfrom_TEMPLATE.mdfor any new or changed UI label
The "match-os" color scheme is a selector setting, not a separate CSS token block. On app startup and when the user switches to "match-os", the frontend resolves the current OS light/dark preference to the existing "light" or "dark" CSS block. It may read the Windows system accent once through the typed get_system_accent_color command and apply it as inline accent-token overrides. Do not add polling or OS theme-change listeners for this setting without a new product decision.
Variable → UI Area Mapping
| Variable | CSS Property | UI Area |
|---|---|---|
--app-bg | background of :root | Full app background (visible behind session pane, settings) |
--chrome | background of .connection-sidebar, .assistant-panel | Connections tree sidebar, AI Assistant panel |
--chrome-strong | background of .workspace | Main workspace / session pane |
--surface | background of .tree-folder-row:hover, .search-box, various cards/dialogs | Hover highlights, search inputs, surface-level containers |
--surface-muted | background of sub-surfaces | Muted surface variant (Settings fieldsets, inactive panels, readonly/disabled controls) |
--terminal | Terminal pane background (xterm theme) | Terminal viewport background |
--terminal-2 | Secondary terminal background | Split pane alternate, terminal chrome |
--terminal-border | border-color of terminal panes | Terminal pane borders |
--text | color of primary text | Body text, headings, primary content |
--text-muted | color of secondary text | Descriptions, secondary labels |
--text-faint | color of tertiary text | Placeholders, disabled text |
--border | border-color of containers | Container borders, separators |
--border-strong | border-color of emphasized containers | Active borders, panel resize handles |
--accent | color / background of primary actions | Buttons, links, active indicators |
--accent-soft | background of accent tints | Accent-tinted backgrounds, selection highlights |
--green | color of success states | Success badges, connection status |
--green-soft | background of green tints | Success-tinted backgrounds |
--amber | color of warning states | Warning badges, pending status |
--amber-soft | background of amber tints | Warning-tinted backgrounds |
--red | color of error states | Error badges, disconnected status |
--notice-info | color of info notification tone | Status Bar notification icon, ring, and progress |
--notice-success | color of success notification tone | Status Bar notification icon, ring, and progress |
--notice-warning | color of warning notification tone | Status Bar notification icon, ring, and progress |
--notice-error | color of error notification tone | Status Bar notification icon, ring, and progress |
--nav-toolbar-bg | background of .workspace-toolbar, top nav bars | Session toolbar, SSH/SFTP/RDP nav bars |
--nav-toolbar-text | color of toolbar text | Toolbar labels, toolbar button text |
--nav-toolbar-hover-bg | background of toolbar button hover | Toolbar button hover state |
--nav-toolbar-accent | color of toolbar accent | Toolbar active indicators, toolbar link color |
--nav-toolbar-warning | color of toolbar warnings | Toolbar warning indicators |
--nav-toolbar-tooltip-bg | background of toolbar tooltips | Toolbar popup tooltip background |
--nav-toolbar-tooltip-text | color of toolbar tooltip text | Toolbar popup tooltip text |
--nav-toolbar-tooltip-border | border-color of toolbar tooltips | Toolbar popup tooltip border |
--shadow | box-shadow of elevated surfaces | Dropdowns, popovers, tooltips |
--app-ui-font-family | font-family of :root | App-wide UI font stack |
Performance and Release
Performance budgets, measurement runbook, terminal renderer notes, and the latest measured numbers live in docs/PERFORMANCE.md. The manual terminal compatibility checklist lives in docs/TERMINAL_COMPATIBILITY_CHECKLIST.md.
Distribution targets, packaging scripts, the no-telemetry posture, diagnostics bundle shape, and v0.2 updater scope live in docs/RELEASE.md.
Architectural principle: startup and session creation must avoid eager initialization and heavyweight dependencies; expensive subsystems initialize lazily. When a change risks regressing a budget in docs/PERFORMANCE.md, treat that doc as the authority.