diff --git a/docs/plans/2026-04-14-manus-workspace-redesign.md b/docs/plans/2026-04-14-manus-workspace-redesign.md new file mode 100644 index 0000000..f05e21f --- /dev/null +++ b/docs/plans/2026-04-14-manus-workspace-redesign.md @@ -0,0 +1,304 @@ +# PRAW Manus-Inspired Workspace Refresh Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Redesign PRAW’s shell, pane, transcript, live console, and settings surfaces into a quieter graphite-blue dual-theme workspace while preserving existing terminal semantics. + +**Architecture:** Keep the implementation token-first. First lock behavior with focused tests for new theme expectations and transcript labeling, then rebuild theme presets and shared app CSS, and finally tune component-level structure and copy where the new visual system needs sharper semantic boundaries. + +**Tech Stack:** React 19, TypeScript, Zustand, Tauri 2, plain CSS, Vitest, Rust unit tests (unchanged runtime unless already patched) + +--- + +### Task 1: Lock the redesigned visual contract in tests + +**Files:** +- Modify: `src/domain/theme/presets.test.ts` +- Modify: `src/features/terminal/components/DialogTranscript.test.tsx` + +**Step 1: Write the failing theme test** + +Add assertions that the refreshed light and dark presets expose the new graphite-blue direction: +- lighter, quieter surface hierarchy +- less absolute black in dark mode +- coherent accent values shared across app + terminal palettes + +**Step 2: Run test to verify it fails** + +Run: + +```bash +npm test -- src/domain/theme/presets.test.ts +``` + +Expected: FAIL because current preset values still reflect the older harsher palette. + +**Step 3: Write the failing transcript label test** + +Add a render test asserting non-command transcript blocks render the new context label (for example `shell output`) instead of `session output`. + +**Step 4: Run test to verify it fails** + +Run: + +```bash +npm test -- src/features/terminal/components/DialogTranscript.test.tsx +``` + +Expected: FAIL because the current component still renders `session output`. + +**Step 5: Commit** + +```bash +git add src/domain/theme/presets.test.ts src/features/terminal/components/DialogTranscript.test.tsx +git commit -m "Lock the refreshed PRAW visual contract in tests" +``` + +### Task 2: Rebuild theme tokens around the graphite-blue system + +**Files:** +- Modify: `src/domain/theme/presets.ts` +- Test: `src/domain/theme/presets.test.ts` + +**Step 1: Implement the minimal token changes** + +Update the theme preset definitions so: +- light theme uses cool whites / mist grays / graphite text / restrained blue accents +- dark theme uses deep graphite surfaces instead of pure black +- app and terminal palettes feel related +- sepia can remain available but should stay visually subordinate to the new primary direction + +**Step 2: Run the targeted test** + +Run: + +```bash +npm test -- src/domain/theme/presets.test.ts +``` + +Expected: PASS + +**Step 3: Sanity-check no config tests regressed** + +Run: + +```bash +npm test -- src/domain/config/model.test.ts +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/domain/theme/presets.ts src/domain/theme/presets.test.ts +git commit -m "Rebuild PRAW theme tokens around a calmer graphite palette" +``` + +### Task 3: Refactor the shared app shell and pane chrome + +**Files:** +- Modify: `src/app/styles.css` +- Modify: `src/app/App.tsx` (only if small structural hooks or labels are needed) +- Modify: `src/features/terminal/components/TerminalPane.tsx` (only if class hooks or tiny structure changes are required) + +**Step 1: Refactor shell-level CSS** + +Update shared CSS for: +- app background / surfaces / borders / overlays +- header composition +- button styling +- workspace canvas +- pane shells, headers, action clusters, overlays + +Keep DOM changes minimal. Prefer class reuse and token-driven CSS. + +**Step 2: Run typecheck** + +Run: + +```bash +npm run typecheck +``` + +Expected: PASS + +**Step 3: Run a targeted app render test if structure changed** + +Run whichever existing affected test is closest to the touched component, for example: + +```bash +npm test -- src/features/terminal/components/ClassicTerminalSurface.test.tsx +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/app/styles.css src/app/App.tsx src/features/terminal/components/TerminalPane.tsx +git commit -m "Give the workspace shell a more restrained product frame" +``` + +### Task 4: Redesign transcript, composer, and live console presentation + +**Files:** +- Modify: `src/app/styles.css` +- Modify: `src/features/terminal/components/DialogTranscript.tsx` +- Modify: `src/features/terminal/components/DialogTerminalSurface.tsx` (only if supporting hooks are needed) +- Modify: `src/features/terminal/components/LiveCommandConsole.tsx` +- Test: `src/features/terminal/components/DialogTranscript.test.tsx` + +**Step 1: Implement transcript and console refinements** + +Apply the new visual system to: +- command blocks +- shell output block labeling + tone +- transcript spacing and separators +- jump-to-latest affordance +- live console header / body hierarchy +- idle composer framing + +Keep behavior unchanged except for the revised context label and presentational structure. + +**Step 2: Run transcript test** + +Run: + +```bash +npm test -- src/features/terminal/components/DialogTranscript.test.tsx +``` + +Expected: PASS + +**Step 3: Run related dialog surface tests** + +Run: + +```bash +npm test -- src/features/terminal/components/DialogTerminalSurface.test.tsx +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/app/styles.css src/features/terminal/components/DialogTranscript.tsx src/features/terminal/components/DialogTerminalSurface.tsx src/features/terminal/components/LiveCommandConsole.tsx src/features/terminal/components/DialogTranscript.test.tsx +git commit -m "Make transcript and live console feel like intentional workspace surfaces" +``` + +### Task 5: Polish the settings panel to match the new system + +**Files:** +- Modify: `src/app/styles.css` +- Modify: `src/features/config/components/SettingsPanel.tsx` (only if minor structure or copy hooks are needed) + +**Step 1: Refine settings visuals** + +Update: +- panel container +- section grouping +- field rhythm +- summary hierarchy +- toggle/select/input styling + +Prefer CSS-only changes unless markup hooks are clearly needed. + +**Step 2: Run targeted settings test if affected** + +Run: + +```bash +npm test -- src/domain/config/settings-panel-language.test.ts +``` + +Expected: PASS + +**Step 3: Run typecheck** + +Run: + +```bash +npm run typecheck +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/app/styles.css src/features/config/components/SettingsPanel.tsx +git commit -m "Bring settings into the redesigned workspace language" +``` + +### Task 6: Full verification and release prep + +**Files:** +- Modify: any final touched implementation files from prior tasks + +**Step 1: Run all frontend tests** + +```bash +npm test +``` + +Expected: PASS + +**Step 2: Run typecheck and production build** + +```bash +npm run typecheck +npm run build +``` + +Expected: PASS + +**Step 3: Run backend tests to ensure the branch remains green** + +```bash +cd src-tauri && cargo test +``` + +Expected: PASS + +**Step 4: Review diff** + +```bash +git status --short +git diff --stat +``` + +Expected: only intended redesign files remain modified. + +**Step 5: Commit final integration pass** + +```bash +git add src app src-tauri docs/plans/2026-04-14-manus-workspace-redesign.md +git commit -m "Land the first Manus-inspired workspace refresh for PRAW" +``` + +### Task 7: Publish branch and open PR + +**Files:** +- none + +**Step 1: Push branch** + +```bash +git push -u origin feat/manus-workspace-redesign +``` + +**Step 2: Open PR** + +Use GitHub CLI if available: + +```bash +gh pr create --fill --base main --head feat/manus-workspace-redesign +``` + +If `--fill` is too weak, provide a manual title/body aligned with the design spec and verification evidence. + +**Step 3: Capture PR URL** + +Record the URL in the final handoff. diff --git a/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md b/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md new file mode 100644 index 0000000..719d870 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md @@ -0,0 +1,382 @@ +# PRAW Workspace Visual Language Redesign + +## Summary + +This design repositions PRAW from an experimental developer-facing terminal shell into a more composed desktop workspace product. + +The target direction is **Manus-inspired restraint without copying Manus page structure**. PRAW should inherit Manus-like qualities—high whitespace, quiet surfaces, disciplined hierarchy, and restrained emphasis—while keeping PRAW’s own product semantics: workspace panes, transcript history, live command console, classic terminal fallback, and settings for long-running shell work. + +The chosen direction is: + +- **Approach**: Manus visual skeleton + PRAW product semantics +- **Palette direction**: graphite blue / cool gray +- **Theme strategy**: dual light + dark themes built from one shared design language + +## Problem + +PRAW currently works, but its visual system still reads more like an internal tool than a polished product workspace: + +- pane chrome is heavy and tool-like +- active / special states rely on strong borders instead of controlled hierarchy +- transcript and session output feel closer to raw logs than composed work history +- settings are functional but visually dense +- the app shell does not yet communicate a premium, focused, modern workspace identity + +There is also a specific product perception issue around `session output`: even when it is technically correct, it can feel abrupt and error-like because it appears as a low-context block in a largely empty transcript area. + +The redesign should solve these perception issues without erasing PRAW’s identity as a terminal-native desktop workspace. + +## Goals + +- Give PRAW a distinct, premium, restrained visual language. +- Make the app feel closer to a modern workspace product than a raw developer utility. +- Keep the terminal product identity intact. +- Rebuild light and dark themes as one coherent system. +- Reduce visual harshness in pane chrome, transcript blocks, and settings surfaces. +- Make `session output`, transcript history, and live console feel intentional instead of incidental. +- Improve first-run and empty-ish states without hiding meaningful shell output. + +## Non-Goals + +- Copying the Manus homepage or marketing page structure verbatim. +- Turning PRAW into a landing-page-like or editorial website UI. +- Replacing terminal semantics with chat or document metaphors. +- Rebuilding the pane layout engine or terminal runtime. +- Introducing large animation systems, glows, or highly decorative visual effects. +- Adding major new product features during this redesign pass. + +## Product Direction + +### 1. Core Positioning + +PRAW should feel like: + +- a calm command workspace +- a premium desktop tool +- a structured shell environment + +PRAW should not feel like: + +- a retro hacker terminal +- a noisy AI dashboard +- a direct clone of Manus marketing UI + +### 2. Design Principle + +The guiding idea is: + +> If the Manus design team designed a serious terminal workspace application, what would that look like? + +That means borrowing from Manus: + +- whitespace discipline +- visual quietness +- low-saturation surfaces +- elegant emphasis +- clean component proportion + +But keeping PRAW-specific structure: + +- panes remain panes +- transcript remains transcript +- live console remains a runtime surface +- classic terminal remains a true terminal + +### 3. Theme Direction + +The redesign uses one shared system across light and dark: + +- **Light**: cool paper whites, mist gray surfaces, graphite text, muted blue accents +- **Dark**: deep graphite surfaces, cool low-contrast borders, soft desaturated blue accents + +The app should never depend on loud contrast for identity. Accent is used sparingly and structurally. + +## Visual Language + +### Surface Model + +The app should use a clearer surface ladder: + +1. **App background** — quiet canvas tone +2. **Primary surface** — pane and settings base +3. **Muted surface** — subtle nested regions +4. **Interactive hover/focus surface** — only slightly stronger than resting state + +The current system overuses hard borders. The redesign should shift the feeling from “boxed tool windows” to “calm modular surfaces.” + +### Borders And Radius + +- Borders become subtler and more consistent. +- Active state should rely more on contrast, inset treatment, and spacing rhythm than on thicker strokes. +- Radius should be present and consistent, but not soft or consumer-app playful. +- Shadow should remain minimal; border and tone do most of the work. + +### Typography + +Typography should become more product-like while preserving terminal readability: + +- app shell labels become cleaner and quieter +- headings are lighter and less boxy +- metadata becomes more legible through spacing and tone, not size inflation +- transcript text remains terminal-respectful but visually more composed + +The redesign is not a typography replatform. It is mainly about hierarchy, spacing, and visual cadence. + +## App Shell + +### Header + +The top app header should be redesigned as a lightweight workspace frame: + +- smaller visual footprint +- more breathing room +- less “developer tool titlebar” energy +- settings action styled as a refined secondary control + +The header should communicate confidence and calm, not utility clutter. + +### Workspace Canvas + +The workspace background should read as a deliberate canvas rather than just the browser body. + +It should: + +- gently frame pane modules +- improve separation between global shell and local pane surfaces +- support both light and dark themes without looking flat or harsh + +## Pane System + +### Pane Container + +Panes remain the product’s core structural unit, but their visual treatment changes: + +- softer hierarchy +- more stable spacing +- quieter edges +- stronger sense of compositional balance + +Current panes feel like engineering boxes. New panes should feel like professional workspace modules. + +**Implementation refinement after visual review:** + +- pane chrome should now lean closer to **GitHub’s repository/file-list card language** than to Manus’s softer card treatment +- pane headers should read like restrained utility bars: pale neutral background, thin divider, compact title weight, low-noise actions +- split seams should stay **very thin visually** even when the hit target remains larger for resizing +- seam endpoints must feel intentional and rounded without turning the split into a heavy pill or decorative rail + +### Active Pane State + +The active pane should no longer announce itself mainly through thicker borders. + +Instead, active state should be communicated through a more premium combination of: + +- slightly elevated contrast +- subtle edge emphasis +- clearer header/body relationship +- controlled accent use + +### Agent Workflow State + +Agent workflow panes still need distinction, but they should look like a special operating mode, not a warning state. + +The redesign should keep visible differentiation while reducing the current harshness of thick accented edge treatment. + +## Dialog Surface + +The dialog surface is where this redesign matters most. + +### Transcript + +Transcript history should feel more like structured session memory than a raw stream dump. + +The redesign should: + +- increase whitespace between blocks +- reduce harsh separators +- improve the rhythm between command headers and outputs +- make command blocks feel intentionally grouped +- let syntax and semantic highlighting breathe without looking noisy + +Command history should remain readable for serious shell work, but feel more curated. + +### Session Output + +`session output` should remain visible when meaningful shell output exists outside a user command context, but its presentation must change. + +Decisions: + +- do not suppress legitimate shell output by default +- do not style it like an error block +- relabel and/or visually demote it so it reads as shell context rather than failure +- reduce its first-screen abruptness through spacing, tone, and label treatment + +Preferred direction: + +- use quieter copy such as `shell output` or `startup output` +- style it as low-emphasis system context +- ensure a lone startup block does not visually dominate an otherwise empty workspace + +### Idle Composer + +The idle composer should become calmer and more premium: + +- cleaner prompt line +- better spacing around input +- less visual noise around helper and suggestion surfaces +- focus treatment that feels intentional, not loud + +### Jump-To-Latest + +The jump affordance should remain available, but visually align with the quieter button system. + +It should feel like a utility refinement, not a floating intervention. + +## Live Command Console + +The live console should look like a focused execution region, not a secondary terminal jammed into the page. + +The redesign should: + +- simplify the console header +- improve separation between command metadata and terminal surface +- reduce visual density in compact mode +- make running-state context feel composed and deliberate + +Command, cwd, and terminal body should form a clear visual stack. + +## Classic Terminal Surface + +Classic mode remains a real terminal and must preserve compatibility-first behavior. + +This redesign should not undermine terminal semantics. Visual changes should stay within: + +- surface framing +- surrounding chrome +- spacing +- token alignment with the redesigned themes + +Classic should still feel native to the new app language, but it must not lose its terminal credibility. + +## Settings Panel + +The settings panel should be redesigned from a dense utility drawer into a more refined system panel. + +Desired changes: + +- clearer section grouping +- better spacing between fields +- calmer controls +- more polished open/close behavior through layout and tone, not animation complexity +- stronger summary hierarchy at the top + +The settings panel should feel like a serious application settings surface, not a debug form. + +## Theme Token Strategy + +This redesign should rebuild the visual language from tokens outward rather than patching scattered CSS values. + +### App Tokens + +Expected token groups: + +- app background +- primary surface +- muted surface +- elevated surface +- text primary / secondary / tertiary +- border strong / border subtle +- overlay +- accent +- semantic status colors + +### Transcript Tokens + +Transcript and command history should gain their own structured token layer for: + +- command text +- metadata +- system context output +- warning/error/success emphasis +- selection/focus accents + +### Terminal Tokens + +The xterm palette should be reviewed so terminal colors still read correctly inside the new shell language. + +The app palette and terminal palette should feel related, not disconnected. + +## File And Module Boundaries + +Expected primary touchpoints: + +- `src/domain/theme/presets.ts` +- `src/app/styles.css` +- `src/app/App.tsx` +- `src/features/terminal/components/TerminalPane.tsx` +- `src/features/terminal/components/DialogTerminalSurface.tsx` +- `src/features/terminal/components/DialogTranscript.tsx` +- `src/features/terminal/components/LiveCommandConsole.tsx` +- `src/features/config/components/SettingsPanel.tsx` + +Potential support touchpoints: + +- terminal settings copy and summary helpers +- small className / copy-level adjustments for session output labeling + +Implementation should prefer: + +- token cleanup first +- shared shell-level styling updates second +- component-specific polish third + +It should avoid large structural rewrites unless visual goals cannot be reached otherwise. + +## Testing Strategy + +### Automated + +Add or update tests for the smallest behavior-level changes introduced by the redesign, especially when copy or rendering conditions change. + +Examples: + +- transcript labeling for non-command session blocks if copy changes +- theme preset normalization if token model changes +- component snapshot or render assertions only where they protect meaningful structure + +The redesign should avoid gratuitous brittle style snapshots. + +### Manual Verification + +Manual review should confirm: + +1. Light theme feels restrained, premium, and clearly readable. +2. Dark theme feels calm and focused, not flashy. +3. Pane hierarchy is clearer without relying on thick borders. +4. Agent workflow panes remain identifiable without looking alarm-like. +5. `session output` no longer feels like an accidental error banner. +6. Live console feels integrated and deliberate. +7. Settings panel feels more polished and less dense. +8. Classic mode still feels trustworthy and usable. + +## Risks + +- Over-indexing on Manus aesthetics could erase PRAW’s identity. +- Over-smoothing the UI could weaken information density for serious users. +- If accent usage becomes too timid, the interface may feel flat. +- If `session output` is visually demoted too far, useful startup context may become hard to notice. +- Large CSS-only changes without token discipline could increase inconsistency instead of reducing it. + +## Recommendation + +Implement the redesign as a **token-first visual refactor** with minimal behavior changes. + +The winning direction is: + +- Manus-inspired restraint +- graphite blue / cool gray dual-theme system +- quieter pane chrome +- more composed transcript and live console +- lower-friction but still visible system-context output + +This is the most credible way to make PRAW feel premium without breaking its terminal-native product identity. diff --git a/src-tauri/src/terminal/shell_integration.rs b/src-tauri/src/terminal/shell_integration.rs index 8aa135f..811204e 100644 --- a/src-tauri/src/terminal/shell_integration.rs +++ b/src-tauri/src/terminal/shell_integration.rs @@ -52,21 +52,29 @@ pub fn install_shell_integration(shell: &str, session_id: &str) -> Result break, + Ok(read) => output.extend_from_slice(&buffer[..read]), + Err(_) => break, + } + } + String::from_utf8_lossy(&output).into_owned() + }); + + thread::sleep(Duration::from_millis(250)); + writer + .write_all(b"exit\n") + .expect("exit should write to zsh"); + writer.flush().expect("commands should flush"); + drop(writer); + + let status = child.wait().expect("zsh should exit cleanly"); + let output = read_handle.join().expect("reader thread should join"); + + for path in cleanup_paths { + if path.is_dir() { + let _ = std::fs::remove_dir_all(path); + } else { + let _ = std::fs::remove_file(path); + } + } + let _ = std::fs::remove_dir_all(&temp_home); + + assert_eq!(status.exit_code(), 0); + assert!( + !output.contains("bad math expression"), + "expected startup without zsh hook math errors, got output: {output:?}" + ); + } } diff --git a/src/app/styles.css b/src/app/styles.css index eea00a3..d7b62f4 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -9,28 +9,40 @@ monospace; line-height: 1.5; font-weight: 400; - background: #ffffff; - color: #000000; - --app-background: #ffffff; - --surface: #ffffff; - --surface-muted: #f5f5f5; - --text-primary: #000000; - --text-muted: #555555; - --border: #000000; - --border-muted: #c8c8c8; - --overlay-backdrop: rgba(0, 0, 0, 0.06); - --history-command: #003d99; - --history-accent: #6b2f8a; - --history-string: #7a3e00; - --history-path: #0f6b45; - --history-url: #005f8f; - --history-error: #9f1d1d; - --history-warning: #9b7500; - --history-success: #0b7a28; - --history-number: #005f5f; - --resize-handle-overlay: rgba(0, 0, 0, 0.04); - --resize-handle-line: #000000; - --ghost-text: #8b8b8b; + background: #f6f3ed; + color: #26211c; + --app-background: #f6f3ed; + --surface: #fcfaf6; + --surface-muted: #f1ede6; + --surface-elevated: #fffdfa; + --text-primary: #26211c; + --text-muted: #72695e; + --text-subtle: #968b80; + --border: #dbd3ca; + --border-muted: #e9e2d8; + --overlay-backdrop: rgba(36, 31, 23, 0.07); + --history-command: #66728a; + --history-accent: #808ba5; + --history-string: #90613a; + --history-path: #4d6f58; + --history-url: #597388; + --history-error: #b36563; + --history-warning: #b38a50; + --history-success: #4f7d69; + --history-number: #6c788f; + --resize-handle-overlay: rgba(102, 114, 138, 0.07); + --resize-handle-line: #b8b5c0; + --ghost-text: #a69c91; + --accent: #76829c; + --accent-strong: #66728a; + --accent-soft: rgba(118, 130, 156, 0.1); + --button-hover: #f7f4ee; + --panel-shadow: 0 18px 44px rgba(17, 24, 39, 0.07); + --radius-sm: 7px; + --radius-md: 9px; + --radius-lg: 11px; + --radius-xl: 14px; + --radius-seam: 8px; --ai-badge-text: #ffffff; } @@ -58,7 +70,6 @@ button, input, select { font: inherit; - border-radius: 0; } button { @@ -71,43 +82,52 @@ button { min-height: 0; flex-direction: column; overflow: hidden; - padding: 12px; + padding: 16px; background: var(--app-background); color: var(--text-primary); } .app-shell[data-theme="dark"] { - --app-background: #0f1115; - --surface: #151922; - --surface-muted: #1b2130; - --text-primary: #f5f7fb; - --text-muted: #a5afc0; - --border: #6b7280; - --border-muted: #313949; - --overlay-backdrop: rgba(3, 6, 12, 0.74); - --history-command: #74a7ff; - --history-accent: #dba6ff; - --history-string: #ffbf7a; - --history-path: #7bd8a8; - --history-url: #73d2ff; - --history-error: #ff9a9a; - --history-warning: #ffd36b; - --history-success: #8be28b; - --history-number: #79e0d8; - --resize-handle-overlay: rgba(245, 247, 251, 0.08); - --resize-handle-line: #f5f7fb; - --ghost-text: #7f8999; + --app-background: #0d1117; + --surface: #121821; + --surface-muted: #18202b; + --surface-elevated: #151d29; + --text-primary: #edf2f7; + --text-muted: #91a0b3; + --text-subtle: #718097; + --border: #2a3442; + --border-muted: #202938; + --overlay-backdrop: rgba(2, 6, 23, 0.74); + --history-command: #93adf5; + --history-accent: #aebcff; + --history-string: #d7b07b; + --history-path: #7fb89a; + --history-url: #80b7d8; + --history-error: #e79097; + --history-warning: #d6bb74; + --history-success: #76b59b; + --history-number: #7ea7d8; + --resize-handle-overlay: rgba(148, 169, 255, 0.12); + --resize-handle-line: #6f86bb; + --ghost-text: #6d7d92; + --accent: #7ea2ff; + --accent-strong: #93adf5; + --accent-soft: rgba(126, 162, 255, 0.16); + --button-hover: #1a2230; + --panel-shadow: 0 24px 64px rgba(1, 4, 12, 0.42); } .app-shell[data-theme="sepia"] { - --app-background: #f4ead7; - --surface: #fbf3e4; - --surface-muted: #efe2cc; - --text-primary: #2f2419; - --text-muted: #705d49; - --border: #6e5a46; - --border-muted: #b8a58b; - --overlay-backdrop: rgba(68, 45, 20, 0.14); + --app-background: #f3eadc; + --surface: #fcf7ef; + --surface-muted: #f1e5d2; + --surface-elevated: #fffaf3; + --text-primary: #312519; + --text-muted: #746351; + --text-subtle: #9a8771; + --border: #c9b79f; + --border-muted: #dfd1bf; + --overlay-backdrop: rgba(68, 45, 20, 0.12); --history-command: #5a4fb2; --history-accent: #8b4f7f; --history-string: #9a5d12; @@ -120,22 +140,26 @@ button { --resize-handle-overlay: rgba(47, 36, 25, 0.06); --resize-handle-line: #2f2419; --ghost-text: #8a7a66; + --accent: #776bd0; + --accent-strong: #5a4fb2; + --accent-soft: rgba(119, 107, 208, 0.12); + --button-hover: #f6efe2; + --panel-shadow: 0 18px 48px rgba(78, 58, 34, 0.12); } .app-header { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding: 0 0 10px; - border-bottom: 1px solid var(--border); + gap: 16px; + padding: 2px 4px 14px; } .app-header h1 { margin: 0; - font-size: 16px; + font-size: 15px; font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.04em; } .app-header__actions { @@ -146,30 +170,43 @@ button { .eyebrow { margin: 0 0 4px; font-size: 11px; - color: var(--text-muted); + color: var(--text-subtle); text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.12em; } .button { - border: 1px solid var(--border); - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--surface-elevated) 96%, transparent); color: var(--text-primary); - padding: 7px 10px; + padding: 8px 12px; + transition: + background 120ms ease, + border-color 120ms ease, + color 120ms ease, + transform 120ms ease; } .button:hover:not(:disabled) { - text-decoration: underline; + background: var(--button-hover); + border-color: var(--border); } .button--primary, .button--ghost, .button--danger, .button--active { - background: var(--surface); + background: var(--surface-elevated); color: var(--text-primary); } +.button--primary, +.button--active { + border-color: color-mix(in srgb, var(--accent) 36%, var(--border-muted)); + background: color-mix(in srgb, var(--accent-soft) 80%, var(--surface-elevated)); +} + .button:disabled { cursor: not-allowed; opacity: 0.45; @@ -184,16 +221,21 @@ button { .settings-panel { position: fixed; - top: 12px; - right: 12px; - width: min(420px, calc(100vw - 24px)); - max-height: calc(100vh - 24px); + top: 16px; + right: 16px; + width: min(440px, calc(100vw - 32px)); + max-height: calc(100vh - 32px); display: flex; flex-direction: column; gap: 16px; - padding: 14px; - border: 1px solid var(--border); - background: var(--surface); + padding: 16px; + border: 1px solid var(--border-muted); + border-radius: calc(var(--radius-xl) + 1px); + background: + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 96%, transparent), color-mix(in srgb, var(--surface) 92%, var(--surface-muted))), + var(--surface-elevated); + box-shadow: var(--panel-shadow); + backdrop-filter: blur(18px) saturate(105%); transform: translateX(calc(100% + 16px)); transition: transform 140ms ease; z-index: 21; @@ -207,18 +249,23 @@ button { display: flex; align-items: flex-start; justify-content: space-between; - gap: 12px; + gap: 16px; + padding-bottom: 2px; + border-bottom: 1px solid color-mix(in srgb, var(--border-muted) 82%, transparent); } .settings-panel__header strong { display: block; - font-size: 14px; + font-size: 15px; + letter-spacing: 0.01em; } .settings-panel__summary { + max-width: 32ch; margin: 6px 0 0; font-size: 12px; - color: var(--text-muted); + color: color-mix(in srgb, var(--text-muted) 92%, var(--text-subtle)); + line-height: 1.6; } .settings-panel__content { @@ -229,13 +276,15 @@ button { } .settings-section { - padding: 12px; - border: 1px solid var(--border); - background: var(--surface); + padding: 14px 14px 15px; + border: 1px solid color-mix(in srgb, var(--border-muted) 78%, transparent); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--surface) 92%, var(--surface-muted)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34); } .settings-section__title { - margin-bottom: 10px; + margin-bottom: 12px; } .settings-section__title strong { @@ -244,9 +293,10 @@ button { } .settings-section__title p { - margin: 4px 0 0; + margin: 5px 0 0; font-size: 12px; color: var(--text-muted); + line-height: 1.55; } .settings-grid { @@ -259,7 +309,7 @@ button { display: flex; flex-direction: column; gap: 6px; - margin-top: 10px; + margin-top: 11px; } .settings-shortcuts { @@ -310,10 +360,19 @@ button { .settings-field input, .settings-field select { width: 100%; - border: 1px solid var(--border); - padding: 8px 10px; - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: var(--radius-md); + padding: 10px 12px; + background: color-mix(in srgb, var(--surface-elevated) 94%, var(--surface)); color: var(--text-primary); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28); +} + +.settings-field input:focus, +.settings-field select:focus { + outline: none; + border-color: color-mix(in srgb, var(--accent) 46%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-soft) 100%, transparent); } .settings-toggle { @@ -348,15 +407,16 @@ button { min-height: 0; flex-direction: column; overflow: hidden; - padding-top: 10px; + padding-top: 6px; } .boot-warning { margin: 0 0 8px; - padding: 8px 10px; - border: 1px solid var(--border); + padding: 10px 12px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); color: var(--text-primary); - background: var(--surface); + background: color-mix(in srgb, var(--surface) 90%, var(--surface-muted)); font-size: 12px; } @@ -373,10 +433,15 @@ button { flex: 1; min-width: 0; min-height: 0; + padding: 6px; + border-radius: calc(var(--radius-xl) + 1px); + background: + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 70%, transparent), transparent 34%), + var(--app-background); } .terminal-pane { - --ai-theme-color: #1f5eff; + --ai-theme-color: var(--accent); --ai-background-color: var(--surface-muted); position: relative; display: flex; @@ -384,22 +449,33 @@ button { flex-direction: column; min-width: 0; min-height: 0; - border: 1px solid var(--border); - background: var(--surface); + border: 1px solid color-mix(in srgb, var(--border) 84%, #d0d7de); + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--surface-elevated) 99%, #ffffff); + box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04); overflow: hidden; container-type: inline-size; } +.terminal-pane--flush-top::after, +.terminal-pane--flush-right::after, +.terminal-pane--flush-bottom::after, +.terminal-pane--flush-left::after { + content: ""; + position: absolute; + pointer-events: none; + background: color-mix(in srgb, var(--app-background) 97%, transparent); + z-index: 1; +} + .terminal-pane--active { - border-width: 2px; + border-color: color-mix(in srgb, var(--border) 84%, #d0d7de); + box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04); } .terminal-pane--agent-workflow { - border-width: 2px; - border-left-width: 8px; - border-color: var(--border); - border-left-color: var(--ai-theme-color); - box-shadow: inset 0 0 0 1px var(--ai-theme-color); + border-color: color-mix(in srgb, var(--ai-theme-color) 18%, var(--border)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--ai-theme-color) 8%, transparent); } .terminal-pane--drag-source { @@ -408,18 +484,58 @@ button { .terminal-pane--flush-top { border-top-width: 0; + border-top-left-radius: var(--radius-seam); + border-top-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-top::after { + top: 0; + left: 5px; + right: 5px; + height: 4px; + border-radius: 0 0 var(--radius-seam) var(--radius-seam); } .terminal-pane--flush-right { border-right-width: 0; + border-top-right-radius: var(--radius-seam); + border-bottom-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-right::after { + top: 5px; + right: 0; + bottom: 5px; + width: 4px; + border-radius: var(--radius-seam) 0 0 var(--radius-seam); } .terminal-pane--flush-bottom { border-bottom-width: 0; + border-bottom-left-radius: var(--radius-seam); + border-bottom-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-bottom::after { + right: 5px; + bottom: 0; + left: 5px; + height: 4px; + border-radius: var(--radius-seam) var(--radius-seam) 0 0; } .terminal-pane--flush-left { border-left-width: 0; + border-top-left-radius: var(--radius-seam); + border-bottom-left-radius: var(--radius-seam); +} + +.terminal-pane--flush-left::after { + top: 5px; + bottom: 5px; + left: 0; + width: 4px; + border-radius: 0 var(--radius-seam) var(--radius-seam) 0; } .terminal-pane__header { @@ -428,17 +544,42 @@ button { align-items: center; justify-content: space-between; gap: 12px; - min-height: 28px; - padding: 4px 8px; - box-shadow: inset 0 -1px 0 var(--border); - background: var(--surface); + min-height: 43px; + padding: 9px 11px; + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--border) 78%, #d8dee4); + background: color-mix(in srgb, var(--surface-muted) 55%, #f6f8fa); cursor: move; user-select: none; } +.terminal-pane--active .terminal-pane__header { + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--accent) 28%, var(--border)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--accent) 7%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 96%, #ffffff) + ); +} + .terminal-pane--agent-workflow .terminal-pane__header { - box-shadow: inset 0 -2px 0 var(--ai-theme-color); - background: var(--ai-background-color); + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 12%, var(--border)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--ai-theme-color) 5%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 97%, #ffffff) + ); +} + +.terminal-pane--agent-workflow.terminal-pane--active .terminal-pane__header { + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 16%, var(--border)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--ai-theme-color) 7%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 97%, #ffffff) + ); } .terminal-pane__title { @@ -453,8 +594,8 @@ button { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - font-size: 12px; - font-weight: 500; + font-size: 12.5px; + font-weight: 600; } .terminal-pane__title--editing { @@ -463,9 +604,10 @@ button { .terminal-pane__title-input { width: min(100%, 280px); - border: 1px solid var(--border); - padding: 2px 6px; - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: var(--radius-md); + padding: 6px 10px; + background: var(--surface-elevated); color: var(--text-primary); font-size: 12px; } @@ -476,20 +618,33 @@ button { .terminal-pane__mode-indicator { margin-left: auto; + display: inline-flex; + align-items: center; + gap: 6px; flex: 0 0 auto; white-space: nowrap; - font-size: 11px; - color: var(--text-muted); - letter-spacing: 0.08em; + font-size: 10px; + color: var(--text-subtle); + letter-spacing: 0.1em; +} + +.terminal-pane__mode-indicator::before { + content: ""; + width: 5px; + height: 5px; + border-radius: 999px; + background: currentColor; + opacity: 0.72; } .terminal-pane--agent-workflow .terminal-pane__mode-indicator { - border: 1px solid var(--ai-theme-color); - background: var(--ai-theme-color); - color: var(--ai-badge-text); - padding: 2px 8px; + border: 0; + border-radius: 0; + background: transparent; + color: color-mix(in srgb, var(--ai-theme-color) 42%, var(--text-muted)); + padding: 0; font-weight: 600; - letter-spacing: 0.14em; + letter-spacing: 0.12em; } .pane-action-menu { @@ -499,11 +654,12 @@ button { } .pane-action-menu__trigger { - min-width: 28px; + min-width: 24px; height: 24px; - border: 0; + border: 1px solid transparent; + border-radius: var(--radius-sm); background: transparent; - color: var(--text-primary); + color: var(--text-muted); padding: 0 6px; } @@ -515,15 +671,17 @@ button { display: flex; min-width: 160px; flex-direction: column; - border: 1px solid var(--border); - background: var(--surface); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08); + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + background: var(--surface-elevated); + box-shadow: var(--panel-shadow); + overflow: hidden; } .pane-action-menu__item { border: 0; - border-bottom: 1px solid var(--border); - background: var(--surface); + border-bottom: 1px solid var(--border-muted); + background: var(--surface-elevated); color: var(--text-primary); text-align: left; padding: 8px 10px; @@ -534,7 +692,7 @@ button { } .pane-action-menu__item:hover:not(:disabled) { - text-decoration: underline; + background: var(--button-hover); } .pane-action-menu__item:disabled { @@ -546,24 +704,29 @@ button { display: inline-flex; flex: 0 0 auto; align-items: center; - gap: 1px; - padding: 2px; - border: 1px solid var(--border); - background: var(--surface); + gap: 4px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } .pane-header-actions__button { - min-width: 28px; + min-width: 24px; height: 24px; - border: 0; + border: 1px solid transparent; + border-radius: var(--radius-sm); background: transparent; - color: var(--text-primary); - padding: 0 6px; + color: var(--text-muted); + padding: 0 5px; } .pane-header-actions__button:hover:not(:disabled), .pane-action-menu__trigger:hover:not(:disabled) { - background: var(--surface-muted); + border-color: color-mix(in srgb, var(--border) 84%, #d0d7de); + background: color-mix(in srgb, var(--surface-elevated) 96%, #ffffff); + color: var(--text-primary); } .pane-header-actions__button:disabled, @@ -573,7 +736,7 @@ button { } .pane-header-actions__button--close:hover:not(:disabled) { - background: var(--surface-muted); + background: color-mix(in srgb, var(--history-error) 12%, var(--button-hover)); } .terminal-pane__body { @@ -582,13 +745,13 @@ button { flex: 1; min-width: 0; min-height: 0; - background: var(--surface); + background: color-mix(in srgb, var(--surface-elevated) 99%, #ffffff); } .terminal-pane--agent-workflow .terminal-pane__body, .terminal-pane--agent-workflow .dialog-terminal, .terminal-pane--agent-workflow .terminal-pane__xterm { - background: var(--ai-background-color); + background: color-mix(in srgb, var(--surface-elevated) 99%, #ffffff); } .terminal-pane__xterm { @@ -597,8 +760,8 @@ button { min-height: 0; width: 100%; height: 100%; - padding: 8px; - background: var(--surface); + padding: 12px; + background: transparent; } .terminal-pane__overlay { @@ -610,9 +773,11 @@ button { align-items: flex-start; gap: 8px; max-width: min(360px, calc(100% - 24px)); - padding: 10px 12px; - border: 1px solid var(--border); - background: var(--surface); + padding: 12px 14px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + background: var(--surface-elevated); + box-shadow: var(--panel-shadow); } .terminal-pane__overlay h2 { @@ -643,7 +808,7 @@ button { flex-direction: column; font-family: var(--dialog-terminal-font-family); font-size: var(--dialog-terminal-font-size); - background: var(--surface); + background: transparent; } .dialog-terminal__history-shell { @@ -659,9 +824,9 @@ button { min-height: 0; flex-direction: column; justify-content: flex-end; - gap: 12px; + gap: 18px; overflow: auto; - padding: 12px; + padding: 20px 20px 15px; } .dialog-terminal__jump { @@ -675,10 +840,10 @@ button { .dialog-terminal__jump .button { pointer-events: auto; - border-radius: 999px; + border-radius: calc(var(--radius-lg) + 5px); padding: 6px 12px; - background: color-mix(in srgb, var(--surface) 84%, transparent); - box-shadow: 0 10px 24px color-mix(in srgb, var(--surface) 72%, transparent); + background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + box-shadow: var(--panel-shadow); backdrop-filter: blur(10px); opacity: 0.92; } @@ -693,15 +858,15 @@ button { display: grid; grid-template-columns: auto 1fr; align-items: center; - gap: 10px; - padding: 8px 12px 12px; - border-top: 1px solid var(--border); - background: var(--surface); + gap: 12px; + padding: 14px 20px 18px; + border-top: 1px solid var(--border-muted); + background: color-mix(in srgb, var(--surface-elevated) 80%, transparent); } .dialog-terminal__prompt { font-size: var(--dialog-terminal-font-size); - color: var(--text-muted); + color: var(--text-subtle); white-space: nowrap; } @@ -714,15 +879,17 @@ button { position: relative; display: grid; align-items: center; - border: 1px solid var(--border); - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--surface-elevated) 95%, var(--surface)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.26); } .dialog-terminal__ghost, .dialog-terminal__input { grid-area: 1 / 1; width: 100%; - padding: 8px 10px; + padding: 11px 13px; font: inherit; } @@ -765,7 +932,8 @@ button { } .dialog-terminal__input-shell:focus-within { - border-color: var(--text-primary); + border-color: color-mix(in srgb, var(--accent) 46%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-soft) 100%, transparent); } .dialog-terminal__candidate-menu { @@ -775,8 +943,10 @@ button { left: 0; display: grid; gap: 1px; - border: 1px solid var(--border); - background: var(--border); + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + background: var(--border-muted); + overflow: hidden; z-index: 2; } @@ -789,10 +959,11 @@ button { display: flex; flex-direction: column; gap: 8px; - padding: 10px; - border: 1px solid var(--border); - background: var(--surface-muted); - box-shadow: 0 12px 24px color-mix(in srgb, var(--surface) 70%, transparent); + padding: 12px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--surface-elevated) 92%, var(--surface-muted)); + box-shadow: var(--panel-shadow); } .dialog-terminal__suggestion-header { @@ -820,6 +991,7 @@ button { width: 100%; padding: 10px 12px; border: 1px solid var(--border-muted); + border-radius: var(--radius-md); background: var(--surface); color: var(--text-primary); font: inherit; @@ -827,8 +999,8 @@ button { } .dialog-terminal__suggestion--active { - border-color: var(--border); - background: var(--surface-muted); + border-color: color-mix(in srgb, var(--accent) 36%, var(--border)); + background: color-mix(in srgb, var(--accent-soft) 70%, var(--surface)); } .dialog-terminal__suggestion-kind { @@ -837,8 +1009,9 @@ button { justify-content: center; min-width: 82px; padding: 3px 8px; - border: 1px solid var(--border); - color: var(--text-muted); + border: 1px solid var(--border-muted); + border-radius: 999px; + color: var(--text-subtle); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; @@ -869,19 +1042,23 @@ button { flex: 0 0 auto; min-height: 0; flex-direction: column; - border-top: 1px solid var(--border); - background: var(--surface-muted); + margin: 0 18px 18px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--surface) 86%, var(--surface-muted)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); + overflow: hidden; } .dialog-live-console__header { display: grid; grid-template-columns: auto auto 1fr; align-items: center; - gap: 10px; - min-height: 34px; - padding: 8px 12px; + gap: 12px; + min-height: 42px; + padding: 11px 14px; border-bottom: 1px solid var(--border-muted); - background: var(--surface); + background: color-mix(in srgb, var(--surface-elevated) 96%, transparent); font-size: 12px; } @@ -892,7 +1069,8 @@ button { .dialog-live-console__label { font-weight: 600; - letter-spacing: 0.04em; + letter-spacing: 0.12em; + text-transform: uppercase; } .dialog-live-console__command { @@ -912,8 +1090,8 @@ button { .dialog-live-console__terminal { width: 100%; height: 100%; - padding: 8px 12px 12px; - background: var(--surface-muted); + padding: 14px 14px 16px; + background: transparent; } .dialog-live-console--compact .dialog-live-console__header { @@ -939,7 +1117,7 @@ button { } .dialog-terminal__candidate--active { - background: var(--surface-muted); + background: color-mix(in srgb, var(--accent-soft) 70%, var(--surface)); } .dialog-terminal__candidate-text { @@ -961,21 +1139,23 @@ button { } .command-block__divider { - margin: 0 0 10px; + margin: 0 0 14px; border: 0; - border-top: 1px solid var(--border-muted); + border-top: 1px solid color-mix(in srgb, var(--border-muted) 86%, transparent); } .command-block__header, .command-block__session-label, .command-block__status { - margin: 0 0 6px; + margin: 0 0 8px; font-size: var(--dialog-terminal-font-size); } .command-block__header { display: flex; gap: 8px; + align-items: baseline; + font-weight: 500; } .command-block__cwd, @@ -985,13 +1165,27 @@ button { color: var(--text-muted); } +.command-block__session-label { + display: inline-flex; + align-items: center; + width: fit-content; + padding: 4px 10px; + border: 1px solid var(--border-muted); + border-radius: 999px; + background: color-mix(in srgb, var(--surface-muted) 66%, var(--surface)); + color: var(--text-subtle); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .command-block__output { margin: 0; white-space: pre-wrap; word-break: break-word; color: var(--text-primary); font-size: var(--dialog-terminal-font-size); - line-height: 1.45; + line-height: 1.62; } .history-token--plain { @@ -1052,9 +1246,10 @@ button { display: grid; place-items: center; height: 100%; - border: 1px solid var(--border); + border: 1px solid var(--border-muted); + border-radius: var(--radius-xl); color: var(--text-muted); - background: var(--surface); + background: color-mix(in srgb, var(--surface) 92%, var(--surface-muted)); } .layout-tree { @@ -1090,6 +1285,7 @@ button { flex: 1; min-width: 0; min-height: 0; + isolation: isolate; } .layout-tree__leaf-resize-handles { @@ -1105,7 +1301,7 @@ button { .layout-tree__divider { position: relative; - z-index: 2; + z-index: 3; flex: 0 0 6px; width: 6px; height: 6px; @@ -1115,12 +1311,34 @@ button { touch-action: none; } +.layout-tree__divider::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: calc(50% - 0.5px); + width: 1px; + border-radius: 999px; + background: color-mix(in srgb, var(--border) 76%, #d0d7de); + transition: background 120ms ease, opacity 120ms ease; + opacity: 0.78; +} + .layout-tree__divider--vertical { width: 100%; height: 6px; cursor: row-resize; } +.layout-tree__divider--vertical::before { + top: calc(50% - 0.5px); + right: 0; + bottom: auto; + left: 0; + width: auto; + height: 1px; +} + .layout-tree__divider--horizontal { width: 6px; height: auto; @@ -1170,7 +1388,8 @@ button { } .terminal-pane__drop-zone--active { - outline: 1px solid var(--border); + outline: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border)); + border-radius: var(--radius-lg); } .terminal-pane__drop-preview { @@ -1183,7 +1402,9 @@ button { .terminal-pane__drop-preview::before { content: ""; position: absolute; - border: 1px dashed var(--border); + border: 1px dashed color-mix(in srgb, var(--accent) 54%, var(--border)); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--accent-soft) 55%, transparent); } .terminal-pane__drop-preview--left::before, @@ -1255,52 +1476,12 @@ button { touch-action: none; } -.terminal-pane__resize-handle::before, -.terminal-pane__resize-handle::after { - content: ""; - position: absolute; - opacity: 0; - transition: opacity 80ms ease; -} - -.terminal-pane__resize-handle::before { - inset: 0; - background: var(--resize-handle-overlay); -} - -.terminal-pane__resize-handle::after { - background: var(--resize-handle-line); -} - -.terminal-pane__resize-handle:hover::before, -.terminal-pane__resize-handle:hover::after, -.terminal-pane__resize-handle--active::before, -.terminal-pane__resize-handle--active::after { - opacity: 1; -} - .terminal-pane__resize-handle--left, .terminal-pane__resize-handle--right { cursor: col-resize; } -.terminal-pane__resize-handle--left::after, -.terminal-pane__resize-handle--right::after { - top: 0; - bottom: 0; - width: 1px; - left: calc(50% - 0.5px); -} - .terminal-pane__resize-handle--top, .terminal-pane__resize-handle--bottom { cursor: row-resize; } - -.terminal-pane__resize-handle--top::after, -.terminal-pane__resize-handle--bottom::after { - left: 0; - right: 0; - height: 1px; - top: calc(50% - 0.5px); -} diff --git a/src/domain/config/model.test.ts b/src/domain/config/model.test.ts index b86ed90..f3e3e76 100644 --- a/src/domain/config/model.test.ts +++ b/src/domain/config/model.test.ts @@ -217,6 +217,21 @@ describe("resolveAppConfig", () => { }); }); + it("migrates legacy default ai colors to the newer lavender defaults", () => { + expect( + resolveAppConfig({ + ai: { + themeColor: "#1f5eff", + backgroundColor: "#eef4ff", + }, + }), + ).toEqual({ + terminal: DEFAULT_APP_CONFIG.terminal, + ai: DEFAULT_APP_CONFIG.ai, + ui: DEFAULT_APP_CONFIG.ui, + }); + }); + it("defaults smart suggestion bubble and preserves explicit overrides", () => { expect( resolveAppConfig({ diff --git a/src/domain/config/model.ts b/src/domain/config/model.ts index 3568e46..7d1aab7 100644 --- a/src/domain/config/model.ts +++ b/src/domain/config/model.ts @@ -24,8 +24,8 @@ export const DEFAULT_APP_CONFIG: AppConfig = { enabled: false, smartSuggestionBubble: true, apiKey: "", - themeColor: "#1f5eff", - backgroundColor: "#eef4ff", + themeColor: "#8b5cf6", + backgroundColor: "#f6f0ff", }, ui: { settingsPanelLanguage: DEFAULT_SETTINGS_PANEL_LANGUAGE, @@ -54,6 +54,8 @@ export interface AppConfigInput { const MIN_FONT_SIZE = 10; const MAX_FONT_SIZE = 32; +const LEGACY_DEFAULT_AI_THEME_COLOR = "#1f5eff"; +const LEGACY_DEFAULT_AI_BACKGROUND_COLOR = "#eef4ff"; export function resolveAppConfig(input?: AppConfigInput | null): AppConfig { const terminal = input?.terminal; @@ -83,8 +85,8 @@ export function resolveAppConfig(input?: AppConfigInput | null): AppConfig { ? ai.smartSuggestionBubble : DEFAULT_APP_CONFIG.ai.smartSuggestionBubble, apiKey: normalizeOptionalString(ai?.apiKey), - themeColor: normalizeHexColor(ai?.themeColor, DEFAULT_APP_CONFIG.ai.themeColor), - backgroundColor: normalizeHexColor(ai?.backgroundColor, DEFAULT_APP_CONFIG.ai.backgroundColor), + themeColor: normalizeAiThemeColor(ai?.themeColor), + backgroundColor: normalizeAiBackgroundColor(ai?.backgroundColor), }, ui: { settingsPanelLanguage: normalizeSettingsPanelLanguage(ui?.settingsPanelLanguage), @@ -150,6 +152,18 @@ function normalizeHexColor(value: string | undefined, fallback: string): string return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(normalized) ? normalized : fallback; } +function normalizeAiThemeColor(value: string | undefined): string { + const normalized = normalizeHexColor(value, DEFAULT_APP_CONFIG.ai.themeColor); + return normalized.toLowerCase() === LEGACY_DEFAULT_AI_THEME_COLOR ? DEFAULT_APP_CONFIG.ai.themeColor : normalized; +} + +function normalizeAiBackgroundColor(value: string | undefined): string { + const normalized = normalizeHexColor(value, DEFAULT_APP_CONFIG.ai.backgroundColor); + return normalized.toLowerCase() === LEGACY_DEFAULT_AI_BACKGROUND_COLOR + ? DEFAULT_APP_CONFIG.ai.backgroundColor + : normalized; +} + function normalizePhraseList(value: string[] | undefined): string[] { if (!Array.isArray(value)) { return []; diff --git a/src/domain/terminal/dialog.test.ts b/src/domain/terminal/dialog.test.ts index e6fdd91..15cd479 100644 --- a/src/domain/terminal/dialog.test.ts +++ b/src/domain/terminal/dialog.test.ts @@ -306,6 +306,24 @@ describe("dialog terminal state", () => { ]); }); + it("strips the echoed command and stray prompt symbol when finalizing deferred command output", () => { + const state = submitDialogCommand(createDialogState("/bin/zsh", "/Users/s"), "cd", () => "cmd:cd"); + const withOutput = appendLiveConsoleOutput(state, "cd\n%\n"); + const finished = applyShellLifecycleEvent(withOutput, { + type: "command-end", + exitCode: 0, + }); + + expect(finished.blocks).toEqual([ + expect.objectContaining({ + id: "cmd:cd", + output: "", + status: "completed", + exitCode: 0, + }), + ]); + }); + it("returns backend-escalated panes back to the preferred mode after the command completes", () => { const state = applyTerminalSemanticEvent( submitDialogCommand(createDialogState("/bin/bash", "/workspace", "classic"), "top", () => "cmd:1"), @@ -372,16 +390,51 @@ describe("dialog terminal state", () => { }); }); - it("captures non-command output in a session output block", () => { + it("suppresses startup output before the first user command runs", () => { const state = appendDialogOutput(createDialogState("/bin/bash", "/workspace"), "Welcome\n"); - expect(state.blocks).toEqual([ + expect(state.blocks).toEqual([]); + }); + + it("captures non-command output in a session output block after the user has run a command", () => { + const started = submitDialogCommand(createDialogState("/bin/bash", "/workspace"), "pwd", () => "cmd:1"); + const finished = applyShellLifecycleEvent(started, { + type: "command-end", + exitCode: 0, + }); + const state = appendDialogOutput(finished, "Welcome\n"); + + expect(state.blocks).toHaveLength(2); + expect(state.blocks[1]).toEqual( expect.objectContaining({ kind: "session", output: "Welcome\n", status: "completed", }), - ]); + ); + }); + + it("ignores whitespace-only startup output when no command block is active", () => { + const state = appendDialogOutput(createDialogState("/bin/bash", "/workspace"), "\r\n \r\n"); + + expect(state.blocks).toEqual([]); + }); + + it("ignores visually blank ANSI-only output after the user has already run a command", () => { + const started = submitDialogCommand(createDialogState("/bin/zsh", "/workspace"), "pwd", () => "cmd:1"); + const finished = applyShellLifecycleEvent(started, { + type: "command-end", + exitCode: 0, + }); + const state = appendDialogOutput(finished, "\u001b[32m \u001b[39m\r\n"); + + expect(state.blocks).toHaveLength(1); + expect(state.blocks[0]).toEqual( + expect.objectContaining({ + kind: "command", + command: "pwd", + }), + ); }); it("updates the cwd from shell prompt markers for the next command header", () => { diff --git a/src/domain/terminal/dialog.ts b/src/domain/terminal/dialog.ts index d73d41f..4c83a3f 100644 --- a/src/domain/terminal/dialog.ts +++ b/src/domain/terminal/dialog.ts @@ -55,6 +55,7 @@ export type ShellLifecycleEvent = | { type: "prompt-state"; cwd: string }; let nextSessionBlockId = 1; +const ANSI_ESCAPE_PATTERN = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g; export function createDialogState(shell: string, cwd: string, preferredMode: PaneRenderMode = "dialog"): DialogState { const supported = isDialogShellSupported(shell); @@ -170,6 +171,14 @@ export function appendDialogOutput(state: DialogState, data: string): DialogStat }; } + if (state.composerHistory.length === 0) { + return state; + } + + if (isVisuallyEmptyOutput(data)) { + return state; + } + const lastBlock = state.blocks[state.blocks.length - 1]; if (lastBlock?.kind === "session") { return { @@ -244,7 +253,7 @@ export function applyShellLifecycleEvent(state: DialogState, event: ShellLifecyc ...block, output: state.transcriptPolicy === "defer-until-exit" && state.liveConsole?.blockId === block.id - ? state.liveConsole.transcriptCapture + ? finalizeDeferredCommandOutput(block.command, state.liveConsole.transcriptCapture) : block.output, status: "completed", exitCode: event.exitCode, @@ -262,6 +271,60 @@ export function applyShellLifecycleEvent(state: DialogState, event: ShellLifecyc } } +function finalizeDeferredCommandOutput(command: string | null, output: string): string { + if (output.length === 0) { + return output; + } + + const commandText = command?.trim() ?? ""; + let normalized = output.replace(/\r/g, ""); + + if (commandText.length > 0) { + const lines = normalized.split("\n"); + const firstNonEmptyLineIndex = lines.findIndex((line) => line.trim().length > 0); + if (firstNonEmptyLineIndex >= 0 && lines[firstNonEmptyLineIndex]?.trim() === commandText) { + lines.splice(firstNonEmptyLineIndex, 1); + normalized = lines.join("\n"); + } + } + + const trimmedTrailingNewlines = normalized.replace(/\n+$/g, ""); + const lines = trimmedTrailingNewlines.split("\n"); + const lastNonEmptyLineIndex = findLastNonEmptyLineIndex(lines); + + if (lastNonEmptyLineIndex >= 0 && isLikelyPromptLine(lines[lastNonEmptyLineIndex] ?? "")) { + lines.splice(lastNonEmptyLineIndex, 1); + normalized = lines.join("\n").replace(/\n+$/g, ""); + } else { + normalized = trimmedTrailingNewlines; + } + + return normalized.length === 0 ? "" : `${normalized}\n`; +} + +function isVisuallyEmptyOutput(output: string): boolean { + return stripAnsi(output).trim().length === 0; +} + +function stripAnsi(output: string): string { + return output.replace(ANSI_ESCAPE_PATTERN, ""); +} + +function findLastNonEmptyLineIndex(lines: string[]): number { + for (let index = lines.length - 1; index >= 0; index -= 1) { + if ((lines[index] ?? "").trim().length > 0) { + return index; + } + } + + return -1; +} + +function isLikelyPromptLine(line: string): boolean { + const trimmed = stripAnsi(line).trim(); + return trimmed === "%" || trimmed === "$" || trimmed === "#" || trimmed === "❯"; +} + export function isDialogShellSupported(shell: string): boolean { return /(^|\/)(bash|zsh)$/.test(shell.trim()); } diff --git a/src/domain/theme/presets.test.ts b/src/domain/theme/presets.test.ts index 2407314..6207b0d 100644 --- a/src/domain/theme/presets.test.ts +++ b/src/domain/theme/presets.test.ts @@ -8,34 +8,36 @@ describe("theme presets", () => { id: "light", colorScheme: "light", app: { - background: "#ffffff", - surface: "#ffffff", - textPrimary: "#000000", + background: "#f5f1e8", + surface: "#fdfaf4", + textPrimary: "#241f17", + historyCommand: "#5c7198", }, terminal: { - background: "#ffffff", - foreground: "#000000", + background: "#fbf8f2", + foreground: "#241f17", + blue: "#6e83ac", }, }); }); - it("returns a dark preset with traditional near-black terminal contrast", () => { + it("returns a dark preset with restrained graphite contrast instead of pure black", () => { expect(getThemePreset("dark")).toMatchObject({ id: "dark", colorScheme: "dark", app: { - background: "#000000", - surface: "#050505", - textPrimary: "#f7f7f7", - historyCommand: "#8ab4ff", - historyAccent: "#e0b7ff", + background: "#0d1117", + surface: "#121821", + textPrimary: "#edf2f7", + historyCommand: "#93adf5", + historyAccent: "#aebcff", }, terminal: { - background: "#000000", - foreground: "#f5f5f5", - cursor: "#f5f5f5", - blue: "#61afef", - brightBlue: "#8fc7ff", + background: "#111723", + foreground: "#e6edf5", + cursor: "#e6edf5", + blue: "#7ea2ff", + brightBlue: "#a9c1ff", }, }); }); @@ -45,12 +47,12 @@ describe("theme presets", () => { id: "sepia", colorScheme: "light", app: { - background: "#f4ead7", - surface: "#fbf3e4", - textPrimary: "#2f2419", + background: "#f3eadc", + surface: "#fcf7ef", + textPrimary: "#312519", }, terminal: { - background: "#f7efe2", + background: "#f8f1e7", foreground: "#2f2419", cursor: "#2f2419", }, diff --git a/src/domain/theme/presets.ts b/src/domain/theme/presets.ts index 68dad51..18bf6ee 100644 --- a/src/domain/theme/presets.ts +++ b/src/domain/theme/presets.ts @@ -62,44 +62,44 @@ const THEME_PRESETS: Record = { label: "Light", colorScheme: "light", app: { - background: "#ffffff", - surface: "#ffffff", - surfaceMuted: "#f5f5f5", - textPrimary: "#000000", - textMuted: "#555555", - border: "#000000", - borderMuted: "#c8c8c8", - overlayBackdrop: "rgba(0, 0, 0, 0.06)", - historyCommand: "#003d99", - historyAccent: "#6b2f8a", - historyString: "#7a3e00", - historyPath: "#0f6b45", - historyUrl: "#005f8f", - historyError: "#9f1d1d", - historyWarning: "#9b7500", - historySuccess: "#0b7a28", - historyNumber: "#005f5f", + background: "#f5f1e8", + surface: "#fdfaf4", + surfaceMuted: "#f3ede2", + textPrimary: "#241f17", + textMuted: "#756b5f", + border: "#d8cec1", + borderMuted: "#e9e0d4", + overlayBackdrop: "rgba(36, 31, 23, 0.08)", + historyCommand: "#5c7198", + historyAccent: "#7c85b2", + historyString: "#90613a", + historyPath: "#4d6f58", + historyUrl: "#55758e", + historyError: "#b36563", + historyWarning: "#b38a50", + historySuccess: "#4f7d69", + historyNumber: "#637d9f", }, terminal: { - background: "#ffffff", - foreground: "#000000", - cursor: "#000000", - black: "#000000", - red: "#8a0000", - green: "#006400", - yellow: "#7a5c00", - blue: "#003d99", - magenta: "#6b2f8a", - cyan: "#005f5f", - white: "#d8d8d8", - brightBlack: "#4a4a4a", - brightRed: "#b30000", - brightGreen: "#008000", - brightYellow: "#9b7500", - brightBlue: "#0052cc", - brightMagenta: "#8a3fb3", - brightCyan: "#007a7a", - brightWhite: "#f2f2f2", + background: "#fbf8f2", + foreground: "#241f17", + cursor: "#5c7198", + black: "#2f2a22", + red: "#b36563", + green: "#4f7d69", + yellow: "#af8750", + blue: "#6e83ac", + magenta: "#7c85b2", + cyan: "#5e8197", + white: "#dfd7cb", + brightBlack: "#7f7568", + brightRed: "#ca807d", + brightGreen: "#668f79", + brightYellow: "#c39a61", + brightBlue: "#8798bf", + brightMagenta: "#969dc6", + brightCyan: "#7497ad", + brightWhite: "#ffffff", }, }, dark: { @@ -107,44 +107,44 @@ const THEME_PRESETS: Record = { label: "Dark", colorScheme: "dark", app: { - background: "#000000", - surface: "#050505", - surfaceMuted: "#0d0d0d", - textPrimary: "#f7f7f7", - textMuted: "#b8b8b8", - border: "#5a5a5a", - borderMuted: "#242424", - overlayBackdrop: "rgba(0, 0, 0, 0.82)", - historyCommand: "#8ab4ff", - historyAccent: "#e0b7ff", - historyString: "#ffd08a", - historyPath: "#98e6a8", - historyUrl: "#8edfff", - historyError: "#ff9c9c", - historyWarning: "#ffe082", - historySuccess: "#9ae6a0", - historyNumber: "#8ce7de", + background: "#0d1117", + surface: "#121821", + surfaceMuted: "#18202b", + textPrimary: "#edf2f7", + textMuted: "#91a0b3", + border: "#2a3442", + borderMuted: "#202938", + overlayBackdrop: "rgba(2, 6, 23, 0.74)", + historyCommand: "#93adf5", + historyAccent: "#aebcff", + historyString: "#d7b07b", + historyPath: "#7fb89a", + historyUrl: "#80b7d8", + historyError: "#e79097", + historyWarning: "#d6bb74", + historySuccess: "#76b59b", + historyNumber: "#7ea7d8", }, terminal: { - background: "#000000", - foreground: "#f5f5f5", - cursor: "#f5f5f5", - black: "#000000", - red: "#e06c75", - green: "#98c379", - yellow: "#e5c07b", - blue: "#61afef", - magenta: "#c678dd", - cyan: "#56b6c2", - white: "#dcdfe4", - brightBlack: "#5c6370", - brightRed: "#ff8f96", - brightGreen: "#b4e28d", - brightYellow: "#ffd98e", - brightBlue: "#8fc7ff", - brightMagenta: "#e2b1ff", - brightCyan: "#7fe4f0", - brightWhite: "#ffffff", + background: "#111723", + foreground: "#e6edf5", + cursor: "#e6edf5", + black: "#101620", + red: "#d97f88", + green: "#73af94", + yellow: "#ccb16a", + blue: "#7ea2ff", + magenta: "#9eaef8", + cyan: "#72a9c7", + white: "#d6dce6", + brightBlack: "#5d6b7d", + brightRed: "#eca2aa", + brightGreen: "#8ec6ad", + brightYellow: "#dfc989", + brightBlue: "#a9c1ff", + brightMagenta: "#becbff", + brightCyan: "#97c1d8", + brightWhite: "#f6f9fc", }, }, sepia: { @@ -152,14 +152,14 @@ const THEME_PRESETS: Record = { label: "Sepia", colorScheme: "light", app: { - background: "#f4ead7", - surface: "#fbf3e4", - surfaceMuted: "#efe2cc", - textPrimary: "#2f2419", - textMuted: "#705d49", - border: "#6e5a46", - borderMuted: "#b8a58b", - overlayBackdrop: "rgba(68, 45, 20, 0.14)", + background: "#f3eadc", + surface: "#fcf7ef", + surfaceMuted: "#f1e5d2", + textPrimary: "#312519", + textMuted: "#746351", + border: "#c9b79f", + borderMuted: "#dfd1bf", + overlayBackdrop: "rgba(68, 45, 20, 0.12)", historyCommand: "#5a4fb2", historyAccent: "#8b4f7f", historyString: "#9a5d12", @@ -171,7 +171,7 @@ const THEME_PRESETS: Record = { historyNumber: "#87612f", }, terminal: { - background: "#f7efe2", + background: "#f8f1e7", foreground: "#2f2419", cursor: "#2f2419", black: "#3b2f22", diff --git a/src/features/config/state/app-config-store.test.ts b/src/features/config/state/app-config-store.test.ts index 7e4c0c0..0be853a 100644 --- a/src/features/config/state/app-config-store.test.ts +++ b/src/features/config/state/app-config-store.test.ts @@ -61,7 +61,7 @@ describe("app-config-store", () => { baseUrl: "https://open.bigmodel.cn/api/paas/v4", apiKey: "secret-key", themeColor: "#2b6fff", - backgroundColor: "#eef4ff", + backgroundColor: DEFAULT_APP_CONFIG.ai.backgroundColor, }); expect(useAppConfigStore.getState().config.terminal).toEqual(DEFAULT_APP_CONFIG.terminal); }); diff --git a/src/features/terminal/components/DialogTranscript.test.tsx b/src/features/terminal/components/DialogTranscript.test.tsx index a27936a..fa7d81c 100644 --- a/src/features/terminal/components/DialogTranscript.test.tsx +++ b/src/features/terminal/components/DialogTranscript.test.tsx @@ -55,4 +55,32 @@ describe("DialogTranscript", () => { expect(host.textContent).not.toContain("exit 0"); expect(host.querySelector(".command-block__status")).toBeNull(); }); + + it("renders shell output blocks with the calmer context label", () => { + const blocks: CommandBlock[] = [ + { + id: "session:1", + kind: "session", + cwd: "/workspace", + command: null, + output: "Welcome back\n", + status: "completed", + interactive: false, + exitCode: null, + }, + ]; + + act(() => { + root.render( + undefined} + />, + ); + }); + + expect(host.textContent).toContain("shell output"); + expect(host.textContent).not.toContain("session output"); + }); }); diff --git a/src/features/terminal/components/DialogTranscript.tsx b/src/features/terminal/components/DialogTranscript.tsx index 1aca262..725047e 100644 --- a/src/features/terminal/components/DialogTranscript.tsx +++ b/src/features/terminal/components/DialogTranscript.tsx @@ -17,7 +17,12 @@ export function DialogTranscript({ blocks, scrollRef, bottomRef, onScroll }: Dia {blocks.map((block, index) => (
{index > 0 ?
: null} - {block.kind === "command" ? :

session output

} + {block.kind === "command" ? ( + + ) : ( + // Internally this remains a session-scoped transcript block; the UI copy is calmer. +

shell output

+ )}
))} diff --git a/src/features/terminal/components/LiveCommandConsole.tsx b/src/features/terminal/components/LiveCommandConsole.tsx index d2f0274..b36ca96 100644 --- a/src/features/terminal/components/LiveCommandConsole.tsx +++ b/src/features/terminal/components/LiveCommandConsole.tsx @@ -37,7 +37,7 @@ export function LiveCommandConsole({ aria-label="Live command console" >
- Live Command Console + Live Console {cwd} {command}
diff --git a/src/features/terminal/components/TerminalPane.tsx b/src/features/terminal/components/TerminalPane.tsx index 7dda667..5435d36 100644 --- a/src/features/terminal/components/TerminalPane.tsx +++ b/src/features/terminal/components/TerminalPane.tsx @@ -269,7 +269,7 @@ export function TerminalPane({ tabId, borderMask }: TerminalPaneProps) { {isAgentWorkflow ? ( - AI MODE + AI ) : null} diff --git a/src/features/terminal/lib/layout-presentation.test.ts b/src/features/terminal/lib/layout-presentation.test.ts index 3a07854..196bf62 100644 --- a/src/features/terminal/lib/layout-presentation.test.ts +++ b/src/features/terminal/lib/layout-presentation.test.ts @@ -15,19 +15,40 @@ describe("layout presentation", () => { }); }); - it("marks every non-terminal child in a horizontal container as flush-right", () => { + it("marks both panes touching a horizontal seam as flush on their shared edge", () => { expect(getChildBorderMask({}, "horizontal", 0, 3)).toEqual({ right: true, }); - expect(getChildBorderMask({}, "horizontal", 2, 3)).toEqual({}); + expect(getChildBorderMask({}, "horizontal", 1, 3)).toEqual({ + left: true, + right: true, + }); + expect(getChildBorderMask({}, "horizontal", 2, 3)).toEqual({ + left: true, + }); }); - it("preserves the two-child compatibility helper", () => { + it("marks both panes touching a vertical seam as flush on their shared edge", () => { + expect(getChildBorderMask({}, "vertical", 0, 3)).toEqual({ + bottom: true, + }); + expect(getChildBorderMask({}, "vertical", 1, 3)).toEqual({ + top: true, + bottom: true, + }); + expect(getChildBorderMask({}, "vertical", 2, 3)).toEqual({ + top: true, + }); + }); + + it("preserves the two-child compatibility helper while rounding both sides of the split", () => { expect(splitPaneBorderMask({}, "vertical")).toEqual({ first: { bottom: true, }, - second: {}, + second: { + top: true, + }, }); }); }); diff --git a/src/features/terminal/lib/layout-presentation.ts b/src/features/terminal/lib/layout-presentation.ts index a3bab40..76e4760 100644 --- a/src/features/terminal/lib/layout-presentation.ts +++ b/src/features/terminal/lib/layout-presentation.ts @@ -22,16 +22,18 @@ export function getChildBorderMask( total: number, ): PaneBorderMask { if (axis === "horizontal") { - return { + return compactMask({ ...mask, + left: index > 0 ? true : mask.left, right: index < total - 1 ? true : mask.right, - }; + }); } - return { + return compactMask({ ...mask, + top: index > 0 ? true : mask.top, bottom: index < total - 1 ? true : mask.bottom, - }; + }); } export function splitPaneBorderMask( @@ -43,3 +45,7 @@ export function splitPaneBorderMask( second: getChildBorderMask(mask, axis, 1, 2), }; } + +function compactMask(mask: PaneBorderMask): PaneBorderMask { + return Object.fromEntries(Object.entries(mask).filter(([, value]) => value === true)) as PaneBorderMask; +}