diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb8b4354..536da9b1 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 4baa4c10..9552362b 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/crates/codirigent-ui/src/terminal_runtime.rs b/crates/codirigent-ui/src/terminal_runtime.rs index 6e349c80..01090bcd 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() { 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 1c26ec0e..5cf336b8 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); } diff --git a/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs b/crates/codirigent-ui/src/workspace/impl_session_lifecycle.rs index 83c177fa..7c0039b5 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(); diff --git a/crates/codirigent-ui/src/workspace/terminal_render.rs b/crates/codirigent-ui/src/workspace/terminal_render.rs index 2870aa75..0d1e427a 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() { 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 ab40bc2d..00000000 --- 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 00000000..bbd915e4 --- /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 b454b24b..00000000 --- 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 00000000..ac10c555 --- /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"