From 84b3bc1c90e422295dc7ca21ca82d634a4f1c153 Mon Sep 17 00:00:00 2001 From: iptoux Date: Fri, 22 May 2026 02:35:59 +0200 Subject: [PATCH 01/48] feat(workbench): implement center tab management and file preview functionality - Added support for center tabs in the workspace, including dynamic tab creation and management. - Introduced `CenterTab` and `CenterTabKind` structures to represent different types of tabs. - Implemented functions to handle opening, closing, and switching between center tabs. - Enhanced the workspace layout to accommodate center tabs and their respective content panels. - Added a file preview feature that allows users to view file contents directly within the workspace. - Updated styles for the new center tab interface and file preview components. - Refactored existing workspace and terminal management code to integrate with the new tab system. --- CHANGELOG.md | 4 + src-tauri/src/commands.rs | 11 + src-tauri/src/fs_entries.rs | 105 +++++- src-tauri/src/lib.rs | 2 + src/config/app.config.rs | 5 + src/i18n/keys.rs | 5 + src/i18n/locales/de_de.rs | 11 +- src/i18n/locales/en_us.rs | 9 +- src/i18n/locales/es_es.rs | 47 +-- src/i18n/locales/fr_fr.rs | 47 +-- src/i18n/locales/hu_hu.rs | 47 +-- src/i18n/locales/it_it.rs | 47 +-- src/i18n/locales/ja_jp.rs | 47 +-- src/i18n/locales/ko_kr.rs | 47 +-- src/i18n/locales/pl_pl.rs | 47 +-- src/i18n/locales/pt_br.rs | 47 +-- src/i18n/locales/ru_ru.rs | 47 +-- src/i18n/locales/zh_cn.rs | 47 +-- src/i18n/locales/zh_tw.rs | 47 +-- src/tauri_bridge.rs | 34 ++ src/workbench/harness_ui.rs | 385 ++++++++------------ src/workbench/mod.rs | 7 +- src/workbench/project_explorer/mod.rs | 10 +- src/workbench/state.rs | 313 +++++++++++++++-- src/workbench/workspace_panel.rs | 488 ++++++++++++++++++-------- styles.css | 286 ++++++++++++++- 26 files changed, 1533 insertions(+), 659 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f489b07..8227fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Center multi-view tabs**: the workspace pane now hosts a VS Code-style tab strip above the terminal grid. The pinned **Terminals** tab is non-closeable and always renders the existing PTY layout; additional tabs are opened dynamically and closed via the strip. Per-workspace state (`center_tabs`, `center_active_tab_id`, `center_next_tab_id`) is persisted in the workspace snapshot with tolerant serde defaults so older snapshots load cleanly and self-heal to include the Terminals tab. Switching tabs hides (rather than unmounts) the terminal grid so xterm sessions and PTYs are never recreated. Active-tab tracking is wired into `is_workspace_active` so terminal focus/resize observers only fire when the Terminals tab is visible. +- **File preview tab**: clicking a file row in the sidebar Project Explorer opens (or reuses) a center tab that renders the file's text contents. New Tauri command `read_workspace_text_file` (`src-tauri/src/fs_entries.rs`) reads UTF-8 text under the workspace root with the same `canonical_root` / `resolve_under_root` sandbox the existing `list_path_entries` uses, hard-caps at 512 KiB (`MAX_TEXT_PREVIEW_BYTES`) and returns `{ content, truncated, byteLen }`. Non-text payloads surface `file is not valid UTF-8 text`, directories `not a file`, and out-of-root paths the existing `outside workspace` error. The new `tauri_bridge::read_workspace_text_file` wrapper plus `TextFilePreview` mirror the payload shape. 4 unit tests pin the happy path, traversal rejection, directory rejection, and missing-file rejection. +- **Docked settings tab**: the harness settings UI moved out of the modal `SettingsChrome` overlay and into a `SettingsDock` component rendered inside a dynamic center tab. Opening settings (command palette `Open Settings`, etc.) now calls `WorkbenchService::open_center_settings_tab` which reuses any existing settings tab for the active workspace instead of stacking duplicates. The legacy scrim + focus-trap helpers (`focus_first_settings_control`, `settings_focusables`, `trap_settings_tab`) and `HarnessUiService::open_settings` are gone; `HarnessSettingsCategory` is now `Serialize/Deserialize` so it can ride along with the snapshot. The chat header, right panel and other categories continue to interact with the existing `settings_category` signal. - **GitHub Releases auto-updater**: BLXCode now wires Tauri v2's updater/process plugins with a signed GitHub Releases `latest.json` endpoint, desktop IPC for `app_version`, `updater_check`, `updater_install_start`, progress polling, and relaunch, plus a Leptos startup banner and themed update dialog with release notes, progress, speed, retry, and restart states. Settings -> App gains a persisted startup auto-check toggle and manual update check. Release automation now supports the hybrid flow: macOS/Windows artifacts are built in GitHub Actions, Linux artifacts can be built locally to save CI cost, `.deb`/`.rpm` stay manual download assets, and signed updater-capable payloads are merged into one canonical `latest.json`. Tauri updater signing is documented as separate from Apple/Microsoft installer certificates. - **Boot loading screen**: new branded loading experience that paints before the WASM bundle is ready and stays on screen until the workbench is mounted. `index.html` ships a static `#blx-static-boot` section (logo, eyebrow, faux workbench preview, animated rail) so the first paint happens immediately on Trunk-served HTML; once Leptos mounts, the static node is removed and `BootLoadingScreen` (`src/boot_loading.rs`) takes over with phased copy (`Starting BLXCode` → `Restoring workspace` → `Opening workbench`). 306 lines of dedicated CSS in `styles.css` add the radial-gradient backdrop, frame-enter / sheen / rail keyframes, and the sidebar/main/panel preview skeleton. The `App` boot fallback now renders `` instead of the prior empty `app-shell--boot` div. - **Agent question card (`harness.ask_user`)**: new client-side tool that lets the coordinator agent ask a clarifying multiple-choice question and receive the user's answer as a structured tool result. Backend registers `harness.ask_user` (`src-tauri/src/agent/tools.rs`) with a JSON schema accepting `question`, optional `header` (≤12 chars), 2–4 `options` (label + optional description), `multiSelect`, and `allowOther`; the tool sits in the `CoordinatorHarness` group (subagents excluded) and the system prompt instructs the model to use it only when 2–4 distinct options would unblock progress. Frontend adds a new `TimelineItem::AskUser` variant and `ask_user_card/` component (Angular-style folder with co-located CSS) rendering a chat bubble with numbered option buttons, single- or multi-select mode, an optional free-text “Other” field, and a cancel button; the card submits via `agent_submit_tool_result` and transitions the row state to `Answered { selected, other }` or `Cancelled` so the bubble stays visible with disabled controls. Persistence drops `Open` ask-user rows from the saved timeline (the awaiting backend loop is dead after reload). Tool-result payload: `{ "selected": [...], "other": "...", "cancelled": false }`; on cancel: `ok=false` + `{ "cancelled": true }`. 8 new i18n keys (`AgAskUser*`) added in `en_us.rs` and `de_de.rs`; other locales receive English placeholders pending a `render_i18n_locales_from_en.py` pass. @@ -17,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **`ChatUsageStats` schema migrated**: `total_cost_usd: f64` and `current_turn_generation: u64` added; legacy `ttft_sum_ms` / `ttft_sample_count` fields removed (TTFT now lives per-row). Older `workbench.json` snapshots deserialize cleanly thanks to `#[serde(default)]` and rewrite without the removed fields on next save. `record_chat_turn_usage` signature changed to accept the new `turn_generation` and `cost_usd` and to return `bool` so callers can drop stale events. +- **Settings panel restructured**: `AppSettingsPane` (`src/workbench/harness_ui.rs`) now reads top-to-bottom as Language → Keyboard shortcuts → Notifications → **Terminal hooks** → **App updates** (updates moved below hooks to put install actions closer to the per-agent hook list). Keyboard shortcuts and Notifications switched from single-column lists to 2-column grids (new `.app-prefs-shortcut-modes--grid` and `.app-prefs-toggle-grid`). Current version no longer renders inside a readonly `` — it's now a styled `
` row, and a sibling **Available version** row only appears when `UpdateService::available_version` is `Some`. Terminal hooks list (`.harness-hooks__list--grid`) lays out as 3 columns on wide screens, collapsing to 2 / 1 below 900 px / 600 px; status badges dropped from pill (`border-radius: 999px`) to sm (`4px`) rounding. The EULA status row was removed from the App pane (acceptance is still gated at boot; the field added no actionable signal here). Workspace pane changed the agent sandbox ` + /> + + {move || i18n.tr(I18nKey::WsRootHint)()} +
+ } + } + /> + >() + } + key=|i| *i + children=move |i| { + view! { + + } + } + /> + +
+ + + + + + } +} + +#[component] +fn CenterTabStrip(workspace_id: u64, active_tab_id: Memo) -> impl IntoView { + let wb = expect_context::(); + + view! { +
+
+
+
+ } +} + +#[component] +fn CenterTabButton(workspace_id: u64, tab: CenterTab, active_tab_id: Memo) -> impl IntoView { + let wb = expect_context::(); + let id = tab.id; + let title = tab.title.clone(); + let closeable = tab.closeable(); + let icon = center_tab_icon(&tab.kind); + + view! { + + } +} + +#[component] +fn DynamicCenterPanels(workspace_id: u64, active_tab_id: Memo) -> impl IntoView { + let wb = expect_context::(); + let ui = expect_context::(); + let embed = expect_context::(); + + view! { + >() + } + key=|tab| tab.id + children=move |tab| { + let tab_id = tab.id; + match tab.kind { + CenterTabKind::Settings => view! { +
+ +
+ }.into_any(), + CenterTabKind::FilePreview { rel_path } => view! { +
+ +
+ }.into_any(), + CenterTabKind::Terminals => view! { <> }.into_any(), + } + } + /> + } +} + +#[component] +fn FilePreviewDock(workspace_id: u64, rel_path: String) -> impl IntoView { + let wb = expect_context::(); + let result = RwSignal::new(None::>); + let load_gen = RwSignal::new(0_u32); + let rel_for_effect = rel_path.clone(); + + Effect::new(move |_| { + let _ = load_gen.get(); + result.set(None); + if !is_tauri_shell() { + result.set(Some(Err( + "File preview is available in the desktop app.".into() + ))); + return; + } + let Some(workspace) = wb + .workspaces() + .get() + .into_iter() + .find(|workspace| workspace.id == workspace_id) + else { + result.set(Some(Err("Workspace not found.".into()))); + return; + }; + let root = workspace.cwd; + let path = rel_for_effect.clone(); + leptos::task::spawn_local(async move { + let next = read_workspace_text_file(root, path).await; + result.set(Some(next)); + }); + }); + + view! { +
+
+
+ + {rel_path.clone()} +
+ +
+ {move || match result.get() { + None => view! { +
"Loading file..."
+ }.into_any(), + Some(Err(err)) => view! { +
{err}
+ }.into_any(), + Some(Ok(preview)) => view! { + +
+ {format!("Preview truncated at 512 KiB of {} bytes.", preview.byte_len)} +
+
+
{preview.content}
+ }.into_any(), + }} +
+ } +} + +fn center_tab_icon(kind: &CenterTabKind) -> icondata::Icon { + match kind { + CenterTabKind::Terminals => icondata::LuTerminal, + CenterTabKind::Settings => icondata::LuSettings2, + CenterTabKind::FilePreview { .. } => icondata::LuFileText, } } diff --git a/styles.css b/styles.css index 13b685e..1ef8b2f 100644 --- a/styles.css +++ b/styles.css @@ -1494,6 +1494,74 @@ button.workbench-shortcut-row--action:focus-visible { gap: 0.45rem; } +.app-prefs-shortcut-modes--grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.45rem 0.9rem; +} + +.app-prefs-toggle-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.55rem 0.9rem; +} + +.app-prefs-toggle-cell { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.app-prefs-version { + margin: 0.55rem 0 0; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.app-prefs-version__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + padding: 0.32rem 0.5rem; + border-radius: 4px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.02); + font-size: 0.78rem; + margin: 0; +} + +.app-prefs-version__row dt { + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: rgba(180, 185, 200, 0.85); + margin: 0; +} + +.app-prefs-version__row dd { + margin: 0; + font-family: var(--font-mono); + color: var(--text); +} + +.app-prefs-version__row--available { + border-color: rgba(104, 220, 159, 0.35); + background: rgba(104, 220, 159, 0.08); +} + +.app-prefs-version__row--available dd { + color: #86e7b2; +} + +@media (max-width: 720px) { + .app-prefs-shortcut-modes--grid, + .app-prefs-toggle-grid { + grid-template-columns: minmax(0, 1fr); + } +} + .app-prefs-radio { display: flex; align-items: center; @@ -4333,6 +4401,24 @@ button.workbench-shortcut-row--action:focus-visible { gap: 0.55rem; } +.harness-hooks__list--grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.5rem; +} + +@media (max-width: 900px) { + .harness-hooks__list--grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 600px) { + .harness-hooks__list--grid { + grid-template-columns: minmax(0, 1fr); + } +} + .harness-hooks__item { display: flex; align-items: center; @@ -4392,8 +4478,8 @@ button.workbench-shortcut-row--action:focus-visible { display: inline-flex; align-items: center; gap: 0.42rem; - padding: 0.28rem 0.5rem; - border-radius: 999px; + padding: 0.22rem 0.45rem; + border-radius: 4px; border: 1px solid rgba(255, 122, 122, 0.32); color: #ff9a9a; background: rgba(255, 122, 122, 0.08); @@ -4924,6 +5010,202 @@ button { display: none; } +.workspace-center-tabs { + flex: 0 0 auto; + min-width: 0; + padding: 0.32rem 0.35rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0)), + rgba(0, 0, 0, 0.18); +} + +.workspace-center-tabs__strip { + display: flex; + align-items: center; + gap: 0.18rem; + min-width: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.workspace-center-tab { + flex: 0 1 13rem; + min-width: 2.35rem; + max-width: 15rem; + height: 32px; + display: flex; + align-items: center; + gap: 0.38rem; + padding: 0 0.35rem 0 0.55rem; + border: 1px solid transparent; + border-bottom: 0; + border-radius: 4px 4px 0 0; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0; + overflow: hidden; +} + +.workspace-center-tab:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.055); +} + +.workspace-center-tab--active { + color: var(--accent); + border-color: rgba(88, 166, 255, 0.32); + background: rgba(88, 166, 255, 0.11); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.workspace-center-tab__icon, +.workspace-center-tab__close { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.workspace-center-tab__label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workspace-center-tab__close { + width: 18px; + height: 18px; + margin-left: auto; + border-radius: 4px; + color: var(--text-muted); +} + +.workspace-center-tab__close:hover, +.workspace-center-tab__close:focus-visible { + color: var(--text); + background: rgba(255, 255, 255, 0.1); +} + +.workspace-center-tab-body { + flex: 1 1 0; + min-width: 0; + min-height: 0; + position: relative; + display: flex; +} + +.workspace-center-panel { + flex: 1 1 0; + min-width: 0; + min-height: 0; + width: 100%; + display: flex; + flex-direction: column; +} + +.workspace-center-panel--hidden { + display: none; +} + +.workspace-center-panel--scroll { + overflow: auto; +} + +.harness-settings-grid--docked { + flex: 1 1 0; + min-height: 0; + height: 100%; + background: rgba(0, 0, 0, 0.12); +} + +.harness-settings-grid--docked .harness-settings-detail { + min-height: 0; +} + +.file-preview { + flex: 1 1 0; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: rgba(8, 10, 14, 0.62); +} + +.file-preview__header { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.65rem; + min-height: 42px; + padding: 0.45rem 0.65rem; + border-bottom: 1px solid var(--border); + background: rgba(0, 0, 0, 0.2); +} + +.file-preview__title { + min-width: 0; + display: flex; + align-items: center; + gap: 0.45rem; + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text); +} + +.file-preview__title span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-preview__icon { + flex-shrink: 0; + color: var(--accent); +} + +.file-preview__status, +.file-preview__notice { + margin: 0.8rem; + color: var(--text-muted); + font-size: 0.82rem; +} + +.file-preview__status--error { + color: #ff8c8c; +} + +.file-preview__notice { + flex: 0 0 auto; + margin-bottom: 0; + padding: 0.48rem 0.58rem; + border: 1px solid rgba(251, 191, 36, 0.28); + border-radius: 4px; + background: rgba(251, 191, 36, 0.08); + color: #f6d58a; +} + +.file-preview__content { + flex: 1 1 0; + min-height: 0; + margin: 0; + padding: 0.75rem 0.85rem; + overflow: auto; + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.55; + white-space: pre; + color: #d9e2f1; + background: transparent; +} + .ws-term-grid__resize { position: absolute; z-index: 6; From 1a73b0b16750c4d4d4df4ad9989087d6a1b3635e Mon Sep 17 00:00:00 2001 From: iptoux Date: Fri, 22 May 2026 02:47:04 +0200 Subject: [PATCH 02/48] style: enhance layout and spacing for app preferences and harness components --- styles.css | 221 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 149 insertions(+), 72 deletions(-) diff --git a/styles.css b/styles.css index 1ef8b2f..3f03c32 100644 --- a/styles.css +++ b/styles.css @@ -1496,27 +1496,40 @@ button.workbench-shortcut-row--action:focus-visible { .app-prefs-shortcut-modes--grid { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 0.45rem 0.9rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.55rem 1rem; } .app-prefs-toggle-grid { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 0.55rem 0.9rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.8rem 1.1rem; } .app-prefs-toggle-cell { display: flex; flex-direction: column; - gap: 0.15rem; + gap: 0.2rem; + padding: 0.55rem 0.7rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.02); +} + +.app-prefs-toggle-cell .app-prefs-toggle { + margin: 0; + font-size: 0.85rem; +} + +.app-prefs-toggle-cell .app-prefs-hint { + margin: 0 0 0 1.55rem; } .app-prefs-version { - margin: 0.55rem 0 0; + margin: 0.35rem 0 0.1rem; display: flex; flex-direction: column; - gap: 0.25rem; + gap: 0.4rem; } .app-prefs-version__row { @@ -1524,26 +1537,33 @@ button.workbench-shortcut-row--action:focus-visible { align-items: center; justify-content: space-between; gap: 0.6rem; - padding: 0.32rem 0.5rem; - border-radius: 4px; + padding: 0.55rem 0.75rem; + border-radius: 0.5rem; border: 1px solid var(--border); - background: rgba(255, 255, 255, 0.02); - font-size: 0.78rem; + background: rgba(255, 255, 255, 0.025); + font-size: 0.85rem; margin: 0; } .app-prefs-version__row dt { display: inline-flex; align-items: center; - gap: 0.4rem; - color: rgba(180, 185, 200, 0.85); + gap: 0.5rem; + color: var(--text-muted); margin: 0; + font-weight: 500; +} + +.app-prefs-version__row dt svg { + width: 0.9rem; + height: 0.9rem; } .app-prefs-version__row dd { margin: 0; font-family: var(--font-mono); color: var(--text); + font-size: 0.9rem; } .app-prefs-version__row--available { @@ -1555,13 +1575,6 @@ button.workbench-shortcut-row--action:focus-visible { color: #86e7b2; } -@media (max-width: 720px) { - .app-prefs-shortcut-modes--grid, - .app-prefs-toggle-grid { - grid-template-columns: minmax(0, 1fr); - } -} - .app-prefs-radio { display: flex; align-items: center; @@ -4115,25 +4128,46 @@ button.workbench-shortcut-row--action:focus-visible { } .harness-settings-detail { - padding: 0.75rem; + padding: 1.4rem clamp(1rem, 3vw, 1.75rem) 2rem; overflow: auto; } +.harness-pane { + display: flex; + flex-direction: column; + gap: 1.25rem; + max-width: 880px; +} + .harness-pane-title { - margin: 0 0 0.45rem; - font-family: var(--font-mono); - font-size: 0.9rem; + margin: 0 0 0.25rem; + font-family: inherit; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; display: flex; align-items: center; - gap: 0.42rem; + gap: 0.65rem; min-width: 0; + color: var(--text); } .harness-pane-title__icon { flex-shrink: 0; - display: grid; - place-items: center; - color: var(--accent); + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + background: var(--accent-soft); + color: var(--accent-cool, var(--accent)); +} + +.harness-pane-title__icon svg { + width: 1.05rem; + height: 1.05rem; } .harness-pane-title__text { @@ -4141,21 +4175,34 @@ button.workbench-shortcut-row--action:focus-visible { } .harness-pane-subhead { - margin: 0; - font-family: var(--font-mono); - font-size: 0.78rem; + margin: 0 0 0.7rem; + font-family: inherit; + font-size: 0.86rem; + font-weight: 600; + letter-spacing: 0.03em; display: flex; align-items: center; - gap: 0.38rem; + gap: 0.55rem; min-width: 0; + color: var(--text); } .harness-pane-subhead__icon { flex-shrink: 0; - display: grid; - place-items: center; - color: var(--accent); - opacity: 0.95; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.4rem; + background: var(--accent-soft); + color: var(--accent-cool, var(--accent)); + opacity: 1; +} + +.harness-pane-subhead__icon svg { + width: 0.9rem; + height: 0.9rem; } .harness-pane-subhead__text { @@ -4165,18 +4212,25 @@ button.workbench-shortcut-row--action:focus-visible { .harness-field-label { display: inline-flex; align-items: center; - gap: 0.35rem; + gap: 0.45rem; min-width: 0; color: var(--text-muted); - font-size: 0.75rem; + font-size: 0.82rem; + font-weight: 500; } .harness-field-label__icon { flex-shrink: 0; - display: grid; - place-items: center; - color: var(--accent); - opacity: 0.85; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--accent-cool, var(--accent)); + opacity: 0.9; +} + +.harness-field-label__icon svg { + width: 0.92rem; + height: 0.92rem; } .harness-field-label__text { @@ -4184,9 +4238,9 @@ button.workbench-shortcut-row--action:focus-visible { } .harness-pane p { - font-size: 0.76rem; + font-size: 0.8rem; color: var(--text-muted); - line-height: 1.62; + line-height: 1.6; } .harness-error-text { @@ -4198,9 +4252,29 @@ button.workbench-shortcut-row--action:focus-visible { } .harness-subpane { - margin-top: 1rem; - padding-top: 0.85rem; - border-top: 1px solid var(--border); + margin: 0; + padding: 1.05rem 1.1rem; + border: 1px solid var(--border); + border-radius: 0.7rem; + background: var(--bg-raised, rgba(255, 255, 255, 0.018)); + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +@media (max-width: 720px) { + .harness-settings-detail { + padding: 1rem 0.9rem 1.5rem; + } + .harness-pane { + gap: 1rem; + } + .harness-subpane { + padding: 0.85rem 0.9rem; + } + .harness-pane-title { + font-size: 0.86rem; + } } .memory-preset-list { @@ -4348,9 +4422,9 @@ button.workbench-shortcut-row--action:focus-visible { .harness-stack { display: flex; flex-direction: column; - gap: 0.35rem; - margin: 0.75rem 0; - font-size: 0.75rem; + gap: 0.45rem; + margin: 0; + font-size: 0.82rem; } .workbench-plain-textarea { @@ -4389,7 +4463,7 @@ button.workbench-shortcut-row--action:focus-visible { .harness-hooks { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.85rem; } .harness-hooks__list { @@ -4403,31 +4477,25 @@ button.workbench-shortcut-row--action:focus-visible { .harness-hooks__list--grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.5rem; -} - -@media (max-width: 900px) { - .harness-hooks__list--grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 600px) { - .harness-hooks__list--grid { - grid-template-columns: minmax(0, 1fr); - } + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.7rem; } .harness-hooks__item { display: flex; align-items: center; justify-content: space-between; - gap: 0.75rem; - padding: 0.6rem 0.7rem; + gap: 0.7rem; + padding: 0.7rem 0.8rem; border: 1px solid var(--border); - border-radius: 6px; - background: rgba(255, 255, 255, 0.02); + border-radius: 0.6rem; + background: var(--bg-panel); + transition: border-color 140ms ease, background 140ms ease; +} + +.harness-hooks__item:hover { + border-color: var(--border-strong, rgba(255, 255, 255, 0.12)); + background: rgba(255, 255, 255, 0.035); } .harness-hooks__main { @@ -4471,6 +4539,9 @@ button.workbench-shortcut-row--action:focus-visible { .harness-hooks__name { display: block; text-transform: lowercase; + font-size: 0.88rem; + font-weight: 600; + color: var(--text); } .harness-hooks__status { @@ -4494,13 +4565,19 @@ button.workbench-shortcut-row--action:focus-visible { .workbench-plain-input { font-family: var(--font-mono); - font-size: 0.75rem; - border-radius: 4px; + font-size: 0.82rem; + border-radius: 6px; border: 1px solid var(--border); background: var(--bg-panel); color: var(--text); - padding: 0.35rem 0.45rem; + padding: 0.55rem 0.7rem; width: 100%; + transition: border-color 140ms ease, box-shadow 140ms ease; +} +.workbench-plain-input:focus { + outline: none; + border-color: rgba(88, 166, 255, 0.55); + box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.18); } @media (max-width: 720px) { From d3997fec50298175fdc359dd79b4d6925ce8b483 Mon Sep 17 00:00:00 2001 From: iptoux Date: Fri, 22 May 2026 02:53:10 +0200 Subject: [PATCH 03/48] feat(i18n): add appearance category and headings to localization files --- src/i18n/keys.rs | 2 + src/i18n/locales/de_de.rs | 2 + src/i18n/locales/en_us.rs | 2 + src/i18n/locales/es_es.rs | 2 + src/i18n/locales/fr_fr.rs | 2 + src/i18n/locales/hu_hu.rs | 2 + src/i18n/locales/it_it.rs | 2 + src/i18n/locales/ja_jp.rs | 2 + src/i18n/locales/ko_kr.rs | 2 + src/i18n/locales/pl_pl.rs | 2 + src/i18n/locales/pt_br.rs | 2 + src/i18n/locales/ru_ru.rs | 2 + src/i18n/locales/zh_cn.rs | 2 + src/i18n/locales/zh_tw.rs | 2 + src/workbench/harness_ui.rs | 76 ++++++++++++++++++++++++------------- src/workbench/state.rs | 1 + styles.css | 32 ++++------------ 17 files changed, 86 insertions(+), 51 deletions(-) diff --git a/src/i18n/keys.rs b/src/i18n/keys.rs index d45f5de..e567a88 100644 --- a/src/i18n/keys.rs +++ b/src/i18n/keys.rs @@ -318,11 +318,13 @@ pub enum I18nKey { HsTitle, HsAriaCats, HsCatApp, + HsCatAppearance, HsCatWorkspace, HsCatProvider, HsCatVoice, HsCatImage, AppHeading, + AppearanceHeading, GenEulaStatus, AppLanguage, AppHooksHeading, diff --git a/src/i18n/locales/de_de.rs b/src/i18n/locales/de_de.rs index b275f4b..5eb7f87 100644 --- a/src/i18n/locales/de_de.rs +++ b/src/i18n/locales/de_de.rs @@ -310,11 +310,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLXCode-Einstellungen", I18nKey::HsAriaCats => "Kategorien", I18nKey::HsCatApp => "App", + I18nKey::HsCatAppearance => "Erscheinungsbild", I18nKey::HsCatWorkspace => "Arbeitsplatz", I18nKey::HsCatProvider => "Agentenanbieter", I18nKey::HsCatVoice => "Stimme", I18nKey::HsCatImage => "Bild", I18nKey::AppHeading => "App", + I18nKey::AppearanceHeading => "Erscheinungsbild", I18nKey::GenEulaStatus => "EULA-Status", I18nKey::AppLanguage => "UI-Sprache", I18nKey::AppHooksHeading => "Terminal-Hooks", diff --git a/src/i18n/locales/en_us.rs b/src/i18n/locales/en_us.rs index 13ccc79..067baa8 100644 --- a/src/i18n/locales/en_us.rs +++ b/src/i18n/locales/en_us.rs @@ -348,11 +348,13 @@ Classic: Ctrl+O quick open, Ctrl+` new terminal, Ctrl+Shift+P palette." I18nKey::HsTitle => "BLXCode settings", I18nKey::HsAriaCats => "Categories", I18nKey::HsCatApp => "App", + I18nKey::HsCatAppearance => "Appearance", I18nKey::HsCatWorkspace => "Workspace", I18nKey::HsCatProvider => "Agent Provider", I18nKey::HsCatVoice => "Voice", I18nKey::HsCatImage => "Image", I18nKey::AppHeading => "App", + I18nKey::AppearanceHeading => "Appearance", I18nKey::GenEulaStatus => "EULA status", I18nKey::AppLanguage => "UI language", I18nKey::AppHooksHeading => "Terminal hooks", diff --git a/src/i18n/locales/es_es.rs b/src/i18n/locales/es_es.rs index f8071b6..a8b24f9 100644 --- a/src/i18n/locales/es_es.rs +++ b/src/i18n/locales/es_es.rs @@ -310,11 +310,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Configuración del código BLX", I18nKey::HsAriaCats => "Categorías", I18nKey::HsCatApp => "Aplicación", + I18nKey::HsCatAppearance => "Apariencia", I18nKey::HsCatWorkspace => "Espacio de trabajo", I18nKey::HsCatProvider => "Proveedor de agentes", I18nKey::HsCatVoice => "Voz", I18nKey::HsCatImage => "Imagen", I18nKey::AppHeading => "Aplicación", + I18nKey::AppearanceHeading => "Apariencia", I18nKey::GenEulaStatus => "Estado del CLUF", I18nKey::AppLanguage => "Idioma de la interfaz de usuario", I18nKey::AppHooksHeading => "Ganchos terminales", diff --git a/src/i18n/locales/fr_fr.rs b/src/i18n/locales/fr_fr.rs index e3077f3..215096e 100644 --- a/src/i18n/locales/fr_fr.rs +++ b/src/i18n/locales/fr_fr.rs @@ -312,11 +312,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Paramètres du code BLX", I18nKey::HsAriaCats => "Catégories", I18nKey::HsCatApp => "Application", + I18nKey::HsCatAppearance => "Apparence", I18nKey::HsCatWorkspace => "Espace de travail", I18nKey::HsCatProvider => "Fournisseur d'agents", I18nKey::HsCatVoice => "Voix", I18nKey::HsCatImage => "Image", I18nKey::AppHeading => "Application", + I18nKey::AppearanceHeading => "Apparence", I18nKey::GenEulaStatus => "Statut du CLUF", I18nKey::AppLanguage => "Langue de l'interface utilisateur", I18nKey::AppHooksHeading => "Crochets de borne", diff --git a/src/i18n/locales/hu_hu.rs b/src/i18n/locales/hu_hu.rs index f8ab4e5..222ce6f 100644 --- a/src/i18n/locales/hu_hu.rs +++ b/src/i18n/locales/hu_hu.rs @@ -310,11 +310,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLXCode beállítások", I18nKey::HsAriaCats => "Kategóriák", I18nKey::HsCatApp => "App", + I18nKey::HsCatAppearance => "Megjelenés", I18nKey::HsCatWorkspace => "Munkaterület", I18nKey::HsCatProvider => "Ügynökszolgáltató", I18nKey::HsCatVoice => "Hang", I18nKey::HsCatImage => "Kép", I18nKey::AppHeading => "App", + I18nKey::AppearanceHeading => "Megjelenés", I18nKey::GenEulaStatus => "EULA állapot", I18nKey::AppLanguage => "UI nyelv", I18nKey::AppHooksHeading => "Terminál horgok", diff --git a/src/i18n/locales/it_it.rs b/src/i18n/locales/it_it.rs index 33042c2..c005940 100644 --- a/src/i18n/locales/it_it.rs +++ b/src/i18n/locales/it_it.rs @@ -310,11 +310,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Impostazioni del codice BLX", I18nKey::HsAriaCats => "Categorie", I18nKey::HsCatApp => "App", + I18nKey::HsCatAppearance => "Aspetto", I18nKey::HsCatWorkspace => "Spazio di lavoro", I18nKey::HsCatProvider => "Fornitore di agenti", I18nKey::HsCatVoice => "Voce", I18nKey::HsCatImage => "Immagine", I18nKey::AppHeading => "App", + I18nKey::AppearanceHeading => "Aspetto", I18nKey::GenEulaStatus => "Stato dell'EULA", I18nKey::AppLanguage => "Lingua dell'interfaccia utente", I18nKey::AppHooksHeading => "Ganci terminali", diff --git a/src/i18n/locales/ja_jp.rs b/src/i18n/locales/ja_jp.rs index fe5452c..2b18d8c 100644 --- a/src/i18n/locales/ja_jp.rs +++ b/src/i18n/locales/ja_jp.rs @@ -304,11 +304,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLXCodeの設定", I18nKey::HsAriaCats => "カテゴリー", I18nKey::HsCatApp => "アプリ", + I18nKey::HsCatAppearance => "外観", I18nKey::HsCatWorkspace => "ワークスペース", I18nKey::HsCatProvider => "エージェントプロバイダー", I18nKey::HsCatVoice => "声", I18nKey::HsCatImage => "画像", I18nKey::AppHeading => "アプリ", + I18nKey::AppearanceHeading => "外観", I18nKey::GenEulaStatus => "EULAステータス", I18nKey::AppLanguage => "UI言語", I18nKey::AppHooksHeading => "端子フック", diff --git a/src/i18n/locales/ko_kr.rs b/src/i18n/locales/ko_kr.rs index 0e4debe..08d1d0a 100644 --- a/src/i18n/locales/ko_kr.rs +++ b/src/i18n/locales/ko_kr.rs @@ -304,11 +304,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLX코드 설정", I18nKey::HsAriaCats => "카테고리", I18nKey::HsCatApp => "앱", + I18nKey::HsCatAppearance => "모양새", I18nKey::HsCatWorkspace => "작업공간", I18nKey::HsCatProvider => "에이전트 제공자", I18nKey::HsCatVoice => "목소리", I18nKey::HsCatImage => "영상", I18nKey::AppHeading => "앱", + I18nKey::AppearanceHeading => "모양새", I18nKey::GenEulaStatus => "EULA 상태", I18nKey::AppLanguage => "UI 언어", I18nKey::AppHooksHeading => "단자 후크", diff --git a/src/i18n/locales/pl_pl.rs b/src/i18n/locales/pl_pl.rs index bcac1da..5acd18f 100644 --- a/src/i18n/locales/pl_pl.rs +++ b/src/i18n/locales/pl_pl.rs @@ -308,11 +308,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Ustawienia BLXCode", I18nKey::HsAriaCats => "Kategorie", I18nKey::HsCatApp => "Aplikacja", + I18nKey::HsCatAppearance => "Wygląd", I18nKey::HsCatWorkspace => "Obszar roboczy", I18nKey::HsCatProvider => "Dostawca agenta", I18nKey::HsCatVoice => "Głos", I18nKey::HsCatImage => "Obraz", I18nKey::AppHeading => "Aplikacja", + I18nKey::AppearanceHeading => "Wygląd", I18nKey::GenEulaStatus => "Stan EULA", I18nKey::AppLanguage => "Język interfejsu", I18nKey::AppHooksHeading => "Haki końcowe", diff --git a/src/i18n/locales/pt_br.rs b/src/i18n/locales/pt_br.rs index 042d2e3..f28cf78 100644 --- a/src/i18n/locales/pt_br.rs +++ b/src/i18n/locales/pt_br.rs @@ -310,11 +310,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Configurações do BLXCode", I18nKey::HsAriaCats => "Categorias", I18nKey::HsCatApp => "Aplicativo", + I18nKey::HsCatAppearance => "Aparência", I18nKey::HsCatWorkspace => "Espaço de trabalho", I18nKey::HsCatProvider => "Provedor de agente", I18nKey::HsCatVoice => "Voz", I18nKey::HsCatImage => "Imagem", I18nKey::AppHeading => "Aplicativo", + I18nKey::AppearanceHeading => "Aparência", I18nKey::GenEulaStatus => "Status do EULA", I18nKey::AppLanguage => "Idioma da IU", I18nKey::AppHooksHeading => "Ganchos terminais", diff --git a/src/i18n/locales/ru_ru.rs b/src/i18n/locales/ru_ru.rs index 1e37cba..100c220 100644 --- a/src/i18n/locales/ru_ru.rs +++ b/src/i18n/locales/ru_ru.rs @@ -308,11 +308,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "Настройки BLXкода", I18nKey::HsAriaCats => "Категории", I18nKey::HsCatApp => "Приложение", + I18nKey::HsCatAppearance => "Внешний вид", I18nKey::HsCatWorkspace => "Рабочая область", I18nKey::HsCatProvider => "Поставщик агентов", I18nKey::HsCatVoice => "Голос", I18nKey::HsCatImage => "Изображение", I18nKey::AppHeading => "Приложение", + I18nKey::AppearanceHeading => "Внешний вид", I18nKey::GenEulaStatus => "Статус лицензионного соглашения", I18nKey::AppLanguage => "язык пользовательского интерфейса", I18nKey::AppHooksHeading => "Терминальные крючки", diff --git a/src/i18n/locales/zh_cn.rs b/src/i18n/locales/zh_cn.rs index 14931db..10789dd 100644 --- a/src/i18n/locales/zh_cn.rs +++ b/src/i18n/locales/zh_cn.rs @@ -302,11 +302,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLX 代码设置", I18nKey::HsAriaCats => "类别", I18nKey::HsCatApp => "应用程序", + I18nKey::HsCatAppearance => "外观", I18nKey::HsCatWorkspace => "工作空间", I18nKey::HsCatProvider => "代理提供商", I18nKey::HsCatVoice => "嗓音", I18nKey::HsCatImage => "图像", I18nKey::AppHeading => "应用程序", + I18nKey::AppearanceHeading => "外观", I18nKey::GenEulaStatus => "EULA 状态", I18nKey::AppLanguage => "用户界面语言", I18nKey::AppHooksHeading => "端子挂钩", diff --git a/src/i18n/locales/zh_tw.rs b/src/i18n/locales/zh_tw.rs index 79beaaf..8b4ae2c 100644 --- a/src/i18n/locales/zh_tw.rs +++ b/src/i18n/locales/zh_tw.rs @@ -302,11 +302,13 @@ pub fn msg(key: I18nKey) -> &'static str { I18nKey::HsTitle => "BLX 代碼設定", I18nKey::HsAriaCats => "類別", I18nKey::HsCatApp => "應用程式", + I18nKey::HsCatAppearance => "外觀", I18nKey::HsCatWorkspace => "工作空間", I18nKey::HsCatProvider => "代理商提供者", I18nKey::HsCatVoice => "嗓音", I18nKey::HsCatImage => "影像", I18nKey::AppHeading => "應用程式", + I18nKey::AppearanceHeading => "外觀", I18nKey::GenEulaStatus => "EULA 狀態", I18nKey::AppLanguage => "使用者介面語言", I18nKey::AppHooksHeading => "端子掛鉤", diff --git a/src/workbench/harness_ui.rs b/src/workbench/harness_ui.rs index 4e9e0b4..fe66a1c 100644 --- a/src/workbench/harness_ui.rs +++ b/src/workbench/harness_ui.rs @@ -670,6 +670,7 @@ fn defer_browser_bounds(wb: WorkbenchService, embed: BrowserEmbedSurface) { fn harness_settings_cat_icon(cat: HarnessSettingsCategory) -> icondata::Icon { match cat { HarnessSettingsCategory::App => icondata::LuLayoutDashboard, + HarnessSettingsCategory::Appearance => icondata::LuSunMoon, HarnessSettingsCategory::Workspace => icondata::LuFolderOpen, HarnessSettingsCategory::AgentProvider => icondata::LuCpu, HarnessSettingsCategory::Memory => icondata::LuPalette, @@ -690,6 +691,7 @@ pub fn SettingsDock(
@@ -334,7 +340,7 @@ fn ApiKeyRow(entry: ApiKeyEntry, drafts: RwSignal) -> impl IntoView { }.into_any() @@ -344,7 +350,7 @@ fn ApiKeyRow(entry: ApiKeyEntry, drafts: RwSignal) -> impl IntoView { }.into_any() @@ -355,10 +361,21 @@ fn ApiKeyRow(entry: ApiKeyEntry, drafts: RwSignal) -> impl IntoView { } -

