Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Terminal slot drag & drop reorder**: terminal slots can now be reordered by dragging a dedicated **grip handle** (`LuGripVertical`) in the slot titlebar onto any other slot in the same workspace grid. New module `src/workbench/terminal_slot_dnd.rs` defines the `application/x-blxcode-terminal-slot` MIME payload, the `TerminalSlotDragService` Leptos context (active drag meta + ghost position, non-reactive `StoredValue<bool>` session flag with generation guard against stale deferred updates), and helpers `set_drag_payload` / `read_drag_payload` / `is_terminal_drag` / `ghost_style`. Drag source and drop target are resolved live by `slot_id` (not array index) via the new `WorkbenchService::terminal_slot_index(workspace_id, slot_id)` accessor, so repeated reorders compose without stale indices. The grid uses HTML5 native drag & drop (`dragstart` / `dragover` / `drop`), with the deferred `try_set_active(gen, meta)` pattern so Leptos doesn't re-render mid-`dragstart` (which would cancel the drag on WebKitGTK / Wry). Permutation runs in pure-Rust `reorder_workspace_slots` against `slot_ids`, `slot_agent_labels`, and `slot_pane_states` in parallel; PTYs are not unmounted because the `For` key is `slot.id` and `terminal_key` stays stable across the reorder. Drag-disabled states: configurator open, full-size slot active, collapsed sidebar. Drag visuals: source slot at `opacity: 0.55`, target slot with `3px dashed var(--accent)` outline (inset via `outline-offset: -3px`), and a transluscent ghost-preview overlay (`.ws-term-slot-ghost`, `2px dashed` border) at the drop target's grid cell — sized via percentage `grid_template` math so it tracks the live grid dims. To keep `dragover` reaching the slot wrapper while xterm canvases would otherwise capture pointer events, `.ws-term-grid--drag-active .ws-term-slot:not(--drag-source) .ws-term-cell__xterm { pointer-events: none }` is applied while a drag is in flight. New i18n key `WsTermDragHandleAria` (handle aria-label / tooltip) authored in all 13 locales. 3 new unit tests in `state.rs` (`reorder_permutes_parallel_vectors`, `reorder_noop_on_same_index`, `reorder_moves_first_slot_to_second`) pin the pure-Rust permutation semantics. Cross-workspace transfer, drag of individual split panes, and a custom HTML5 drag-image (`setDragImage`) are **out of scope** — `setDragImage` with live or cloned DOM proved to crash WebKitGTK in the Tauri webview under Wry; the current accent-dashed target outline + transluscent source slot is the stable Linux-safe UX.

