diff --git a/.kiro/specs/ci-workflow-gaps/.config.kiro b/.kiro/specs/ci-workflow-gaps/.config.kiro new file mode 100644 index 0000000..dbffc35 --- /dev/null +++ b/.kiro/specs/ci-workflow-gaps/.config.kiro @@ -0,0 +1 @@ +{"specId": "8a94958a-9861-4602-9055-fe1f500825d6", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/ci-workflow-gaps/design.md b/.kiro/specs/ci-workflow-gaps/design.md new file mode 100644 index 0000000..89c10f3 --- /dev/null +++ b/.kiro/specs/ci-workflow-gaps/design.md @@ -0,0 +1,200 @@ +# Design Document: CI Workflow Gaps + +## Overview + +This design closes three gaps left open after the initial `ci.yml` merge (issue #19): + +1. **Branch trigger** — add `phase-1-scaffold` to the `on.push.branches` and `on.pull_request.branches` lists so the workflow fires on that branch too. +2. **Inline comments** — annotate `ci.yml` with YAML comments that tell contributors where and how to add new steps. +3. **CI documentation** — insert a "CI / Continuous Integration" section into `CONTRIBUTING.md` covering what runs, when it runs, and how to replicate it locally. + +All changes are purely textual edits to two existing files. No new source code, dependencies, or infrastructure is introduced. + +--- + +## Architecture + +No architectural changes are required. The feature operates entirely at the repository-metadata layer: + +``` +Repository root +├── .github/ +│ └── workflows/ +│ └── ci.yml ← edit: branch triggers + inline comments +└── CONTRIBUTING.md ← edit: add CI section +``` + +The GitHub Actions runtime interprets `ci.yml` declaratively; the runner topology (ubuntu-latest, the job matrix, caching) is unchanged. + +--- + +## Components and Interfaces + +### Component 1: `ci.yml` — Branch Triggers + +**Location**: `.github/workflows/ci.yml`, `on` block (lines 3–7). + +**Current state**: +```yaml +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] +``` + +**Target state**: +```yaml +on: + push: + branches: [ "main", "phase-1-scaffold" ] + pull_request: + branches: [ "main", "phase-1-scaffold" ] +``` + +**Interface contract**: GitHub Actions evaluates the `branches` list as an ordered set of glob patterns. Adding a literal branch name has no side-effects on existing triggers. + +--- + +### Component 2: `ci.yml` — Inline Comments + +**Location**: `.github/workflows/ci.yml`, `steps` block. + +Two comment zones are required: + +1. **Top-of-steps comment** — immediately above the first step, explaining the purpose of the job and inviting extension. +2. **End-of-steps comment** — after the last step (`Vitest Check`), marking the point where new steps should be inserted and giving a concrete example. + +Example placement: + +```yaml + steps: + # ─── Core checks ──────────────────────────────────────────────────────────── + # To add a new step (e.g., a build or integration-test step), append it below + # the last existing step, following the same `- name: / run:` pattern. + - uses: actions/checkout@v4 + ... + - name: Vitest Check + run: npx vitest run + + # ─── Add new steps here ────────────────────────────────────────────────────── + # Example: + # - name: Build Tauri App + # run: npm run tauri build -- --debug +``` + +**Constraint**: Every existing step name, `run` command, `working-directory`, and `uses` directive must remain byte-for-byte identical after this edit. + +--- + +### Component 3: `CONTRIBUTING.md` — CI Section + +**Location**: `CONTRIBUTING.md`. + +**Insertion point**: After the existing "🧪 Verification Checks" section and before the "🔀 Pull Request Process" section. This placement is logical because a contributor reading the verification steps will naturally want to know how those same steps are enforced automatically. + +**Section content requirements** (from Requirements 3.1–3.4): + +| Item | Content | +|---|---| +| Heading | `## 🤖 CI / Continuous Integration` | +| When CI runs | Every push to `main` and `phase-1-scaffold`; every PR targeting those branches | +| Checks listed | Rust Clippy, Rust tests, Rust format check, TypeScript type check, ESLint check, Vitest tests | +| Local commands | Exact commands to reproduce each check | + +**Target section**: + +```markdown +## 🤖 CI / Continuous Integration + +CI runs automatically via GitHub Actions on every **pull request** and every **push to `main` or `phase-1-scaffold`**. + +### What CI checks + +| Check | Tool | +|---|---| +| Rust linting | `cargo clippy --all-targets --all-features -- -D warnings` | +| Rust tests | `cargo test --all` | +| Rust formatting | `cargo fmt --check` | +| TypeScript types | `npx tsc --noEmit` | +| ESLint | `npx eslint . --max-warnings 0` | +| Frontend tests | `npx vitest run` | + +### Running CI checks locally + +Run the following from the repository root to replicate every CI check: + +```bash +# Rust checks (run from src-tauri/) +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all +cargo fmt --check + +# Frontend checks (run from repo root) +npx tsc --noEmit +npx eslint . --max-warnings 0 +npx vitest run +``` + +If all commands exit with code 0, your branch will pass CI. +``` + +**Constraint**: The five existing sections (Prerequisites, Development Setup, Code Style & Guidelines, Verification Checks, Pull Request Process) must remain present and unmodified. + +--- + +## Data Models + +No data models are introduced. The only data artefacts are: + +- A YAML scalar list (`branches`) in `ci.yml` — extended with one string element per trigger type. +- A Markdown string (the new CI section) — inserted into `CONTRIBUTING.md`. + +--- + +## Error Handling + +| Scenario | Mitigation | +|---|---| +| YAML parse error after edit | Validate `ci.yml` with `yamllint` or GitHub's workflow linter before merging | +| Accidental removal of existing steps | Review the diff to confirm all original step blocks are present | +| Markdown rendering issues | Preview `CONTRIBUTING.md` in a Markdown renderer before merging | +| Branch name typo in trigger | Cross-check the exact branch name against `git branch -a` output | + +--- + +## Testing Strategy + +Property-based testing does **not** apply to this feature. All acceptance criteria are structural/configuration checks on two files — they verify presence or absence of specific textual content in a YAML file and a Markdown file. The input space does not vary meaningfully; running 100 iterations would not surface additional defects. Appropriate test strategies are: + +### Smoke tests (manual or CI-based) + +These checks should be performed before merging the PR: + +1. **Branch trigger check** (Requirements 1.1–1.3) + - Parse `ci.yml` and assert `on.push.branches` equals `["main", "phase-1-scaffold"]`. + - Assert `on.pull_request.branches` equals `["main", "phase-1-scaffold"]`. + +2. **Inline comment check** (Requirements 2.1–2.2) + - Assert the `steps` block contains at least two YAML comment lines (`#`) that reference adding or extending steps. + +3. **Existing steps preservation** (Requirement 2.3) + - Assert all six original step names (`Rust Clippy`, `Rust Tests`, `Rust Format`, `TypeScript Check`, `ESLint Check`, `Vitest Check`) remain present. + +4. **CI section heading** (Requirement 3.1) + - Assert `CONTRIBUTING.md` contains the heading `CI / Continuous Integration`. + +5. **CI section content** (Requirements 3.2–3.4) + - Assert the section names all six checks. + - Assert the section mentions `pull request`, `main`, and `phase-1-scaffold`. + - Assert the section contains code blocks with the six local commands. + +6. **Existing CONTRIBUTING sections preserved** (Requirement 3.5) + - Assert all five original headings remain present in `CONTRIBUTING.md`. + +### Manual review checklist + +- [ ] `ci.yml` YAML is valid (no syntax errors). +- [ ] Workflow is readable and comments are clear to a first-time contributor. +- [ ] `CONTRIBUTING.md` renders correctly in GitHub's Markdown viewer. +- [ ] New CI section sits between "Verification Checks" and "Pull Request Process". diff --git a/.kiro/specs/ci-workflow-gaps/requirements.md b/.kiro/specs/ci-workflow-gaps/requirements.md new file mode 100644 index 0000000..f84d73e --- /dev/null +++ b/.kiro/specs/ci-workflow-gaps/requirements.md @@ -0,0 +1,57 @@ +# Requirements Document + +## Introduction + +The CNTRL Browser project has an existing `ci.yml` GitHub Actions workflow that runs lint and test checks on the `main` branch. Three gaps remain from the original issue (#19) that need to be addressed: + +1. The workflow does not trigger on the `phase-1-scaffold` branch. +2. The workflow file contains no inline comments guiding contributors on how to extend it. +3. `CONTRIBUTING.md` has no CI section explaining what CI does, when it runs, or how to replicate its checks locally. + +This feature closes those three gaps through targeted edits to `.github/workflows/ci.yml` and `CONTRIBUTING.md`. + +## Glossary + +- **CI_Workflow**: The GitHub Actions workflow defined in `.github/workflows/ci.yml`. +- **CONTRIBUTING_Doc**: The contributor guide located at `CONTRIBUTING.md` in the repository root. +- **Branch_Trigger**: The `on.push.branches` and `on.pull_request.branches` lists in `ci.yml` that determine which branches activate the workflow. +- **Inline_Comment**: A YAML line comment (prefixed with `#`) placed inside `ci.yml` to explain the purpose of a section or how to extend it. +- **CI_Section**: A dedicated Markdown section in `CONTRIBUTING_Doc` describing CI behaviour and local equivalents. + +## Requirements + +### Requirement 1: Branch Trigger Coverage + +**User Story:** As a contributor working on the `phase-1-scaffold` branch, I want CI to run on my pushes and pull requests, so that I receive the same automated feedback as contributors targeting `main`. + +#### Acceptance Criteria + +1. WHEN a push is made to the `phase-1-scaffold` branch, THE CI_Workflow SHALL execute the `test-and-lint` job. +2. WHEN a pull request targets the `phase-1-scaffold` branch, THE CI_Workflow SHALL execute the `test-and-lint` job. +3. THE CI_Workflow SHALL continue to execute the `test-and-lint` job on pushes to `main` and on pull requests targeting `main`. + +--- + +### Requirement 2: Inline Contributor Guidance in ci.yml + +**User Story:** As a new contributor, I want inline comments in `ci.yml` explaining where and how to add new steps, so that I can extend the workflow without needing to consult external documentation. + +#### Acceptance Criteria + +1. THE CI_Workflow file SHALL contain at least one Inline_Comment explaining that additional steps (such as build or integration tests) can be appended after the existing steps. +2. THE CI_Workflow file SHALL contain at least one Inline_Comment identifying the location within the `steps` list where a new step should be added. +3. WHEN the CI_Workflow file is modified to add the comments, THE existing step names, commands, and job structure SHALL remain unchanged. + +--- + +### Requirement 3: CI Documentation in CONTRIBUTING.md + +**User Story:** As a contributor, I want a CI section in `CONTRIBUTING.md` explaining what CI checks run, when they run, and how to reproduce them locally, so that I can understand and meet CI requirements before opening a pull request. + +#### Acceptance Criteria + +1. THE CONTRIBUTING_Doc SHALL contain a CI_Section with a heading titled "CI / Continuous Integration". +2. THE CI_Section SHALL list each check that CI runs: Rust Clippy, Rust tests, Rust format check, TypeScript type check, ESLint check, and Vitest tests. +3. THE CI_Section SHALL state that CI runs automatically on every pull request and on every push to `main` and `phase-1-scaffold`. +4. THE CI_Section SHALL provide the exact local shell commands a contributor can run to reproduce each CI check. +5. WHEN `CONTRIBUTING.md` is modified to add the CI_Section, THE existing sections (Prerequisites, Development Setup, Code Style, Verification Checks, Pull Request Process) SHALL remain unchanged. diff --git a/.kiro/specs/ci-workflow-gaps/tasks.md b/.kiro/specs/ci-workflow-gaps/tasks.md new file mode 100644 index 0000000..e529990 --- /dev/null +++ b/.kiro/specs/ci-workflow-gaps/tasks.md @@ -0,0 +1,56 @@ +# Implementation Plan: CI Workflow Gaps + +## Overview + +Three targeted file edits close the remaining gaps from issue #19: +1. Extend the branch trigger list in `ci.yml`. +2. Add inline contributor-guidance comments to `ci.yml`. +3. Add a CI section to `CONTRIBUTING.md`. + +All tasks involve editing existing files only — no new files, dependencies, or infrastructure. + +## Tasks + +- [ ] 1. Add `phase-1-scaffold` to branch triggers in `ci.yml` + - In `.github/workflows/ci.yml`, locate the `on.push.branches` list and add `"phase-1-scaffold"` as a second element: `[ "main", "phase-1-scaffold" ]`. + - Apply the identical change to the `on.pull_request.branches` list. + - Verify the file parses as valid YAML after the edit. + - _Requirements: 1.1, 1.2, 1.3_ + +- [ ] 2. Add inline comments to `ci.yml` + - Immediately above the first step (`uses: actions/checkout@v4`), insert a block of YAML comments that: + - Describe the purpose of the `steps` section at a glance. + - Tell contributors they can append new steps below the last existing step. + - Show the `- name: / run:` pattern as a brief example. + - After the last step (`Vitest Check`), insert a clearly marked comment block that says "Add new steps here" and shows a concrete example step (e.g., a Tauri debug build). + - Confirm all original step names, `run` commands, `working-directory` fields, and `uses` directives are byte-for-byte unchanged. + - _Requirements: 2.1, 2.2, 2.3_ + +- [ ] 3. Checkpoint — validate ci.yml + - Ensure all tests pass, ask the user if questions arise. + - Confirm `ci.yml` is valid YAML (no parse errors). + - Confirm `on.push.branches` and `on.pull_request.branches` each contain exactly `["main", "phase-1-scaffold"]`. + - Confirm all six original step names are still present. + +- [ ] 4. Add CI section to `CONTRIBUTING.md` + - Open `CONTRIBUTING.md` and locate the position between the "🧪 Verification Checks" section and the "🔀 Pull Request Process" section. + - Insert a new section with the heading `## 🤖 CI / Continuous Integration` at that position. + - The section body must include: + - A sentence stating that CI runs on every PR and every push to `main` and `phase-1-scaffold`. + - A table (or equivalent list) naming all six checks: Rust Clippy, Rust tests, Rust format check, TypeScript type check, ESLint check, Vitest tests — each paired with its exact command. + - A fenced code block containing the six local shell commands a contributor can run to replicate CI. + - A note that exit code 0 on all commands means the branch will pass CI. + - Verify the five original sections (Prerequisites, Development Setup, Code Style & Guidelines, Verification Checks, Pull Request Process) are all still present and unmodified. + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_ + +- [ ] 5. Final checkpoint — Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + - Confirm `CONTRIBUTING.md` renders correctly (no broken Markdown syntax). + - Confirm the new CI section appears between "Verification Checks" and "Pull Request Process". + - Confirm all five original headings remain present in `CONTRIBUTING.md`. + +## Notes + +- No property-based tests apply to this feature — all acceptance criteria are structural checks on YAML and Markdown files. +- Each task references specific requirements from `requirements.md` for full traceability. +- The two checkpoint tasks serve as explicit review gates before and after the documentation change. diff --git a/src/components/CommandBar.css b/src/components/CommandBar.css index db948f1..3e009dc 100644 --- a/src/components/CommandBar.css +++ b/src/components/CommandBar.css @@ -1,129 +1,376 @@ -.cmd-bar-overlay { +/* ========================================================================== + CommandBar — Raycast-style omnibar overlay + Uses design tokens from src/styles/tokens.css + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Overlay / Backdrop + -------------------------------------------------------------------------- */ + +.cmd-overlay { position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(4px); + inset: 0; z-index: 10000; display: flex; justify-content: center; align-items: flex-start; - padding-top: 10vh; + padding-top: 12vh; + background-color: rgba(0, 0, 0, 0.45); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + animation: cmd-overlay-in 140ms ease-out both; +} + +@keyframes cmd-overlay-in { + from { + opacity: 0; + } + to { + opacity: 1; + } } -.cmd-bar { +/* -------------------------------------------------------------------------- + Modal panel + -------------------------------------------------------------------------- */ + +.cmd-panel { width: 90%; - max-width: 640px; - background-color: var(--cntrl-bg); - border: 1px solid var(--cntrl-border); - border-radius: 8px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + max-width: 660px; + background-color: var(--color-bg-panel); + border: 1px solid var(--color-border); + border-radius: 10px; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.3), + 0 12px 40px rgba(0, 0, 0, 0.45); overflow: hidden; display: flex; flex-direction: column; - transition: border-color 0.2s, box-shadow 0.2s; + + /* Entry animation: slide down + fade in */ + animation: cmd-panel-in 160ms cubic-bezier(0.16, 1, 0.3, 1) both; + transform-origin: top center; } -.cmd-bar.privacy-active { +@keyframes cmd-panel-in { + from { + opacity: 0; + transform: translateY(-12px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Privacy mode: red border glow */ +.cmd-panel.privacy-active { border-color: #ef4444; - box-shadow: 0 0 16px rgba(239, 68, 68, 0.5), 0 8px 32px rgba(0, 0, 0, 0.4); + box-shadow: + 0 0 0 1px #ef4444, + 0 0 20px rgba(239, 68, 68, 0.3), + 0 12px 40px rgba(0, 0, 0, 0.45); } -.cmd-bar-input-wrapper { +/* -------------------------------------------------------------------------- + Input row + -------------------------------------------------------------------------- */ + +.cmd-input-row { display: flex; align-items: center; - padding: 12px 16px; - border-bottom: 1px solid var(--cntrl-border); - gap: 12px; - position: relative; + gap: 10px; + padding: 14px 16px; + border-bottom: 1px solid var(--color-border); + background-color: var(--color-bg-elevated); +} + +.cmd-icon { + flex-shrink: 0; + color: var(--color-accent); + display: flex; + align-items: center; +} + +.cmd-input { + flex: 1; + min-width: 0; + background: transparent; + border: none; + outline: none; + color: var(--color-text-primary); + font-family: var(--font-sans); + font-size: 17px; + line-height: 1.4; + caret-color: var(--color-accent); +} + +.cmd-input::placeholder { + color: var(--color-text-secondary); + font-size: 15px; +} + +.cmd-input:disabled { + opacity: 0.6; + cursor: not-allowed; } .cmd-privacy-badge { - font-size: 11px; - font-weight: 600; - color: #ffffff; + flex-shrink: 0; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #fff; background-color: #ef4444; padding: 2px 8px; - border-radius: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; - animation: pulse-red 2s infinite; + border-radius: 20px; + animation: cmd-badge-pulse 2s ease-in-out infinite; } -@keyframes pulse-red { - 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } - 70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } - 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } +@keyframes cmd-badge-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6); } + 50% { box-shadow: 0 0 0 5px rgba(239, 68, 68, 0); } } +.cmd-hint { + flex-shrink: 0; + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-secondary); + background-color: var(--color-bg-base); + border: 1px solid var(--color-border); + border-radius: 4px; + padding: 1px 5px; + pointer-events: none; +} + +/* -------------------------------------------------------------------------- + Suggestions list + -------------------------------------------------------------------------- */ + +.cmd-suggestions { + list-style: none; + margin: 0; + padding: 6px 0; + max-height: 304px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; +} -.cmd-bar-input { +.cmd-suggestion-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 9px 16px; + cursor: pointer; + border-radius: 6px; + margin: 0 6px; + transition: + background-color 80ms ease, + color 80ms ease; + /* Keyboard/mouse active state */ +} + +.cmd-suggestion-item.active, +.cmd-suggestion-item:hover { + background-color: var(--color-bg-elevated); +} + +.cmd-suggestion-item.active { + background-color: var(--color-accent-dim); +} + +.cmd-suggestion-label { flex: 1; - background: transparent; - border: none; - color: var(--cntrl-fg); - font-size: 18px; - outline: none; - font-family: inherit; + min-width: 0; + color: var(--color-text-primary); + font-size: 13.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cmd-suggestion-meta { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; } -.cmd-bar-input::placeholder { - color: var(--cntrl-fg-muted); +.cmd-suggestion-subtitle { + font-size: 11.5px; + color: var(--color-text-secondary); + white-space: nowrap; } -.cmd-bar-results { +/* Category badges */ +.cmd-suggestion-category { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + border-radius: 4px; + padding: 1px 6px; + background-color: var(--color-bg-base); + color: var(--color-text-secondary); + border: 1px solid var(--color-border); + white-space: nowrap; +} + +.cmd-category-history { color: var(--color-text-secondary); } +.cmd-category-navigation { color: #60a5fa; border-color: #60a5fa; background-color: rgba(96, 165, 250, 0.08); } +.cmd-category-search { color: #34d399; border-color: #34d399; background-color: rgba(52, 211, 153, 0.08); } +.cmd-category-system { color: var(--color-accent); border-color: var(--color-accent-dim); background-color: rgba(232, 160, 32, 0.08); } +.cmd-category-macro { color: #c084fc; border-color: #c084fc; background-color: rgba(192, 132, 252, 0.08); } + +/* -------------------------------------------------------------------------- + Results / Steps + -------------------------------------------------------------------------- */ + +.cmd-results { max-height: 50vh; overflow-y: auto; - padding: 16px; + padding: 12px; display: flex; flex-direction: column; - gap: 12px; + gap: 8px; + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; } .cmd-step { - padding: 12px; - border-radius: 6px; - background-color: var(--cntrl-bg-surface); - border: 1px solid var(--cntrl-border); + padding: 12px 14px; + border-radius: 8px; + background-color: var(--color-bg-elevated); + border: 1px solid var(--color-border); + animation: cmd-step-in 140ms ease-out both; +} + +@keyframes cmd-step-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } } .cmd-step-header { display: flex; + align-items: center; justify-content: space-between; margin-bottom: 8px; - font-size: 14px; - color: var(--cntrl-fg-muted); } -.cmd-step-status { +.cmd-step-number { + font-size: 11px; font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-secondary); +} + +.cmd-step-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.cmd-step-status.pending { color: var(--color-text-secondary); } +.cmd-step-status.running { color: #60a5fa; } +.cmd-step-status.done { color: var(--color-text-success); } +.cmd-step-status.failed { color: var(--color-text-danger); } + +/* Spinner for running steps */ +.cmd-spinner { + display: inline-block; + width: 10px; + height: 10px; + border: 2px solid transparent; + border-top-color: #60a5fa; + border-radius: 50%; + animation: cmd-spin 0.6s linear infinite; } -.cmd-step-status.pending { color: var(--cntrl-fg-muted); } -.cmd-step-status.running { color: #3b82f6; } -.cmd-step-status.done { color: #10b981; } -.cmd-step-status.failed { color: #ef4444; } +@keyframes cmd-spin { + to { transform: rotate(360deg); } +} +/* Markdown result content */ .cmd-step-result { - font-size: 15px; - line-height: 1.5; + font-size: 13.5px; + line-height: 1.6; + color: var(--color-text-primary); +} + +.cmd-step-result p { + margin-top: 0; + margin-bottom: 8px; +} + +.cmd-step-result p:last-child { + margin-bottom: 0; } -.cmd-step-result p { margin-top: 0; margin-bottom: 8px; } -.cmd-step-result p:last-child { margin-bottom: 0; } .cmd-step-result code { - background-color: var(--cntrl-bg); - padding: 2px 4px; + font-family: var(--font-mono); + font-size: 12px; + background-color: var(--color-bg-base); + border: 1px solid var(--color-border); + padding: 1px 5px; border-radius: 4px; - font-family: monospace; } + .cmd-step-result pre { - background-color: var(--cntrl-bg); - padding: 8px; - border-radius: 4px; + background-color: var(--color-bg-base); + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 10px 12px; overflow-x: auto; + font-family: var(--font-mono); + font-size: 12px; +} + +.cmd-step-result a { + color: #60a5fa; + text-decoration: none; +} + +.cmd-step-result a:hover { + text-decoration: underline; +} + +.cmd-step-result ul, +.cmd-step-result ol { + padding-left: 1.4em; + margin-bottom: 8px; +} + +/* -------------------------------------------------------------------------- + Footer + -------------------------------------------------------------------------- */ + +.cmd-footer { + display: flex; + align-items: center; + gap: 16px; + padding: 7px 14px; + border-top: 1px solid var(--color-border); + background-color: var(--color-bg-base); + font-size: 11px; + color: var(--color-text-secondary); +} + +.cmd-footer kbd { + font-family: var(--font-mono); + font-size: 10px; + background-color: var(--color-bg-panel); + border: 1px solid var(--color-border); + border-radius: 3px; + padding: 0 4px; + margin-right: 3px; + color: var(--color-text-primary); } -.cmd-step-result a { color: #3b82f6; text-decoration: none; } -.cmd-step-result a:hover { text-decoration: underline; } diff --git a/src/components/CommandBar.tsx b/src/components/CommandBar.tsx index c86e3e5..5ff5940 100644 --- a/src/components/CommandBar.tsx +++ b/src/components/CommandBar.tsx @@ -1,11 +1,25 @@ -import { Component, createSignal, onCleanup, onMount, For, Show } from "solid-js"; +import { + Component, + For, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, +} from "solid-js"; import { invoke } from "@tauri-apps/api/core"; -import { listen, UnlistenFn } from "@tauri-apps/api/event"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { marked } from "marked"; import DOMPurify from "dompurify"; -import "./CommandBar.css"; import { SparklesIcon } from "./Icons"; import { macroState } from "../stores/macroStore"; +import { commandHistoryStore, type Suggestion } from "../stores/commandHistoryStore"; +import "./CommandBar.css"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- interface StepStatusEvent { step_index: number; @@ -19,15 +33,49 @@ interface StepState { result: string | null; } +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + export const CommandBar: Component = () => { + // -- State ----------------------------------------------------------------- const [isOpen, setIsOpen] = createSignal(false); const [input, setInput] = createSignal(""); const [steps, setSteps] = createSignal([]); const [isProcessing, setIsProcessing] = createSignal(false); const [isPrivacyEnabled, setIsPrivacyEnabled] = createSignal(false); + + /** Index of the currently highlighted suggestion (-1 = none / input has focus) */ + const [activeIndex, setActiveIndex] = createSignal(-1); + let inputRef: HTMLInputElement | undefined; + let listRef: HTMLUListElement | undefined; let unlisten: UnlistenFn | undefined; + // -- Derived --------------------------------------------------------------- + + const suggestions = createMemo(() => + // Only show suggestions when no results are displayed yet + steps().length === 0 ? commandHistoryStore.getSuggestions(input()) : [], + ); + + // Reset active index whenever suggestions change + createEffect(() => { + suggestions(); // subscribe + setActiveIndex(-1); + }); + + // Scroll highlighted item into view + createEffect(() => { + const idx = activeIndex(); + if (idx >= 0 && listRef) { + const item = listRef.children[idx] as HTMLElement | undefined; + item?.scrollIntoView({ block: "nearest" }); + } + }); + + // -- Privacy --------------------------------------------------------------- + const checkPrivacyMode = async () => { try { const enabled = await invoke("is_privacy_mode_enabled"); @@ -37,21 +85,127 @@ export const CommandBar: Component = () => { } }; + // -- Open / Close ---------------------------------------------------------- + + const open = () => { + setIsOpen(true); + setInput(""); + setSteps([]); + setActiveIndex(-1); + void checkPrivacyMode(); + // Wait one tick for the DOM to mount before focusing + setTimeout(() => inputRef?.focus(), 50); + }; + + const close = () => { + setIsOpen(false); + setInput(""); + setSteps([]); + setActiveIndex(-1); + setIsProcessing(false); + }; + + // -- Submit ---------------------------------------------------------------- + + const submitCommand = async (query: string) => { + const trimmed = query.trim(); + if (!trimmed || isProcessing()) return; + + setInput(trimmed); + setIsProcessing(true); + setSteps([]); + commandHistoryStore.push(trimmed); + + try { + await invoke("submit_intent", { input: trimmed }); + + // Phase 6: capture intent for macro recording + if (macroState.isRecording) { + try { + await invoke("capture_intent", { intent: trimmed }); + } catch (captureErr) { + console.error("Failed to capture intent for macro:", captureErr); + } + } + } catch (err) { + console.error(err); + setSteps([{ status: "Failed", result: String(err) }]); + setIsProcessing(false); + } + }; + + const handleSubmit = async (e: Event) => { + e.preventDefault(); + await submitCommand(input()); + }; + + // -- Keyboard navigation --------------------------------------------------- + + const handleInputKeyDown = (e: KeyboardEvent) => { + const list = suggestions(); + + switch (e.key) { + case "ArrowDown": { + e.preventDefault(); + if (list.length === 0) return; + setActiveIndex((prev) => (prev + 1) % list.length); + break; + } + case "ArrowUp": { + e.preventDefault(); + if (list.length === 0) return; + setActiveIndex((prev) => (prev <= 0 ? list.length - 1 : prev - 1)); + break; + } + case "Enter": { + const idx = activeIndex(); + if (idx >= 0 && list[idx]) { + e.preventDefault(); + void submitCommand(list[idx].label); + } + // If no suggestion is highlighted, let the form's onSubmit handle it + break; + } + case "Tab": { + // Auto-complete first suggestion into the input + if (list.length > 0) { + e.preventDefault(); + const pickIdx = activeIndex() >= 0 ? activeIndex() : 0; + const pick = list[pickIdx]; + if (pick) { + setInput(pick.label); + setActiveIndex(-1); + } + } + break; + } + case "Escape": { + e.preventDefault(); + close(); + break; + } + } + }; + + // -- Event listeners ------------------------------------------------------- + onMount(async () => { const handleGlobalKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); - setIsOpen(true); - void checkPrivacyMode(); - setTimeout(() => inputRef?.focus(), 50); + if (isOpen()) { + close(); + } else { + open(); + } } else if (e.key === "Escape" && isOpen()) { e.preventDefault(); - setIsOpen(false); + close(); } }; window.addEventListener("keydown", handleGlobalKeyDown); - void checkPrivacyMode(); + void checkPrivacyMode(); unlisten = await listen("intent://step-status", (event) => { const payload = event.payload; @@ -67,7 +221,8 @@ export const CommandBar: Component = () => { return newSteps; }); - if (payload.step_index === payload.total_steps - 1 && (payload.status === "Done" || payload.status === "Failed")) { + const isLast = payload.step_index === payload.total_steps - 1; + if (isLast && (payload.status === "Done" || payload.status === "Failed")) { setIsProcessing(false); } }); @@ -78,77 +233,128 @@ export const CommandBar: Component = () => { }); }); - const handleSubmit = async (e: Event) => { - e.preventDefault(); - const query = input().trim(); - if (!query || isProcessing()) return; - - setIsProcessing(true); - setSteps([]); + // -- Helpers --------------------------------------------------------------- + const renderMarkdown = (markdown: string): string => { try { - await invoke("submit_intent", { input: query }); - - // Phase 6: If recording, capture this intent - if (macroState.isRecording) { - try { - await invoke("capture_intent", { intent: query }); - } catch (captureErr) { - console.error("Failed to capture intent for macro:", captureErr); - } - } - } catch (err) { - console.error(err); - setSteps([{ status: "Failed", result: String(err) }]); - setIsProcessing(false); - } - }; - - const renderMarkdown = (markdown: string) => { - try { - const html = marked(markdown) as string; - return DOMPurify.sanitize(html); + return DOMPurify.sanitize(marked(markdown) as string); } catch { return DOMPurify.sanitize(markdown); } }; + const categoryLabel: Record = { + history: "history", + navigation: "nav", + search: "search", + system: "system", + macro: "macro", + }; + + // -- Render ---------------------------------------------------------------- + return ( -
{ if (e.target === e.currentTarget) setIsOpen(false); }}> -
-
- + {/* Backdrop */} +