From 10505628550311511d1ebb100f69e29cae1f0be1 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:09:46 +0800 Subject: [PATCH 01/13] Prevent zsh startup noise from leaking into fresh sessions The shell integration script assumed preexec/precmd hook arrays already existed, which let zsh emit bad math expression errors on clean startup. Initializing those arrays keeps PRAW's own startup path quiet and stops accidental session-output pollution before the user runs a command. Constraint: Keep the existing shell integration approach and prompt marker flow intact Rejected: Rewrite zsh hook registration around add-zsh-hook | larger behavior change than needed for the regression Confidence: high Scope-risk: narrow Reversibility: clean Directive: If zsh hook registration changes again, keep a runtime test that asserts startup produces no shell math errors Tested: cargo test; npm test; npm run typecheck; npm run build Not-tested: Finder-launched macOS bundle runtime after this exact patch --- src-tauri/src/terminal/shell_integration.rs | 28 +++-- .../src/terminal/shell_integration_test.rs | 118 ++++++++++++++++-- 2 files changed, 129 insertions(+), 17 deletions(-) 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:?}" + ); + } } From 2e6afd50024eab42a189f7ae50b850481fe6ed8e Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:09:53 +0800 Subject: [PATCH 02/13] Record the visual north star for PRAW's workspace refresh The redesign needs a stable product lens before implementation starts. This spec locks the chosen direction: Manus-inspired restraint, graphite-blue dual themes, quieter pane chrome, and a more intentional treatment of transcript, live console, and shell-output context. Constraint: Must preserve PRAW's terminal-native workspace semantics rather than imitating a marketing page structure Rejected: Directly clone Manus page styling verbatim | would blur product identity and confuse component semantics Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep future UI edits aligned with this token-first, restraint-first direction unless a new design spec explicitly replaces it Tested: Manual spec self-review for scope, consistency, and ambiguity Not-tested: Implementation viability against current CSS until planning/execution --- .../2026-04-14-praw-manus-workspace-design.md | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md 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..87bbef8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md @@ -0,0 +1,375 @@ +# 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. + +### 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. From e90708f61cb9db32bbb843958609ee19e869586b Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:10:43 +0800 Subject: [PATCH 03/13] Stage the workspace refresh behind an execution-ready plan The redesign now has a concrete task sequence: lock expectations in tests, rebuild tokens, refactor shell CSS, polish transcript/live console/settings, then verify and publish. This keeps the visual rewrite incremental instead of turning into an unbounded styling sweep. Constraint: Must preserve ongoing branch momentum while keeping each redesign slice reviewable Rejected: Implement the visual rewrite in one large styling pass | too hard to verify or bisect if regressions appear Confidence: high Scope-risk: narrow Reversibility: clean Directive: Follow the plan task order unless new evidence shows a dependency was misunderstood Tested: Manual plan review for task order, file scope, and verification coverage Not-tested: Actual implementation outcomes until execution --- .../2026-04-14-manus-workspace-redesign.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 docs/plans/2026-04-14-manus-workspace-redesign.md 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. From 4d8dfeefc3f8b1ad845f7bd51104a5be4d460693 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:20:10 +0800 Subject: [PATCH 04/13] Make the workspace feel calmer without muting terminal intent This first visual pass swaps the harsher black-box shell language for a graphite-blue system, softens pane chrome, and makes transcript context blocks feel intentional instead of error-like. It keeps the same workspace semantics while moving the product closer to the Manus-inspired restraint captured in the design spec. Constraint: Preserve current pane and terminal behavior while changing mostly tokens, copy, and CSS surfaces Rejected: Rebuild component structure around a brand-new layout shell | too much behavior risk for the first polish pass Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep future polish passes additive to this token system instead of reintroducing hard black borders or loud accent states Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Human visual QA in a real desktop window after this exact CSS pass --- src/app/styles.css | 449 +++++++++++------- src/domain/theme/presets.test.ts | 42 +- src/domain/theme/presets.ts | 162 +++---- .../components/DialogTranscript.test.tsx | 28 ++ .../terminal/components/DialogTranscript.tsx | 6 +- .../components/LiveCommandConsole.tsx | 2 +- 6 files changed, 410 insertions(+), 279 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index eea00a3..0c883a0 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -9,28 +9,39 @@ 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: #f3f5f8; + color: #16202b; + --app-background: #f3f5f8; + --surface: #fcfdff; + --surface-muted: #eef2f7; + --surface-elevated: #ffffff; + --text-primary: #16202b; + --text-muted: #67768a; + --text-subtle: #8c98a8; + --border: #c8d2df; + --border-muted: #dde5ef; + --overlay-backdrop: rgba(15, 23, 42, 0.08); + --history-command: #3159b8; + --history-accent: #6b77d4; + --history-string: #8b5a2b; + --history-path: #2d6a4f; + --history-url: #2d6f95; + --history-error: #b14b52; + --history-warning: #b2863a; + --history-success: #2d7c65; + --history-number: #3d6f9c; + --resize-handle-overlay: rgba(49, 89, 184, 0.08); + --resize-handle-line: #90a6d8; + --ghost-text: #9aa7b7; + --accent: #4f6fb8; + --accent-strong: #3159b8; + --accent-soft: rgba(79, 111, 184, 0.12); + --button-hover: #f3f6fb; + --panel-shadow: 0 18px 48px rgba(17, 24, 39, 0.08); + --radius-sm: 10px; + --radius-md: 14px; + --radius-lg: 18px; + --radius-xl: 22px; --ai-badge-text: #ffffff; } @@ -58,7 +69,6 @@ button, input, select { font: inherit; - border-radius: 0; } button { @@ -71,43 +81,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 +139,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 +169,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 +220,18 @@ 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); + gap: 18px; + padding: 18px; + border: 1px solid var(--border-muted); + border-radius: calc(var(--radius-xl) + 2px); + background: color-mix(in srgb, var(--surface-elevated) 98%, transparent); + box-shadow: var(--panel-shadow); transform: translateX(calc(100% + 16px)); transition: transform 140ms ease; z-index: 21; @@ -207,31 +245,33 @@ button { display: flex; align-items: flex-start; justify-content: space-between; - gap: 12px; + gap: 16px; } .settings-panel__header strong { display: block; - font-size: 14px; + font-size: 15px; } .settings-panel__summary { - margin: 6px 0 0; + margin: 8px 0 0; font-size: 12px; color: var(--text-muted); + line-height: 1.65; } .settings-panel__content { display: flex; flex-direction: column; - gap: 14px; + gap: 16px; overflow: auto; } .settings-section { - padding: 12px; - border: 1px solid var(--border); - background: var(--surface); + padding: 14px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--surface) 90%, var(--surface-muted)); } .settings-section__title { @@ -310,12 +350,20 @@ 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: var(--surface-elevated); color: var(--text-primary); } +.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 { display: flex; align-items: center; @@ -348,15 +396,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 +422,15 @@ button { flex: 1; min-width: 0; min-height: 0; + padding: 4px; + border-radius: calc(var(--radius-xl) + 2px); + background: + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 68%, 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 +438,26 @@ button { flex-direction: column; min-width: 0; min-height: 0; - border: 1px solid var(--border); - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--surface) 96%, var(--surface-muted)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32); overflow: hidden; container-type: inline-size; } .terminal-pane--active { - border-width: 2px; + border-color: color-mix(in srgb, var(--accent) 24%, var(--border)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.32), + 0 0 0 1px color-mix(in srgb, var(--accent-soft) 100%, transparent); } .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) 34%, var(--border)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--ai-theme-color) 16%, transparent), + 0 0 0 1px color-mix(in srgb, var(--ai-theme-color) 12%, transparent); } .terminal-pane--drag-source { @@ -428,17 +486,17 @@ 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: 44px; + padding: 10px 12px; + box-shadow: inset 0 -1px 0 var(--border-muted); + background: color-mix(in srgb, var(--surface-elevated) 94%, transparent); cursor: move; user-select: none; } .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) 28%, var(--border-muted)); + background: color-mix(in srgb, var(--ai-background-color) 78%, var(--surface-elevated)); } .terminal-pane__title { @@ -453,7 +511,7 @@ button { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - font-size: 12px; + font-size: 13px; font-weight: 500; } @@ -463,9 +521,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; } @@ -479,17 +538,18 @@ button { flex: 0 0 auto; white-space: nowrap; font-size: 11px; - color: var(--text-muted); - letter-spacing: 0.08em; + color: var(--text-subtle); + letter-spacing: 0.12em; } .terminal-pane--agent-workflow .terminal-pane__mode-indicator { - border: 1px solid var(--ai-theme-color); - background: var(--ai-theme-color); + border: 1px solid color-mix(in srgb, var(--ai-theme-color) 28%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--ai-theme-color) 18%, transparent); color: var(--ai-badge-text); - padding: 2px 8px; + padding: 4px 10px; font-weight: 600; - letter-spacing: 0.14em; + letter-spacing: 0.12em; } .pane-action-menu { @@ -515,15 +575,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 +596,7 @@ button { } .pane-action-menu__item:hover:not(:disabled) { - text-decoration: underline; + background: var(--button-hover); } .pane-action-menu__item:disabled { @@ -546,10 +608,11 @@ 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: 4px; + border: 1px solid var(--border-muted); + border-radius: 999px; + background: color-mix(in srgb, var(--surface-elevated) 94%, transparent); } .pane-header-actions__button { @@ -563,7 +626,8 @@ button { .pane-header-actions__button:hover:not(:disabled), .pane-action-menu__trigger:hover:not(:disabled) { - background: var(--surface-muted); + background: var(--button-hover); + border-radius: 999px; } .pane-header-actions__button:disabled, @@ -573,7 +637,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,7 +646,7 @@ button { flex: 1; min-width: 0; min-height: 0; - background: var(--surface); + background: transparent; } .terminal-pane--agent-workflow .terminal-pane__body, @@ -597,8 +661,8 @@ button { min-height: 0; width: 100%; height: 100%; - padding: 8px; - background: var(--surface); + padding: 12px; + background: transparent; } .terminal-pane__overlay { @@ -610,9 +674,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 +709,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 +725,9 @@ button { min-height: 0; flex-direction: column; justify-content: flex-end; - gap: 12px; + gap: 16px; overflow: auto; - padding: 12px; + padding: 18px 18px 14px; } .dialog-terminal__jump { @@ -677,8 +743,8 @@ button { pointer-events: auto; border-radius: 999px; 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 +759,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 18px 18px; + border-top: 1px solid var(--border-muted); + background: color-mix(in srgb, var(--surface-elevated) 78%, transparent); } .dialog-terminal__prompt { font-size: var(--dialog-terminal-font-size); - color: var(--text-muted); + color: var(--text-subtle); white-space: nowrap; } @@ -714,15 +780,16 @@ button { position: relative; display: grid; align-items: center; - border: 1px solid var(--border); - background: var(--surface); + border: 1px solid var(--border-muted); + border-radius: calc(var(--radius-lg) - 2px); + background: var(--surface-elevated); } .dialog-terminal__ghost, .dialog-terminal__input { grid-area: 1 / 1; width: 100%; - padding: 8px 10px; + padding: 11px 13px; font: inherit; } @@ -765,7 +832,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 +843,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 +859,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 +891,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 +899,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 +909,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 +942,22 @@ 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) 84%, var(--surface-muted)); + 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: 12px 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 +968,8 @@ button { .dialog-live-console__label { font-weight: 600; - letter-spacing: 0.04em; + letter-spacing: 0.08em; + text-transform: uppercase; } .dialog-live-console__command { @@ -912,8 +989,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 +1016,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 +1038,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 +1064,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 +1145,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 { @@ -1106,9 +1200,9 @@ button { .layout-tree__divider { position: relative; z-index: 2; - flex: 0 0 6px; - width: 6px; - height: 6px; + flex: 0 0 10px; + width: 10px; + height: 10px; border: 0; background: transparent; padding: 0; @@ -1117,12 +1211,12 @@ button { .layout-tree__divider--vertical { width: 100%; - height: 6px; + height: 10px; cursor: row-resize; } .layout-tree__divider--horizontal { - width: 6px; + width: 10px; height: auto; cursor: col-resize; } @@ -1170,7 +1264,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 +1278,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, diff --git a/src/domain/theme/presets.test.ts b/src/domain/theme/presets.test.ts index 2407314..cb8477c 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: "#f3f5f8", + surface: "#fcfdff", + textPrimary: "#16202b", + historyCommand: "#3159b8", }, terminal: { - background: "#ffffff", - foreground: "#000000", + background: "#f9fbff", + foreground: "#16202b", + blue: "#4f6fb8", }, }); }); - 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..998e15b 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: "#f3f5f8", + surface: "#fcfdff", + surfaceMuted: "#eef2f7", + textPrimary: "#16202b", + textMuted: "#67768a", + border: "#c8d2df", + borderMuted: "#dde5ef", + overlayBackdrop: "rgba(15, 23, 42, 0.08)", + historyCommand: "#3159b8", + historyAccent: "#6b77d4", + historyString: "#8b5a2b", + historyPath: "#2d6a4f", + historyUrl: "#2d6f95", + historyError: "#b14b52", + historyWarning: "#b2863a", + historySuccess: "#2d7c65", + historyNumber: "#3d6f9c", }, 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: "#f9fbff", + foreground: "#16202b", + cursor: "#3159b8", + black: "#1f2a36", + red: "#b14b52", + green: "#2d7c65", + yellow: "#a67f3e", + blue: "#4f6fb8", + magenta: "#6b77d4", + cyan: "#3b7c8f", + white: "#d7dee8", + brightBlack: "#5f6f84", + brightRed: "#cb6870", + brightGreen: "#3d9478", + brightYellow: "#c39a4d", + brightBlue: "#7394dd", + brightMagenta: "#8d97ea", + brightCyan: "#5297ab", + 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/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..6e6d1f0 100644 --- a/src/features/terminal/components/DialogTranscript.tsx +++ b/src/features/terminal/components/DialogTranscript.tsx @@ -17,7 +17,11 @@ export function DialogTranscript({ blocks, scrollRef, bottomRef, onScroll }: Dia {blocks.map((block, index) => (
{index > 0 ?
: null} - {block.kind === "command" ? :

session output

} + {block.kind === "command" ? ( + + ) : ( +

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}
From 968e67caed11cb5af1a4af0f9f0913fa598edfa2 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:21:14 +0800 Subject: [PATCH 05/13] Align workspace polish with existing layout geometry The first visual pass widened split dividers and rounded every pane edge, but those choices conflicted with the layout engine's fixed 6px divider math and left inner flush seams vulnerable. This follow-up keeps the calmer visual language while restoring geometry consistency and cleaning up shared-edge radii. Constraint: Must preserve the current layout engine's divider calculations without reopening geometry code Rejected: Change SPLIT_DIVIDER_SIZE_PX and all related layout assumptions to match the CSS experiment | unnecessary behavior risk for a polish-only fix Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep divider sizing and pane seam styling coordinated with layout geometry any time pane chrome changes Tested: npm test; npm run typecheck; npm run build Not-tested: Manual drag-resize feel in a real desktop window after this seam fix --- src/app/styles.css | 18 +++++++++++++----- .../terminal/components/DialogTranscript.tsx | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 0c883a0..ed50e1b 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -466,18 +466,26 @@ button { .terminal-pane--flush-top { border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; } .terminal-pane--flush-right { border-right-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } .terminal-pane--flush-bottom { border-bottom-width: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } .terminal-pane--flush-left { border-left-width: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } .terminal-pane__header { @@ -1200,9 +1208,9 @@ button { .layout-tree__divider { position: relative; z-index: 2; - flex: 0 0 10px; - width: 10px; - height: 10px; + flex: 0 0 6px; + width: 6px; + height: 6px; border: 0; background: transparent; padding: 0; @@ -1211,12 +1219,12 @@ button { .layout-tree__divider--vertical { width: 100%; - height: 10px; + height: 6px; cursor: row-resize; } .layout-tree__divider--horizontal { - width: 10px; + width: 6px; height: auto; cursor: col-resize; } diff --git a/src/features/terminal/components/DialogTranscript.tsx b/src/features/terminal/components/DialogTranscript.tsx index 6e6d1f0..725047e 100644 --- a/src/features/terminal/components/DialogTranscript.tsx +++ b/src/features/terminal/components/DialogTranscript.tsx @@ -20,6 +20,7 @@ export function DialogTranscript({ blocks, scrollRef, bottomRef, onScroll }: Dia {block.kind === "command" ? ( ) : ( + // Internally this remains a session-scoped transcript block; the UI copy is calmer.

shell output

)} From 7f90ba253f79cbc011a11d68b949b5c5a3f137c8 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:27:23 +0800 Subject: [PATCH 06/13] Warm the light theme and stop empty shells from cluttering every pane The first Manus-inspired pass still left two sharp edges: the light palette felt too cold-blue, and blank startup noise could still materialize as shell-output blocks across panes. This follow-up shifts light mode toward a bone-white workspace, softens divider polish, and suppresses whitespace-only startup transcript blocks so the interface feels more intentional. Constraint: Keep genuine non-command shell output visible while filtering only empty startup clutter Rejected: Hide all session-scoped shell output by default | would bury legitimate shell context and make debugging harder Confidence: high Scope-risk: narrow Reversibility: clean Directive: If shell-output presentation changes again, keep behavior tests around whitespace-only startup suppression Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual visual QA across multiple pane split topologies after the warmer light-theme pass --- src/app/styles.css | 77 +++++++++++++++++++----------- src/domain/terminal/dialog.test.ts | 6 +++ src/domain/terminal/dialog.ts | 4 ++ src/domain/theme/presets.test.ts | 14 +++--- src/domain/theme/presets.ts | 70 +++++++++++++-------------- 5 files changed, 100 insertions(+), 71 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index ed50e1b..4bd7677 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -9,34 +9,34 @@ monospace; line-height: 1.5; font-weight: 400; - background: #f3f5f8; - color: #16202b; - --app-background: #f3f5f8; - --surface: #fcfdff; - --surface-muted: #eef2f7; - --surface-elevated: #ffffff; - --text-primary: #16202b; - --text-muted: #67768a; - --text-subtle: #8c98a8; - --border: #c8d2df; - --border-muted: #dde5ef; - --overlay-backdrop: rgba(15, 23, 42, 0.08); - --history-command: #3159b8; - --history-accent: #6b77d4; - --history-string: #8b5a2b; - --history-path: #2d6a4f; - --history-url: #2d6f95; - --history-error: #b14b52; - --history-warning: #b2863a; - --history-success: #2d7c65; - --history-number: #3d6f9c; - --resize-handle-overlay: rgba(49, 89, 184, 0.08); - --resize-handle-line: #90a6d8; - --ghost-text: #9aa7b7; - --accent: #4f6fb8; - --accent-strong: #3159b8; - --accent-soft: rgba(79, 111, 184, 0.12); - --button-hover: #f3f6fb; + background: #f5f1e8; + color: #241f17; + --app-background: #f5f1e8; + --surface: #fdfaf4; + --surface-muted: #f3ede2; + --surface-elevated: #fffdf8; + --text-primary: #241f17; + --text-muted: #756b5f; + --text-subtle: #9a8f82; + --border: #d8cec1; + --border-muted: #e9e0d4; + --overlay-backdrop: rgba(36, 31, 23, 0.08); + --history-command: #5c7198; + --history-accent: #7c85b2; + --history-string: #90613a; + --history-path: #4d6f58; + --history-url: #55758e; + --history-error: #b36563; + --history-warning: #b38a50; + --history-success: #4f7d69; + --history-number: #637d9f; + --resize-handle-overlay: rgba(92, 113, 152, 0.08); + --resize-handle-line: #b4b0ca; + --ghost-text: #aaa194; + --accent: #6e83ac; + --accent-strong: #5c7198; + --accent-soft: rgba(110, 131, 172, 0.11); + --button-hover: #f8f3ea; --panel-shadow: 0 18px 48px rgba(17, 24, 39, 0.08); --radius-sm: 10px; --radius-md: 14px; @@ -425,7 +425,7 @@ button { padding: 4px; border-radius: calc(var(--radius-xl) + 2px); background: - linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 68%, transparent), transparent 34%), + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 74%, transparent), transparent 34%), var(--app-background); } @@ -1217,12 +1217,26 @@ button { touch-action: none; } +.layout-tree__divider::before { + content: ""; + position: absolute; + inset: 1px 2px; + border-radius: 999px; + background: color-mix(in srgb, var(--border-muted) 72%, transparent); + transition: background 120ms ease, opacity 120ms ease; + opacity: 0.9; +} + .layout-tree__divider--vertical { width: 100%; height: 6px; cursor: row-resize; } +.layout-tree__divider--vertical::before { + inset: 2px 1px; +} + .layout-tree__divider--horizontal { width: 6px; height: auto; @@ -1384,6 +1398,11 @@ button { opacity: 1; } +.layout-tree__divider:hover::before, +.layout-tree--resizing .layout-tree__divider::before { + background: color-mix(in srgb, var(--accent) 42%, var(--border-muted)); +} + .terminal-pane__resize-handle--left, .terminal-pane__resize-handle--right { cursor: col-resize; diff --git a/src/domain/terminal/dialog.test.ts b/src/domain/terminal/dialog.test.ts index e6fdd91..77d5aa8 100644 --- a/src/domain/terminal/dialog.test.ts +++ b/src/domain/terminal/dialog.test.ts @@ -384,6 +384,12 @@ describe("dialog terminal state", () => { ]); }); + 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("updates the cwd from shell prompt markers for the next command header", () => { const state = applyShellLifecycleEvent(createDialogState("/bin/bash", "/workspace"), { type: "prompt-state", diff --git a/src/domain/terminal/dialog.ts b/src/domain/terminal/dialog.ts index d73d41f..8e50c11 100644 --- a/src/domain/terminal/dialog.ts +++ b/src/domain/terminal/dialog.ts @@ -170,6 +170,10 @@ export function appendDialogOutput(state: DialogState, data: string): DialogStat }; } + if (data.trim().length === 0) { + return state; + } + const lastBlock = state.blocks[state.blocks.length - 1]; if (lastBlock?.kind === "session") { return { diff --git a/src/domain/theme/presets.test.ts b/src/domain/theme/presets.test.ts index cb8477c..6207b0d 100644 --- a/src/domain/theme/presets.test.ts +++ b/src/domain/theme/presets.test.ts @@ -8,15 +8,15 @@ describe("theme presets", () => { id: "light", colorScheme: "light", app: { - background: "#f3f5f8", - surface: "#fcfdff", - textPrimary: "#16202b", - historyCommand: "#3159b8", + background: "#f5f1e8", + surface: "#fdfaf4", + textPrimary: "#241f17", + historyCommand: "#5c7198", }, terminal: { - background: "#f9fbff", - foreground: "#16202b", - blue: "#4f6fb8", + background: "#fbf8f2", + foreground: "#241f17", + blue: "#6e83ac", }, }); }); diff --git a/src/domain/theme/presets.ts b/src/domain/theme/presets.ts index 998e15b..18bf6ee 100644 --- a/src/domain/theme/presets.ts +++ b/src/domain/theme/presets.ts @@ -62,43 +62,43 @@ const THEME_PRESETS: Record = { label: "Light", colorScheme: "light", app: { - background: "#f3f5f8", - surface: "#fcfdff", - surfaceMuted: "#eef2f7", - textPrimary: "#16202b", - textMuted: "#67768a", - border: "#c8d2df", - borderMuted: "#dde5ef", - overlayBackdrop: "rgba(15, 23, 42, 0.08)", - historyCommand: "#3159b8", - historyAccent: "#6b77d4", - historyString: "#8b5a2b", - historyPath: "#2d6a4f", - historyUrl: "#2d6f95", - historyError: "#b14b52", - historyWarning: "#b2863a", - historySuccess: "#2d7c65", - historyNumber: "#3d6f9c", + 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: "#f9fbff", - foreground: "#16202b", - cursor: "#3159b8", - black: "#1f2a36", - red: "#b14b52", - green: "#2d7c65", - yellow: "#a67f3e", - blue: "#4f6fb8", - magenta: "#6b77d4", - cyan: "#3b7c8f", - white: "#d7dee8", - brightBlack: "#5f6f84", - brightRed: "#cb6870", - brightGreen: "#3d9478", - brightYellow: "#c39a4d", - brightBlue: "#7394dd", - brightMagenta: "#8d97ea", - brightCyan: "#5297ab", + 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", }, }, From be9b9f3e48ccc204a7889eba16f6271dfcc29001 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:32:04 +0800 Subject: [PATCH 07/13] Open into a cleaner blank workspace with squarer edges Startup shell chrome still felt too present: panes could open with shell output labels and prompt residue, and the rounded geometry remained softer than desired. This pass suppresses all pre-first-command startup transcript blocks, warms light mode further toward bone white, and tightens the shared radius scale so the workspace opens quieter and more composed. Constraint: Preserve post-command session output behavior while keeping first-open panes visually blank Rejected: Special-case only '%' prompts in transcript rendering | too brittle compared with suppressing all pre-first-command startup output Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat the pre-first-command workspace as a blank canvas unless a future product decision explicitly reintroduces startup context Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual visual QA of nested split layouts after the tighter radius pass --- src/app/styles.css | 8 ++++---- src/domain/terminal/dialog.test.ts | 18 +++++++++++++++--- src/domain/terminal/dialog.ts | 4 ++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 4bd7677..5bf5935 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -38,10 +38,10 @@ --accent-soft: rgba(110, 131, 172, 0.11); --button-hover: #f8f3ea; --panel-shadow: 0 18px 48px rgba(17, 24, 39, 0.08); - --radius-sm: 10px; - --radius-md: 14px; - --radius-lg: 18px; - --radius-xl: 22px; + --radius-sm: 8px; + --radius-md: 11px; + --radius-lg: 14px; + --radius-xl: 17px; --ai-badge-text: #ffffff; } diff --git a/src/domain/terminal/dialog.test.ts b/src/domain/terminal/dialog.test.ts index 77d5aa8..2bcf96f 100644 --- a/src/domain/terminal/dialog.test.ts +++ b/src/domain/terminal/dialog.test.ts @@ -372,16 +372,28 @@ 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", () => { diff --git a/src/domain/terminal/dialog.ts b/src/domain/terminal/dialog.ts index 8e50c11..8132cb4 100644 --- a/src/domain/terminal/dialog.ts +++ b/src/domain/terminal/dialog.ts @@ -170,6 +170,10 @@ export function appendDialogOutput(state: DialogState, data: string): DialogStat }; } + if (state.composerHistory.length === 0) { + return state; + } + if (data.trim().length === 0) { return state; } From 59ef1f51495f8685bad37e4b3dbc4a8de5b08298 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:41:17 +0800 Subject: [PATCH 08/13] Restore rounded split seams so split panes feel deliberate The seam presentation only marked the trailing child edge as flush, which left the original pane with a square cut whenever a split was created. This update marks both sides of each shared seam, keeps a tighter seam radius instead of flattening it entirely, and adds a final light polish pass to the pane chrome and settings surfaces so the workspace reads calmer. Constraint: Keep the existing layout geometry and divider hit targets intact Rejected: Widen the divider gutter or rewrite LayoutTree structure | too much layout risk for a visual defect Confidence: high Scope-risk: narrow Reversibility: clean Directive: Shared seam styling depends on both adjacent panes receiving flush masks; do not revert to one-sided masks without rechecking split visuals Tested: npm test Tested: npm run typecheck Tested: npm run build Tested: cargo test --- src/app/styles.css | 208 +++++++++++------- .../terminal/lib/layout-presentation.test.ts | 29 ++- .../terminal/lib/layout-presentation.ts | 14 +- 3 files changed, 169 insertions(+), 82 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 5bf5935..17f078b 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -9,39 +9,40 @@ monospace; line-height: 1.5; font-weight: 400; - background: #f5f1e8; - color: #241f17; - --app-background: #f5f1e8; - --surface: #fdfaf4; - --surface-muted: #f3ede2; - --surface-elevated: #fffdf8; - --text-primary: #241f17; - --text-muted: #756b5f; - --text-subtle: #9a8f82; - --border: #d8cec1; - --border-muted: #e9e0d4; - --overlay-backdrop: rgba(36, 31, 23, 0.08); - --history-command: #5c7198; - --history-accent: #7c85b2; + 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: #55758e; + --history-url: #597388; --history-error: #b36563; --history-warning: #b38a50; --history-success: #4f7d69; - --history-number: #637d9f; - --resize-handle-overlay: rgba(92, 113, 152, 0.08); - --resize-handle-line: #b4b0ca; - --ghost-text: #aaa194; - --accent: #6e83ac; - --accent-strong: #5c7198; - --accent-soft: rgba(110, 131, 172, 0.11); - --button-hover: #f8f3ea; - --panel-shadow: 0 18px 48px rgba(17, 24, 39, 0.08); - --radius-sm: 8px; - --radius-md: 11px; - --radius-lg: 14px; - --radius-xl: 17px; + --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; } @@ -226,12 +227,15 @@ button { max-height: calc(100vh - 32px); display: flex; flex-direction: column; - gap: 18px; - padding: 18px; + gap: 16px; + padding: 16px; border: 1px solid var(--border-muted); - border-radius: calc(var(--radius-xl) + 2px); - background: color-mix(in srgb, var(--surface-elevated) 98%, transparent); + 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; @@ -246,36 +250,41 @@ button { align-items: flex-start; justify-content: space-between; 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: 15px; + letter-spacing: 0.01em; } .settings-panel__summary { - margin: 8px 0 0; + max-width: 32ch; + margin: 6px 0 0; font-size: 12px; - color: var(--text-muted); - line-height: 1.65; + color: color-mix(in srgb, var(--text-muted) 92%, var(--text-subtle)); + line-height: 1.6; } .settings-panel__content { display: flex; flex-direction: column; - gap: 16px; + gap: 14px; overflow: auto; } .settings-section { - padding: 14px; - border: 1px solid var(--border-muted); + 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) 90%, var(--surface-muted)); + 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 { @@ -284,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 { @@ -299,7 +309,7 @@ button { display: flex; flex-direction: column; gap: 6px; - margin-top: 10px; + margin-top: 11px; } .settings-shortcuts { @@ -353,8 +363,9 @@ button { border: 1px solid var(--border-muted); border-radius: var(--radius-md); padding: 10px 12px; - background: var(--surface-elevated); + 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, @@ -422,10 +433,10 @@ button { flex: 1; min-width: 0; min-height: 0; - padding: 4px; - border-radius: calc(var(--radius-xl) + 2px); + padding: 5px; + border-radius: calc(var(--radius-xl) + 1px); background: - linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 74%, transparent), transparent 34%), + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 72%, transparent), transparent 34%), var(--app-background); } @@ -446,6 +457,17 @@ button { 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) 94%, transparent); + z-index: 1; +} + .terminal-pane--active { border-color: color-mix(in srgb, var(--accent) 24%, var(--border)); box-shadow: @@ -466,26 +488,58 @@ button { .terminal-pane--flush-top { border-top-width: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; + border-top-left-radius: var(--radius-seam); + border-top-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-top::after { + top: 0; + left: 6px; + right: 6px; + height: 6px; + border-radius: 0 0 var(--radius-seam) var(--radius-seam); } .terminal-pane--flush-right { border-right-width: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border-top-right-radius: var(--radius-seam); + border-bottom-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-right::after { + top: 6px; + right: 0; + bottom: 6px; + width: 6px; + border-radius: var(--radius-seam) 0 0 var(--radius-seam); } .terminal-pane--flush-bottom { border-bottom-width: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + border-bottom-left-radius: var(--radius-seam); + border-bottom-right-radius: var(--radius-seam); +} + +.terminal-pane--flush-bottom::after { + right: 6px; + bottom: 0; + left: 6px; + height: 6px; + border-radius: var(--radius-seam) var(--radius-seam) 0 0; } .terminal-pane--flush-left { border-left-width: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-top-left-radius: var(--radius-seam); + border-bottom-left-radius: var(--radius-seam); +} + +.terminal-pane--flush-left::after { + top: 6px; + bottom: 6px; + left: 0; + width: 6px; + border-radius: 0 var(--radius-seam) var(--radius-seam) 0; } .terminal-pane__header { @@ -494,8 +548,8 @@ button { align-items: center; justify-content: space-between; gap: 12px; - min-height: 44px; - padding: 10px 12px; + min-height: 43px; + padding: 10px 11px; box-shadow: inset 0 -1px 0 var(--border-muted); background: color-mix(in srgb, var(--surface-elevated) 94%, transparent); cursor: move; @@ -616,26 +670,28 @@ button { display: inline-flex; flex: 0 0 auto; align-items: center; - gap: 4px; - padding: 4px; + gap: 3px; + padding: 3px; border: 1px solid var(--border-muted); - border-radius: 999px; - background: color-mix(in srgb, var(--surface-elevated) 94%, transparent); + border-radius: calc(var(--radius-lg) + 3px); + background: color-mix(in srgb, var(--surface-elevated) 95%, transparent); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.26); } .pane-header-actions__button { - min-width: 28px; - height: 24px; + min-width: 26px; + height: 26px; border: 0; background: transparent; - color: var(--text-primary); + color: var(--text-muted); padding: 0 6px; } .pane-header-actions__button:hover:not(:disabled), .pane-action-menu__trigger:hover:not(:disabled) { background: var(--button-hover); - border-radius: 999px; + border-radius: calc(var(--radius-md) + 3px); + color: var(--text-primary); } .pane-header-actions__button:disabled, @@ -733,9 +789,9 @@ button { min-height: 0; flex-direction: column; justify-content: flex-end; - gap: 16px; + gap: 18px; overflow: auto; - padding: 18px 18px 14px; + padding: 20px 20px 15px; } .dialog-terminal__jump { @@ -749,7 +805,7 @@ 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-elevated) 88%, transparent); box-shadow: var(--panel-shadow); @@ -768,9 +824,9 @@ button { grid-template-columns: auto 1fr; align-items: center; gap: 12px; - padding: 14px 18px 18px; + padding: 14px 20px 18px; border-top: 1px solid var(--border-muted); - background: color-mix(in srgb, var(--surface-elevated) 78%, transparent); + background: color-mix(in srgb, var(--surface-elevated) 80%, transparent); } .dialog-terminal__prompt { @@ -789,8 +845,9 @@ button { display: grid; align-items: center; border: 1px solid var(--border-muted); - border-radius: calc(var(--radius-lg) - 2px); - background: var(--surface-elevated); + 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, @@ -953,7 +1010,8 @@ button { margin: 0 18px 18px; border: 1px solid var(--border-muted); border-radius: var(--radius-xl); - background: color-mix(in srgb, var(--surface) 84%, var(--surface-muted)); + 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; } @@ -963,7 +1021,7 @@ button { align-items: center; gap: 12px; min-height: 42px; - padding: 12px 14px; + padding: 11px 14px; border-bottom: 1px solid var(--border-muted); background: color-mix(in srgb, var(--surface-elevated) 96%, transparent); font-size: 12px; @@ -976,7 +1034,7 @@ button { .dialog-live-console__label { font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.12em; text-transform: uppercase; } @@ -1192,6 +1250,7 @@ button { flex: 1; min-width: 0; min-height: 0; + isolation: isolate; } .layout-tree__leaf-resize-handles { @@ -1223,6 +1282,7 @@ button { inset: 1px 2px; border-radius: 999px; background: color-mix(in srgb, var(--border-muted) 72%, transparent); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface-elevated) 72%, transparent); transition: background 120ms ease, opacity 120ms ease; opacity: 0.9; } 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; +} From 75b70e78f9d90e3efcdae34e92df86fcd2bbce4a Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 22:44:28 +0800 Subject: [PATCH 09/13] Keep transcript output aligned with what the user actually ran Deferred live-console capture was replaying the echoed command line back into the finished command block, and ANSI-only prompt residue could still create an empty shell-output block after completion. This made simple commands like cd feel wrong even though the shell session itself was healthy. The fix normalizes deferred command output at command-end, drops a leading echoed command line when it matches the submitted command, trims a stray bare prompt symbol, and ignores visually blank post-command output after stripping ANSI escape sequences. Constraint: Preserve legitimate non-command session output after a user has run commands Rejected: Hide all session-scoped output blocks | would discard real shell context that still matters Confidence: medium Scope-risk: narrow Reversibility: clean Directive: Keep transcript cleanup tied to shell echo/prompt artifacts only; do not broaden the heuristics without a concrete reproduction Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual GUI confirmation on the user's exact local shell prompt theme --- src/domain/terminal/dialog.test.ts | 35 ++++++++++++++++++ src/domain/terminal/dialog.ts | 59 +++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/domain/terminal/dialog.test.ts b/src/domain/terminal/dialog.test.ts index 2bcf96f..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"), @@ -402,6 +420,23 @@ describe("dialog terminal state", () => { 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", () => { const state = applyShellLifecycleEvent(createDialogState("/bin/bash", "/workspace"), { type: "prompt-state", diff --git a/src/domain/terminal/dialog.ts b/src/domain/terminal/dialog.ts index 8132cb4..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); @@ -174,7 +175,7 @@ export function appendDialogOutput(state: DialogState, data: string): DialogStat return state; } - if (data.trim().length === 0) { + if (isVisuallyEmptyOutput(data)) { return state; } @@ -252,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, @@ -270,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()); } From 30ebc054ec71c06eb525dff5dbedfb06cac45f3c Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 23:08:01 +0800 Subject: [PATCH 10/13] Make pane chrome feel closer to GitHub without thickening the split seam The previous pane styling was still carrying too much of the softer workspace language. After visual review, the pane shell now leans into GitHub-like card chrome: paler utility headers, tighter title weight, quieter action buttons, and cleaner white pane bodies. The split seam keeps the same resize hit area but reduces the visible separator to a very thin line, while the seam masks stay rounded and restrained so split panes do not collapse back into harsh square cuts. Constraint: Keep resize affordances usable while making the seam itself visually minimal Rejected: Turn the seam into a thicker pill divider | looked decorative and too heavy for the requested GitHub direction Confidence: medium Scope-risk: narrow Reversibility: clean Directive: If pane chrome is refined again, keep the seam line optically thin and avoid reintroducing wrapper chips around header actions Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual pixel-level comparison against the user's local split layout on every theme --- .../2026-04-14-praw-manus-workspace-design.md | 7 + src/app/styles.css | 122 ++++++++++-------- 2 files changed, 72 insertions(+), 57 deletions(-) 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 index 87bbef8..719d870 100644 --- a/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md +++ b/docs/superpowers/specs/2026-04-14-praw-manus-workspace-design.md @@ -158,6 +158,13 @@ Panes remain the product’s core structural unit, but their visual treatment ch 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. diff --git a/src/app/styles.css b/src/app/styles.css index 17f078b..1b54a79 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -433,10 +433,10 @@ button { flex: 1; min-width: 0; min-height: 0; - padding: 5px; + padding: 6px; border-radius: calc(var(--radius-xl) + 1px); background: - linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 72%, transparent), transparent 34%), + linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 70%, transparent), transparent 34%), var(--app-background); } @@ -449,10 +449,10 @@ button { flex-direction: column; min-width: 0; min-height: 0; - border: 1px solid var(--border-muted); + border: 1px solid color-mix(in srgb, var(--border) 84%, #d0d7de); border-radius: var(--radius-xl); - background: color-mix(in srgb, var(--surface) 96%, var(--surface-muted)); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32); + 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; } @@ -464,21 +464,19 @@ button { content: ""; position: absolute; pointer-events: none; - background: color-mix(in srgb, var(--app-background) 94%, transparent); + background: color-mix(in srgb, var(--app-background) 97%, transparent); z-index: 1; } .terminal-pane--active { - border-color: color-mix(in srgb, var(--accent) 24%, var(--border)); + border-color: color-mix(in srgb, var(--accent) 24%, #0969da); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.32), - 0 0 0 1px color-mix(in srgb, var(--accent-soft) 100%, transparent); + 0 0 0 1px color-mix(in srgb, var(--accent-soft) 86%, transparent); } .terminal-pane--agent-workflow { border-color: color-mix(in srgb, var(--ai-theme-color) 34%, var(--border)); box-shadow: - inset 0 1px 0 color-mix(in srgb, var(--ai-theme-color) 16%, transparent), 0 0 0 1px color-mix(in srgb, var(--ai-theme-color) 12%, transparent); } @@ -494,9 +492,9 @@ button { .terminal-pane--flush-top::after { top: 0; - left: 6px; - right: 6px; - height: 6px; + left: 5px; + right: 5px; + height: 4px; border-radius: 0 0 var(--radius-seam) var(--radius-seam); } @@ -507,10 +505,10 @@ button { } .terminal-pane--flush-right::after { - top: 6px; + top: 5px; right: 0; - bottom: 6px; - width: 6px; + bottom: 5px; + width: 4px; border-radius: var(--radius-seam) 0 0 var(--radius-seam); } @@ -521,10 +519,10 @@ button { } .terminal-pane--flush-bottom::after { - right: 6px; + right: 5px; bottom: 0; - left: 6px; - height: 6px; + left: 5px; + height: 4px; border-radius: var(--radius-seam) var(--radius-seam) 0 0; } @@ -535,10 +533,10 @@ button { } .terminal-pane--flush-left::after { - top: 6px; - bottom: 6px; + top: 5px; + bottom: 5px; left: 0; - width: 6px; + width: 4px; border-radius: 0 var(--radius-seam) var(--radius-seam) 0; } @@ -549,16 +547,16 @@ button { justify-content: space-between; gap: 12px; min-height: 43px; - padding: 10px 11px; - box-shadow: inset 0 -1px 0 var(--border-muted); - background: color-mix(in srgb, var(--surface-elevated) 94%, transparent); + 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--agent-workflow .terminal-pane__header { box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 28%, var(--border-muted)); - background: color-mix(in srgb, var(--ai-background-color) 78%, var(--surface-elevated)); + background: color-mix(in srgb, var(--ai-background-color) 52%, #f6f8fa); } .terminal-pane__title { @@ -573,8 +571,8 @@ button { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - font-size: 13px; - font-weight: 500; + font-size: 12.5px; + font-weight: 600; } .terminal-pane__title--editing { @@ -599,19 +597,19 @@ button { margin-left: auto; flex: 0 0 auto; white-space: nowrap; - font-size: 11px; + font-size: 10px; color: var(--text-subtle); - letter-spacing: 0.12em; + letter-spacing: 0.1em; } .terminal-pane--agent-workflow .terminal-pane__mode-indicator { border: 1px solid color-mix(in srgb, var(--ai-theme-color) 28%, transparent); - border-radius: 999px; - background: color-mix(in srgb, var(--ai-theme-color) 18%, transparent); + border-radius: calc(var(--radius-md) + 2px); + background: color-mix(in srgb, var(--ai-theme-color) 14%, transparent); color: var(--ai-badge-text); - padding: 4px 10px; + padding: 3px 8px; font-weight: 600; - letter-spacing: 0.12em; + letter-spacing: 0.1em; } .pane-action-menu { @@ -621,11 +619,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; } @@ -670,27 +669,28 @@ button { display: inline-flex; flex: 0 0 auto; align-items: center; - gap: 3px; - padding: 3px; - border: 1px solid var(--border-muted); - border-radius: calc(var(--radius-lg) + 3px); - background: color-mix(in srgb, var(--surface-elevated) 95%, transparent); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.26); + gap: 4px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } .pane-header-actions__button { - min-width: 26px; - height: 26px; - border: 0; + min-width: 24px; + height: 24px; + border: 1px solid transparent; + border-radius: var(--radius-sm); background: transparent; color: var(--text-muted); - padding: 0 6px; + padding: 0 5px; } .pane-header-actions__button:hover:not(:disabled), .pane-action-menu__trigger:hover:not(:disabled) { - background: var(--button-hover); - border-radius: calc(var(--radius-md) + 3px); + border-color: color-mix(in srgb, var(--border) 84%, #d0d7de); + background: color-mix(in srgb, var(--surface-elevated) 96%, #ffffff); color: var(--text-primary); } @@ -710,7 +710,7 @@ button { flex: 1; min-width: 0; min-height: 0; - background: transparent; + background: color-mix(in srgb, var(--surface-elevated) 99%, #ffffff); } .terminal-pane--agent-workflow .terminal-pane__body, @@ -1266,7 +1266,7 @@ button { .layout-tree__divider { position: relative; - z-index: 2; + z-index: 3; flex: 0 0 6px; width: 6px; height: 6px; @@ -1279,12 +1279,14 @@ button { .layout-tree__divider::before { content: ""; position: absolute; - inset: 1px 2px; + top: 0; + bottom: 0; + left: calc(50% - 0.5px); + width: 1px; border-radius: 999px; - background: color-mix(in srgb, var(--border-muted) 72%, transparent); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface-elevated) 72%, transparent); + background: color-mix(in srgb, var(--border) 76%, #d0d7de); transition: background 120ms ease, opacity 120ms ease; - opacity: 0.9; + opacity: 0.78; } .layout-tree__divider--vertical { @@ -1294,7 +1296,12 @@ button { } .layout-tree__divider--vertical::before { - inset: 2px 1px; + top: calc(50% - 0.5px); + right: 0; + bottom: auto; + left: 0; + width: auto; + height: 1px; } .layout-tree__divider--horizontal { @@ -1460,7 +1467,8 @@ button { .layout-tree__divider:hover::before, .layout-tree--resizing .layout-tree__divider::before { - background: color-mix(in srgb, var(--accent) 42%, var(--border-muted)); + background: color-mix(in srgb, var(--accent) 38%, var(--border)); + opacity: 1; } .terminal-pane__resize-handle--left, From 5421e038742cdbd4f04e6b671d0e8e5ba611727f Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 23:14:37 +0800 Subject: [PATCH 11/13] Keep pane splits quiet and make AI mode a lighter state The seam still reacted too loudly on hover because both the divider and edge-resize handles were drawing visual affordances. That made the split feel thicker than intended even after the GitHub-style pane cleanup. This change removes the visual hover/active overlays from seam-adjacent resize affordances and leaves only the cursor feedback. It also reworks AI workflow panes so they stay within the same GitHub-like card system: normal pane body, gentler header tint, and a compact AI state marker instead of a special badge block. Constraint: Preserve resize usability without adding visual noise at the seam Rejected: Keep a highlighted seam only while dragging | still read as unnecessary decoration next to the requested ultra-thin divider Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat AI mode as a pane state, not a separate visual surface family, unless product semantics change materially Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual visual review on the user's exact display scaling and theme mix --- src/app/styles.css | 86 ++++++------------- .../terminal/components/TerminalPane.tsx | 2 +- 2 files changed, 29 insertions(+), 59 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 1b54a79..7785d47 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -475,9 +475,8 @@ button { } .terminal-pane--agent-workflow { - border-color: color-mix(in srgb, var(--ai-theme-color) 34%, var(--border)); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--ai-theme-color) 12%, transparent); + 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 { @@ -555,8 +554,13 @@ button { } .terminal-pane--agent-workflow .terminal-pane__header { - box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 28%, var(--border-muted)); - background: color-mix(in srgb, var(--ai-background-color) 52%, #f6f8fa); + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 18%, var(--border)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--ai-theme-color) 6%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 96%, #ffffff) + ); } .terminal-pane__title { @@ -595,6 +599,9 @@ 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: 10px; @@ -602,14 +609,23 @@ button { 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 color-mix(in srgb, var(--ai-theme-color) 28%, transparent); - border-radius: calc(var(--radius-md) + 2px); - background: color-mix(in srgb, var(--ai-theme-color) 14%, transparent); - color: var(--ai-badge-text); - padding: 3px 8px; + border: 0; + border-radius: 0; + background: transparent; + color: color-mix(in srgb, var(--ai-theme-color) 56%, var(--text-muted)); + padding: 0; font-weight: 600; - letter-spacing: 0.1em; + letter-spacing: 0.12em; } .pane-action-menu { @@ -716,7 +732,7 @@ button { .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 { @@ -1441,58 +1457,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; -} - -.layout-tree__divider:hover::before, -.layout-tree--resizing .layout-tree__divider::before { - background: color-mix(in srgb, var(--accent) 38%, var(--border)); - 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/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} From e4f42caa12a1cea815be7252792a151f5b062539 Mon Sep 17 00:00:00 2001 From: Rayn Date: Tue, 14 Apr 2026 23:16:16 +0800 Subject: [PATCH 12/13] Move pane focus emphasis into the header instead of the border The user preferred a GitHub-like focus treatment where the pane stays structurally quiet and the header carries the focus signal. The previous version still left a pane-wide accent ring that read too much like a code editor selection state. This change removes the active-pane outer accent treatment and shifts focus emphasis into the header background and divider line. AI workflow panes follow the same rule with their own gentler tint, so focus stays consistent without turning AI panes into separate chrome. Constraint: Preserve a clearly readable active pane state without reverting to heavy pane-wide outlines Rejected: Keep both header emphasis and outer accent ring | visually redundant and louder than requested Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep future focus tweaks localized to header rhythm first; only reintroduce pane-wide emphasis with a fresh visual need Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual review against every pane density/state combination in the running app --- src/app/styles.css | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 7785d47..18cee30 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -469,9 +469,8 @@ button { } .terminal-pane--active { - border-color: color-mix(in srgb, var(--accent) 24%, #0969da); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--accent-soft) 86%, transparent); + 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 { @@ -553,6 +552,16 @@ button { 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 -1px 0 color-mix(in srgb, var(--ai-theme-color) 18%, var(--border)); background: @@ -563,6 +572,16 @@ button { ); } +.terminal-pane--agent-workflow.terminal-pane--active .terminal-pane__header { + box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 28%, var(--border)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--ai-theme-color) 9%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 96%, #ffffff) + ); +} + .terminal-pane__title { display: flex; flex: 1 1 auto; From 6ecee992c8790e5612121c4fd3004ee616e863da Mon Sep 17 00:00:00 2001 From: Rayn Date: Wed, 15 Apr 2026 08:40:59 +0800 Subject: [PATCH 13/13] Soften AI pane chrome into a quieter lavender default The previous AI pane highlight was still leaning too blue and a little too present in the header. The updated treatment follows the approved Whisper Lavender direction: lighter violet tint, gentler active emphasis, and a calmer AI label tone. To make the visual change actually reach existing local setups, the old built-in blue AI defaults are migrated to the new lavender defaults during config normalization, while still preserving user-chosen custom colors. Constraint: Preserve AI color configurability while ensuring legacy default blue installations adopt the new visual direction Rejected: Hardcode a fixed purple only in CSS | would leave existing saved default-blue configs unchanged and create a mismatch with settings Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep AI pane distinction in the header layer; do not spread lavender tint into the full pane body unless product semantics change Tested: npm test; npm run typecheck; npm run build; cargo test Not-tested: Manual review on a machine with a deliberately custom non-default AI theme color --- src/app/styles.css | 14 ++++++------ src/domain/config/model.test.ts | 15 +++++++++++++ src/domain/config/model.ts | 22 +++++++++++++++---- .../config/state/app-config-store.test.ts | 2 +- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/app/styles.css b/src/app/styles.css index 18cee30..d7b62f4 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -563,22 +563,22 @@ button { } .terminal-pane--agent-workflow .terminal-pane__header { - box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ai-theme-color) 18%, var(--border)); + 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) 6%, #f6f8fa), - color-mix(in srgb, var(--surface-elevated) 96%, #ffffff) + 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) 28%, var(--border)); + 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) 9%, #f6f8fa), - color-mix(in srgb, var(--surface-elevated) 96%, #ffffff) + color-mix(in srgb, var(--ai-theme-color) 7%, #f6f8fa), + color-mix(in srgb, var(--surface-elevated) 97%, #ffffff) ); } @@ -641,7 +641,7 @@ button { border: 0; border-radius: 0; background: transparent; - color: color-mix(in srgb, var(--ai-theme-color) 56%, var(--text-muted)); + color: color-mix(in srgb, var(--ai-theme-color) 42%, var(--text-muted)); padding: 0; font-weight: 600; letter-spacing: 0.12em; 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/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); });