"Pending — press Save to apply."

+

{move || i18n.tr(I18nKey::ApiKeysPending)()}

- -

{move || env_hint_text.get_value()}

+ +

+ {move || { + env_var + .with_value(|v| { + v.as_ref().map(|name| { + i18n.tr(I18nKey::ApiKeysEnvFallback)().replace("{var}", name) + }) + }) + .flatten() + .unwrap_or_default() + }} +

diff --git a/src/workbench/harness_ui.rs b/src/workbench/harness_ui.rs index debd106..3076ac4 100644 --- a/src/workbench/harness_ui.rs +++ b/src/workbench/harness_ui.rs @@ -1891,7 +1891,7 @@ fn AgentProviderPane() -> impl IntoView { {move || i18n.tr(I18nKey::AgApiKeyField)()} -

"Manage API keys in Settings → API Keys."

+

{move || i18n.tr(I18nKey::ApiKeysManageHint)()}

{move || { settings @@ -1958,7 +1958,7 @@ fn AgentProviderPane() -> impl IntoView { -

"Manage Tavily / Brave keys in Settings → API Keys."

+

{move || i18n.tr(I18nKey::ApiKeysManageHintWeb)()}

- {move || i18n.tr(I18nKey::ApiKeysUnsaved)()} + {move || i18n.tr(I18nKey::ApiKeysUnsaved)()} @@ -288,7 +288,7 @@ fn ApiKeyRow(entry: ApiKeyEntry, drafts: RwSignal) -> impl IntoView { view! {
  • diff --git a/src/workbench/harness_ui.rs b/src/workbench/harness_ui.rs index 3076ac4..e67c324 100644 --- a/src/workbench/harness_ui.rs +++ b/src/workbench/harness_ui.rs @@ -712,7 +712,7 @@ pub fn SettingsDock( }.into_any(), HarnessSettingsCategory::Workspace => view! { - + }.into_any(), HarnessSettingsCategory::AgentProvider => view! { @@ -942,7 +942,7 @@ fn AppSettingsPane() -> impl IntoView { let ui = expect_context::(); let updates = expect_context::(); view! { -
    +