From 62374ee898386e26f5af8a8e7c63ed3fba074556 Mon Sep 17 00:00:00 2001 From: cyw <86410452+oso95@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:22:46 -0500 Subject: [PATCH 1/5] fix: fill terminal canvas background to prevent display cut-off after resize Empty terminal rows use the default background color and produce no bg_rects_hsla entries, so they showed the panel_bg from the outer div rather than the terminal background color. This was visible as a visual cut-off when dragging a session from a smaller pane into a larger one. Fix by painting the full canvas bounds with terminal_bg at the start of the paint closure, before any cell-specific backgrounds. Non-default background cells still paint their own color on top, as before. --- crates/codirigent-ui/src/workspace/terminal_render.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/codirigent-ui/src/workspace/terminal_render.rs b/crates/codirigent-ui/src/workspace/terminal_render.rs index 2870aa7..0d1e427 100644 --- a/crates/codirigent-ui/src/workspace/terminal_render.rs +++ b/crates/codirigent-ui/src/workspace/terminal_render.rs @@ -201,6 +201,11 @@ impl WorkspaceView { } } + // 0. Fill canvas with terminal background so empty rows (default-color cells + // that produce no bg_rects_hsla entries) don't show panel_bg below them. + // This fixes the visual "cut-off" after dragging to a larger pane. + window.paint_quad(gpui::fill(bounds, terminal_bg)); + // 1. Paint background rectangles for row in &cached_rows { for (rect_row, start_col, end_col, bg_color) in row.bg_rects_hsla.iter() { From d1801341f508c6194a0bb3c781debbc59ee33e63 Mon Sep 17 00:00:00 2001 From: cyw <86410452+oso95@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:58:49 -0500 Subject: [PATCH 2/5] fix: force full row rebuild when scrolled back to prevent display cycling When new output arrives while the viewport is scrolled into scrollback, alacritty's partial damage tracking only reports rows changed in the active area. But new output can push lines into scrollback, shifting all content visible in the scrolled viewport. Using partial damage leaves most rows stale, causing a cycling/repeating display artifact. Skip the partial-damage optimization when display_offset > 0 so all viewport rows are rebuilt from the current grid state. --- crates/codirigent-ui/src/terminal_runtime.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/codirigent-ui/src/terminal_runtime.rs b/crates/codirigent-ui/src/terminal_runtime.rs index 6e349c8..01090bc 100644 --- a/crates/codirigent-ui/src/terminal_runtime.rs +++ b/crates/codirigent-ui/src/terminal_runtime.rs @@ -199,10 +199,12 @@ impl TerminalRuntime { fn snapshot_from_damage(&mut self) -> TerminalRenderSnapshot { let rows = self.terminal.rows() as usize; let cols = self.terminal.cols() as usize; - let damage = if self - .cached_rows - .as_ref() - .is_some_and(|cached_rows| cached_rows.len() == rows) + let scrolled_back = self.terminal.term().grid().display_offset() > 0; + let damage = if !scrolled_back + && self + .cached_rows + .as_ref() + .is_some_and(|cached_rows| cached_rows.len() == rows) { let term = self.terminal.term_mut(); let damage = match term.damage() { From c9cfdc5dc278e8234c018d83c06242daceb45950 Mon Sep 17 00:00:00 2001 From: cyw <86410452+oso95@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:08:35 -0500 Subject: [PATCH 3/5] fix: sort hook signals by timestamp to prevent stale status overwrites When multiple signal files target the same session (e.g., parent + subagent), non-deterministic read_dir order could let a stale "idle" from the parent overwrite a newer "working" from the subagent, keeping the status stuck on idle. --- .../src/workspace/impl_output_polling/hook_signals.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/codirigent-ui/src/workspace/impl_output_polling/hook_signals.rs b/crates/codirigent-ui/src/workspace/impl_output_polling/hook_signals.rs index 1c26ec0..5cf336b 100644 --- a/crates/codirigent-ui/src/workspace/impl_output_polling/hook_signals.rs +++ b/crates/codirigent-ui/src/workspace/impl_output_polling/hook_signals.rs @@ -366,6 +366,14 @@ impl WorkspaceView { let _ = this.update(cx, |this, cx| { this.polling.hook_signal_check_in_flight = false; + // Sort by timestamp so that the newest signal for each + // session is processed last and wins the cached status. + // Without this, non-deterministic read_dir order can let + // a stale "idle" signal overwrite a newer "working" signal + // when multiple signal files target the same session + // (e.g., parent + subagent Claude Code sessions). + let mut updates = updates; + updates.sort_by_key(|u| u.ts); for update in updates { this.apply_hook_signal_update(update, cx); } From 8d746d3e8893f86609b62d21d8100b3546a0cf89 Mon Sep 17 00:00:00 2001 From: cyw <86410452+oso95@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:03:48 -0500 Subject: [PATCH 4/5] fix: stop overwriting session UUID after PTY spawn on restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On app restart, finalize_restored_session_bootstrap overwrote the workspace session's session_uuid with the old UUID from state.json. But the PTY was already spawned with the new UUID from Session::new() as the CODIRIGENT_SESSION_UUID env var. This mismatch caused hook signal routing to fail silently — signals carried the PTY's UUID but the workspace had the old one — keeping status stuck on Idle. --- .../src/workspace/impl_session_lifecycle.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs b/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs index 83c177f..7c0039b 100644 --- a/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs +++ b/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs @@ -1443,19 +1443,19 @@ impl WorkspaceView { let codex_started_at = plan.codex_started_at; if let Ok(manager) = self.session_manager.lock() { manager.with_session_state_mut(bootstrapped.session_id, |state| { - // session_uuid is set only inside this restore_cli-gated block; - // the local session struct receives the same guard at the assignment below. - state.session.session_uuid = plan.session_uuid.clone(); state.session.codex_execution_mode = codex_execution_mode; state.session.codex_started_at = codex_started_at; }); } } + // NOTE: Do NOT overwrite session.session_uuid with plan.session_uuid here. + // The PTY was already spawned with the UUID from Session::new() as the + // CODIRIGENT_SESSION_UUID env var. Overwriting with the saved UUID from a + // previous app instance creates a mismatch: hook signals carry the PTY's + // UUID but the workspace session has the old one, causing routing to fail + // and status to stay stuck on Idle. let mut session = bootstrapped.session; - if restore_cli { - session.session_uuid = plan.session_uuid.clone(); - } session.shell = bootstrapped.request.requested_shell.clone(); session.group = plan.group.clone(); session.color = plan.color.clone(); From 80a43329f69c495662b3d409ff00a35f34d79cd8 Mon Sep 17 00:00:00 2001 From: cyw <86410452+oso95@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:48:55 -0500 Subject: [PATCH 5/5] chore: add Windows MSI signing workflow and reorganize docs - Add sign-and-upload.ps1 script for local MSI signing with Certum cert - Add windows-release-signing.md documenting the full signing workflow - Exclude MSI from CI checksums (replaced by signed version post-build) - Reorganize docs into features/, release/, workflows/ subdirectories - Remove obsolete plan files and gitignore specs/ --- .github/workflows/release.yml | 3 +- .gitignore | 4 + docs/{ => features}/hook-and-status-system.md | 0 docs/{ => features}/session-resume.md | 0 docs/ghostty-theme-registry-plan.md | 546 ------------------ docs/{ => release}/local-dmg-build.md | 0 docs/release/windows-release-signing.md | 189 ++++++ ...-03-18-terminal-scrollbar-search-design.md | 188 ------ .../task-verification-workflow.md | 0 scripts/sign-and-upload.ps1 | 77 +++ 10 files changed, 272 insertions(+), 735 deletions(-) rename docs/{ => features}/hook-and-status-system.md (100%) rename docs/{ => features}/session-resume.md (100%) delete mode 100644 docs/ghostty-theme-registry-plan.md rename docs/{ => release}/local-dmg-build.md (100%) create mode 100644 docs/release/windows-release-signing.md delete mode 100644 docs/specs/2026-03-18-terminal-scrollbar-search-design.md rename docs/{ => workflows}/task-verification-workflow.md (100%) create mode 100644 scripts/sign-and-upload.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb8b435..536da9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -267,7 +267,8 @@ jobs: - name: Generate checksums run: | cd artifacts - sha256sum * > checksums-sha256.txt + # Exclude MSI — it will be replaced by a locally signed version + sha256sum $(ls | grep -v '\.msi$') > checksums-sha256.txt cat checksums-sha256.txt - name: Create GitHub Release diff --git a/.gitignore b/.gitignore index 4baa4c1..9552362 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,13 @@ # ----------------------------------------------------------------------------- +# Claude Code project instructions (local only) +CLAUDE.md + # Development plans plans/ docs/superpowers/ +docs/specs/ # Superpowers skill state .superpowers/ diff --git a/docs/hook-and-status-system.md b/docs/features/hook-and-status-system.md similarity index 100% rename from docs/hook-and-status-system.md rename to docs/features/hook-and-status-system.md diff --git a/docs/session-resume.md b/docs/features/session-resume.md similarity index 100% rename from docs/session-resume.md rename to docs/features/session-resume.md diff --git a/docs/ghostty-theme-registry-plan.md b/docs/ghostty-theme-registry-plan.md deleted file mode 100644 index ab40bc2..0000000 --- a/docs/ghostty-theme-registry-plan.md +++ /dev/null @@ -1,546 +0,0 @@ -# Ghostty-Style Theme Registry Plan - -Implementation plan for expanding Codirigent's theme system from a built-in -`dark/light` toggle into a registry-backed theme model with custom theme files, -runtime theme IDs, and terminal palette behavior that can scale toward a -Ghostty-style theme experience. - -This document is intentionally written before code changes. It is the working -plan for the branch `feat/ghostty-theme-registry`. - ---- - -## Execution Summary - -This branch is now complete. The implementation landed as the planned task -series with small reviewable commits: - -1. `ced6530` `Add theme registry conversion backbone` -2. `d2c0be0` `Apply saved theme IDs during settings load` -3. `03d208f` `Make settings theme picker registry-driven` -4. `f632676` `Load custom themes during settings startup` -5. `4df2b36` `Apply selection text color in terminal rendering` -6. `eb020b1` `Update lockfile for theme rendering dependency` - -Final outcome: - -- theme selection is registry-based instead of hardcoded `dark/light` -- `appearance.theme` now round-trips as a durable theme ID -- startup/settings load resolves and applies saved theme IDs through one path -- custom themes load from the user config `themes/` directory off the UI thread -- terminal fg/bg/cursor/selection/ANSI palette are all on the active theme path -- terminal selection foreground is now actually rendered, not just stored in the schema - ---- - -## Purpose - -Codirigent already has the terminal rendering primitives needed for richer -themes, but the application still behaves like a two-theme product: - -- the runtime UI uses `CodirigentTheme` -- the settings UI only exposes `dark` and `light` -- the saved setting is treated like a boolean mode rather than a durable - registry theme ID -- custom theme loading infrastructure exists separately but is not wired into - app startup or live theme application - -The goal of this task series is to make themes a first-class product feature -instead of a hardcoded toggle. - ---- - -## Problem Statement - -The current implementation has four structural gaps: - -1. **Two parallel theme models** - - `crates/codirigent-ui/src/theme.rs` defines the runtime theme actually used - by UI and terminal rendering. - - `crates/codirigent-ui/src/theme_config.rs` and - `crates/codirigent-ui/src/theme_manager.rs` define a serializable theme - model and registry, but they are not the active runtime path. - -2. **Theme selection is hardcoded** - - Settings only present `dark` and `light`. - - Theme switching constructs `CodirigentTheme::dark()` or - `CodirigentTheme::light()` directly. - -3. **Saved theme identity is not durable** - - `appearance.theme` is stored as a `String`, but the settings page rebuild - currently infers the value from current background lightness instead of - preserving the active theme ID. - -4. **Load/apply path is incomplete** - - User settings are loaded and cached, but the selected theme is not treated - as a registry-resolved startup input. - ---- - -## Current Architecture Inventory - -### Runtime Theme Path - -- `crates/codirigent-ui/src/theme.rs` - - owns `CodirigentTheme` - - contains UI colors, terminal colors, typography, spacing - - contains ANSI 16-color palette and 256-color indexed conversion - -- `crates/codirigent-ui/src/terminal_colors.rs` - - maps terminal named/indexed/spec colors into runtime theme colors - -- `crates/codirigent-ui/src/terminal_view.rs` - - caches terminal bg/fg from `CodirigentTheme` - - updates terminal runtime when theme changes - -- `crates/codirigent-ui/src/workspace/core.rs` - - stores the active `CodirigentTheme` - -### Settings and Persistence Path - -- `crates/codirigent-core/src/config.rs` - - `AppearanceSettings.theme: String` - - `TerminalSettings` stores font/cursor/line-height preferences - -- `crates/codirigent-ui/src/workspace/settings_panels.rs` - - theme dropdown is currently `["dark", "light"]` - - directly constructs built-in runtime themes - -- `crates/codirigent-ui/src/workspace/impl_settings.rs` - - settings page rebuild overwrites `appearance.theme` based on background - lightness - - settings load path updates cached settings but does not appear to resolve - and apply an arbitrary theme ID through a registry - -### Unused or Underused Theme Registry Path - -- `crates/codirigent-ui/src/theme_config.rs` - - serializable `Theme` - - `ThemeColors`, `TerminalColors`, typography, spacing - -- `crates/codirigent-ui/src/theme_manager.rs` - - registry for built-in and JSON-loaded themes - - theme loading from a directory or file - - active theme switching by ID - ---- - -## Target End State - -After this work series: - -- Codirigent loads a theme registry on startup. -- The active theme is identified by a durable theme ID. -- `appearance.theme` means "selected theme ID", not "dark mode boolean". -- Settings list all available themes, not just `dark/light`. -- Built-in themes and custom JSON themes use the same application path. -- Runtime theme application updates both UI and terminal state consistently. -- The terminal palette model is structured so it can grow toward a - Ghostty-style theme schema without another large refactor. - ---- - -## Non-Goals For The First Pass - -The first implementation pass should not try to do all theme features at once. -These are explicitly out of scope unless they fall out naturally: - -- importing Ghostty theme files verbatim with full syntax compatibility -- automatic OS appearance switching -- a theme editor UI -- remote theme downloads -- dynamic generation of 256-color cube replacements on the first pass - -The first pass is about establishing the correct architecture and durable -runtime behavior. - ---- - -## Design Principles - -1. **One runtime source of truth** - - The app should resolve every selected theme into one runtime - `CodirigentTheme`. - -2. **Theme ID is stable** - - Any active theme must have a durable identifier that round-trips through - settings and app restart. - -3. **Custom themes should not be a side path** - - Built-in and file-loaded themes should use the same selection and apply - flow. - -4. **Terminal fidelity matters** - - ANSI palette, foreground/background, cursor, and selection colors must all - switch with the active theme. - -5. **Incremental delivery** - - The work should land as small, reviewable tasks following - `docs/task-verification-workflow.md`. - ---- - -## Implementation Constraints - -These constraints apply to every task in this branch: - -1. **Do not load theme files on the UI thread** - - theme discovery, directory scans, and file reads must happen on a - background executor - - the UI thread may receive resolved theme data and apply it, but must not - block on filesystem traversal or JSON file IO - -2. **Keep files at manageable length** - - do not keep expanding already-large files with unrelated theme logic - - when a change starts to push a file into "grab bag" territory, extract a - focused helper/module instead - - prefer small, reviewable modules over one large integration file - -3. **Prefer reusable components over duplicated wiring** - - shared theme resolution, fallback, conversion, and apply behavior should be - centralized - - avoid copy-pasting theme selection logic across startup, settings, and - terminal update paths - -4. **Avoid magic numbers unless they are inherent to the domain** - - filesystem polling delays, cache TTLs, directory limits, and fallback - constants must be named - - if a number is part of a terminal standard or palette definition, document - why it is fixed - -5. **Separate IO, state, and presentation concerns** - - file loading belongs in a theme loading/service layer - - theme registry state belongs in app/workspace state - - settings UI should only render options and trigger actions - ---- - -## Proposed Implementation Shape - -### 1. Introduce a Registry-to-Runtime Conversion Layer - -Create a conversion path from the serializable registry theme model into -`CodirigentTheme`. - -Options: - -- add `impl TryFrom for CodirigentTheme` -- or add `Theme::to_runtime_theme() -> Result` - -Expected result: - -- the registry model becomes useful without replacing the runtime renderer -- theme parsing and theme application stop being separate systems - -### 2. Make Theme Selection Registry-Driven - -Replace direct `dark/light` branching with: - -1. resolve selected theme ID from settings -2. look it up in the theme registry -3. convert it into `CodirigentTheme` -4. apply it to workspace and terminals - -Fallback behavior: - -- if the theme ID is missing or invalid, fall back to built-in `dark` -- log the failure with enough detail to diagnose bad custom themes - -### 3. Preserve Theme IDs In Settings - -Remove the current behavior that reconstructs `appearance.theme` by inspecting -background lightness. - -Instead: - -- track the current active theme ID in workspace settings state -- persist and rebuild the settings page using that actual ID - -### 4. Load Custom Themes From A Well-Defined Directory - -Decide and document the custom theme directory. Likely candidate: - -- `%APPDATA%/codirigent/themes/` on Windows -- `~/.config/codirigent/themes/` on Linux/macOS - -The initial implementation should: - -- load built-in themes first -- then overlay custom themes from disk -- allow custom themes to coexist with built-ins under unique IDs -- perform file discovery and JSON loading off the UI thread - -### 5. Keep Runtime Theme Mutations Compatible - -Today the code mutates parts of the active runtime theme after applying a base -theme, for example: - -- grid gap -- UI font size -- terminal font size -- terminal font family -- terminal line height - -The new registry-driven apply path must preserve those user overrides rather -than resetting them when a theme changes. - -### 6. Prepare For Ghostty-Style Theme Growth - -The first pass does not need full Ghostty config syntax, but the schema should -be able to expand toward these terminal concepts cleanly: - -- background -- foreground -- cursor color -- cursor text color -- selection background -- selection foreground -- ANSI 16 palette -- optional split between light and dark variants - -If a schema change is needed, prefer a backward-compatible addition over a -throwaway one-off field. - ---- - -## Task Series - -This branch should be executed as a small task series, not one large patch. - -### Task 1. Document and Wire The Runtime Registry Backbone - -Deliverables: - -- conversion path from serializable theme model to runtime `CodirigentTheme` -- built-in themes exposed through the registry path -- unit tests for conversion and fallback behavior - -Done when: - -- a theme ID can produce a runtime theme without `if theme == "light"` - -Status: - -- complete in `ced6530` - -### Task 2. Apply Saved Theme IDs During Settings Load / Startup - -Deliverables: - -- startup or settings load path resolves `appearance.theme` -- invalid IDs fall back safely -- active theme ID is retained in workspace state - -Done when: - -- restarting the app with a non-default theme keeps the same theme selected - -Status: - -- complete in `d2c0be0` - -### Task 3. Make The Settings Theme Picker Dynamic - -Deliverables: - -- settings theme dropdown is populated from the registry -- selection applies by theme ID -- settings rebuild preserves the active theme ID - -Done when: - -- custom or built-in registry themes are selectable from settings without - hardcoded branching - -Status: - -- complete in `03d208f` - -### Task 4. Load Custom Theme Files From Disk - -Deliverables: - -- custom theme directory resolution -- file loading on startup -- invalid file handling with non-fatal logging -- tests for loading valid and invalid theme files - -Done when: - -- dropping a valid theme JSON file into the theme directory makes it selectable - -Status: - -- complete in `f632676` - -### Task 5. Expand Terminal Theme Fidelity Where Needed - -Deliverables: - -- review the serializable theme schema against runtime terminal needs -- add missing fields only if required for correct runtime parity -- verify terminal fg/bg/cursor/selection/ANSI palette switch correctly - -Done when: - -- terminal behavior remains visually consistent after switching among themes - -Status: - -- complete in `4df2b36` - ---- - -## Risks And Review Focus - -### Risk 1. Theme Drift Between Models - -If `theme_config::Theme` cannot fully represent runtime needs, conversion logic -may silently drop behavior. - -Review focus: - -- terminal fields -- status colors -- typography/spacings that are currently mutated at runtime - -### Risk 2. Settings Page Regressions - -The current settings page rebuild flow reconstructs display state from runtime -theme values. That can easily wipe out the selected theme ID. - -Review focus: - -- open settings after switching themes -- close and reopen settings -- restart app and reopen settings - -### Risk 3. Startup Ordering - -If theme loading happens after UI creation or after terminal views are -constructed, the app may flash the wrong theme or only partially update. - -Review focus: - -- initial workspace creation -- settings background load path -- terminal creation after theme application -- background theme loading handoff back to UI state application - -### Risk 4. Overwriting User Overrides - -Applying a new base theme must not discard user font size, terminal font -preferences, or grid gap choices. - -Review focus: - -- theme switch after changing font sizes -- theme switch after changing terminal line height -- theme switch after changing terminal font family - ---- - -## Verification Strategy - -This task series follows `docs/task-verification-workflow.md`. - -Per task, after implementation: - -```bash -cargo clean -cargo build --all-features -cargo test --all --all-targets --all-features -cargo test -p codirigent-ui --lib --features gpui-full -cargo clippy --all --all-targets --all-features -- -D warnings -cargo fmt --all --check -bash scripts/audit-unwraps.sh -``` - -Required review pass after verification: - -- inspect the diff for dead theme paths and duplicate logic -- review fallback behavior for invalid theme IDs and broken JSON files -- review startup ordering and settings rebuild behavior -- review terminal palette behavior, not just UI chrome colors -- confirm file IO and theme discovery do not happen on the UI thread -- confirm new constants are named and justified -- confirm touched files remain at maintainable size - -Final verification executed on the completed branch: - -```bash -cargo build --all-features -cargo test --all --all-targets --all-features -cargo test -p codirigent-ui --lib --features gpui-full -cargo clippy --all --all-targets --all-features -- -D warnings -cargo fmt --all --check -bash scripts/audit-unwraps.sh -``` - -Result: - -- build passed -- full test suite passed -- `gpui-full` UI tests passed -- clippy passed with `-D warnings` -- formatting check passed -- unwrap audit reported only the existing repository-wide baseline - ---- - -## Suggested File Touch Order - -To keep the series reviewable, prefer this order: - -1. `crates/codirigent-ui/src/theme.rs` -2. `crates/codirigent-ui/src/theme_config.rs` -3. `crates/codirigent-ui/src/theme_manager.rs` -4. `crates/codirigent-ui/src/workspace/impl_settings.rs` -5. `crates/codirigent-ui/src/workspace/settings_panels.rs` -6. any startup/bootstrap files that need registry initialization -7. tests -8. follow-up docs updates if behavior changes materially - -This order keeps model changes ahead of UI wiring. - ---- - -## Open Questions Before Implementation - -1. Where should the registry live at runtime? - - central app state - - workspace state - - settings state - -2. Should built-in themes remain defined in `theme.rs`, or should they be - generated from `theme_config.rs` and then converted into runtime themes? - -3. Do we want the first pass to add richer terminal fields to - `theme_config.rs`, or keep schema changes minimal and only fill the missing - runtime wiring? - -4. Should custom theme discovery be automatic on every startup, or only when - the settings panel opens? - -Recommended answers for the first pass: - -- keep the registry in app/workspace state -- preserve `theme.rs` as the runtime authority initially -- add only the schema fields required for parity -- load custom themes on startup so the selected theme is valid before settings - open - ---- - -## Completion Standard - -This plan is complete only when all of the following are true: - -- theme selection is registry-based -- `appearance.theme` stores and preserves a real theme ID -- startup and settings load paths apply the saved theme -- custom themes can be loaded from disk -- terminal colors switch consistently with the active theme -- each task is verified and reviewed per `docs/task-verification-workflow.md` - -This completion standard is now satisfied for `feat/ghostty-theme-registry`. diff --git a/docs/local-dmg-build.md b/docs/release/local-dmg-build.md similarity index 100% rename from docs/local-dmg-build.md rename to docs/release/local-dmg-build.md diff --git a/docs/release/windows-release-signing.md b/docs/release/windows-release-signing.md new file mode 100644 index 0000000..bbd915e --- /dev/null +++ b/docs/release/windows-release-signing.md @@ -0,0 +1,189 @@ +# Windows Release Signing Workflow + +Required procedure for signing the Windows MSI installer after CI builds a +release. + +--- + +## Goal + +The Certum Open Source Code Signing certificate private key is non-exportable +(SimplySign protected). CI cannot sign the MSI. After CI completes, the MSI +must be signed locally on the machine where SimplySign Desktop is installed, +then uploaded back to the GitHub Release with updated checksums. + +--- + +## Prerequisites + +- SimplySign Desktop installed and logged in (system tray icon visible) +- Windows SDK installed (`signtool.exe` available) +- WiX Toolset available at `tools/wix/` in the repo +- `gh` CLI authenticated with push access to the repo +- Certum certificate visible in `certmgr.msc` under Personal > Certificates + +--- + +## Automated Flow (CI Release) + +When a tag is pushed, CI handles everything except Windows signing: + +### 1. Push the Tag + +```bash +git tag -a v0.X.Y -m "Release v0.X.Y" +git push origin v0.X.Y +``` + +### 2. Wait for CI + +The GitHub Actions `Release` workflow builds: + +- Windows x64: `.msi` (unsigned) + `.zip` +- macOS Apple Silicon: `.dmg` (signed + notarized) + `.tar.gz` + +`checksums-sha256.txt` intentionally excludes the MSI. + +### 3. Sign and Upload + +Run the signing script after CI completes: + +```powershell +.\scripts\sign-and-upload.ps1 -Tag v0.X.Y +``` + +The script: + +1. downloads the unsigned MSI from the release +2. finds the Certum certificate by issuer in the Windows cert store +3. signs the MSI with SHA-256 + Certum timestamp server +4. verifies the signature +5. computes SHA-256 hash and appends to `checksums-sha256.txt` +6. uploads signed MSI and updated checksums to the release + +### 4. Verify + +After the script completes, confirm: + +- [ ] MSI on the release page has a newer timestamp than the original +- [ ] `checksums-sha256.txt` includes a line for the `.msi` file +- [ ] Download the signed MSI and check Properties > Digital Signatures + +--- + +## Local Build Flow (Testing) + +For testing installer changes before pushing a tag. + +### 1. Set the Version + +The workspace version in `Cargo.toml` must match the installer version. +CI does this automatically from the tag. Locally, do it manually: + +```bash +# Check current version +grep '^version' Cargo.toml | head -1 + +# If it needs updating (revert after testing): +# Edit Cargo.toml line 16: version = "0.X.Y" +``` + +### 2. Build Binaries + +```bash +export PATH="$PATH:/c/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x64" +cargo build --profile dist --features gpui-full -p codirigent -p codirigent-hook +``` + +Binaries output to `target/dist/`. + +### 3. Build MSI + +```bash +WIX_BIN="tools/wix" +"$WIX_BIN/candle.exe" \ + -dBinaryPath="target/dist" \ + -dVersion=0.X.Y.0 \ + -dLicensePath=wix/License.rtf \ + -arch x64 wix/main.wxs -o wix/main.wixobj + +"$WIX_BIN/light.exe" \ + -ext "$WIX_BIN/WixUIExtension.dll" \ + wix/main.wixobj -o dist/codirigent.msi +``` + +Note: WiX version format requires `x.x.x.x` (four integers, no prerelease +suffix). Use `0.1.3.0` not `0.1.3-alpha.1`. + +### 4. Sign MSI + +Ensure SimplySign Desktop is running and logged in, then: + +```powershell +$signtool = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" | + Sort-Object FullName -Descending | Select-Object -First 1).FullName + +$cert = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { $_.Issuer -like "*Certum*" } | + Select-Object -First 1 + +& $signtool sign ` + /sha1 $cert.Thumbprint ` + /fd sha256 ` + /tr http://time.certum.pl ` + /td sha256 ` + /v dist\codirigent.msi +``` + +### 5. Verify Signature + +```powershell +& $signtool verify /pa /v dist\codirigent.msi +``` + +Or: right-click `dist\codirigent.msi` > Properties > Digital Signatures. + +### 6. Test Install + +Double-click `dist\codirigent.msi` and verify: + +- [ ] Installer shows correct version +- [ ] `codirigent.exe` runs and reports the correct version +- [ ] `codirigent-hook.exe` is installed alongside +- [ ] PATH is updated (new terminal session required) + +### 7. Revert Version (if changed) + +If you modified `Cargo.toml` for testing, revert it: + +```bash +git checkout Cargo.toml Cargo.lock +``` + +--- + +## Failure Handling + +- **SimplySign not running:** signtool returns "No certificates were found". + Launch SimplySign Desktop and log in. +- **Certificate not found:** Run `certmgr.msc` and confirm the Certum cert is + under Personal > Certificates with the key icon. +- **Timestamp server unreachable:** Retry. If `time.certum.pl` is down, try + `http://timestamp.digicert.com` as a fallback. +- **WiX candle/light fails:** Ensure `tools/wix/` exists. If missing, download + WiX Toolset v3.14 binaries and extract to `tools/wix/`. +- **Version mismatch (MSI vs binary):** The `-dVersion` passed to WiX controls + what Windows shows in Add/Remove Programs. The binary version comes from + `Cargo.toml`. Both must be updated for a consistent release. + +--- + +## Completion Standard + +A Windows release is only complete when: + +- [ ] CI workflow finished successfully +- [ ] MSI is signed with the Certum certificate +- [ ] `checksums-sha256.txt` includes the signed MSI hash +- [ ] Both files are uploaded to the GitHub Release +- [ ] The release page shows the correct file sizes (signed MSI is larger) diff --git a/docs/specs/2026-03-18-terminal-scrollbar-search-design.md b/docs/specs/2026-03-18-terminal-scrollbar-search-design.md deleted file mode 100644 index b454b24..0000000 --- a/docs/specs/2026-03-18-terminal-scrollbar-search-design.md +++ /dev/null @@ -1,188 +0,0 @@ -# Terminal Scrollbar & Search Design - -**Date:** 2026-03-18 -**Status:** Approved - -## Overview - -Add two features to the terminal pane: -1. An interactive scrollbar with drag-to-scroll, click-to-jump, and auto-hide -2. A find-in-terminal overlay (Cmd+F / Ctrl+F) with match highlighting, navigation, and scrollbar match markers - -## Prerequisite APIs - -### Total Scrollback Lines - -The scrollbar and search both need to know the total scrollback size. Alacritty's `Term` provides this via `grid().total_lines() - grid().screen_lines()` (history size) and `topmost_line().0.unsigned_abs()` (max scroll offset). Currently `TerminalSize::total_lines()` in `terminal.rs` returns only the visible row count. - -**Changes required:** -- Add a `history_size: usize` field to `TerminalRenderSnapshot` in `terminal_runtime.rs`, populated from `term.topmost_line().0.unsigned_abs()` during snapshot generation -- Add `total_scrollback_lines() -> usize` to `TerminalView`, reading from the snapshot -- The scrollbar uses this value for thumb sizing and position math - -### Scroll-to-Absolute-Position - -The scrollbar drag and track-click need to set the viewport to an arbitrary position. Only relative scroll APIs exist today (`scroll_up`, `scroll_down`, `scroll_to_bottom`). - -**Approach:** Compute a delta from the current `display_offset` to the target offset using `i32` arithmetic to avoid underflow: `Scroll::Delta((target as i32) - (current_display_offset as i32))`. Alacritty clamps the result to `[0, history_size]`. Add a `scroll_to_offset(target: usize)` method on `TerminalRuntimeHandle` that performs this computation internally. - -### Search Grid Access - -The search engine needs to iterate the `Term` grid cells. The `Term` is owned by `TerminalRuntime` behind `Arc>`. - -**Approach:** Add a `search(query: &str) -> Vec` method on `TerminalRuntimeHandle` that acquires the mutex lock and runs the scan, consistent with how `get_selected_text()` already works. The debounce timer resets on each keystroke so only the final query triggers a scan. If large-scrollback performance becomes an issue, this can be moved to a background task later. - -## Feature 1: Interactive Scrollbar - -### Rendering - -Overlay div on the right edge of the terminal pane, rendered as a sibling of the terminal canvas inside `grid_render.rs`. Positioned absolute, sits on top of terminal content. - -- **Track:** Full-height div, transparent by default, semi-transparent on hover -- **Thumb:** Colored div inside the track - - Height: `max(30px, (visible_rows / total_lines) * track_height)` - - Position: proportional to `display_offset / total_scrollback_lines` -- **Width:** 8px default, expands to 12px on hover - -### Interaction - -- **Drag thumb:** `on_mouse_down` on thumb captures drag start offset. `on_mouse_move` on track converts pixel Y delta to proportional scrollback position. `on_mouse_up` releases. -- **Click track:** Jump to proportional position — `(click_y / track_height) * total_scrollback_lines` -- **Mouse wheel:** Existing handler unchanged. Thumb position updates reactively from `display_offset`. - -### Auto-Hide - -- Default opacity: 0 (hidden) -- Fade in on: mouse enters terminal area, scroll wheel activity, scrollback position changes -- Fade out after: 1.5s of no scroll activity AND mouse not hovering the scrollbar -- While mouse hovers the scrollbar: stay visible, expand width -- Timer: use `cx.spawn()` with `Timer::after(Duration::from_millis(1500))` to schedule fade-out; cancel and restart on any scroll activity or hover. Update opacity via `cx.notify()`. -- The scrollbar track height accounts for `TERMINAL_CONTENT_PADDING` so the thumb range matches the visible content area. - -### State - -```rust -struct ScrollbarState { - /// Current opacity (0.0 = hidden, 1.0 = fully visible). - opacity: f32, - /// Mouse is hovering the scrollbar track or thumb. - hovered: bool, - /// Active drag: stores Y offset from thumb top at drag start. - dragging: Option, - /// Timestamp of last scroll activity (for auto-hide timer). - last_scroll_activity: Instant, -} -``` - -## Feature 2: Terminal Search - -### Search Overlay - -Floating bar at the top-right of the terminal pane, approximately 300px wide. Contains: -- Text input field (focused on open) -- Match count label: "3 of 47" -- Prev/Next buttons (up/down arrow icons, or keyboard Enter/Shift+Enter) -- Close button (X) or Escape to dismiss - -Rendered as an absolute-positioned div inside the terminal pane container. - -### Activation - -- **Open:** Cmd+F (macOS) / Ctrl+F (Windows/Linux) — registers a `SearchTerminal` GPUI action -- **Close:** Escape key or click X — clears highlights and returns focus to terminal -- **Input routing:** While search bar is open, keystrokes go to the search input, not the terminal PTY - -### Search Engine - -Location: `crates/codirigent-ui/src/terminal_search.rs` - -- Scans alacritty `Term` grid from bottom of scrollback to top (most recent content first) -- Iterates cells row by row, concatenating characters into line strings -- Handles wrapped lines as a single logical line -- Case-insensitive matching -- Returns match positions: - -```rust -struct SearchMatch { - /// Absolute grid line coordinate (matches alacritty's Line(i32) convention). - /// Negative = scrollback history, 0 = top of visible screen, positive = below. - /// This is independent of display_offset — the viewport maps absolute lines - /// to screen rows. Scrollbar marker positions are computed from these absolute - /// coordinates relative to the total scrollback range. - grid_line: i32, - /// Start column (inclusive). - start_col: usize, - /// End column (exclusive). - end_col: usize, -} -``` - -- Wrapped lines detected via alacritty's `WRAPLINE` cell flag — consecutive flagged rows are concatenated into a single logical line for matching, with column offsets adjusted accordingly -- Debounce: 150ms, timer resets on each keystroke so only the final query triggers a scan; search runs synchronously under the `TerminalRuntimeHandle` mutex (consistent with `get_selected_text()`) -- **Output during search:** When new terminal output arrives while search is active, matches are kept as-is (stale) until the user modifies the query. Match indices may shift due to new output; if the user navigates to a match whose text no longer matches the query at that position, skip to the next valid match. This avoids re-scanning on every output event. - -### Match Highlighting - -- All matches: colored background rects rendered during terminal paint phase (same layer as selection rects in `terminal_render.rs`) -- Active/current match: brighter highlight color to distinguish from other matches -- Only matches within the current viewport need rects computed — filter by visible row range during render - -### Navigation - -- Enter or Down arrow: jump to next match -- Shift+Enter or Up arrow: jump to previous match -- Jumping scrolls the viewport to center the match on screen -- Match index wraps around (last match → first match) - -### Scrollbar Match Markers - -- While search is active, render small horizontal ticks on the scrollbar track -- Each tick: 2px tall, full scrollbar width, positioned at proportional Y for the match's grid line -- Uses the search highlight color -- Only visible while search overlay is open -- Marker Y position formula: `marker_y_fraction = (history_size + grid_line) / (history_size + screen_lines)` — this maps absolute grid line coordinates to the same proportional space used by the scrollbar thumb - -### Search State - -```rust -struct SearchState { - /// Whether the search overlay is open. - active: bool, - /// Current search query. - query: String, - /// All matches found in the terminal grid. - matches: Vec, - /// Index of the currently focused match (for navigation). - current_match: Option, -} -``` - -## File Organization - -### New Files - -| File | Purpose | -|------|---------| -| `crates/codirigent-ui/src/workspace/scrollbar_render.rs` | Scrollbar rendering helper called from within the session cell render path in `grid_render.rs`; not a standalone workspace component | -| `crates/codirigent-ui/src/workspace/search_render.rs` | Search overlay: text input, match count, prev/next buttons | -| `crates/codirigent-ui/src/terminal_search.rs` | Search engine: grid scanning, match collection, result types | - -### Modified Files - -| File | Change | -|------|--------| -| `terminal_view.rs` | Add `ScrollbarState`, `SearchState` fields; expose `total_scrollback_lines()` | -| `grid_render.rs` | Compose scrollbar and search overlay into terminal pane div; wire Cmd+F | -| `terminal_render.rs` | Render search match highlight rects during paint phase | -| `app.rs` | Define `SearchTerminal` action struct and register keybinding (Cmd+F / Ctrl+F), alongside existing actions like `Copy`, `Paste` | -| `workspace/mod.rs` | Declare new modules | - -### Modified (minimal) - -- `terminal_runtime.rs` — add `history_size` to snapshot, add `scroll_to_offset()` and `search()` methods on handle - -### Unchanged - -- `terminal.rs` — alacritty wrapper stays untouched -- Session management, persistence, layout systems -- Existing mouse scroll and text selection behavior (selection continues to work normally beneath the search overlay; search matches are purely visual and do not interact with the selection system) diff --git a/docs/task-verification-workflow.md b/docs/workflows/task-verification-workflow.md similarity index 100% rename from docs/task-verification-workflow.md rename to docs/workflows/task-verification-workflow.md diff --git a/scripts/sign-and-upload.ps1 b/scripts/sign-and-upload.ps1 new file mode 100644 index 0000000..ac10c55 --- /dev/null +++ b/scripts/sign-and-upload.ps1 @@ -0,0 +1,77 @@ +# Usage: .\scripts\sign-and-upload.ps1 -Tag v0.1.3-alpha3 +param( + [Parameter(Mandatory)] + [string]$Tag +) + +$ErrorActionPreference = "Stop" + +$archiveName = "codirigent-$Tag-x86_64-pc-windows-msvc" +$msiName = "$archiveName.msi" +$tmpDir = Join-Path $env:TEMP "codirigent-sign-$Tag" + +New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + +# ── 1. Download unsigned MSI from the release ───────────────────────────────── +Write-Host "Downloading $msiName from release $Tag..." +gh release download $Tag --pattern $msiName --output "$tmpDir\$msiName" --clobber + +# ── 2. Find signtool ────────────────────────────────────────────────────────── +$signtool = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" | + Sort-Object FullName -Descending | Select-Object -First 1).FullName + +if (-not $signtool) { + Write-Error "signtool.exe not found. Install Windows SDK." +} + +# ── 3. Find the Certum certificate ─────────────────────────────────────────── +$cert = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { $_.Issuer -like "*Certum*" -and $_.EnhancedKeyUsageList.FriendlyName -contains "Code Signing" } | + Select-Object -First 1 + +if (-not $cert) { + Write-Error "Certum code signing certificate not found in CurrentUser\My store." +} + +Write-Host "Using certificate: $($cert.Subject)" + +# ── 4. Sign the MSI ─────────────────────────────────────────────────────────── +Write-Host "Signing $msiName..." +& $signtool sign ` + /sha1 $cert.Thumbprint ` + /fd sha256 ` + /tr http://time.certum.pl ` + /td sha256 ` + /v "$tmpDir\$msiName" + +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# ── 5. Verify signature ─────────────────────────────────────────────────────── +Write-Host "Verifying signature..." +& $signtool verify /pa /v "$tmpDir\$msiName" +if ($LASTEXITCODE -ne 0) { Write-Warning "Signature verification failed!" } + +# ── 6. Compute new checksum ─────────────────────────────────────────────────── +Write-Host "Computing checksum..." +$hash = (Get-FileHash "$tmpDir\$msiName" -Algorithm SHA256).Hash.ToLower() +$checksumLine = "$hash $msiName" +Write-Host "Checksum: $checksumLine" + +# ── 7. Download existing checksums-sha256.txt and append MSI checksum ───────── +$checksumFile = "$tmpDir\checksums-sha256.txt" +gh release download $Tag --pattern "checksums-sha256.txt" --output $checksumFile --clobber + +# Remove any stale MSI line (in case of re-run) then append new one +$lines = Get-Content $checksumFile | Where-Object { $_ -notmatch '\.msi' } +$lines + $checksumLine | Set-Content $checksumFile + +Write-Host "`nUpdated checksums-sha256.txt:" +Get-Content $checksumFile + +# ── 8. Upload signed MSI and updated checksums ──────────────────────────────── +Write-Host "`nUploading signed MSI and updated checksums to release $Tag..." +gh release upload $Tag "$tmpDir\$msiName" --clobber +gh release upload $Tag $checksumFile --clobber + +Write-Host "`nDone! Signed MSI and checksums uploaded to release $Tag." +Write-Host "Temp files at: $tmpDir"