- **Code preview drag-range selection + right-click handoff to terminals and agent**: the new `CodeView` now supports **drag-based line range selection** — press the left mouse button on a line and drag up or down to extend the selection (`code-view__row--selected` highlight follows continuously). The selection model moved from `Option<usize>` to an ordered `Option<(usize, usize)>` 1-based inclusive range; single-clicks still toggle a single line, and `Escape` clears nothing implicitly (it closes the new context menu only). A window-level `mouseup` listener (installed once per `CodeView` mount, cleaned up via `on_cleanup`) ends drags even when the cursor leaves the gutter. `.code-view` gained `user-select: none` so accidental text selection no longer fights the row drag. A new **right-click context menu** (`src/workbench/file_preview/code_context_menu.rs`) opens at the click position whenever the user invokes `contextmenu` on a row; if the click lands outside the current range the selection is first replaced with that single line, otherwise the existing range stays. The menu is grouped into four sections: **Snippet → Insert into terminal** (lists every terminal in every workspace via the new cross-workspace enumerator `list_terminal_targets_all_workspaces` — the preview's own workspace is moved to the front with a localized "current" badge), **Full context block → Insert into terminal** (wraps the snippet in a `⟪ BLXCode attached context ⟫` envelope so the receiving terminal CLI parses it just like a workspace handoff), **Snippet → Attach to agent** (per-workspace `upsert_workspace_agent_context` against the new `AgentContextKind::FileSnippet` kind), and **Clipboard** (`Copy snippet`, `Copy line range`, `Copy raw text` via `navigator.clipboard.writeText` with per-action toast feedback). Cross-workspace inserts and agent attaches add a `source workspace:` line to the snippet/envelope header so the model can disambiguate when the file lives in a different workspace than the target. New helper `build_file_snippet_block` in `src/workbench/file_preview/util.rs` produces fenced markdown blocks (clamped to in-range indices, UTF-8 safe; 6 unit tests cover single-line, range, cross-workspace header, no-language fence, UTF-8 codepoints, and out-of-range clamping). New `render_file_snippet_envelope` in `src/workbench/agent_context_handoff.rs` emits the BLXCode-style handoff envelope without the heavy memory/plans/images sections (2 unit tests cover the in-workspace and cross-workspace variants). `AgentContextItem` gained an optional `content: Option<String>` field on both `src/agent_wire.rs` and `src-tauri/src/agent/protocol.rs` so file-snippet items can ship their fenced block inline; the existing six `AgentContextItem` constructors across the codebase (memory/learning notes + categories, plans, terminal session, "Memory" fallback) now pass `content: None` explicitly. Backend `render_context_prompt` in `src-tauri/src/agent/session_orchestrator.rs` partitions `FileSnippet` items into a dedicated `Attached file snippets (verbatim, line-numbered headers):` prompt section that renders each item's inline content; `render_agent_context_block` in `agent_context_handoff.rs` mirrors this with an `## Attached file snippets` section (memory/plans filters now skip snippet items). Toasts use `WorkbenchService`'s existing `ToastService`. A global `mousedown` window listener closes the menu on any outside click; `Escape` closes too. Drag selection works on both `Code` and `Text` files since both route through the same `CodeView`. 22 new i18n keys (`CodeViewMenuAria`, `CodeViewMenuSectionSnippetTerminal/SnippetAgent/EnvelopeTerminal/Clipboard`, `CodeViewMenuWorkspaceGroup`, `CodeViewMenuTerminalSlotLabel`, `CodeViewMenuAttachAgentLabel`, `CodeViewMenuNoTerminals`, `CodeViewMenuCopySnippet/Range/Raw`, `CodeViewMenuPreviewWorkspaceBadge`, and six `CodeViewToast*` variants for success/failure) were authored with real translations in all 13 locales (en, de, fr, es, it, pt_br, pl, hu, ru, ja, ko, zh_cn, zh_tw) and use `{workspace}`, `{terminal}`, `{slot}`, `{agent}`, `{error}` placeholders. CSS adds `.code-context-menu` and friends with sections, workspace-group headers, slot sublists, accent-pill `code-context-menu__badge`, and themed hover via `--overlay-2`. The `WorkspaceTerminalGroup` helper in `agent_context_handoff.rs` filters shell workspaces via the existing `is_shell_workspace` predicate.
- **Closeable Terminals tab + Settings without workspace**: every center tab — including the pinned Terminals tab — now exposes a close button. Clicking the Terminals close button raises a new confirmation overlay (`src/workbench/close_terminals_tab_dialog/`) with a 3-second countdown that keeps the primary button disabled; confirming routes through `WorkbenchService::close_center_terminals_tab`, which saves the workspace, terminates its PTYs, and pushes it onto the recent list (same path as the sidebar close). Closing the last non-Terminals tab in a real workspace likewise triggers `close_workspace` so the welcome screen reappears when no workspaces remain. **Settings** can now open without an active workspace: `open_center_settings_tab` lazily provisions an ephemeral "shell workspace" (empty `cwd`, `configuring: false`, no terminal slots) that hosts only the Settings tab and is hidden from the sidebar via the new `is_shell_workspace` filter; closing the shell's Settings tab disposes the shell automatically. `ensure_center_tabs` was renamed to `repair_center_tab_state` and no longer auto-reinserts a Terminals tab; `open_center_terminals_tab` is the explicit reopener and `open_new_terminal` calls it before appending a slot. `HarnessUiService` gained a `close_terminals_confirm` signal + generation guard (so rapid re-opens cancel stale timers); the harness keyboard handler swallows shortcuts while the dialog is up and routes `Escape` to dismiss. New command palette entry **Terminals** (`PaletteAction::OpenTerminalsTab`, `CmdTermTitle/Sub`) reopens the Terminals tab without spawning a new PTY. `finalize_workspace_close` centralizes wizard-draft cleanup (`workspace_drafts` + `workspace_config_steps`) so every close path drops state. 7 new i18n keys (`CmdTermTitle`, `CmdTermSub`, `CenterTabCloseTerminalsTitle/Body/Confirm/Cancel`, `CenterTabCloseAria`) localized into all 13 locales. 4 new unit tests (`center_tab_tests`) cover the repair semantics, empty-tabs branch, dangling active-id repair, and shell-workspace predicate.
- **Code preview with line numbers, syntax highlighting & row selection**: the file preview now ships a dedicated `CodeView` (`src/workbench/file_preview/code_view.rs`) that handles every source-code file with a real two-column layout — a sticky line-number gutter (right-aligned, tabular numerals, hairline divider) and an `hljs`-colored code column — instead of the prior plain `<pre><code>` fallback. Clicking any row toggles a selection highlight (accent-soft background + accent-color left bar, click again to clear) so users can mark and refer back to a specific line. Backend `classify_kind` in `src-tauri/src/fs_entries.rs` now distinguishes `FileKind::Code` (Rust/TS/JS/JSX/TSX/MJS/CJS, Python, Go, Java/Kotlin/Scala/Groovy/Gradle, Swift/Obj-C, C/C++/C#/F#/VB, Ruby/PHP/Lua/Perl/Dart/R/Julia, Clojure/Elixir/Erlang/Haskell/Elm/Nim/Zig/OCaml, HTML/Vue/Svelte/CSS/SCSS/Sass/Less, JSON/JSON5/JSONC/TOML/YAML/XML/Plist, Shell/Bash/Zsh/Fish/PowerShell/Bat/Cmd, SQL/GraphQL/Protobuf/Thrift/HCL/Terraform/Nix, Dockerfile/Makefile/CMake, diff/patch) from plain `FileKind::Text` (txt/log/ini/conf/env/properties/csv/tsv/gitignore/editorconfig); both kinds route through `CodeView`, but only `Code` gets syntax highlighting. The 11-test fs_entries unit suite now asserts the new mapping including ts/tsx/js/py/go/html/json → Code and txt/log/env/gitignore → Text. The frontend bridge (`tauri_bridge.rs`) mirrors the new `FileKind::Code` variant. New module `src/workbench/file_preview/hljs_glue.rs` lazy-loads the vendored highlight.js 11.11 common bundle (`public/vendor/highlight/highlight.min.js`, ~127 KiB, 38 languages) via a `<script>` tag inserted on first use, polls `globalThis.hljs` for up to 5 s, and exposes `highlight(code, language)` calling `hljs.highlight(..., { ignoreIllegals: true })`. `src/workbench/file_preview/util.rs` adds `hljs_lang_for_ext` (full extension → hljs alias map covering 60+ extensions), `html_escape` (used for unhighlighted plain-text branches), and `split_highlighted_into_lines` — a UTF-8-safe HTML splitter that walks hljs output, tracks the open-`<span>` stack, and at every `\n` closes all open spans (in reverse order) before pushing the line and reopening the same spans on the next line, producing one valid balanced HTML fragment per source line. Six new unit tests cover simple text, UTF-8 codepoints, balanced cross-line spans, nested span reopening, blank-line preservation, and the extension/escape helpers. The `CodeView` component pre-renders one `<div class="code-view__row" data-line="N">` per line (no `<For>` clone overhead on large files), tracks selection in an `RwSignal<Option<usize>>` and uses event-delegated click on the container (`closest("[data-line]")`) to toggle the row. Truncation notice (`FilePreviewTextTruncated`) still surfaces above the gutter when the backend's 512 KiB text cap kicks in. CSS adds `.code-view`, `.code-view__row[--selected]`, `.code-view__lineno`, `.code-view__line` plus an hljs token mapping that uses `color-mix` against existing theme tokens (`--accent`, `--text`, `--text-muted`, `--surface`, `--border`) for the gutter / hover / selection chrome, with bespoke light-theme overrides for `[data-theme="blxcode-light"|"solarized-light"|"gruvbox-light"|"catppuccin-latte"]` so strings/numbers/types stay legible on light backgrounds. Mermaid lazy-load pattern is reused (single `<script>` insertion + polling) so the highlight bundle is paid for only when a code file is opened. Pre-existing `TextFallbackView` was removed; the dispatcher now routes `Code | Text → CodeView`.
Expand Down
12 changes: 12 additions & 0 deletions docs/user/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ Individual terminal slots can also keep split-pane state. BLXCode persists pane
<img src="../images/screenshot-2026-05-18_18-10-48.png" alt="Workspace terminal grid after the agent opens two additional Claude terminal slots" />
</p>

### Reordering terminals with drag & drop

Every terminal slot exposes a grip handle (`⋮⋮`) on the far left of its titlebar. Drag a slot by its handle and drop it on any other slot in the same workspace grid to swap positions — the two slots exchange their cells in the grid, agent labels and split-pane layout travel along, and running PTY sessions are preserved (no shell restart, no agent CLI re-launch).

Visual cues while dragging:

- The source slot fades to ~55% opacity.
- The slot under the cursor gets a dashed accent outline.
- A transluscent ghost preview marks the target grid cell.

Drag is disabled while the workspace configurator is open, while a slot is in full-size mode, and while the sidebar is collapsed. Drag direction is unconstrained — any source slot can be dropped on any other slot, and repeated reorders compose freely. Cross-workspace transfer is not supported in this release; individual split panes inside a slot cannot be dragged out on their own.

## Shell Environment

The backend spawns PTY sessions through `portable-pty`. On Unix-like systems it uses `$SHELL`, falling back to `/bin/sh`.
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,9 @@ pub enum I18nKey {
WsTermRestoreSize,
WsTermPaneTitleSingle,
WsTermPaneTitleMulti,
WsTermDragHandleAria,
WsTermDropCreateNewAria,
WsTermDragMaxSlots,
WsTermBootstrapFailed,

// Voice subsystem
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/de_de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "Terminalgröße wiederherstellen",
I18nKey::WsTermPaneTitleSingle => "{Rolle} · {Begriff} {n}",
I18nKey::WsTermPaneTitleMulti => "{role} · {term} {slot}.{pane}",
I18nKey::WsTermDragHandleAria => "Terminal zum Umsortieren ziehen",
I18nKey::WsTermDropCreateNewAria => "Ablegen, um Terminal-Slot hinzuzufügen",
I18nKey::WsTermDragMaxSlots => "Maximale Terminal-Slots erreicht (16)",
I18nKey::WsTermBootstrapFailed => {
"Die Terminal-Benutzeroberfläche konnte nicht geladen werden. Überprüfen Sie die Browserkonsole. Das Bootstrap-Skript oder das xterm-CDN sind möglicherweise blockiert."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en_us.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,9 @@ Classic: Ctrl+O quick open, Ctrl+` new terminal, Ctrl+Shift+P palette."
I18nKey::WsTermRestoreSize => "Restore terminal size",
I18nKey::WsTermPaneTitleSingle => "{role} · {term} {n}",
I18nKey::WsTermPaneTitleMulti => "{role} · {term} {slot}.{pane}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => {
"Terminal UI failed to load. Check the browser console; the bootstrap script or xterm CDN may be blocked."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/es_es.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "Restaurar el tamaño del terminal",
I18nKey::WsTermPaneTitleSingle => "{rol} · {término} {n}",
I18nKey::WsTermPaneTitleMulti => "{rol} · {término} {espacio}.{panel}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => {
"La interfaz de usuario del terminal no se pudo cargar. Verifique la consola del navegador; Es posible que el script de arranque o la CDN de xterm estén bloqueados."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/fr_fr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "Restaurer la taille du terminal",
I18nKey::WsTermPaneTitleSingle => "{rôle} · {terme} {n}",
I18nKey::WsTermPaneTitleMulti => "{rôle} · {terme} {emplacement}.{volet}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => {
"L'interface utilisateur du terminal n'a pas pu se charger. Vérifiez la console du navigateur ; le script d'amorçage ou le CDN xterm peut être bloqué."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/hu_hu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "A terminál méretének visszaállítása",
I18nKey::WsTermPaneTitleSingle => "{role} · {term} {n}",
I18nKey::WsTermPaneTitleMulti => "{role} · {term} {slot}.{pane}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => {
"Nem sikerült betölteni a terminál felhasználói felületét. Ellenőrizze a böngésző konzolját; a bootstrap szkript vagy az xterm CDN blokkolva lehet."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/it_it.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "Ripristina le dimensioni del terminale",
I18nKey::WsTermPaneTitleSingle => "{ruolo} · {termine} {n}",
I18nKey::WsTermPaneTitleMulti => "{ruolo} · {termine} {slot}.{riquadro}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => {
"Impossibile caricare l'interfaccia utente del terminale. Controlla la console del browser; lo script bootstrap o il CDN xterm potrebbero essere bloccati."
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/ja_jp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,9 @@ pub fn msg(key: I18nKey) -> &'static str {
I18nKey::WsTermRestoreSize => "端末サイズを復元する",
I18nKey::WsTermPaneTitleSingle => "{役割} · {用語} {n}",
I18nKey::WsTermPaneTitleMulti => "{役割} · {用語} {スロット}.{ペイン}",
I18nKey::WsTermDragHandleAria => "Drag to reorder terminal",
I18nKey::WsTermDropCreateNewAria => "Drop to add terminal slot",
I18nKey::WsTermDragMaxSlots => "Maximum terminal slots reached (16)",
I18nKey::WsTermBootstrapFailed => "ターミナル UI のロードに失敗しました。ブラウザコンソールを確認してください。ブートストラップ スクリプトまたは xterm CDN がブロックされる可能性があります。",
I18nKey::EulaAccepted => "承認されました",
I18nKey::EulaUnknown => "未知",
Expand Down
Loading
Loading