refactor to clean up the codebase (#412)

* chore: clean up repo root for faster README visibility

- Delete unused images (debug_orca.png, orca_3d.jpg, screenshot.png)
- Delete stale design docs from docs/
- Move tsconfig sub-configs, electron-builder config, and vitest config to config/
- Move file-drag.gif to docs/assets/ and design doc to docs/
- Update all path references in package.json, tsconfig.json, and moved configs

* fix: remove stale worktree dialog callback dependency
This commit is contained in:
Jinjing 2026-04-08 22:49:16 -07:00 committed by GitHub
parent 05f84da4e1
commit d546af1b51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 31 additions and 4942 deletions

View File

@ -22,7 +22,7 @@
</p>
<p align="center">
<img src="file-drag.gif" alt="Orca Screenshot" width="800" />
<img src="docs/assets/file-drag.gif" alt="Orca Screenshot" width="800" />
</p>
## Introducing the Orca CLI

View File

@ -13,7 +13,8 @@ module.exports = {
'!electron.vite.config.{js,ts,mjs,cjs}',
'!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}',
'!{.env,.env.*,.npmrc,pnpm-lock.yaml}',
'!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
'!tsconfig.json',
'!config/*'
],
// Why: the CLI entry-point lives in out/cli/ but imports shared modules
// from out/shared/ (e.g. runtime-bootstrap). Both directories must be

View File

@ -1,11 +1,11 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["src/cli/**/*", "src/shared/**/*", "src/main/runtime/runtime-metadata.ts"],
"include": ["../src/cli/**/*", "../src/shared/**/*", "../src/main/runtime/runtime-metadata.ts"],
"compilerOptions": {
"composite": true,
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "out"
"rootDir": "../src",
"outDir": "../out"
}
}

View File

@ -1,6 +1,6 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
"include": ["../electron.vite.config.*", "../src/main/**/*", "../src/preload/**/*", "../src/shared/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]

View File

@ -3,8 +3,8 @@
"compilerOptions": {
"baseUrl": null,
"paths": {
"@renderer/*": ["./src/renderer/src/*"],
"@/*": ["./src/renderer/src/*"]
"@renderer/*": ["../src/renderer/src/*"],
"@/*": ["../src/renderer/src/*"]
}
}
}

View File

@ -1,16 +1,16 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts",
"src/shared/**/*"
"../src/renderer/src/env.d.ts",
"../src/renderer/src/**/*",
"../src/renderer/src/**/*.tsx",
"../src/preload/*.d.ts",
"../src/shared/**/*"
],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"baseUrl": ".",
"baseUrl": "..",
"paths": {
"@renderer/*": ["src/renderer/src/*"],
"@/*": ["src/renderer/src/*"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -1,365 +0,0 @@
# Non-Git Folder Support Plan
## Summary
Orca should support opening non-git folders in a limited "folder mode" so users can still use:
- terminal
- file explorer
- editor
- search
- quick open
Git-dependent features should remain unavailable in this mode:
- creating worktrees
- removing git worktrees
- source control
- branch/base-ref workflows
- pull request and checks integrations
The recommended implementation is to model a non-git folder as a repo with exactly one synthetic worktree representing the folder itself.
## Product Model
### Repo Types
Introduce two repo modes:
- `git`
- `folder`
This can be stored directly on `Repo`, for example as `kind: 'git' | 'folder'`.
Why: Orca currently assumes every connected repo can enumerate git worktrees and branch metadata. Making repo type explicit lets the UI and IPC handlers suppress git-only functionality without scattered heuristics.
### Synthetic Worktree for Folder Mode
For a non-git folder, Orca should synthesize exactly one worktree-like entry:
- `path = repo.path`
- `displayName = repo.displayName`
- `isMainWorktree = true`
- `branch = ''`
- `head = ''`
- `isBare = false`
The worktree ID can remain `${repo.id}::${repo.path}` to preserve the existing store shape.
This ID contract should be treated as stable for folder mode.
Why: much of the app is worktree-centric. Reusing the existing worktree abstraction is less invasive than teaching the editor, terminal, explorer, quick-open, and selection state to operate without any worktree at all.
## User Experience
### On Add / Load Attempt
When a selected folder is not a git repo, Orca should show a confirmation dialog instead of hard-failing:
- Title: `Open as Folder?`
- Body: `This folder is not a Git repository. Orca can open it for editing, terminal, and search, but Git-based features like worktrees, source control, pull requests, and checks will be unavailable.`
- Actions:
- `Open Folder`
- `Cancel`
- optional: `Initialize Git Instead`
Why: users need to understand the capability downgrade before the folder is added to the workspace.
### Left Sidebar
A folder-mode repo should appear in the same left sidebar structure as existing repos, with one row underneath it:
- repo row
- one synthetic worktree row representing the folder
Recommended indicator:
- repo badge or row badge: `Folder` or `Non-Git`
The synthetic row should use a folder-specific subtitle treatment instead of an empty branch slot.
Recommendation:
- primary label: folder display name
- subtitle: `Folder` or a short path label
Do not style this as an error. It is a supported limited mode, not a broken state.
### Disabled / Hidden Git Features
Git-only features should either be hidden or disabled with an explanation.
Recommended behavior:
- `Create Worktree`: disabled or hidden for folder-mode repos
- `Delete Worktree`: do not reuse worktree-delete semantics for folder-mode rows
- Source Control tab: show inline empty state explaining git is required
- Checks tab: show inline empty state explaining a git branch / PR context is required
- branch/base-ref settings: hidden or disabled with explanation
Why: silent disappearance can feel broken. When users intentionally open a folder, the app should explain why a git surface is unavailable.
### Remove vs Delete Semantics
Folder mode must not reuse the current worktree deletion flow.
Recommendation:
- folder-mode row action: `Remove Folder from Orca`
- git worktree row action: `Delete Worktree`
Why: for real git worktrees, the current delete flow can remove filesystem content as part of worktree cleanup. Reusing that path for a synthetic folder row would be unsafe because users may intend to disconnect the folder from Orca, not delete the folder tree from disk.
### Settings Page
Keep folder-mode entries in the existing `Repositories` settings list, but show a type label:
- `Git`
- `Folder`
For folder-mode entries, keep generic settings:
- display name
- badge color
- remove from Orca
Hide or disable git-specific settings:
- default worktree base
- base ref picker/search
- branch-related settings
- PR/check-related settings
Recommended note near the top of a folder-mode settings card:
`Opened as folder. Git features are unavailable for this workspace.`
Why: the Settings page is where users will verify what Orca thinks this connected root is. The settings surface must stay consistent with the sidebar and runtime behavior.
The settings view should also skip eager git-specific checks for folder-mode repos, including hook-related checks unless folder hooks are explicitly supported.
## Functional Scope
### Should Work in Folder Mode
- add folder to Orca
- select the folder entry in the sidebar
- open terminal in that folder
- browse files
- read and edit files
- search files
- quick open files
- open external files within the authorized folder root
- restore terminal/editor session state against the synthetic worktree across app restarts
### Should Not Work in Folder Mode
- create additional worktrees
- remove git worktrees
- branch naming flows
- base branch selection
- git status / diff / stage / unstage / discard
- conflict and rebase state
- PR linking based on branch identity
- checks derived from PR head / branch
- git polling / refresh loops for source-control state
## Implementation Outline
### 1. Add Repo Type
Update the shared repo type to distinguish git repos from folder-mode repos.
Potential shape:
```ts
type Repo = {
id: string
path: string
displayName: string
badgeColor: string
addedAt: number
kind?: 'git' | 'folder'
gitUsername?: string
worktreeBaseRef?: string
hookSettings?: RepoHookSettings
}
```
Why: existing persisted data may not have this field, so `git` should be treated as the default for backward compatibility.
### 2. Change Add-Repo Flow
Current behavior rejects non-git folders.
New behavior:
- detect whether selected path is git
- if yes, add as `kind: 'git'`
- if no, ask for confirmation and add as `kind: 'folder'` if accepted
This applies to:
- renderer add flow
- main-process `repos:add`
- runtime/CLI add flow if it should support folders too
### 3. Synthesize a Worktree for Folder Repos
Update worktree listing so folder-mode repos return a single synthetic worktree instead of `[]`.
This should apply to:
- `worktrees:list`
- `worktrees:listAll`
- any runtime-managed worktree listing APIs
Why: the app currently gates most of the workspace UI on `activeWorktreeId`. Returning no worktrees leaves the app stuck on the landing state even though the filesystem APIs could operate on the folder.
The synthetic worktree ID must be deterministic across restarts so session restore can reattach tabs, active selection, and terminal state correctly.
### 4. Suppress Git-Only Mutations
Guard git-only IPC and UI entry points for folder-mode repos:
- worktree creation
- worktree removal
- source control actions
- base ref queries/search
- branch-based PR/check flows where appropriate
- git status polling / conflict polling / branch compare refresh loops
These guards should fail clearly with a user-facing explanation when reached.
This must cover all create-worktree entry points, not just one visible button:
- landing page CTA
- keyboard shortcut
- add-worktree dialog repo picker / submit path
- any runtime or CLI create path that remains exposed
### 5. Update Sidebar and Settings Presentation
Add a neutral repo-type indicator in:
- left sidebar
- settings repo cards
Ensure git-only controls are hidden or disabled for folder mode.
### 6. Handle Search / Quick Open Fallbacks
Quick open and text search currently fall back to git-based commands when `rg` is unavailable.
Folder-mode support needs one of these decisions:
1. Require `rg` for folder mode and surface a clear error when it is unavailable.
2. Add non-git filesystem fallbacks for file listing and text search.
Recommendation: start with option 1 if we want a smaller implementation.
Why: the product value of folder mode is mainly unlocked on machines where `rg` exists. Non-git fallback walkers/searchers can be added later if needed.
If option 1 is chosen, the product should surface this clearly as a limitation rather than failing silently into empty quick-open or search results.
## Open Questions
### Hooks
Should `orca.yaml` hooks work for folder-mode repos?
Recommendation: do not include them in the initial scope unless there is a strong use case.
Reasoning: current hook behavior is designed around worktree creation/archive lifecycle, which folder mode does not have.
### CLI Semantics
Should the runtime/CLI also allow adding folder-mode repos, or should folder mode be UI-only at first?
Recommendation: keep CLI behavior aligned with the UI if feasible, but this can be phased.
If folder mode is UI-only initially, runtime and CLI commands should fail with an explicit folder-mode / unsupported message rather than the generic `Not a valid git repository`.
### Naming
Should the product call these `folders`, `workspaces`, or still `repositories`?
Recommendation: keep the top-level Settings section as `Repositories`, but label each entry as `Git` or `Folder`.
## Recommended Initial Scope
Ship the smallest coherent version:
- allow adding non-git folders
- show one synthetic worktree row per folder
- allow terminal, explorer, editor, search, and quick open
- show a persistent `Folder` indicator
- disable or hide git-only functionality
- document that worktrees, source control, PRs, and checks are unavailable
This provides immediate utility without trying to redefine Orca's core worktree-oriented architecture.
## Risks and Gaps
### Unsafe Deletion Path
The current worktree delete path should not be reused for folder mode.
Why: deleting a synthetic folder row via worktree removal semantics could remove the real folder contents from disk instead of just disconnecting it from Orca.
### Incomplete Create-Worktree Suppression
Worktree creation must be blocked at every entry point.
Why: if only one button is hidden, users can still reach the flow through shortcuts, the landing page, dialogs, or runtime/CLI paths and hit confusing git-only failures.
### Background Git Polling
Folder mode must opt out of git polling loops.
Why: repeated git status/conflict polling against a non-git folder would create noisy logs, unnecessary subprocess churn, and avoidable UI work.
### Stable Synthetic Identity
Synthetic worktree IDs must remain deterministic.
Why: session restore keys open tabs, active selection, and terminal reattachment off worktree identity.
### Sidebar Presentation
Folder rows need intentional presentation rather than inheriting blank branch UI.
Why: a technically valid row with no branch text will look unfinished and make the mode feel accidental.
### Settings / Hooks Ambiguity
Folder-mode settings must not eagerly present or execute git-specific controls/checks.
Why: the settings surface is the canonical place where users verify the capabilities of a connected root.
### Runtime / CLI Divergence
Folder support needs an explicit cross-surface decision.
Why: allowing folders in the UI but rejecting them in runtime/CLI without a clear explanation will create inconsistent product behavior.
### `rg` as a Practical Requirement
Folder mode depends on `rg` unless non-git fallbacks are added for quick-open and search.
Why: current fallback implementations use `git ls-files` and `git grep`, which do not work for non-git folders.
## Test Checklist
- add-folder confirmation flow
- persisted repo kind / backward compatibility
- synthetic worktree listing for folder repos
- deterministic synthetic worktree ID across restarts
- folder-mode session restore
- folder row uses `Remove Folder from Orca`, not worktree delete semantics
- create-worktree entry points are all gated for folder repos
- git polling is suppressed for folder repos
- settings sections are gated by repo kind
- folder sidebar row renders a non-branch subtitle
- runtime/CLI behavior is explicit for folder repos
- `rg`-missing behavior is covered for folder mode

View File

@ -1,189 +0,0 @@
# Orca CLI Bundled Distribution
## Goal
Ship `orca` as a companion CLI that is bundled with the Orca desktop app.
`orca` will **not** be separately distributed on npm for the initial public release.
## Product Direction
The CLI is version-coupled to the running Orca app because it depends on the app's local runtime RPC contract.
That means the primary user story should be:
1. Install Orca desktop.
2. Register the `orca` command using the platform-native path:
- macOS: from Orca Settings, like VS Code's shell-command install
- Linux package builds: from package install scripts
- Windows installer builds: from installer-managed PATH registration
3. The user can run `orca ...` from any terminal while Orca is running.
This is closer to VS Code's platform-specific model than to a standalone npm package:
- the app is the primary product
- the CLI is an app capability
- CLI installation is explicit and opt-in
- version drift between app and CLI is minimized
## UX
Add a Settings section for CLI installation, likely under Advanced or Developer.
This Settings UI is primarily for macOS and for status/help across platforms.
Suggested UX:
- Setting label: `Command line interface`
- Description: `Allow terminal tools and coding agents to interact with this running Orca app.`
- Toggle:
- off: CLI not registered
- on: Orca prompts to install/register `orca` where that is app-managed
When toggled on for the first time, show a modal like:
- Title: `Set up CLI to work in the terminal`
- Body: `Register "orca" in PATH to enable accessing the "orca" command anywhere from your terminal.`
- Actions:
- `Cancel`
- `Register`
Platform note:
- on macOS, registration may require an administrator prompt because Orca installs `/usr/local/bin/orca` as a symlink to the app-bundled launcher, following the same general pattern VS Code uses for `code`
- on Windows and Linux package builds, registration should prefer installer/package integration over post-install GUI mutation
After success:
- show the installed path
- provide `Reinstall`
- provide `Remove from PATH`
- optionally provide `Copy setup instructions`
If installation fails:
- show the exact install location
- show the exact shell snippet or manual step needed
## Distribution Model
The packaged app should contain the CLI artifact and launcher files in stable internal locations.
Registration should follow the verified VS Code model by platform:
- macOS: app-driven shell command install
- Linux package builds: package-managed symlink
- Windows installer builds: installer-managed PATH registration
Why:
- simpler macOS story
- clearer user consent where the app owns registration
- more robust Linux/Windows behavior by using package/installer hooks
- keeps the CLI tied to the installed app version
## Installation Strategy
### macOS
Follow the VS Code pattern:
- ship an app-bundled launcher script
- install `/usr/local/bin/orca` as a symlink to that launcher
- if needed, prompt for elevation explicitly from the app
This avoids shell rc editing and gives a stable command location.
### Linux
For package-managed builds, follow the VS Code pattern:
- ship an app-bundled launcher script
- install `/usr/bin/orca` from the package as a symlink to that launcher
For AppImage and other non-package-managed distributions:
- do not assume a robust global PATH install exists
- either fall back to a user-level wrapper with manual instructions
- or disable CLI install in v1 if path stability is not good enough
### Windows
Follow the VS Code pattern:
- ship `orca.cmd` and related launcher files under `<install dir>\\bin`
- let the installer add `<install dir>\\bin` to PATH
- optionally register Windows App Paths for Explorer/address bar launching
This is more robust than trying to add PATH from the running GUI app after install.
## Runtime Expectations
The bundled `orca` command remains a thin client:
- it reads Orca runtime metadata
- it connects to the local Orca runtime endpoint
- it fails clearly if Orca is not running or is incompatible
Runtime startup rules:
- ordinary `orca ...` commands do not auto-launch Orca
- `orca open` explicitly launches Orca and waits for the runtime
- the CLI must detect stale runtime metadata before trusting a local runtime
- the CLI should only proceed once it observes a healthy current runtime, not just the existence of a metadata file
- `orca open` should be idempotent and cheap when Orca is already running
Error and preflight rules:
- when the runtime is missing, ordinary commands should explicitly tell the user to run `orca open`
- `orca status --json` should be the primary preflight command for agents and scripts
- `orca status --json` should distinguish:
- app not running
- app starting
- runtime reachable
- runtime reachable but terminal graph not ready
This design does not turn `orca` into a standalone daemon or independently useful tool.
The Orca desktop app remains the runtime owner even when the CLI launches it on demand.
The Orca desktop app remains the runtime owner, and `orca open` is the explicit way to start it from the CLI.
## Compatibility Rules
Because the CLI is bundled with the app:
- CLI version should match app version
- `orca version` should report both CLI and runtime compatibility details
- incompatible runtime versions should fail with a precise error
This is one of the main reasons not to ship npm-first.
## Security Notes
Bundling the CLI with the app does not change the runtime security model.
The important properties remain:
- local-only IPC
- runtime auth token
- user-scoped metadata and endpoint permissions
- no remote network listener by default
The launcher registration should only point to the bundled CLI. It should not expose any extra background service.
## Non-Goals
For the initial version:
- no separate npm distribution
- no remote CLI-to-Orca connectivity
- no standalone daemon mode independent of the Orca app
## Recommended First Slice
1. Bundle the CLI artifact and launcher files into packaged app builds.
2. macOS: add a Settings toggle and modal for shell-command registration.
3. Linux package builds: register `/usr/bin/orca` from packaging.
4. Windows installer builds: register `<install dir>\\bin` on PATH.
5. Show install/status/help flows in Settings.
This is the smallest coherent implementation that matches the desired product story.

View File

@ -1,105 +0,0 @@
# Orca CLI Focused V1 Status
## Purpose
This document records the focused Orca CLI v1 that is now implemented.
The broader design docs still describe a larger eventual CLI surface. This file exists so maintainers can see:
- what is actually shipped now
- what has been intentionally deferred
- the one remaining gap if we want to call the worktree/terminal surface fully complete
## Focused V1 Goal
The focused v1 CLI is intentionally narrow.
It optimizes for Orca's core differentiator:
- managing parallel worktrees from a running Orca editor
- discovering live terminals in those worktrees
- reading and replying to those terminals from an agent
This keeps the public surface centered on Orca's orchestration value instead of reimplementing every editor-adjacent capability in the first CLI release.
## Implemented Now
The following commands are implemented against the running Orca app:
- `orca status`
- `orca repo list`
- `orca repo add`
- `orca repo show`
- `orca repo set-base-ref`
- `orca repo search-refs`
- `orca worktree list`
- `orca worktree show`
- `orca worktree current`
- `orca worktree create`
- `orca worktree set`
- `orca worktree rm`
- `orca worktree ps`
- `orca terminal list`
- `orca terminal show`
- `orca terminal read`
- `orca terminal send`
- `orca terminal wait --for exit`
- `orca terminal stop`
## What These Commands Cover
Focused v1 supports the complete agent loop for worktree orchestration:
1. Inspect current Orca runtime availability.
2. Discover the enclosing Orca-managed worktree from the current shell directory.
3. Discover repos indirectly through existing worktrees and summary views.
4. Create a new worktree in a chosen repo.
5. Attach or update worktree metadata like display name, linked issue, and comment.
6. Inspect many worktrees at once with `worktree ps`.
7. Discover live terminal handles in a worktree.
8. Read terminal output with bounded token-efficient reads.
9. Send input back to the terminal.
10. Stop live terminals for a worktree when needed.
It also covers the adjacent setup tasks needed to make worktree creation usable:
- discover and inspect repos already known to Orca
- add a repo path to Orca
- set or inspect a repo base ref
## Intentionally Omitted Wait Modes
Focused v1 includes `terminal wait --for exit`.
The richer wait modes remain intentionally out of scope:
- `--for input`
- `--for idle`
- `--for output`
Those modes require stronger runtime instrumentation and should not be shipped as guesses.
## Intentionally Deferred Beyond Focused V1
These command groups are still deferred:
- `git`
- `gh`
They may still be useful later, but they are not required for the core Orca CLI story.
`git` and `gh` are still deferred because they would further expand the public runtime surface and deserve a separate pass on selector shape, output contracts, and failure handling.
The design reason is simple:
- agents often already have other tools for file, git, GitHub, and search access
- Orca is differentiated by worktree and live terminal orchestration
- broadening the CLI too early would increase surface area faster than it increases unique agent capability
## Relationship To Other Docs
- [orca-cli-v1-spec.md](./orca-cli-v1-spec.md) defines the stricter command contract and runtime assumptions.
- [orca-runtime-layer-design.md](./orca-runtime-layer-design.md) explains the runtime architecture that makes the live terminal surface safe.
- [orca-cli-bundled-distribution.md](./orca-cli-bundled-distribution.md) explains how the bundled desktop-app installation and PATH registration model works.
This status file is the source of truth for the currently implemented focused v1 scope.

View File

@ -1,798 +0,0 @@
# Orca CLI V1 Spec
## Goal
Define the first strict `orca` CLI contract for agents.
This spec focuses on:
- exact commands
- exact selector grammar
- exact handle semantics
- exact JSON contract
- what is in v1 now
- what is explicitly deferred because the current runtime does not yet support it cleanly
This document is intended to be implementation-facing.
## Scope
The CLI connects to a running Orca editor.
The v1 contract is split into two buckets:
- `v1-now`: can be grounded in current Orca persistence and IPC behavior with limited new plumbing
- `v1-runtime-layer`: desirable v1 public contract, but requires a shared runtime/orchestration layer before it is safe to ship
## Global Rules
### Output modes
All agent-facing commands must support `--json`.
When `--json` is used:
- stdout contains exactly one JSON object
- stdout contains no progress text, logs, or prose
- stderr is reserved for failures and unexpected diagnostics
- non-zero exit code indicates command failure
Human-readable output may exist without `--json`, but `--json` is the normative contract for agents.
### Metadata
All `--json` responses include:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"requestId": "req_123"
}
}
```
Commands that depend on the live runtime layer also include:
```json
{
"_meta": {
"runtimeId": "runtime_abc123"
}
}
```
### Errors
All failures in `--json` mode must return:
```json
{
"_meta": {
"requestId": "req_123"
},
"error": {
"code": "selector_not_found",
"message": "No worktree matched selector \"branch:feature/foo\".",
"retryable": false
}
}
```
Minimum standard error codes:
- `orca_not_running`
- `runtime_unavailable`
- `selector_not_found`
- `selector_ambiguous`
- `terminal_handle_stale`
- `terminal_not_found`
- `repo_not_found`
- `worktree_not_found`
- `not_supported_in_v1`
- `invalid_argument`
## Selectors
Selectors are command-time identifiers.
### Repo selector grammar
Explicit forms:
- `id:<repo-id>`
- `path:<absolute-path>`
- `name:<display-name>`
Bare fallback order:
1. exact repo id match
2. exact absolute path match
3. exact display name match
If more than one repo matches a bare selector, fail with `selector_ambiguous`.
### Worktree selector grammar
Explicit forms:
- `id:<worktree-id>`
- `path:<absolute-path>`
- `branch:<branch-name>`
- `issue:<number>`
Bare fallback order:
1. exact worktree id match
2. exact absolute path match
3. exact branch name match
If more than one worktree matches a bare selector, fail with `selector_ambiguous`.
`issue:<number>` must fail with `selector_ambiguous` if multiple worktrees share the same linked issue.
### Terminal selector grammar
There is no durable selector for repeated live interaction in v1.
Discovery returns runtime handles. Follow-up commands use:
- `--terminal <handle>`
Human-friendly targeting like `title:<name>` may be useful later, but it is not part of the strict repeated-interaction contract.
## Runtime Handles
Handles identify live terminal targets.
Rules:
- handles are opaque
- handles are scoped to a specific `runtimeId`
- handles are ephemeral by default
- runtime restart or renderer reload may invalidate all existing handles
- callers must reacquire handles after reconnect/reload unless the runtime explicitly guarantees continuity
- stale handles must fail with `terminal_handle_stale`
Example stale-handle error:
```json
{
"_meta": {
"runtimeId": "runtime_new456",
"requestId": "req_123"
},
"error": {
"code": "terminal_handle_stale",
"message": "The terminal handle is no longer valid for the current Orca runtime.",
"retryable": true
}
}
```
## Commands
## `orca status`
Purpose:
- confirm Orca is running
- return current runtime identity
Status:
- `v1-runtime-layer`
Example:
```bash
orca status --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"runtimeId": "runtime_abc123",
"requestId": "req_123"
},
"status": {
"running": true,
"runtimeAvailable": true,
"capabilities": {
"repo": true,
"worktree": true,
"file": true,
"search": true,
"git": true,
"gh": true,
"terminal": false,
"worktreePs": false
}
}
}
```
Implementation note:
- a minimal “is Orca running” probe may be possible earlier
- the strict `status` contract in this spec assumes the runtime layer exists and can issue a real `runtimeId`
## `orca repo list`
Status:
- `v1-now`
```bash
orca repo list --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"requestId": "req_123"
},
"repos": [
{
"id": "repo_1",
"path": "/abs/repo",
"displayName": "orca",
"worktreeBaseRef": "origin/main"
}
]
}
```
## `orca repo add`
Status:
- `v1-now`
```bash
orca repo add --path /abs/repo --json
```
## `orca repo show`
Status:
- `v1-now`
```bash
orca repo show --repo path:/abs/repo --json
```
## `orca repo set-base-ref`
Status:
- `v1-now`
```bash
orca repo set-base-ref --repo id:repo_1 --ref origin/main --json
```
## `orca repo search-refs`
Renamed from `search-base-refs` for better verb consistency.
Status:
- `v1-now`
```bash
orca repo search-refs --repo id:repo_1 --query main --json
```
## `orca worktree list`
Full listing command.
Status:
- `v1-now`
```bash
orca worktree list --repo id:repo_1 --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"requestId": "req_123"
},
"worktrees": [
{
"id": "repo_1::/abs/wt",
"repoId": "repo_1",
"path": "/abs/wt",
"branch": "refs/heads/feature/foo",
"displayName": "Feature Foo",
"linkedIssue": 123,
"comment": "parser work"
}
]
}
```
## `orca worktree ps`
Compact orchestration summary command.
Status:
- `v1-runtime-layer`
Rationale:
- desirable public contract
- requires a shared live-runtime summary service, not just persisted state
```bash
orca worktree ps --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"runtimeId": "runtime_abc123",
"requestId": "req_123"
},
"worktrees": [
{
"id": "repo_1::/abs/wt",
"repo": "orca",
"branch": "feature/foo",
"linkedIssue": 123,
"unread": false,
"liveTerminals": 2,
"status": "active"
}
]
}
```
## `orca worktree show`
Status:
- `v1-now`
```bash
orca worktree show --worktree branch:feature/foo --json
```
Focused v1 also accepts `active` / `current` as CLI-only shortcuts for worktree
selectors. The CLI resolves them from the caller's current directory and sends a
`path:` selector to the runtime.
## `orca worktree current`
Status:
- `v1-now`
```bash
orca worktree current --json
```
## `orca worktree create`
Status:
- `v1-now`
This must preserve current editor behavior:
- sanitize name
- compute branch name from settings
- reject branch conflicts
- best-effort reject historical PR head-name reuse
- compute path under workspace root
- use chosen/default base ref
- create worktree
- best-effort apply linked issue/comment metadata
```bash
orca worktree create --repo path:/abs/repo --name feature-foo --issue 123 --comment "parser work" --json
```
## `orca worktree set`
Status:
- `v1-now`
```bash
orca worktree set --worktree branch:feature/foo --display-name "Parser" --issue 123 --comment "parser work" --json
orca worktree set --worktree active --comment "parser work" --json
```
## `orca worktree rm`
Status:
- `v1-now`
```bash
orca worktree rm --worktree path:/abs/wt --force --json
```
## `orca terminal list`
Purpose:
- discover live terminal handles for a worktree
Status:
- `v1-runtime-layer`
Rationale:
- requires shared orchestration service over renderer-owned layout plus main-owned PTYs
```bash
orca terminal list --worktree id:repo_1::/repo/.worktrees/feature-foo --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"runtimeId": "runtime_abc123",
"requestId": "req_123"
},
"terminals": [
{
"handle": "term_a2",
"title": "claude",
"status": "running",
"worktree": "branch:feature/foo",
"tabId": "tab_1",
"tabTitle": "Claude Code",
"leafId": "leaf_2",
"preview": "I updated the parser. Do you want me to run the full suite?"
}
]
}
```
Optional:
- `--layout` may include secondary tab/layout context when the caller needs it
## `orca terminal show`
Metadata-only.
Status:
- `v1-runtime-layer`
```bash
orca terminal show --terminal term_a2 --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"runtimeId": "runtime_abc123",
"requestId": "req_123"
},
"terminal": {
"handle": "term_a2",
"title": "claude",
"status": "running",
"cwd": "/abs/wt",
"tabId": "tab_1",
"tabTitle": "Claude Code",
"leafId": "leaf_2",
"lastOutputAt": 1712345678,
"lastInputAt": 1712345600,
"preview": "I updated the parser. Do you want me to run the full suite?"
}
}
```
## `orca terminal read`
Content-only, bounded.
Status:
- `v1-runtime-layer`
```bash
orca terminal read --terminal term_a2 --json
```
Response:
```json
{
"_meta": {
"orcaVersion": "1.0.0",
"runtimeId": "runtime_abc123",
"requestId": "req_123",
"truncated": false
},
"terminal": {
"handle": "term_a2",
"status": "running",
"tail": [
"Running targeted tests...",
"3 passed",
"I updated the parser and fixed the failing snapshot."
],
"nextCursor": null
}
}
```
## `orca terminal send`
Status:
- `v1-runtime-layer`
Supported forms:
- `--text <text>`
- `--enter`
- `--interrupt`
```bash
orca terminal send --terminal term_a2 --text "continue" --json
```
## `orca terminal wait`
Status:
- `exit`: `v1-runtime-layer`
- `input`: deferred
- `idle`: deferred
- `output`: deferred
Rationale:
- `exit` can be grounded in PTY exit events
- the others require new runtime instrumentation and/or heuristics
```bash
orca terminal wait --terminal term_a2 --for exit --json
```
For unsupported wait modes in initial v1:
```json
{
"_meta": {
"requestId": "req_123"
},
"error": {
"code": "not_supported_in_v1",
"message": "terminal wait --for input requires runtime instrumentation that is not available in v1.",
"retryable": false
}
}
```
## `orca terminal stop`
Stop live terminals for a worktree.
Status:
- `v1-runtime-layer`
This replaces `worktree shutdown` in the primary surface because the action is terminal-oriented.
Initial supported target:
- `--worktree <selector>`
Later extension:
- `--terminal <handle>`
## `orca file ls`
Status:
- `v1-now`
```bash
orca file ls --worktree id:repo_1::/repo/.worktrees/feature-foo --path src --json
```
## `orca file read`
Status:
- `v1-now`
```bash
orca file read --worktree id:repo_1::/repo/.worktrees/feature-foo --path src/main.ts --json
```
## `orca file write`
Status:
- `v1-now`
```bash
orca file write --worktree id:repo_1::/repo/.worktrees/feature-foo --path src/main.ts --stdin --json
```
## `orca file create`
Status:
- `v1-now`
## `orca file mkdir`
Status:
- `v1-now`
## `orca file rename`
Status:
- `v1-now`
## `orca file rm`
Status:
- `v1-now`
## `orca file stat`
Status:
- `v1-now`
## `orca search text`
Status:
- `v1-now`
```bash
orca search text --worktree id:repo_1::/repo/.worktrees/feature-foo --query worktree --json
```
## `orca search files`
Status:
- `v1-now`
Keep separate from `file ls --query` in v1.
Rationale:
- clearer distinction between tree listing and search behavior
- aligns with existing product/search mental model
## `orca git status`
Status:
- `v1-now`
## `orca git diff`
Status:
- `v1-now`
## `orca git stage`
Status:
- `v1-now`
## `orca git unstage`
Status:
- `v1-now`
## `orca git discard`
Status:
- `v1-now`
## `orca git branch-compare`
Status:
- `v1-now`
## `orca gh pr`
Status:
- `v1-now`
## `orca gh issue`
Status:
- `v1-now`
## `orca gh issues`
Status:
- `v1-now`
## `orca gh checks`
Status:
- `v1-now`
## Explicitly Deferred From V1
Deferred:
- tab creation and closure
- pane split and close
- tab reordering
- tab colors
- unread/read metadata commands in the core surface
- terminal wait modes other than `exit`
Rationale:
- either too UI-shaped
- or not grounded in current backend ownership
## Recommended Implementation Order
1. repo/worktree/file/search/git/gh `v1-now` commands
2. shared runtime/orchestration layer with `runtimeId`
3. `status`
4. terminal handle issuance and validation
5. `terminal list/show/read/send`
6. `worktree ps`
7. `terminal wait --for exit`
## Recommendation
This spec is the contract to review next.
It is intentionally strict and narrower than the broader design docs:
- selectors are formal
- handles are ephemeral by default
- JSON is contractual
- terminal features are split between `v1-now` and `v1-runtime-layer`
That should let us optimize for both agent clarity and implementation honesty.

View File

@ -1,897 +0,0 @@
# Orca Runtime Layer Design
## Goal
Define the shared runtime/orchestration layer that makes the Orca CLI's live terminal contract implementable.
This layer is required because the current codebase splits ownership across:
- Electron main process:
- PTY process lifecycle
- PTY IDs
- PTY data and exit events
- Renderer:
- tabs
- split-pane layout
- active pane
- terminal titles
- buffered offscreen writes
- unread/activity side effects
- Persistence:
- repo config
- worktree metadata
- saved terminal layout snapshots
- saved tab state
That split is fine for the editor UI, but it is not enough for a CLI that needs:
- a stable `runtimeId`
- live terminal handles
- safe stale-handle rejection
- compact live summaries like `worktree ps`
- terminal reads and writes that do not depend on renderer-local pane IDs
- a real external transport path from the `orca` CLI into the running app
## Problem Statement
Today there is no single shared service that can answer:
- what live terminal targets currently exist
- which worktree/tab/leaf each target belongs to
- which PTY each target is connected to
- what a safe public handle for that live target should be
- whether a handle is still valid
Relevant current ownership:
- PTY ownership: [../src/main/ipc/pty.ts](../src/main/ipc/pty.ts)
- Renderer tab state: [../src/renderer/src/store/slices/terminals.ts](../src/renderer/src/store/slices/terminals.ts)
- Pane lifecycle and PTY connection: [../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts](../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts)
- Pane -> PTY wiring: [../src/renderer/src/components/terminal-pane/pty-connection.ts](../src/renderer/src/components/terminal-pane/pty-connection.ts)
- Leaf ID serialization: [../src/renderer/src/components/terminal-pane/layout-serialization.ts](../src/renderer/src/components/terminal-pane/layout-serialization.ts)
## Non-Goals
This runtime layer does not try to:
- replace the renderer store
- replace the PTY implementation
- make pane IDs durable across reloads
- implement every future terminal automation feature in one step
The first purpose is to provide a shared control plane for the current app and CLI.
## Current Constraints To Preserve
The design should stay honest about three existing realities:
1. Orca is effectively single-window today.
The current PTY IPC wiring is attached to one `mainWindow` and forwards PTY data back through that window's `webContents`.
2. Leaf-level terminal state is not yet first-class renderer state.
Today Orca persists tab-level state and layout snapshots, but it does not persist a canonical renderer-side record for each leaf's title, preview, or screen snapshot.
3. Hidden terminals already accumulate deferred output in the renderer.
The runtime layer cannot assume every hidden leaf has a continuously updated visible-screen model without adding new explicit publication behavior.
This means the first runtime layer should optimize for correctness in the current single-window app before trying to generalize further.
## Core Design Principle
The runtime layer should be a main-process service that maintains a live registry built from:
- main-process PTY events
- renderer lifecycle registrations
- persisted repo/worktree metadata when useful
It should be the only place that:
- issues live terminal handles
- validates or rejects handles
- answers live summary queries
- exposes terminal read/write operations to the CLI
This avoids editor/CLI drift.
## Source Of Truth Boundaries
The runtime layer must not replace existing durable sources of truth.
Durable truth remains:
- Git for worktree existence and branch state
- `Store` persistence for repo config, worktree metadata, and saved session snapshots
Live truth becomes:
- runtime layer for terminal handles, live leaf/PTy mappings, and live summaries
This means:
- the runtime layer may cache and index persisted state
- but it should not become the canonical persistence owner for repo or worktree metadata
- renderer/UI code should stop inventing separate live-terminal contracts once the runtime layer exists
## Why Main Process Ownership
The runtime layer should live in the main process, not the renderer.
Reasons:
- the CLI will need to call into it even when no renderer component currently has focus
- PTY ownership already lives in the main process
- handle validation and stale-handle rejection are security and correctness boundaries
- renderer reloads should not destroy the authoritative registry object itself, even if they invalidate live handles
The renderer should publish registrations and updates into the runtime layer, not own the runtime layer.
Scope note:
- v1 runtime-layer design assumes one active Orca window
- multi-window support should be treated as a later extension, not an implicit requirement of the first implementation
## CLI Transport Boundary
The runtime layer also needs a transport boundary for the external CLI.
Recommendation:
- expose a local-only RPC endpoint from the main process
- use:
- Unix domain socket on macOS/Linux
- named pipe on Windows
- persist connection metadata in Orca user data:
- `runtimeId`
- endpoint path
- auth token
- pid
Suggested flow:
1. Orca main process starts the runtime service.
2. Orca opens the local RPC endpoint.
3. Orca writes connection metadata.
4. CLI reads connection metadata.
5. CLI connects locally and authenticates.
6. Runtime service handles CLI requests against the live registry.
Security properties:
- local machine only
- random auth token required
- stale pid/socket detection on startup
Why this matters:
- Electron renderer IPC is not the CLI transport
- the main process runtime service is the right authority for requests coming from the external CLI
## Runtime Identity
The runtime layer must generate a `runtimeId` when Orca launches.
Rules:
- `runtimeId` is unique per Orca process lifetime
- any full app restart creates a new `runtimeId`
- renderer reloads do not necessarily require a new `runtimeId`, but they may invalidate all live handles
Recommendation:
- keep `runtimeId` stable for the lifetime of the main Electron process
- separately track a renderer graph epoch that increments only when the renderer graph is explicitly reset or replaced in a way that breaks existing leaf mappings
Why:
- `runtimeId` is the coarse session identity exposed in CLI responses
- the renderer graph epoch is the finer invalidation boundary for ephemeral handles
CLI-facing simplification:
- handles are treated as ephemeral by default
- if the live graph is rebuilt in a way that invalidates mappings, all prior handles become stale
## Public Responsibilities
The runtime layer must support:
1. `status`
2. live terminal discovery
3. canonical selector resolution for CLI-facing repo/worktree lookups
4. handle issuance
5. handle validation
6. handle-based terminal reads
7. handle-based terminal writes
8. compact worktree live summaries
## Internal Data Model
The runtime layer should maintain the following registry objects.
### RuntimeState
```ts
type RuntimeState = {
runtimeId: string
rendererGraphEpoch: number
graphStatus: 'ready' | 'reloading' | 'unavailable'
authoritativeWindowId: number | null
}
```
### RegisteredTab
```ts
type RegisteredTab = {
tabId: string
worktreeId: string
title: string | null
activeLeafId: string | null
layout: TerminalPaneLayoutNode | null
lastSeenAt: number
}
```
### RegisteredLeaf
```ts
type RegisteredLeaf = {
tabId: string
worktreeId: string
leafId: string
paneRuntimeId: number
ptyId: string | null
ptyGeneration: number
lastOutputAt: number | null
lastExitCode: number | null
preview: string
tailBuffer: string[]
connected: boolean
writable: boolean
lastSeenAt: number
}
```
### TerminalHandleRecord
```ts
type TerminalHandleRecord = {
handle: string
runtimeId: string
rendererGraphEpoch: number
worktreeId: string
tabId: string
leafId: string
ptyId: string | null
ptyGeneration: number
createdAt: number
}
```
Why these fields matter:
- `leafId` gives stable layout identity within the current renderer graph
- `ptyId` is needed for actual write routing
- `ptyGeneration` prevents a restarted PTY in the same leaf from inheriting an old handle
- `tailBuffer` powers `terminal read`
- `preview` powers cheap discovery and `worktree ps`
- `writable` prevents CLI writes from racing against renderer-driven close or detach flows
## Handle Semantics
Handles are synthetic public identifiers issued by the runtime layer.
Rules:
- handles are opaque
- handles bind to:
- `runtimeId`
- `rendererGraphEpoch`
- `worktreeId`
- `tabId`
- `leafId`
- current `ptyId`
- current `ptyGeneration`
- handles are invalid if:
- `runtimeId` no longer matches
- `rendererGraphEpoch` has advanced past the handle's epoch
- the leaf registration no longer exists
- the leaf now points at a different `ptyId` or `ptyGeneration`
- the handle's current target cannot be resolved
This is intentionally strict.
Why:
- the CLI must never silently retarget input to a different live terminal
- handle invalidation should happen only for real remapping events, not every routine reconciliation pass
Stale-handle ergonomics:
- stale-handle errors should include the current `runtimeId`
- if the target leaf still exists but the specific handle is stale, the runtime layer may include a rediscovery hint scoped to that worktree or leaf
- the runtime layer should not implement a magical handle refresh that silently retargets the caller
## Event Sources
The runtime layer needs two classes of inputs.
### A. Main-process PTY events
Current source:
- [../src/main/ipc/pty.ts](../src/main/ipc/pty.ts)
Add runtime-layer integration points for:
- PTY spawned
- PTY data
- PTY exit
- PTY kill
What the runtime layer should record:
- `ptyId`
- PTY generation changes for a leaf
- data arrival timestamps
- exit code
- a bounded text tail buffer
### B. Renderer graph publication
The renderer already knows:
- when a tab exists
- what the saved and current layout is
- which leaf is active
- which pane has which current PTY
- titles derived from OSC updates
The runtime layer needs renderer-published graph state like:
- which tabs currently exist
- which leaves currently exist
- which worktree each tab belongs to
- which PTY each leaf is currently attached to
- which leaf is active within each tab
- what the current layout tree is for each tab
These are not current public APIs. They should be introduced as an explicit internal IPC channel.
Important source-of-truth rule:
- leaf records in the runtime registry are authoritative only when published by the renderer's full-graph sync
- persisted session state and renderer store state remain advisory inputs for tabs and worktrees, not a substitute for live leaf publication
## Suggested Internal IPC Contract
These are not CLI commands. They are editor-runtime plumbing.
### Renderer -> Main
- `runtime:syncWindowGraph`
Recommendation:
- start with one idempotent full-graph message as the source of truth for renderer-owned tab and leaf structure
- allow the renderer to resend the full graph whenever tab, layout, active-leaf, or PTY attachment state changes
- add narrower incremental messages later only if performance proves it necessary
Suggested payloads:
```ts
type RuntimeSyncWindowGraph = {
windowId: number
tabs: Array<{
tabId: string
worktreeId: string
title: string | null
activeLeafId: string | null
layout: TerminalPaneLayoutNode | null
}>
leaves: Array<{
tabId: string
worktreeId: string
leafId: string
paneRuntimeId: number
ptyId: string | null
}>
}
```
Why payloads matter:
- this is where ownership boundaries become real
- if these messages stay vague, implementation will drift back into ad hoc IPC
- treat full-graph sync as both the normal publication path and the repair path if an earlier renderer event was missed
Single-window v1 rule:
- Orca should accept exactly one authoritative publishing window in v1
- if a second window starts publishing, the runtime layer should reject it or mark the graph unavailable until the conflict is resolved
- `windowId` exists to make that restriction explicit now and extensible later
### Main -> Renderer
Only if needed for editor features:
- `runtime:handleInvalidated`
- `runtime:statusChanged`
The initial version can keep the runtime layer mostly main-owned and query-driven.
## How The Renderer Should Integrate
The renderer should publish runtime graph snapshots from the same places that already own lifecycle.
Recommendation:
- start with event-driven full snapshot publication
- do not add granular register/update/remove messages unless profiling shows the full graph is too expensive
- build the sync payload in one renderer-side collector/helper, and let lifecycle sites only schedule that helper rather than hand-assembling payload fragments
Why:
- renderer lifecycle is complex
- split/close/reload sequences are easy places to lose one incremental event
- a full snapshot lets the main process repair drift instead of accumulating ghost leaves or stale mappings
- the initial implementation needs correctness more than minimal event chatter
- a single collector reduces the risk that `runtime:syncWindowGraph` logic gets duplicated across store and pane lifecycle code
### Tab lifecycle
Source:
- [../src/renderer/src/store/slices/terminals.ts](../src/renderer/src/store/slices/terminals.ts)
Integration:
- when a tab is created, changed, or closed, republish the full graph
- when layout snapshot changes, republish the full graph
### Leaf/pane lifecycle
Source:
- [../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts](../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts)
- [../src/renderer/src/components/terminal-pane/pty-connection.ts](../src/renderer/src/components/terminal-pane/pty-connection.ts)
Integration:
- on pane created or closed, republish the full graph
- on active pane change, republish the full graph
- on PTY spawn or detach, republish the full graph
- on PTY respawn for an existing leaf, republish the full graph and let the runtime layer advance `ptyGeneration`
### Why the code needs comments
When this runtime graph publication is added, it needs comments explaining why Orca duplicates renderer lifecycle into a main-process registry:
- the CLI needs a shared live control plane
- pane IDs are renderer-local and not safe as a public contract
- handle validation must not depend on renderer-local assumptions
Those are design-driven constraints and should be documented in code comments per `AGENTS.md`.
## How The Main PTY Layer Should Integrate
Current PTY code:
- [../src/main/ipc/pty.ts](../src/main/ipc/pty.ts)
Required additions:
- publish PTY spawn/exit/data events to the runtime service
- maintain a lightweight PTY registry accessible to the runtime service
Suggested PTY event shape:
```ts
type RuntimePtySpawned = {
ptyId: string
loadGeneration: number
}
type RuntimePtyData = {
ptyId: string
data: string
at: number
}
type RuntimePtyExit = {
ptyId: string
exitCode: number
at: number
}
```
The runtime service should not parse terminal DOM state. It should build read models from:
- PTY output bytes
- renderer registrations
## Selector Resolution Service
The runtime layer should own canonical selector resolution for repo and worktree selectors rather than leaving it to the CLI frontend.
Why:
- selector semantics are part of the public contract, not presentation glue
- if the CLI resolves selectors differently from editor-driven integrations, Orca will drift
This service should:
- accept tagged selectors like `id:`, `path:`, `branch:`, and `issue:`
- reject ambiguous bare values with structured ambiguity errors
- return stable repo or worktree identities that downstream runtime operations can use
The terminal layer should remain handle-first once discovery is complete, but selector resolution must still be runtime-owned for consistent discovery semantics.
## Single-Window Assumption In V1
The current app is effectively single-window, and the first runtime layer should embrace that instead of pretending multi-window support already exists.
Recommendation:
- one runtime service per app process
- one CLI target runtime per app process
- v1 should permit only one authoritative publishing window
- if multiple windows appear later, they may register into the same runtime service only after Orca has an explicit multi-window routing model
The runtime layer should not issue window-scoped handles.
## Terminal Read Model
`terminal show` and `terminal read` need cheap buffers.
The runtime layer should maintain:
- `preview`
- `tailBuffer`
### Preview
Purpose:
- cheap discovery
- worktree summary
Strategy:
- derived from most recent meaningful lines
- capped to a few hundred characters
- should be main-owned once PTY data reaches the runtime service
### Tail buffer
Purpose:
- powers `terminal read`
Strategy:
- bounded ring buffer by line count and char count
- updated from PTY output
### Visible screen snapshots
Visible screen snapshots should be treated as a later enhancement, not a required v1 runtime primitive.
Why:
- hidden panes currently accumulate deferred output in `pendingWritesRef`, so a renderer-owned "current screen" is not uniformly trustworthy across visible and hidden leaves
- the CLI needs an honest contract more than a more ambitious but misleading one
So:
- `terminal show` should rely on runtime-owned metadata plus preview
- initial `terminal read` should rely on runtime-owned PTY tail data only
- if Orca later adds explicit visible-screen publication, that can be layered on as an optional richer read mode rather than a v1 requirement
## Drift Recovery
The runtime layer should assume registrations can drift.
Examples:
- renderer reload before all leaf removals are delivered
- pane closes while PTY exit is also firing
- a restored tab graph replaces leaf IDs
Recovery strategy:
1. event-driven full graph sync for correctness
2. explicit epoch bump only when the renderer graph is reset or replaced incompatibly
3. reject stale handles instead of trying to preserve them through remaps
This is another reason handles should be treated as ephemeral by default.
## Reload And Unavailable States
The runtime layer needs an explicit graph-availability state rather than assuming the renderer graph is always present when PTYs exist.
Recommendation:
- enter `graphStatus: 'reloading'` when the authoritative renderer is tearing down or the window is reloading
- enter `graphStatus: 'unavailable'` if no authoritative renderer graph is available
- return to `graphStatus: 'ready'` only after a fresh successful `runtime:syncWindowGraph`
Why:
- the current PTY layer can briefly keep PTYs alive while the renderer graph is gone or rebuilding
- CLI calls should fail closed during that window instead of acting on stale registry state
Behavior:
- `terminal list`, `terminal show`, `terminal read`, and `terminal send` should reject with a distinct runtime-unavailable error while `graphStatus != 'ready'`
- `status` should still work and report why the live terminal graph is unavailable
## `worktree ps` Summary Model
`worktree ps` should be powered by the runtime layer, not persistence alone.
For each worktree, it should summarize:
- repo
- branch
- linked issue
- unread metadata
- live terminal count
- whether any terminal is attached to a live PTY
- last output time if known
- recent preview if useful
Recommendation:
- compute this in the runtime service from:
- persisted worktree metadata
- live tab/leaf registrations
- PTY connectivity
Batch-read note:
- `worktree ps` is the preferred cheap batched live summary for many worktrees in v1
- Orca should avoid a second overlapping batch-preview primitive until real usage shows `worktree ps` is insufficient
The runtime layer should expose a single summary builder used by both:
- CLI `worktree ps`
- any future editor surfaces that want the same live summary semantics
Why:
- the CLI needs a cheap orchestration summary across many worktrees
## Wait Semantics
`terminal wait` needs to be split by what is actually observable.
### Safe first support
- `exit`
This can be grounded in PTY exit events.
### Later support requiring instrumentation or heuristics
- `output`
- `idle`
- `input`
Why:
- current code does not expose a first-class “waiting for input” state
- title heuristics exist in the renderer, but they are not sufficient as a strong CLI contract
Recommendation:
- runtime layer v1 supports only `wait --for exit`
- later phases may add:
- output wait from PTY data arrival
- idle wait from time-based quiescence
- input wait from agent-specific instrumentation, not generic shell guessing
## Failure Modes And Safety Rules
### 1. Stale handles
Must fail explicitly.
Never silently redirect to:
- another leaf with the same title
- the current active leaf
- another PTY in the same tab
This includes PTY restarts inside the same leaf. A restarted process must not inherit an old handle.
### 2. Renderer reload
The current code already kills prior-generation PTYs on page reload in [../src/main/ipc/pty.ts](../src/main/ipc/pty.ts).
The runtime layer should treat renderer reload as a graph invalidation event:
- bump `rendererGraphEpoch`
- invalidate all old handles
- require fresh discovery
During the reload window:
- set `graphStatus` to `reloading`
- reject live terminal operations until a fresh graph sync completes
### 3. Missing renderer registrations
If the runtime layer has PTYs but no renderer graph for a target:
- `status` may report degraded runtime health
- but terminal discovery and live terminal operations must not surface orphan PTYs as valid targets
This should surface as capability truth, not silent omission.
### 4. Closing or detached targets
If a leaf is present in the graph but is no longer writable:
- mark it `writable: false`
- reject `terminal send`
- continue to allow metadata reads when useful
Why:
- current Orca shutdown and PTY replacement flows are partly renderer-driven
- the CLI should not race writes into a target that Orca is intentionally closing or detaching
V1 definition:
- `writable` should be computed from facts Orca can actually observe now
- a target is writable only when:
- `graphStatus === 'ready'`
- the leaf exists in the current authoritative graph
- `ptyId != null`
- the leaf is still marked `connected`
- if Orca later adds an explicit renderer-side closing or detaching marker, that can tighten `writable` further
## Proposed Implementation Phases
### Phase 1: Runtime identity and service skeleton
Deliver:
- `runtimeId`
- main-process runtime service object
- `status` support
- lifecycle wiring hooks only
### Phase 2: Local CLI RPC transport and runtime metadata
Deliver:
- local socket/pipe listener
- auth token bootstrap
- request/response envelope shared by the editor and CLI
- runtime metadata file in Orca user data
Why this comes early:
- the CLI contract depends on a real runtime transport boundary
- it is better to lock the transport and auth model before layering more command handlers on top
### Phase 3: Renderer graph sync and PTY event ingestion
Deliver:
- `runtime:syncWindowGraph`
- tab/leaf graph registry
- PTY attach/detach mapping
- tail buffer updates from PTY events
- preview generation
### Phase 4: Handle issuance and validation
Deliver:
- handle generation
- handle lookup
- stale-handle rejection
- replacement hints in stale-handle errors when safe
- `terminal list`
- `terminal show`
### Phase 5: Read and write surface
Deliver:
- main-owned tail ring buffer
- `terminal read`
- `terminal send`
- `graphStatus`-aware rejection during reload and unavailable windows
### Phase 6: Summary service
Deliver:
- `worktree ps`
### Phase 7: Optional richer terminal reads
Deliver:
- renderer-published visible screen snapshots if Orca proves it needs them
### Phase 8: Wait support beyond exit
Deliver:
- `wait --for exit`
- explicitly defer the rest until instrumentation exists
## Recommended File/Module Shape
Main process:
- `src/main/runtime/orca-runtime.ts`
- `src/main/ipc/runtime.ts`
Renderer integration:
- `src/renderer/src/runtime/sync-runtime-graph.ts`
- targeted calls from:
- `terminals.ts`
- `use-terminal-pane-lifecycle.ts`
- `pty-connection.ts`
Why separate files:
- start with one runtime service and one IPC entrypoint so the design stays easy to land
- split handle, registry, and buffer helpers into separate modules later only if the implementation earns that complexity
## Open Questions
1. Should `runtimeId` change only on app restart, or also on explicit renderer graph reset?
Recommendation:
- app restart only
- use `rendererGraphEpoch` for graph invalidation
2. Should handles encode any meaning, or be fully opaque?
Recommendation:
- fully opaque
3. Should visible screen snapshots be pushed continuously or only on demand if Orca adds them later?
Recommendation:
- defer this until after the tail-buffer-based runtime contract is stable
- if added later, start with on-demand or throttled publication for visible leaves only
4. Should terminal previews come from screen snapshots or tail buffers?
Recommendation:
- use tail buffer for preview generation
- reserve visible screen snapshots for an optional richer read mode later
5. Should Orca support more than one publishing window in v1?
Recommendation:
- no
- keep one authoritative publishing window until PTY routing and renderer graph ownership are explicitly multi-window-safe
## Recommendation
Build the runtime layer as a main-process orchestration service with:
- stable `runtimeId`
- renderer-published full tab/leaf graph sync
- PTY-event integration
- local CLI RPC transport
- opaque handle issuance
- strict stale-handle rejection
- bounded read models for discovery and terminal reads
That is the smallest honest architecture that can support the Orca CLI's live terminal contract without drifting from the editor.

View File

@ -1,822 +0,0 @@
# Right Sidebar Conflict Resolution Status
## Goal
Show active conflict state during merge, rebase, and cherry-pick operations directly inside the existing `Staged Changes` and `Changes` lists in the right sidebar, similar to VS Code and GitHub Desktop.
All three operations produce the same porcelain v2 `u` records when conflicts arise. This design applies uniformly to all of them.
The panel should answer four questions without opening a diff:
1. Is this file in a merge conflict?
2. What kind of conflict is it?
3. Is this conflict still unresolved right now?
4. If it is no longer unresolved, what should I do next?
It should not claim more than Git can prove in the current refresh.
In v1, live unresolved conflict state must come from the current Git status output.
However, the UI may also carry a clearly-labeled local session state for already-open tabs and recently-resolved rows when that state is derived from a file the user opened from a live unresolved conflict in the current session.
That local state is UX scaffolding, not Git truth, and must never be labeled as if Git is still reporting an unmerged entry.
V1 does not attempt to detect conflict markers in working-tree file contents.
If the user resolves a conflict outside the app (e.g., runs `git add <file>` in a terminal), the `u` record disappears from `git status` and the app treats the file as no longer conflicted.
This means a file can leave the conflict state while still containing unresolved conflict markers.
This is a known limitation; v1 trusts Git's index state as the sole authority for whether a file is currently in an unresolved merge conflict.
## Current State
Today the right sidebar groups files only by:
- `staged`
- `unstaged`
- `untracked`
Each row only knows (via `GitUncommittedEntry`, aliased as `GitStatusEntry`):
- `path`
- `status`
- `area`
- `oldPath?`
That is enough for normal file changes, but not for merge conflict UX:
- unresolved conflicts are not represented explicitly
- the user cannot tell conflict type (`both modified`, `deleted by them`, etc.)
- the user cannot tell whether a file is currently unresolved
- conflict resolution progress becomes hard to follow once a file leaves the live `u` state
- the panel does not visually prioritize conflicted files
Relevant code:
- [src/main/git/status.ts](../src/main/git/status.ts)
- [src/shared/types.ts](../src/shared/types.ts)
- [src/renderer/src/components/right-sidebar/SourceControl.tsx](../src/renderer/src/components/right-sidebar/SourceControl.tsx)
## Design Principles
- Keep the current sidebar structure. Users already understand `Staged Changes` and `Changes`.
- Add conflict state to rows, but also provide a merge-resolution summary so users can scan progress quickly.
- Make unresolved conflicts impossible to miss.
- Show only conflict state that Git can prove in the current refresh.
- Use the same file in both sections when Git says both staged and unstaged states exist.
- When the UI shows session-local post-conflict state, label it explicitly as local UI state rather than current Git conflict truth.
## Proposed UX
### 0. Merge summary
When at least one unresolved conflict exists, show a compact merge summary above the normal sections.
Recommended presentation:
- `Merge conflicts: 3 unresolved` (count reflects only live `u`-record conflicts, not `Resolved locally` rows)
- the label should reflect the active operation when detectable:
- `Merge conflicts: 3 unresolved` when `MERGE_HEAD` exists
- `Rebase conflicts: 3 unresolved` when `REBASE_HEAD` exists
- `Cherry-pick conflicts: 3 unresolved` when `CHERRY_PICK_HEAD` exists
- `Conflicts: 3 unresolved` as a fallback when no ref file is found or multiple exist
- operation detection uses `fs.existsSync` on `.git/MERGE_HEAD`, `.git/REBASE_HEAD`, and `.git/CHERRY_PICK_HEAD` in the main process, performed alongside the existing status poll (note: there is a race between the `git status` call and the `fs.existsSync` call — the HEAD file may not yet exist or may already be cleaned up — in that case the operation falls back to `'unknown'` for one poll cycle, which is acceptable)
- secondary action: `Review conflicts`
- optional tertiary hint: `Resolved files move back to normal changes after they leave the live conflict state`
Why:
- users in a merge flow think first in terms of “how many conflicts are left”
- this preserves the existing section structure without forcing conflict work to compete visually with every other file change
- it gives the user a stable place to orient before scanning individual rows
V1 may keep `Changes` and `Staged Changes` as the main lists, but unresolved conflicts should also be discoverable through this merge summary entry point.
`Review conflicts` must have a concrete v1 behavior:
- it opens a lightweight conflict-review tab scoped to the current live unresolved conflict set
- the tab lists only unresolved conflict rows, not ordinary diffs
- each item opens the same conflict-safe single-file entry point described in section 5
- if the unresolved set changes later, the already-open review tab may keep showing the snapshot it was opened with, as long as the UI labels it as a snapshot and offers a refresh or reopen action
- if all conflicts in the snapshot are resolved and the list becomes empty, the tab must show an explicit "all conflicts resolved" state with a dismiss action, not a blank list
- the "all conflicts resolved" state should also offer a link back to `Source Control` to continue the merge workflow
V1 does not require a merge-aware three-way diff queue. A lightweight conflict list view is sufficient.
Information architecture decision for v1:
- `Review conflicts` belongs to `Source Control`, not `Checks`
- it is launched from `Source Control` into the editor area as a dedicated review tab, similar to `Open all diffs`
- do not create a new permanent right-sidebar top-level tab for conflicts in v1
Why:
- merge-conflict review is source-control work, not CI/PR status
- the app already uses editor tabs as the place where review workflows expand beyond the sidebar
- this improves the conflict workflow without fragmenting navigation
### 1. File row anatomy
Each file row keeps the current filename and directory, and may also show a normal status letter when one exists for the current row model.
Conflict rows add explicit conflict UI instead of pretending they are ordinary file states.
Row layout:
`[icon] filename dir [status letter?] [conflict badge]`
Conflict badge values for v1:
- `Unresolved`
- `Resolved locally`
Conflict subtype text:
- `Both modified`
- `Both added`
- `Deleted by us`
- `Deleted by them`
- `Added by us`
- `Added by them`
- `Both deleted`
Recommended presentation:
- badge is compact and high-contrast
- subtype appears as muted secondary text under or beside the filename
- unresolved uses destructive color
- `Resolved locally` uses a quieter success or accent treatment and must include tooltip/help text that it is derived from the current session, not from live Git conflict output
- if a conflict row does not have a meaningful ordinary status letter, omit the letter instead of inventing one
- conflict rows may replace ordinary status letters entirely when showing a normal status letter would create contradictory meaning
Action-oriented helper text:
- `Both modified` -> `Open and edit the final contents`
- `Both added` -> `Choose which version to keep, or combine them`
- `Deleted by us` -> `Decide whether to restore the file`
- `Deleted by them` -> `Decide whether to keep the file or accept deletion`
- `Added by us` -> `Review whether to keep the added file`
- `Added by them` -> `Review the added file before keeping it`
- `Both deleted` -> `Resolve in Git or restore one side before editing`
Non-goal for v1:
- carrying conflict history forward after the `u` record disappears
Clarification:
- v1 may show `Resolved locally` only when the app can tie that row or tab to a file that was opened from a live unresolved conflict during the current session
- v1 should not invent historical conflict state for files the user never interacted with in the current session
### 2. Section behavior
Keep the existing sections, but add a merge-resolution summary above them and order conflicted files first within `Changes` and `Staged Changes`.
#### `Changes`
This section should contain:
- normal unstaged files
- unresolved conflicts
- optionally, recently resolved files from the current session that still have unstaged changes and need review
Expected labels:
- unresolved conflict: `Unresolved`
- recently resolved in current session: `Resolved locally`
This matches the user expectation: the working tree still needs attention.
#### `Staged Changes`
This section should contain:
- normal staged files
- optionally, recently resolved files from the current session that were staged after conflict resolution
`Resolved locally` badge lifecycle is governed by the state machine defined in the Representing resolved conflicts section.
This state is tied to the file's presence in the current sidebar workflow, not to whether its tab happens to remain open.
### 3. Summary counts
Section headers should surface unresolved conflict counts when present.
Examples:
- `Changes 5 · 2 conflicts`
- merge summary: `Merge conflicts: 2 unresolved`
This should remain terse. The row badge carries the detailed meaning.
### 4. Row actions
Conflict-aware row actions:
- unresolved in `Changes`: open editor, no discard shortcut, no stage shortcut
- resolved locally in `Changes`: open editor, stage is allowed, discard is hidden in v1 (discarding a just-resolved conflict file can silently re-create the conflict or lose the resolution — v1 does not have the UX to explain this clearly, so hiding it is the safe default)
- resolved locally in `Staged Changes`: open editor, unstage is allowed
Reasoning:
- unresolved conflicts are higher risk than normal edits
- a one-click discard on an unresolved conflict is too easy to misfire
- a one-click stage on an unresolved conflict can immediately erase the sidebar conflict signal because Git stops reporting the `u` record after `git add`
- users should resolve conflicts in the editor first, then stage from a state that still preserves continuity that this file just came out of conflict resolution
### 5. Diff/editor entry point
Opening a conflicted file should preserve the current navigation flow, but v1 must not assume the existing two-way diff backend can render unresolved conflicts correctly.
For unresolved conflicts, the safe requirement is:
- clicking the row opens the existing file entry point for the file
- the header reflects the conflict badge and subtype when that metadata exists
- unresolved conflicts must not be routed into a misleading two-way diff view
- if the app cannot render a merge-aware view in v1, open the editable file view with conflict metadata instead of the normal diff view
- any fallback state must be explicit and explain that merge-aware diff rendering is not available yet
- the opened view should include action-oriented guidance for the current conflict subtype rather than only Git terminology
Do not claim VS Code style merge presentation in v1 unless the diff backend is updated to read conflict stages explicitly.
This applies to every entry point, including section-level `Open all diffs`.
Required routing rule for v1:
- if the `GitStatusEntry` for the row has `conflictStatus === 'unresolved'`, never call the normal uncommitted diff opener for that row
- instead, route to a conflict-safe entry point that opens either:
- the editable working-tree file view, when a working-tree file exists
- a conflict details panel or read-only placeholder view, when no working-tree file exists
Required bulk-review behavior for v1:
- the product must not present a bulk action that appears to review every file while silently excluding unresolved conflicts
- if a section-level `Open all diffs` action remains, its label or adjacent copy must make the scope explicit when unresolved conflicts are present
- preferred v1 behavior:
- keep `Open all diffs` for normal change review
- add `Review conflicts` from the merge summary when unresolved conflicts exist
- if both actions are shown together, the UI must explain the split clearly
- acceptable fallback behavior:
- `Open all diffs` opens normal diffs only
- unresolved conflicts are excluded from the combined diff set
- the trigger and resulting view both state that conflicted files are reviewed separately
- if rows are excluded, the combined-diff tab state must carry an explicit `skippedConflicts` payload so the notice is deterministic and does not depend on reconstructing the skipped set later from live status alone
- if every candidate row for a given combined-diff open is excluded, the tab must render a conflict-specific state rather than a generic `No changes to display`
- that excluded-only state must list the conflicted paths and provide a direct `Review conflicts` action
Definition of `Review conflicts` in this context:
- if invoked from the merge summary, open the unresolved-conflict review tab for the full live unresolved set
- if invoked from an excluded-only combined-diff state, open the unresolved-conflict review tab preloaded with that tab's stored skipped-conflict snapshot
- the action must never route unresolved conflicts into the ordinary two-way diff viewer
This requirement is intentionally strict because the current diff pipeline reads normal index and worktree content, not merge stages, and because bulk actions must not undermine user trust in the review queue.
Examples:
- `app.tsx · Unresolved conflict · Both modified`
This keeps the sidebar and editor consistent.
### 6. Conflict kinds without a working-tree file
Not every unresolved conflict can be opened as a normal editable file.
Examples:
- `both_deleted`
- some `deleted_by_us` / `deleted_by_them` states, depending on which side leaves a working-tree file behind
For these cases, v1 must not attempt to open the normal editable file view and then show a generic file-read error.
Instead, the entry point should show an explicit conflict state such as:
- `This file is in an unresolved merge conflict. No working-tree file is available to edit.`
- subtype text such as `Both deleted`
- guidance to resolve via Git or restore one side before editing
- action-oriented next step when possible, such as `Restore one side, then reopen this file`
This can be a lightweight placeholder screen in the editor area.
The important requirement is that the state is conflict-aware and not presented as a broken file open.
Required state shape for v1:
- opening a non-editable unresolved conflict must not reuse plain `mode: 'edit'`
- the opened tab state must distinguish `conflict-placeholder` from ordinary editable files and ordinary diffs
- the placeholder state must carry at least:
- `path`
- `conflictStatus`
- `conflictKind`
- `message`
- optional `guidance`
## Proposed Data Model
Extend `GitUncommittedEntry` (the base type behind `GitStatusEntry`) with conflict metadata, and extend `OpenFile` (the editor tab state in `src/renderer/src/store/slices/editor.ts`) with conflict-aware metadata instead of relying on sidebar rows as the only source of truth.
```ts
export type GitConflictKind =
| 'both_modified'
| 'both_added'
| 'both_deleted'
| 'added_by_us'
| 'added_by_them'
| 'deleted_by_us'
| 'deleted_by_them'
export type GitConflictResolutionStatus = 'unresolved' | 'resolved_locally'
export type GitConflictStatusSource = 'git' | 'session'
// Extend GitUncommittedEntry (src/shared/types.ts)
// Currently: { path, status, area, oldPath? }
// GitStatusEntry is an alias for GitUncommittedEntry; extending the base extends both.
// Note: conflictHint is NOT included here. The main process returns only
// Git-derived data. Hint text is derived in the renderer from conflictKind
// using a CONFLICT_HINT_MAP lookup (see Renderer hint derivation below).
//
// conflictStatusSource is NOT set by the main process. The main process
// returns only conflictKind and conflictStatus (always 'unresolved') for
// live u records. The renderer sets conflictStatusSource: 'git' when
// populating from IPC data, and 'session' when applying Resolved locally
// state from trackedConflictPaths. This keeps the main process free of
// session-awareness while letting the renderer distinguish the two sources.
export type GitUncommittedEntry = {
path: string
status: GitFileStatus
area: GitStagingArea
oldPath?: string
conflictKind?: GitConflictKind
conflictStatus?: GitConflictResolutionStatus
conflictStatusSource?: GitConflictStatusSource
}
// Active operation detected by the main process alongside status polling.
// Used by the renderer to label the merge summary correctly.
export type GitConflictOperation = 'merge' | 'rebase' | 'cherry-pick' | 'unknown'
// Renderer hint derivation:
// The renderer maps conflictKind to a user-facing hint string using a
// CONFLICT_HINT_MAP constant (e.g., both_modified -> 'Open and edit the
// final contents'). This keeps UI copy out of the main process parser.
export type OpenConflictMetadata = {
conflictKind: GitConflictKind
conflictStatus: GitConflictResolutionStatus
conflictStatusSource: GitConflictStatusSource
}
export type OpenConflictPlaceholder = OpenConflictMetadata & {
kind: 'conflict-placeholder'
message: string
guidance?: string
}
export type OpenConflictEditable = OpenConflictMetadata & {
kind: 'conflict-editable'
}
export type ConflictReviewState = {
kind: 'conflict-review'
source: 'live-summary' | 'combined-diff-exclusion'
/** Timestamp (ms since epoch) when the snapshot was taken. The renderer
* derives the display label at render time (e.g., '3 unresolved conflicts
* at 2:34 PM') from snapshotTimestamp + entries.length, keeping UI copy
* out of stored state — consistent with the CONFLICT_HINT_MAP approach. */
snapshotTimestamp: number
entries: ConflictReviewEntry[]
}
export type CombinedDiffSkippedConflict = {
path: string
conflictKind: GitConflictKind
}
export type ConflictSummaryState = {
unresolvedCount: number
paths: string[]
}
export type ConflictReviewEntry = {
path: string
conflictKind: GitConflictKind
}
// Extend OpenFile (src/renderer/src/store/slices/editor.ts)
// Current shape:
// { id, filePath, relativePath, worktreeId, language, isDirty,
// mode: 'edit' | 'diff', diffSource?, branchCompare?,
// branchOldPath?, combinedAlternate?, combinedAreaFilter?, isPreview? }
//
// OpenFile uses a discriminated union on `mode` so that conflict-review tabs
// do not require a filePath (they are list views, not file views).
// Normal file tabs ('edit' | 'diff') keep filePath required.
// Add:
type OpenFileBase = {
id: string
worktreeId: string
isPreview?: boolean
}
type OpenFileTab = OpenFileBase & {
mode: 'edit' | 'diff'
filePath: string
relativePath: string
language: string
isDirty: boolean
diffSource?: DiffSource
branchCompare?: BranchCompareSnapshot
branchOldPath?: string
combinedAlternate?: CombinedDiffAlternate
combinedAreaFilter?: string
// conflict fields for single-file tabs
conflict?: OpenConflictEditable | OpenConflictPlaceholder
skippedConflicts?: CombinedDiffSkippedConflict[]
}
type OpenConflictReviewTab = OpenFileBase & {
mode: 'conflict-review'
/** id for conflict-review tabs uses a deterministic scheme:
* `conflict-review-${worktreeId}` — at most one conflict-review tab
* per worktree. Opening a new review replaces the existing one. */
conflictReview: ConflictReviewState
}
export type OpenFile = OpenFileTab | OpenConflictReviewTab
```
Notes:
- `conflictKind` describes the merge shape
- `conflictStatus` describes whether the UI is showing a live unresolved state or a session-local resolved state
- `conflictStatusSource` distinguishes live Git truth from session-local continuity state
- action-oriented hint text (e.g., `Open and edit the final contents` for `both_modified`) is derived at render time from `conflictKind` via the renderer-side `CONFLICT_HINT_MAP` constant — it is not a field on `GitUncommittedEntry` and is never returned by the main process
- treat a row as conflicted when `conflictStatus` is present
- the `status` value on unresolved conflicts is a rendering compatibility choice for existing icon/color plumbing, not a semantic claim — the conflict badge carries the real semantics
- opened editor tabs are a separate state domain from live git-status rows and need their own conflict metadata
- `Review conflicts` is also an editor-tab state, but it is not a diff tab and should not be forced into `mode: 'diff'`
- v1 therefore needs an explicit editor-tab representation for conflict review rather than overloading combined-diff state
Compatibility rule for non-upgraded consumers:
- any consumer of `GitStatusEntry` that has not been upgraded to read `conflictStatus` may still render `modified` styling, but must not offer file-existence-dependent affordances (diff loading, drag payloads, editable-file opening) for unresolved conflicts
- this affects file explorer decorations, tab badges, and any other surface outside `Source Control`
- any consumer of `OpenFile` that accesses `filePath` must narrow on `mode` first, because `OpenConflictReviewTab` does not carry `filePath` — code that assumes `openFile.filePath` exists without checking `mode` will break at compile time once the discriminated union is in place
Known limitation:
- passive file explorer and tab badge surfaces may show `modified` styling for unresolved conflicts in v1
Explicit v1 scope decision:
- required to upgrade in v1: `Source Control`, editor tab state, single-file conflict opening, section-level bulk actions, and any shared open-file helper they depend on
- allowed to defer in v1: passive decorations in file explorer and tab-strip visuals
- not allowed to defer in v1: any entry point that can directly open an unresolved-conflict file into the ordinary diff or ordinary editable path without the conflict-aware routing rules
- not allowed to defer in v1: the dedicated editor-tab state and open action for `Review conflicts`
## Git Parsing Plan
### Source of truth
Use `git status --porcelain=v2 --untracked-files=all` as the primary source, but start parsing `u` records in addition to `1`, `2`, and `?`.
Today [status.ts](../src/main/git/status.ts) ignores unmerged records entirely.
### Performance
Parsing `u` records adds no meaningful overhead — they use the same line-by-line parsing loop as `1`/`2`/`?` records, and the number of unmerged entries during a typical merge is small (usually single digits). The filesystem existence check for ambiguous conflict kinds (`deleted_by_us`, `deleted_by_them`, etc.) adds one `fs.existsSync` call per ambiguous entry, which is negligible within the 3-second polling interval.
If the filesystem existence check throws (permissions error, unmounted path, etc.), default to `status: 'modified'`. This is the safer fallback because it avoids suppressing the conflict row from the sidebar and avoids presenting a `deleted` status that could mislead the user into thinking the file is gone when the check simply failed. The conflict badge and subtype still carry the real semantics regardless of the `status` fallback.
### Mapping unmerged records
Porcelain v2 unmerged `XY` states should map like this:
- `UU` -> `both_modified`
- `AA` -> `both_added`
- `DD` -> `both_deleted`
- `AU` -> `added_by_us`
- `UA` -> `added_by_them`
- `DU` -> `deleted_by_us`
- `UD` -> `deleted_by_them`
V1 should also map every unresolved `u` record to:
- `area: 'unstaged'`
- `status`: determined by whether a working-tree file exists for the conflict kind:
- `'modified'` for kinds that always leave a working-tree file: `both_modified`, `both_added`
- `'deleted'` for kinds that never leave a working-tree file: `both_deleted`
- for `deleted_by_us`, `deleted_by_them`, `added_by_us`, `added_by_them`: check whether the working-tree file exists at parse time and use `'modified'` if it does, `'deleted'` if it does not (for `added_by_us`/`added_by_them`, Git typically does leave a working-tree file, so the check is defensive — the primary ambiguity is in `deleted_by_*` variants where the merge strategy determines whether the surviving side's content is written to the worktree)
Why:
- the current sidebar, tab decorations, and file explorer already expect a `GitFileStatus`
- `modified` is the least misleading compatibility fallback for conflicts where a working-tree file exists
- `deleted` is a better fallback when no working-tree file exists because calling it `modified` would be contradictory
- `deleted_by_us` / `deleted_by_them` and the `added_by_*` variants do not have a guaranteed working-tree file — Git's behavior depends on the merge strategy and the specific conflict — so the parser must check the filesystem rather than hardcoding an assumption
- the conflict badge/subtype still carries the real semantics in all cases
- v1 should not invent new single-letter codes for unmerged rows without a broader status-system redesign
### Representing unresolved conflicts in the existing sections
Unresolved conflicts should be emitted as `area: 'unstaged'` entries with:
- `status`: determined by working-tree file existence (see Mapping unmerged records above)
- `conflictStatus: 'unresolved'`
- `conflictKind: ...`
Why:
- the user still needs to act in the working tree
- this keeps the existing panel layout intact
- it matches the mental model of “this is still pending”
Ordering requirement:
- conflicted entries sort before ordinary entries within `Changes`
- `Staged Changes` has no conflict rows in v1 because unresolved `u` records are emitted only into `unstaged`
### Representing resolved conflicts
In v1, live Git status must stop representing a file as conflicted after Git stops reporting a `u` record.
However, the UI may carry forward a temporary `Resolved locally` state for files the user opened from a live unresolved conflict in the current session.
Why:
- `git status --porcelain=v2` no longer marks the file as unmerged after resolution is staged
- users still need continuity that the file they were just resolving is now in the review-or-stage step of the same workflow
- limiting this to files the user explicitly opened in the current session keeps the scope auditable and avoids broad historical inference
Corollary:
- unresolved rows must not expose a `Stage` shortcut, because that shortcut can remove the only live conflict signal before the user has actually completed review of the file
- `Resolved locally` rows may expose normal safe actions again because the user is now in the post-resolution workflow stage
### `Resolved locally` state machine
#### Store location
`trackedConflictPaths` is a `Map<string, Set<string>>` keyed by `worktreeId`, stored in the renderer-side Zustand git store (the same store that holds `gitStatus`). It is not component-local state — it must survive across re-renders of `SourceControl` and be accessible to both the click handler that adds paths and the polling hook that checks for `u`-record disappearance. Keying by worktree ensures paths are scoped correctly when the app has multiple worktrees open.
The map is populated by the `Source Control` click handler and read by the status-polling reconciliation logic. It is never sent to the main process.
#### State transitions
A file enters `Resolved locally` through a precise sequence:
1. **Track**: when the user clicks an unresolved conflict row in `Source Control` to open or focus a tab, record that path in the `trackedConflictPaths` set. Opening the same file from the file explorer, terminal, or any non-conflict-row entry point does **not** add it to this set.
2. **Transition**: on the next `git status` poll, if a path in `trackedConflictPaths` no longer has a `u` record, mark that path as `Resolved locally` with `conflictStatusSource: 'session'`.
3. **Re-enter**: if a path currently in `Resolved locally` state reappears as a `u` record (e.g., the user ran `git checkout -m <file>` to re-create the conflict), replace the session-local resolved state with live Git conflict state (`conflictStatus: 'unresolved'`, `conflictStatusSource: 'git'`). The path remains in `trackedConflictPaths` so it can transition back to `Resolved locally` if the `u` record disappears again.
4. **Expire**: clear the `Resolved locally` state when any of these happens:
- the file leaves the sidebar entirely (no staged or unstaged entry)
- the app session resets (window reload, app restart)
- the file re-enters a live unresolved `u` state, which replaces the local resolved state with live Git conflict state again
5. **Abort**: when the merge/rebase/cherry-pick operation is aborted (`git merge --abort`, `git rebase --abort`, `git cherry-pick --abort`), all `u` records disappear simultaneously and the operation HEAD file (`.git/MERGE_HEAD`, etc.) is cleaned up. On the next poll, if the detected operation changes to `'unknown'` (no HEAD file found) and the unresolved count drops to zero in the same poll cycle, clear the entire `trackedConflictPaths` set for that worktree rather than transitioning each path to `Resolved locally`. Abort is not resolution — showing `Resolved locally` on every previously-conflicted file after an abort would be misleading.
If a file's `u` record disappears but the path was never in `trackedConflictPaths`, the file simply reverts to its ordinary `GitFileStatus` with no `Resolved locally` badge.
Important consequence:
- closing a tab does not clear `Resolved locally` by itself
- if a file is still present in `Changes` or `Staged Changes`, the continuity badge should remain visible until the file leaves the sidebar, the session resets, or the file becomes live-unresolved again
Guardrails for `Resolved locally`:
- only show it when the path is in `trackedConflictPaths` and the `u` record has disappeared
- label it as local/session-derived, not as live Git conflict output
- never recreate it from polling history alone for files the user did not actively open from a conflict row
## IPC Boundary
Git status parsing runs in the main process (`src/main/git/status.ts`). The renderer receives status data via the `git:status` IPC channel (`src/main/ipc/filesystem.ts`), which returns `GitStatusEntry[]` directly. The polling hook (`src/renderer/src/components/right-sidebar/useGitStatusPolling.ts`) stores the result in the Zustand store without field filtering.
Electron's structured clone serialization preserves all enumerable properties on returned objects. Adding new optional fields to `GitUncommittedEntry` (and therefore `GitStatusEntry`) requires no IPC layer changes -- the new fields will serialize and deserialize automatically.
Session-local state (`Resolved locally` tracking) lives entirely in the renderer and does not cross the IPC boundary. The main process returns only what `git status` reports.
The main process should also return the detected `GitConflictOperation` alongside `GitStatusEntry[]` so the renderer can label the merge summary correctly. This can be added to the existing `git:status` IPC response shape as an optional field.
Required renderer-store change:
- `setGitStatus` must treat conflict metadata changes as meaningful updates
- equality checks that compare only `path`, `status`, and `area` are insufficient for this design
- at minimum, renderer cache invalidation must also react to `conflictStatus`, `conflictKind`, and `conflictStatusSource`
- otherwise a row can remain visually stale when conflict state changes without changing its base `GitFileStatus`
## Sorting
Within each section, sort by:
1. unresolved conflicts
2. resolved locally
3. normal file changes
4. path name
This mirrors the urgency ordering used by editor UIs.
## Visual Spec
### Icons
Keep the existing file-type/status icon, but add a conflict badge rather than replacing the file icon.
Reason:
- users still need file identity and normal file-type affordances
- unresolved conflict is more important than ordinary status letters when the two signals compete
- the UI should prefer the conflict badge over a potentially misleading ordinary status letter
### Badge styles
- `Unresolved`: red background, red text emphasis
- `Resolved locally`: success or accent styling with a tooltip that says the state came from this app session, not from current Git conflict output
### Secondary text
Show conflict subtype in a small muted label:
- `Both modified`
- `Deleted by them`
This is more useful than raw `UU` / `UD` codes.
When space allows, add helper text focused on the next decision rather than only the Git term.
## Interaction Details
### Hover
On hover, keep existing stage/unstage actions where they are safe, but do not hide conflict badges.
For unresolved conflicts in `Changes`:
- hide `Discard`
- hide `Stage`
For `Resolved locally` rows:
- restore safe actions appropriate to the row's current section
- keep the badge visible until the session-local continuity state expires
### Click
Clicking a conflicted row follows the routing rules defined in section 5 (Diff/editor entry point). Key constraints:
- do not reuse `openDiff(...)` unchanged for unresolved conflict rows — use the conflict-aware open path
- the opened tab must carry `conflictStatus` and `conflictKind` so the header can render them without re-querying sidebar state
- use the same tab identity as ordinary editable tabs; attach `conflict.kind: 'conflict-editable'` metadata rather than inventing a second tab namespace
- if no working-tree file exists, open a conflict-placeholder tab instead of an ordinary file tab
- `Review conflicts` uses a separate editor-tab mode and open action; it is launched from `Source Control`, not from `Checks`
Tab reconciliation on status refresh:
- editable tabs: downgrade conflict metadata in place (same tab id, no duplicate)
- placeholder tabs: close or convert deterministically when the path is no longer unresolved
- conflict-review tabs: preserve their stored snapshot unless the user explicitly refreshes or reopens from the current merge summary
- tab closure alone must not clear sidebar `Resolved locally` state while the file still appears in `Changes` or `Staged Changes`
### Section actions
Section-level actions follow the bulk-review rules defined in section 5 (Required bulk-review behavior for v1). The key invariant: do not leave a hidden unsafe path where single-row clicks are safe but bulk-open actions are not.
### Empty state
If all remaining files are conflicted, do not show `No changes detected`. The normal section rendering already covers this once conflicts are included in status parsing.
## Edge Cases
### Rename plus conflict
Do not solve this specially in v1. Prefer showing the destination path and conflict badge.
Important constraint:
- porcelain v2 `u` records do not provide rename-origin metadata like `2` records do
- assume `oldPath` is unavailable for unresolved conflicts unless a separate Git query is added later
- v1 should not promise rename ancestry in conflict rows
### Submodule conflicts
Porcelain v2 also emits `u` records for submodule conflicts. Submodule conflicts are out of scope for v1. The parser should skip `u` records where any of the `h1`/`h2`/`h3` mode fields indicates a submodule (mode `160000`) and leave them unhandled rather than presenting them with the same UX as normal file conflicts.
### Binary conflicts
Use the same row badge model even if the diff viewer cannot render a normal text diff.
## Implementation Plan
### Phase 1a: Parsing and types
- extend shared types with conflict metadata and `GitConflictOperation`
- parse porcelain v2 `u` entries in [status.ts](../src/main/git/status.ts), including filesystem existence checks for ambiguous conflict kinds (with `'modified'` fallback on fs errors)
- detect active operation via `.git/MERGE_HEAD`, `.git/REBASE_HEAD`, `.git/CHERRY_PICK_HEAD` and return `GitConflictOperation` alongside status entries
- add renderer-side `CONFLICT_HINT_MAP` constant that maps `GitConflictKind` to user-facing hint strings
- update store equality checks so conflict metadata changes trigger UI updates
- add tests for parsing each porcelain v2 unmerged `XY` variant (`UU`, `UD`, `DU`, `AA`, `AU`, `UA`, `DD`) into the expected `conflictKind`
- add tests for filesystem existence check fallback behavior on error
### Phase 1b: Sidebar UI
- render conflict badges and subtype text in [SourceControl.tsx](../src/renderer/src/components/right-sidebar/SourceControl.tsx)
- add merge-summary state derived from live unresolved entries, with operation-aware label and the concrete `Review conflicts` list-view entry point defined above
- add conflict helper text and action-oriented next-step messaging using renderer-side `CONFLICT_HINT_MAP`
- suppress `Discard` and `Stage` for unresolved conflicts; suppress `Discard` for `Resolved locally` rows
- add `trackedConflictPaths` set to the renderer Zustand git store
- add accessibility attributes: `role="status"` on badges, `aria-live="polite"` on merge summary count
- sort conflicted rows to the top of their section
- add UI tests for badge rendering, ordering, merge summary, and suppressed actions
### Phase 1c: Editor and tab integration
- extend opened editor tab state with optional conflict metadata
- add a dedicated `openConflictReview(...)` store action that opens the lightweight review tab from `Source Control`
- add a dedicated conflict-aware open action for unresolved rows instead of routing them through `openDiff(...)`
- add a dedicated conflict-placeholder tab for unresolved conflicts without a working-tree file
- add reconciliation logic that downgrades or clears conflict metadata on already-open tabs when live status changes
- render `Resolved locally` only for files tracked via the state machine (see `Resolved locally` state machine section)
- add session-local continuity cleanup so `Resolved locally` expires deterministically based on sidebar presence, not tab closure
- add the lightweight conflict-review tab used by the merge summary and excluded-only bulk-review states
### Phase 1d: Bulk actions and guardrails
- make section-level `Open all diffs` explicit about its scope, backed by stored `skippedConflicts` tab state
- add the dedicated conflict-review handoff state for bulk actions where every candidate row was excluded
- ensure any open-capable consumer that assumes file existence branches on `conflictStatus`
- leave passive explorer/tab decorations as plain `modified` in v1 unless separately upgraded, but do not allow unsafe open routing from those surfaces
- add tests that bulk-open paths do not route unresolved conflicts into the normal two-way diff viewer
### Phase 2
- decide whether to build a merge-aware diff view based on index stages
- decide whether to expand the v1 lightweight `Review conflicts` list into a fuller multi-file conflict queue or merge-aware review surface
- consider a dedicated conflict filter if repos with many changes become noisy
Explicitly out of scope until a stronger design exists:
- session-based reconstruction for files the user never opened from a live unresolved conflict
- broad resolved-state reconstruction for files the user never opened from a live unresolved conflict
## Test Cases
### Parsing
- each porcelain v2 unmerged `XY` variant (`UU`, `AA`, `DD`, `AU`, `UA`, `DU`, `UD`) maps to the expected `conflictKind`
- unresolved conflict rows are emitted as `area: 'unstaged'` with `conflictStatus: 'unresolved'`
- `status` is `'modified'` when a working-tree file exists, `'deleted'` when it does not (including `both_deleted` and ambiguous `deleted_by_*` / `added_by_*` cases)
### Sidebar rendering
- unresolved `both modified` file appears in `Changes` with `Unresolved` badge and subtype
- unresolved `deleted by them` file appears in `Changes` with correct subtype
- merge summary shows unresolved count and a `Review conflicts` action when conflicts are present
- unresolved conflicts do not show the `Stage` or `Discard` actions
- conflict rows sort above normal modified files; `Resolved locally` rows sort between unresolved and ordinary rows
- changing only conflict metadata still triggers a re-render of the affected row
### Editor / tab integration
- clicking an unresolved conflict row opens the conflict-aware editable-file path, not the normal unstaged diff path
- the editor header repeats the unresolved badge and subtype for a conflicted file tab
- opening a `both deleted` conflict (or any conflict without a working-tree file) creates a conflict-placeholder tab, not a generic file-load failure
- placeholder and editable conflict tabs show action-oriented next-step guidance
- clicking `Review conflicts` from `Source Control` opens a dedicated conflict-review editor tab, not a `Checks` surface and not a right-sidebar top-level tab
- the conflict-review tab renders from stored snapshot entries rather than reconstructing its contents from live status on every paint
### Tab reconciliation
- an already-open conflicted tab downgrades to `Resolved locally` after the `u` record disappears on the next status poll
- downgrading preserves the same tab identity — no duplicate tab is created
- a stale conflict-placeholder tab is closed or converted deterministically once the path is no longer unresolved
- closing a tab does not clear `Resolved locally` while the file still appears in the sidebar
### Bulk actions
- `Open all diffs` does not send unresolved conflicts through the normal two-way diff viewer
- the exclusion of unresolved conflicts is explicit in both the trigger label and the resulting view
- the skipped-conflicts notice is stable for the lifetime of the combined-diff tab (sourced from stored `skippedConflicts`, not live status)
- when every candidate row is skipped, the dedicated conflict-review handoff state is shown instead of a generic empty state
### Resolved locally lifecycle
- `Resolved locally` appears only for files the user opened from an unresolved conflict row in the current session
- `Resolved locally` clears when the file leaves the sidebar, the session resets, or the file becomes live-unresolved again
- a file in `Resolved locally` state that reappears as a `u` record reverts to `Unresolved` with `conflictStatusSource: 'git'`
- after re-entering unresolved state, the file can transition back to `Resolved locally` if the `u` record disappears again (path remains in `trackedConflictPaths`)
- files that were never opened from a conflict row do not get `Resolved locally` after their `u` record disappears
- a file resolved externally (e.g., `git add` in terminal) drops the `Unresolved` badge on the next poll and reverts to ordinary `GitFileStatus` without showing `Resolved locally`, because the file was never opened from a conflict row
- aborting the operation (`git merge --abort`) clears the entire `trackedConflictPaths` set — no files show `Resolved locally` after an abort
### Merge summary
- merge summary label reflects the active operation (`Merge conflicts`, `Rebase conflicts`, `Cherry-pick conflicts`, or `Conflicts` as fallback)
- conflict-review tab empty state shows "all conflicts resolved" with dismiss action when snapshot entries are all resolved
### Accessibility
- conflict badges use `role="status"` and include `aria-label` text (e.g., "Unresolved conflict, both modified") so screen readers announce conflict state without requiring visual inspection
- the merge summary unresolved count is a live region (`aria-live="polite"`) so count changes are announced
- focus management (stretch goal for Phase 1b — implement badge and live-region accessibility first): `Review conflicts` moves focus to the opened conflict-review tab; closing the review tab returns focus to the merge summary action
### Regression
- normal non-conflict rows keep current behavior
## Recommendation
Implement the v1 design by enriching the existing row model and adding a compact merge summary above the existing sections.
That gives the app the quick scan value users need for active merge conflicts while preserving the current right-sidebar layout, avoiding unsafe fake diff behavior, and keeping the workflow legible after a file leaves the live `u` state.

View File

@ -1,126 +0,0 @@
# Settings Search — Design Document
## Problem
Orca's settings UI currently has no search capability. Users must manually browse through separate panes (General, Appearance, Terminal, Shortcuts, Repository) to find the setting they want. As the number of settings grows, this becomes increasingly painful.
## Research & Alternatives
### 1. The VS Code Approach (Heavyweight)
VS Code uses a multi-provider architecture (TF-IDF, Embeddings, Local Search) with complex scoring, fuzzy matching, and metadata filtering.
_Pros_: Scales to thousands of settings. _Cons_: Massive over-engineering for an app with ~25 settings.
### 2. The Flat Registry + Pane Auto-Navigation (Original Proposal)
Maintain a separate JSON/JS array of all settings. When the user types, the UI auto-navigates to the first pane containing a match.
_Pros_: Simple substring search. _Cons_: **Jarring UX**. The entire screen changes underneath the user mid-keystroke as the "first match" shifts from the General pane to the Terminal pane. Also suffers from **Data Drift**—developers must remember to update a separate registry file when they rename a UI label.
### 3. Single-Page Continuous Scroll + Component-Level Filtering (The Winner)
Instead of distinct pages that replace each other, all settings are rendered in a single continuously scrolling list, grouped by section. The sidebar acts as a Table of Contents (anchor links).
Search is handled locally at the component level. If a setting doesn't match the query, it hides itself. If a section has no visible settings, the section header hides itself.
_Pros_: Silky smooth UX (no page jumping), native feel (like Discord, Linear, or macOS Settings), and zero data drift.
## Decision
We will implement **Alternative 3: Single-Page Continuous Scroll with Component-Level Filtering**.
At our scale (~25 settings), this provides the absolute best user experience. It avoids jarring layout shifts during search, natively supports a global empty state, and keeps the codebase highly maintainable.
## Design
### 1. Layout Architecture (Single-Page)
We will refactor the Settings layout from a "Router/Tab" model to a "ScrollSpy" model:
- The right-hand content area renders `<GeneralPane />`, `<AppearancePane />`, `<TerminalPane />`, etc., all stacked vertically in a single `overflow-y-auto` container.
- The left-hand sidebar contains the Search Input at the top, and a list of anchor links below it.
- Clicking a sidebar link smoothly scrolls the right-hand container to that section.
### 2. State Management
We only need to track the search query in the Zustand store.
```typescript
type SettingsSlice = {
settings: GlobalSettings | null
settingsSearchQuery: string // NEW
setSettingsSearchQuery: (q: string) => void // NEW
// ... existing methods
}
```
### 3. Component-Level Filtering (No separate registry)
To prevent the search index from drifting away from the UI, search metadata is colocated with the UI component itself.
We introduce a `<SearchableSetting>` wrapper component. Every setting control in the UI is wrapped in this.
```tsx
interface SearchableSettingProps {
title: string
description?: string
keywords?: string[]
children: React.ReactNode
}
export function SearchableSetting({
title,
description,
keywords,
children
}: SearchableSettingProps) {
const query = useSettingsStore((s) => s.settingsSearchQuery).toLowerCase()
if (query) {
const matchesTitle = title.toLowerCase().includes(query)
const matchesDesc = description?.toLowerCase().includes(query)
const matchesKw = keywords?.some((k) => k.toLowerCase().includes(query))
if (!matchesTitle && !matchesDesc && !matchesKw) {
return null // Hide this setting if it doesn't match
}
}
return (
<div className="setting-row">
{/* Title, description, and children (the actual input control) */}
</div>
)
}
```
### 4. Section Visibility & Empty States
If all `<SearchableSetting>` components inside `<TerminalPane />` return `null`, the Terminal pane will be empty.
To handle this cleanly:
- We can track section matches via a lightweight Context, OR
- Since React renders top-down, we can simply apply CSS: `div:empty { display: none }` or use a `useMemo` to check visibility of children arrays if data-driven.
- For the easiest React implementation: a `SettingsSection` wrapper that reads the query, knows its children's search metadata, and hides its own `<h2>` header if no children match.
**Global Empty State:**
If the overall `searchQuery` yields 0 matches across the entire settings page, we display a clear centered message in the main content area:
`No settings found for "{query}"`
### 5. File Changes
```text
src/renderer/src/components/settings/
Settings.tsx — Add search input, change layout to stacked scroll
SearchableSetting.tsx — NEW: Wrapper component for filtering
SettingsSection.tsx — NEW: Wrapper for sections to hide headers
panes/
GeneralPane.tsx — Wrap items in <SearchableSetting>
AppearancePane.tsx — Wrap items in <SearchableSetting>
TerminalPane.tsx — Wrap items in <SearchableSetting>
ShortcutsPane.tsx — Wrap items in <SearchableSetting>
RepositoryPane.tsx — Wrap items in <SearchableSetting>
```
### 6. Workflow for Adding New Settings
When a developer adds a new setting, they simply wrap it in `<SearchableSetting title="..." keywords={['...']}>`.
Because the UI component _is_ the search index, it is impossible for the setting to exist in the UI but be missing from the search logic, guaranteeing long-term maintainability.

View File

@ -1,837 +0,0 @@
# Source Control Branch Diff Design
## Problem
The current Source Control view only reflects `git status` data:
- staged changes
- unstaged changes
- untracked files
When a branch has committed changes relative to its base branch, but no uncommitted changes, the UI incorrectly appears empty and shows "No changes detected."
This is misleading. Users need to understand both:
1. what is currently uncommitted in the worktree
2. what has changed on this branch relative to the repo base ref
## Goals
- Show all files changed on the current branch, not just uncommitted files.
- Preserve fast local-edit workflows for staging, unstaging, and discarding.
- Make the active compare target explicit.
- Keep File Explorer decorations legible and low-noise.
- Avoid conflating "working tree state" with "branch compare state."
- Ship in one implementation pass without requiring a follow-up architecture rewrite.
## Non-Goals
- Reproducing the full GitHub compare page inside the sidebar
- Replacing the existing PR or Checks surfaces
- Adding dense per-commit browsing in the initial version
- Changing File Explorer to decorate every branch-diff file
## Core Model
The UI should treat these as separate data sources.
### 1. Uncommitted Changes
Derived from local SCM state, equivalent to `git status`.
Includes:
- staged
- unstaged
- untracked
- conflicts if later added
This answers: "What have I changed locally that is not fully committed yet?"
### 2. Branch Changes
Derived from branch-vs-base comparison, equivalent to `git diff <baseRef>...HEAD`.
Includes all files changed on the current branch relative to the configured base ref, even when the worktree is clean.
This answers: "What is different on this branch compared with the configured base ref?"
These two models must remain distinct in state, UI labels, badge semantics, and diff behavior.
## Base Ref
Branch compare should use the repo's configured base ref:
- `repo.worktreeBaseRef` when set
- otherwise the detected default base ref, typically `origin/main` or `origin/master`
The active base ref must be visible in the Source Control UI so the compare scope is never ambiguous.
### Base Ref Validation
Before running any branch compare query, the app must verify that `<baseRef>` resolves in the current repo.
If the configured or detected base ref does not resolve:
- do not treat that as "no branch changes"
- keep uncommitted changes fully functional
- show the branch compare surface in an unavailable state
- surface a clear recovery action to change the base ref
Recommended copy:
- heading: `Branch compare unavailable`
- supporting text: `Base ref <baseRef> could not be resolved in this repository.`
- actions: `Change Base Ref`, `Retry`
This is required because default-base fallback may produce a syntactically valid ref name that does not actually exist locally.
## Source Control Layout
This design applies inside the existing right sidebar tab named `Source Control`.
It does not introduce new top-level sidebar tabs.
The existing top-level app navigation remains:
- `Explorer`
- `Search`
- `Source Control`
- `Checks`
Within the `Source Control` panel, add a scope selector:
- `All`
- `Uncommitted`
- `Branch`
Default selection: `All`
Rationale:
- `All` best matches user intent when opening Source Control on a branch
- it prevents the false-empty case when committed branch changes exist
- it preserves the current Source Control entry point instead of inventing a parallel navigation model
## Compare Summary Bar
At the top of the `Source Control` panel, show a compact compare summary when branch compare data is available:
- `base: origin/main`
- `compare: <current branch>`
- `<n> files changed`
- `<m> commits ahead` when available
- PR pill if PR metadata is already available for the branch
This should be visible in `All` and `Branch` modes.
When branch compare is unavailable because the base ref is invalid, replace the normal summary with the unavailable state described above.
### Ahead / Behind Semantics
Phase 1 branch compare is primarily an "ahead of base" view derived from `git diff <baseRef>...HEAD`.
That means:
- changed-file results represent changes reachable from `HEAD` since the merge base
- `commits ahead` is the required branch-topology metric in v1
- `behind` or `diverged` indicators are optional in v1 and must not block landing
Because of this, the UI must not claim the branch "matches `<baseRef>`" unless the implementation has explicitly computed that stronger condition.
## Changes View Behavior
### All
Show two top-level sections:
- `Uncommitted`
- `Committed on Branch`
`Uncommitted` contains:
- `Staged Changes`
- `Changes`
- `Untracked Files`
`Committed on Branch` contains:
- all files changed in `baseRef...HEAD`
Ordering:
1. Uncommitted section first
2. Committed on Branch second
Rationale:
- local in-progress work is usually more actionable
- branch-level history remains visible even when local state is clean
### Uncommitted
Show only working tree/index state.
Keep existing actions:
- stage
- unstage
- discard
### Branch
Show only `baseRef...HEAD` changed files.
Actions are compare-oriented, not working-tree-oriented:
- open file diff against base
- open combined branch diff
- change base ref
- retry branch compare when unavailable
Do not show stage, unstage, or discard actions in `Branch`.
## Empty State Rules
Do not show "No changes detected" unless both conditions are true:
- there are no uncommitted changes
- branch compare is available and there are no branch changes relative to base
### Empty State Copy
If no uncommitted changes exist but branch changes do exist:
- heading: `No uncommitted changes`
- supporting text: `<n> files changed on this branch since <baseRef>`
If neither kind of change exists:
- heading: `No changes on this branch`
- supporting text: `This worktree is clean and this branch has no changes ahead of <baseRef>`
If branch compare is unavailable:
- keep the uncommitted section visible if it has entries
- do not collapse the whole panel to a generic empty state
## Diff Semantics
The doc must define exact left and right sides so the same path can appear in multiple sections without ambiguity.
### Unstaged Diff
Used when opening an entry from `Changes`.
- left: index if present, otherwise `HEAD`
- right: working tree
Required v1 behavior:
- do not reuse a `HEAD -> working tree` diff for unstaged entries when an index version exists
- if a file has staged and unstaged changes, the `Changes` entry must show only the unstaged delta
- implement this with an explicit `index -> working tree` loader path in main-process git code
### Staged Diff
Used when opening an entry from `Staged Changes`.
- left: `HEAD`
- right: index
### Branch Diff
Used when opening an entry from `Committed on Branch` or `Branch`.
- left: merge-base of `<baseRef>` and `HEAD`
- right: `HEAD`
This is the per-file interpretation of `git diff <baseRef>...HEAD`.
Branch diff must load content from the resolved compare snapshot, not from symbolic refs at render time.
Required v1 behavior:
- branch diff content queries use the resolved `mergeBase` oid and `headOid` captured in `GitBranchCompareSummary`
- do not re-resolve `HEAD` while loading a branch diff tab
- if `HEAD` moves later, an existing branch diff tab may remain open, but its identity and content must continue to reflect the snapshot it was opened from until the user refreshes or reopens against the newer snapshot
#### Branch Diff File Resolution
Branch diff cannot reuse the working-tree diff loader as-is.
For branch compare entries:
- `modified` / `added`: read left content from the merge-base tree and right content from the resolved `headOid` tree
- `deleted`: read left content from the merge-base tree and use empty content on the right
- `renamed`: read left content from `oldPath` in the merge-base tree and right content from `path` in the resolved `headOid` tree
- `copied`: read left content from `oldPath` in the merge-base tree and right content from `path` in the resolved `headOid` tree
If a file also has local uncommitted edits, branch diff must still render the committed branch comparison only. It must not silently substitute working-tree content on the right side.
### Combined Uncommitted Diff
Used by `View All Changes` in `Uncommitted`.
- includes staged and unstaged entries
- may continue to omit untracked files in v1 if the existing combined diff viewer does so
### Combined Branch Diff
Used by `View All Changes` in `Branch`.
- includes files from `git diff --name-status <baseRef>...HEAD`
- each section uses branch diff semantics
- is read-only in v1
### Combined All Diff
Used by `View All Changes` in `All`.
v1 behavior:
- if uncommitted entries exist, open the combined uncommitted diff by default
- if no uncommitted entries exist and branch compare is available, open the combined branch diff by default
- provide a visible secondary action to switch to the other combined diff when both data sets are available
This is intentionally the v1 contract. A true mixed combined view is deferred.
## File Status Semantics
The same status letters should not mean different things in different parts of the app without a label.
### Uncommitted Statuses
These keep the existing meanings:
- `M` modified
- `A` added
- `D` deleted
- `R` renamed
- `?` untracked
These are working tree or index states.
### Branch Statuses
These represent compare-to-base states, not local edit state.
They may reuse the same letters in the Source Control branch section if clearly labeled under `Committed on Branch` or `Branch`, because the section title provides the necessary context.
They must not silently replace Explorer decorations.
## Precedence Rules
When a file appears in both uncommitted and branch-compare results:
- in Source Control `All`, show it in both relevant sections
- in Explorer, show only uncommitted decoration by default
- when opened from an uncommitted section, open uncommitted diff semantics
- when opened from a branch section, open branch diff semantics
- when opened from generic file navigation, prefer working tree edit or uncommitted diff over branch diff
Branch and uncommitted diff tabs must have distinct tab identities. Do not key both off only `filePath + staged/unstaged`.
### Tab Identity Requirement
This must be explicit in the implementation contract because the current editor model keys diffs too loosely for this feature.
Minimum tab identity dimensions:
- diff source: `unstaged` | `staged` | `branch` | `combined-uncommitted` | `combined-branch`
- worktree id
- file path
- base ref for branch compare tabs
- compare version for branch compare tabs, derived from the resolved compare snapshot
Examples:
- uncommitted file diff: `<worktreeId>::diff::unstaged::<path>`
- staged file diff: `<worktreeId>::diff::staged::<path>`
- branch file diff: `<worktreeId>::diff::branch::<baseRef>::<compareVersion>::<path>`
- combined uncommitted diff: `<worktreeId>::all-diffs::uncommitted`
- combined branch diff: `<worktreeId>::all-diffs::branch::<baseRef>::<compareVersion>`
Without this, opening the same file from different sections will collide and produce incorrect editor reuse.
`compareVersion` must change whenever the branch compare snapshot changes in a way that affects diff content.
Minimum required inputs:
- `baseRef`
- resolved base oid
- resolved `HEAD` oid
- resolved `mergeBase` oid
This may be implemented either by:
- including `HEAD` and/or `mergeBase` in the tab id directly, or
- invalidating and regenerating all open branch compare tabs whenever a refreshed compare snapshot changes either value
Phase 1 must choose one of these approaches explicitly. Reusing a branch diff tab keyed only by `baseRef` is not correct.
## File Explorer Rules
### Principle
File Explorer should remain conservative and readable.
Per-file Explorer badges should represent local SCM state by default, not all files changed on the branch.
This matches the useful part of VS Code's behavior: Explorer decorations come from SCM resource groups for current working state, not generic branch compare.
### Default Explorer Behavior
Show per-file decorations only for:
- staged
- unstaged
- untracked
- conflicts
Do not show branch-diff-only files with normal `M/A/D/R` Explorer badges when the worktree is clean.
Reason:
- users read Explorer badges as "this file is currently dirty"
- branch compare files are a different concept
- reusing the same badges for both creates ambiguity and noise
### Explorer Branch Awareness
Branch compare may still influence the Explorer only at higher-level summary surfaces:
- Source Control tab header badge
- worktree header pill
- compare summary row inside Source Control
Examples:
- `12 changed`
- `3 commits ahead`
- `Diff vs origin/main`
### Explorer Interaction Design
When the selected file has uncommitted changes:
- primary action should open working tree diff
When the selected file has no uncommitted changes but is changed on the branch:
- primary file-open behavior should remain normal edit open
- context menu may offer `Open Branch Diff`
When the file has both:
- primary action should open working tree diff from Source Control
- secondary action should allow compare vs base
Priority rule:
1. working tree diff
2. staged diff if explicitly selected from staged section
3. branch compare diff when explicitly requested
### Optional Future Setting
If branch compare decorations are added to Explorer later, they should be:
- off by default
- visually distinct from uncommitted badges
- clearly labeled as branch compare state
Do not reuse the exact same badge style as local SCM state.
## Folder Aggregation Rules
Folder styling in Explorer should aggregate uncommitted state only in v1.
Do not turn entire directory trees into "changed" folders just because files differ from base.
If desired, branch-level aggregation can appear in a separate summary surface:
- Source Control section counts
- worktree header pill
- branch compare summary row
## Data Model Recommendation
Keep the state separate.
Recommended types:
```ts
type GitUncommittedEntry = {
path: string
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'copied'
area: 'staged' | 'unstaged' | 'untracked'
oldPath?: string
}
type GitBranchChangeEntry = {
path: string
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'copied'
oldPath?: string
}
type GitBranchCompareSummary = {
baseRef: string
baseOid: string
compareRef: string
headOid: string
mergeBase: string
changedFiles: number
commitsAhead?: number
status: 'ready' | 'invalid-base' | 'unborn-head' | 'no-merge-base' | 'loading' | 'error'
errorMessage?: string
}
```
Recommended store shape:
```ts
gitStatusByWorktree: Record<string, GitUncommittedEntry[]>
gitBranchChangesByWorktree: Record<string, GitBranchChangeEntry[]>
gitBranchCompareSummaryByWorktree: Record<string, GitBranchCompareSummary>
```
Do not overload a single `GitStatusEntry[]` to represent both concepts.
### Async Consistency Requirement
Branch compare refresh is asynchronous and may be triggered repeatedly while the user changes worktrees, changes base refs, or moves `HEAD`.
Phase 1 must prevent stale compare results from overwriting newer ones.
Required implementation contract:
- each branch-compare request carries a request token or snapshot key
- reducer/store writes only apply if the response still matches the latest in-flight request for that worktree
- the snapshot key must include at least `worktreeId` and requested `baseRef`
- if the implementation already knows the triggering `baseOid` and/or `HEAD` oid, include them as well
This is required so a slower response for an old base ref or old branch state cannot replace a newer compare result.
## Git Queries
### Uncommitted
Keep the current status query:
```sh
git status --porcelain=v2 --untracked-files=all
```
### Branch Compare
Recommended query sequence:
1. Resolve `HEAD`
```sh
git rev-parse HEAD
```
2. Resolve and validate base ref
```sh
git rev-parse --verify <baseRef>
```
3. Resolve merge base from the pinned oids
```sh
git merge-base <baseOid> <headOid>
```
4. Load changed files from the pinned snapshot
```sh
git diff --name-status -M -C <mergeBase> <headOid>
```
5. Load ahead count from the pinned snapshot
```sh
git rev-list --count <baseOid>..<headOid>
```
Notes:
- `...` is important because it compares from merge base
- use repo-configured base ref, not a hardcoded branch name
- use `-M -C` so the query can actually produce the rename/copy statuses promised by the data model
- execute git with argv via `execFile` or equivalent, not shell-interpolated strings, because `<baseRef>` is user-configurable input
- store the resolved `baseOid`, `HEAD` oid, and `mergeBase` in the compare summary so UI identity and invalidation can key off the actual compare snapshot
- if step 1 fails because `HEAD` is unborn, branch compare enters the unavailable `unborn-head` state instead of generic error
- if step 2 fails, branch compare enters the unavailable `invalid-base` state instead of pretending there are zero changes
- if step 3 fails because the refs have no merge base, branch compare enters the unavailable `no-merge-base` state instead of generic error
- the changed-files query, per-file branch diff, and combined branch diff must all use the same resolved snapshot inputs: `baseRef`, `baseOid`, `headOid`, and `mergeBase`
- do not mix a file list produced from symbolic refs with per-file content produced from pinned oids; the summary, list, and file content must describe the same snapshot
### Snapshot Contract
Branch compare v1 must be snapshot-based, not "latest ref at render time."
Required contract:
- first resolve `headOid`
- then resolve `baseOid`
- then resolve `mergeBase` against those exact pinned oids
- derive the changed-file list, ahead count, per-file branch diff content, and combined branch diff content from those pinned values
- persist `baseRef`, `baseOid`, `headOid`, and `mergeBase` together as the compare snapshot
If `HEAD` or `<baseRef>` moves while the query is in flight:
- the in-flight result may be discarded as stale
- the UI must not combine old file-list data with new per-file content or vice versa
### Unborn HEAD Handling
Branch compare depends on a resolvable `HEAD`.
If the repository or worktree has no commits yet, or `HEAD` otherwise cannot be resolved:
- do not treat that as "no branch changes"
- keep uncommitted changes fully functional
- show the branch compare surface in an unavailable state distinct from invalid-base
- preserve the same recovery affordances as other unavailable states where applicable
Recommended copy:
- heading: `Branch compare unavailable`
- supporting text: `This branch does not have a committed HEAD yet, so compare-to-base is unavailable.`
- actions: `Retry`
### No Merge Base Handling
Branch compare also depends on `HEAD` and `<baseRef>` sharing a merge base.
If both refs resolve but `git merge-base <baseOid> <headOid>` fails because the histories are unrelated:
- do not treat that as "no branch changes"
- keep uncommitted changes fully functional
- show the branch compare surface in an unavailable state distinct from invalid-base and unborn-head
- preserve the same recovery affordances as other unavailable states where applicable
Recommended copy:
- heading: `Branch compare unavailable`
- supporting text: `This branch and <baseRef> do not share a merge base, so compare-to-base is unavailable.`
- actions: `Change Base Ref`, `Retry`
### Branch Diff Content Query
Per-file branch diff content needs a dedicated path-aware query path in main-process git code.
Recommended primitives:
```sh
git show <mergeBase>:<path>
git show <headOid>:<path>
```
Use `oldPath` on the merge-base side for renames and copies. Missing blobs should resolve to empty content rather than hard failure so added/deleted files render correctly.
### Unstaged Diff Content Query
Per-file unstaged diff content should also use dedicated git primitives rather than assuming `HEAD` on the left side.
Recommended primitives:
```sh
git show :<path>
git show HEAD:<path>
```
Rules:
- for unstaged entries, prefer index content on the left side
- if the path is not present in the index, fall back to `HEAD`
- read working-tree content from disk for the right side
- if the file is deleted in the working tree, the right side is empty
- for renamed unstaged entries, use `oldPath` for the left-side lookup when required by the parsed status entry
## Refresh Rules
Branch compare data should not be recomputed on the same fixed loop as `git status`.
Instead, branch compare should refresh on explicit invalidation events:
- active worktree changes
- Source Control tab becomes visible for the active worktree
- app startup hydration for the active worktree
- repo base ref changes
- explicit user action: `Retry`
- after fetch or any other operation that updates the resolved compare base ref, even if `HEAD` does not move
- after operations that may change `HEAD` or branch topology:
- commit
- amend
- checkout / switch
- merge
- rebase
- cherry-pick
- pull
- reset that changes `HEAD`
Refresh does not need to run after pure working-tree mutations such as:
- stage
- unstage
- discard
- editing files without creating a commit
because those operations do not change `baseRef...HEAD`.
Refreshing after fetch is required because `baseRef...HEAD` changes when the base ref moves, even if `HEAD` stays on the same commit.
### Runtime Freshness Requirement
The app cannot rely only on app-owned git operations for freshness because users may commit, rebase, fetch, or switch branches from the embedded terminal or other external tools.
Required v1 contract:
- branch compare refresh remains primarily event-driven
- when the `Source Control` panel is visible for the active worktree, the app must also run a lightweight compare-snapshot freshness check on an interval
- that check may be cheaper than a full branch compare refresh; it only needs to detect whether `headOid` or `baseOid` has changed
- if the freshness check detects a change, trigger a full branch compare refresh
- polling may stop when `Source Control` is not the visible right-sidebar tab
This keeps the visible branch compare state from going stale during terminal-driven git activity without paying the full compare cost continuously in the background.
The implementation may debounce or coalesce refresh triggers fired in quick succession. The important contract is: visible branch compare state must converge automatically after external git activity, not only after explicit user actions.
## Base Ref Recovery Path
`Change Base Ref` should reuse the existing repo base-ref management surface instead of inventing a second editor for the same setting.
Required v1 behavior:
- activating `Change Base Ref` opens a modal or sheet that reuses the existing repository base-ref search-and-select UI logic
- the control logic should be shared with the repository settings implementation rather than duplicated
- it must not navigate the user away from `Source Control` into the full Settings screen just to recover from an invalid base ref
- after the user picks a new base ref, branch compare refreshes immediately for the active worktree
- if the user cancels, keep the current unavailable state visible
This keeps base-ref editing in one canonical implementation while still making the recovery path direct from Source Control.
## Binary File Handling
Branch compare and combined diff must define non-text behavior explicitly.
This requires a diff payload contract richer than the current text-only `{ originalContent, modifiedContent }` shape.
Recommended payload shape:
```ts
type GitDiffTextResult = {
kind: 'text'
originalContent: string
modifiedContent: string
}
type GitDiffBinaryResult = {
kind: 'binary'
originalIsBinary: boolean
modifiedIsBinary: boolean
}
type GitDiffResult = GitDiffTextResult | GitDiffBinaryResult
```
The same union may be reused for uncommitted and branch diff loaders. Branch diff metadata such as file status and compare context should travel separately in the branch entry / compare summary models rather than being embedded in the diff payload.
For per-file branch diff and combined branch diff:
- if either side resolves to binary content, do not attempt to render a text diff in Monaco
- show a binary-file placeholder row instead
- include:
- file path
- branch compare status (`added`, `modified`, `deleted`, `renamed`, or `copied`)
- compare context (`<baseRef>...HEAD`)
Recommended copy:
- title: `Binary file changed`
- supporting text: `Text diff is unavailable for this file in branch compare.`
For mixed repositories, binary files should still count toward changed-file totals and remain visible in section lists.
v1 refresh behavior:
- refresh uncommitted status on the existing poll loop
- refresh branch compare on worktree switch
- refresh branch compare when the Source Control panel first mounts for that worktree
- refresh branch compare after any operation that may move `HEAD`
- refresh branch compare after base-ref change
- while the Source Control panel is visible, run the lightweight freshness check described above so terminal-driven git activity is detected automatically
- provide a manual `Retry` or refresh action in the compare summary area
This keeps branch compare reasonably fresh without forcing a costly `git diff <baseRef>...HEAD` loop every few seconds.
## Loading And Error States
The panel must distinguish these states explicitly:
- `loading`: branch compare summary shows loading treatment; `All` still shows uncommitted sections if present
- `invalid-base`: use the unavailable state defined above
- `unborn-head`: use the unavailable state defined above for missing committed `HEAD`
- `no-merge-base`: use the unavailable state defined above for unrelated histories
- `error`: show `Branch compare failed` with retry action and preserve uncommitted sections
Do not reuse the generic empty state for `loading` or `error`.
## Visual Design Guidance
### Source Control
- Keep section headers compact and count-based
- Make the compare summary always visible in `All` and `Branch`
- Use explicit labels like `Committed on Branch` instead of vague labels like `Other Changes`
- Keep unavailable and loading states distinct from empty states
### Explorer
- Keep badges sparse
- Favor summary pills over duplicative per-file branch markers
- Avoid a second noisy alphabet of overlapping status badges
## Implementation Plan
Ship this in two phases, but make Phase 1 complete and shippable on its own.
### Phase 1
- Add branch compare data model and IPC surface
- Add base-ref validation and unavailable-state UI
- Update Source Control to show `All`, `Uncommitted`, `Branch`
- Add compare summary bar
- Add per-file branch diff open behavior
- Add read-only combined branch diff viewer
- Make `View All Changes` scope-aware
- Fix empty state logic
- Keep Explorer decorations uncommitted-only
- Pin branch compare to resolved snapshot oids and use the same snapshot for summary, list, and diff content
- Add visible-tab freshness detection so external terminal git activity refreshes branch compare automatically
- Reuse the repo base-ref picker logic in a Source Control recovery modal/sheet
- Upgrade diff IPC/result types so binary branch diffs are representable without ad hoc UI guesses
- Treat PR pill as optional enrichment only when branch PR data is already available; do not make GitHub lookup a phase-1 blocker
- Include explicit compare-snapshot invalidation/versioning so branch diff tabs cannot go stale across base-ref or `HEAD` changes
Phase 1 is the required landing scope.
### Phase 2
- Add a true mixed combined diff that renders both branch and uncommitted sections in one viewer
- Add commit summary or commit dropdown
- Optionally add a distinct Explorer branch-compare mode behind a setting
## Decisions
- Source Control should show both uncommitted and branch-level changes.
- The feature lives inside the existing `Source Control` sidebar tab.
- File Explorer should continue to show local SCM state only by default.
- Branch compare should be visible in summary surfaces, not normal per-file Explorer badges.
- Invalid base refs must produce an explicit unavailable state, not an empty state.
- Phase 1 includes minimal but complete branch diff viewing so the feature can land in one go.

View File

@ -1,122 +0,0 @@
╔══════════════════════════════════════════════════════════════════╗
║ DESIGN REVIEW ║
╠══════════════════════════════════════════════════════════════════╣
║ Document: docs/worktree-setup-script-design.md ║
║ Reviewer: Gemini CLI (First Principles Design Review) ║
╠══════════════════════════════════════════════════════════════════╣
║ VERDICT: 🟢 PROCEED WITH CAUTION (Design Updated) ║
╚══════════════════════════════════════════════════════════════════╝
═══════════════════════════════════════════════════════════
PHASE A: DESIGN CHALLENGE
═══════════════════════════════════════════════════════════
## Premise & Problem Assessment
The problem diagnosis is highly accurate. Implicit execution of setup scripts creates a hostile UX when things fail (e.g., missing auth, uninstalled dependencies). Exposing this as an explicit, terminal-first user choice directly addresses Issue #238.
## Alternative Approaches Considered
### Alternative 1: Dedicated Background Log Panel
- **Approach**: Run the setup script as a background process (using `node-pty` but not interactive) and stream output to a read-only UI panel in Orca.
- **How it works**: Uses a similar `exec` execution context as today, but surfaces logs.
- **Why it might be better**: Guarantees execution semantics (`set -e` equivalent) and prevents the user from accidentally typing into the terminal mid-setup and messing up the command.
- **Tradeoff**: Cannot handle interactive prompts (e.g., SSH keys, 2FA, package manager choices), which is the primary reason the design rejected background execution.
- **Effort**: 1.5x (Requires building a log viewer UI).
- **Risk**: Hanging setups due to hidden auth prompts.
### Alternative 2: Generated Runner Script (Recommended)
- **Approach**: Main process generates a temporary executable script (e.g., `.orca-setup.sh` or `.orca-setup.cmd`) containing the setup commands wrapped in strict error handling (`set -e`). It then spawns the PTY and injects `source .orca-setup.sh`.
- **How it works**: The terminal remains interactive, but the execution of multiline commands is handled safely by the script runner.
- **Why it might be better**: Fixes the catastrophic failure containment issue of pasting multiline commands into an interactive terminal (see Phase B).
- **Tradeoff**: Requires writing temporary files and platform-specific wrappers.
- **Effort**: 1.2x.
- **Risk**: Edge cases in path resolution or permissions for the temporary script.
### Recommendation
The proposed **Terminal-First** design is fundamentally the right approach for visibility and interactivity. However, the execution model is **💡 BETTER ALTERNATIVE EXISTS**: You must adopt Alternative 2 (Generated Runner Script) to safely execute multiline commands in an interactive shell.
## UX & User Journey Issues
### Interaction State Coverage
| Flow | Loading | Empty | Error | Success | Partial | Notes |
| --------------- | ------- | ----- | ----- | ------- | ------- | ------------------------------------------------------------------------------------------------- |
| Create Worktree | ❌ | ✅ | ✅ | ✅ | ✅ | What does the UI show between clicking "Create" and the terminal opening? Git cloning takes time. |
### UX Findings
| Issue | Severity | User Impact | Suggested Fix |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| Context Switching | P2 | If user creates Worktree A, but clicks Worktree B while Git clone is running, the terminal might steal focus or open in the wrong context. | Define focus-stealing rules: Does the newly created worktree auto-focus when creation completes? |
| Terminal Identification | P3 | User has multiple terminals open. They don't know which one is running the setup script. | Give the setup PTY a specific title or header (e.g., `[Orca Setup]`). |
## Architectural Fit
```text
[UI Dialog] -> IPC: worktrees:create(repoId, ..., setupDecision)
|
[Main Process] -> Git Clone
|
Returns: { worktree, shouldRunSetup, envVars }
|
[Renderer updates UI] <------+
|
[Renderer opens PTY] <------+ (Needs to pass envVars to PTY but cannot!)
|
[PTY runs setup script]
```
### Data Flow (4 paths)
```text
Happy path: [User clicks create] → [Main creates worktree] → [Renderer opens PTY] → [Setup runs]
Nil/missing: [User selects skip] → [Main creates worktree] → [Returns shouldRunSetup=false] → [No terminal]
Empty: [No setup config] → [Dialog hides setup section] → [Normal create]
Upstream error: [Git clone fails] → [Main returns error] → 💥 [UI shows error toast, setup never runs]
```
| Issue | Severity | Example | Why | Evidence |
| ------- | -------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------- |
| API Gap | P0 | Renderer needs to pass `ORCA_WORKTREE_PATH` to the PTY. | The `pty:spawn` IPC handler does not accept custom environment variables. | `src/main/ipc/pty.ts` lines 65-72 |
═══════════════════════════════════════════════════════════
PHASE B: DESIGN AUDIT
═══════════════════════════════════════════════════════════
## Critical Blockers (P0/P1 - Must Fix Before Implementation)
| Blocker | Severity | Example | Why | Evidence |
| --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **PTY Env Injection API** | P0 | The design states: "These existing variables should be passed to the setup command PTY". The renderer opens the PTY. | `pty:spawn` hardcodes `process.env`. The requested feature cannot be built without modifying this IPC handler to accept `env?: Record<string, string>`. | `src/main/ipc/pty.ts` |
| **Multiline Failure Containment** | P0 | `orca.yaml` contains: `pnpm install \n pnpm build`. | Pasting multiline text into an interactive bash/zsh prompt executes line by line. If `pnpm install` fails, the shell will STILL execute `pnpm build`. This violates idempotency and can corrupt the environment. | Standard bash/zsh interactive behavior vs script behavior (lack of `set -e`). |
| **Hanging Script Cancellation** | P1 | Setup command hangs. User presses `Ctrl+C`. | If the command was injected as multiline text, `Ctrl+C` only cancels the _currently executing line_, not the rest of the buffered text. The script will plow forward. | Standard interactive shell buffer behavior. |
| **PTY Race Conditions** | P1 | Injecting commands via `pty.write()` on startup. | Depending on the shell (e.g., heavy `.zshrc`), writing to the PTY immediately after spawn can result in dropped characters or execution before the prompt is ready. | Known `node-pty` limitation. |
## Unverified Assumptions
| Assumption | Evidence Required | Severity | Example | Why |
| ---------------------- | ----------------------------------------------------------------------- | -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| PTY Execution Strategy | A proven mechanism to execute arbitrary strings in `node-pty` robustly. | P0 | "Orca starts the setup command in that terminal" | Doing this reliably across Windows (cmd/pwsh) and Mac (bash/zsh) is notoriously difficult without wrapper scripts. |
## Hidden Complexity
| Hidden Issue | Why It Will Surface | Severity | Example | Evidence |
| ------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| UI State Sync | The `setupDecision` logic | P2 | The dialog needs to resolve `getEffectiveHooks()` to know if the `Setup` section should appear, meaning the Renderer needs real-time access to the resolved repo policy. | `worktree-setup-script-design.md` -> Edge Cases -> Repo Changes While Dialog Is Open |
═══════════════════════════════════════════════════════════
QUESTIONS FOR THE AUTHOR
═══════════════════════════════════════════════════════════
1. **How exactly will the Renderer execute the string in the PTY?**
→ What we need: A concrete execution strategy (e.g., wrapper script, `\r` injection) that mitigates the P0 multiline failure containment issue and race conditions.
2. **How will `ORCA_WORKTREE_PATH` reach the PTY?**
→ What we need: Explicit mention of updating `pty:spawn` in `src/main/ipc/pty.ts` to accept an `env` override payload.
3. **What happens in the UI during the "creating..." phase?**
→ What we need: A definition of the UI state between clicking the dialog and the terminal opening. If the user clicks away to another worktree, does the terminal open in the background, or does it force-switch them back?

View File

@ -1,650 +0,0 @@
# Worktree Setup Command Design
## Problem
Issue [#238](https://github.com/stablyai/orca/issues/238) asks for two behaviors:
1. let a repo point to a setup script
2. let the user decide whether that setup should run during worktree creation
Orca already has a partial implementation:
- repo-level `setup` hooks exist today
- hooks can come from `orca.yaml` or repo settings UI
- the setup hook already runs in the new worktree after creation
What is missing is the product model:
- the create-worktree flow does not surface setup at all
- users cannot make a per-create decision
- the current UX frames setup as a generic lifecycle hook instead of a first-class workspace setup step
- setup failures are not visible enough to be actionable
## Research Summary
Broader developer tooling suggests a stronger design than a simple "run hook or not" toggle.
Conductor models setup as a repo-level command that runs in the new workspace and exposes workspace env vars. It also supports repo-committed configuration. That is the closest direct precedent.
Remote-dev tools such as Codespaces, dev containers, and Gitpod separate environment setup from app start:
- one-time or infrequent setup commands install dependencies and prepare the workspace
- interactive commands start services later
- expensive setup work should be explicit, repeatable, and ideally idempotent
Engineering blog guidance around `bin/setup` and setup scripts is consistent:
- provide one obvious command
- make it safe to rerun
- keep it readable
- use it to prepare the environment, not to hide unrelated workflow logic
## Goals
- Make worktree setup explicit in Orca.
- Preserve the existing hook plumbing instead of inventing a second execution system.
- Let the user decide whether setup runs for each new worktree.
- Keep repo-committed configuration possible.
- Improve compatibility with Conductor-style setup commands where it is low-cost and safe.
- Set a clearer product boundary between "prepare this worktree" and "run my app."
## Non-Goals
- Adding a full task runner or process manager UI
- Supporting multiple named setup phases in v1
- Parsing multiple repo config formats with complicated precedence rules
- Replacing the existing archive hook model
- Automatically starting long-running dev servers as part of worktree creation
## Design Principles
- Reuse the existing hook pipeline.
- Treat setup as a repo-level setup command, not as a path-only script field.
- Put the user decision in the create-worktree flow.
- Make the default policy configurable per repo.
- Require setup commands to be safe to rerun.
- Prefer cross-platform command guidance over shell-specific examples.
- Make failure visible enough that the user can recover without guessing.
## Terminology
This feature should be framed in the product as a `Setup Command` or `Setup Command`, not just a generic hook.
Why:
- "hook" is implementation language
- "setup" better communicates that the command prepares a fresh worktree
- it aligns better with established patterns like `bin/setup`, devcontainer lifecycle commands, and Gitpod init tasks
Internally, the existing `setup` hook plumbing can remain.
## Current State In Orca
Relevant code:
- [src/main/hooks.ts](../src/main/hooks.ts)
- [src/main/ipc/worktrees.ts](../src/main/ipc/worktrees.ts)
- [src/renderer/src/components/sidebar/AddWorktreeDialog.tsx](../src/renderer/src/components/sidebar/AddWorktreeDialog.tsx)
- [src/renderer/src/components/settings/RepositoryPane.tsx](../src/renderer/src/components/settings/RepositoryPane.tsx)
- [src/renderer/src/components/settings/HookEditor.tsx](../src/renderer/src/components/settings/HookEditor.tsx)
- [src/shared/types.ts](../src/shared/types.ts)
Behavior today:
- `getEffectiveHooks(repo)` resolves `setup` from `orca.yaml` or UI settings
- `runHook('setup', worktreePath, repo)` executes the command in the new worktree
- `worktrees:create` always runs setup when an effective setup hook exists
- the create-worktree dialog has no setup visibility or opt-out
- setup result visibility is limited to internal logging
Important nuance:
The backend already supports command strings, which means the configured setup may invoke:
- a package manager script like `pnpm worktree:setup`
- a Node entrypoint like `node scripts/setup-worktree.mjs`
- a repo-local shell script like `bash scripts/setup-worktree.sh`
- inline shell commands
So the main missing feature is not command execution support. It is product framing, policy, and UX.
## What The Ecosystem Suggests
## 1. Keep Setup As A Command String
Conductor, dev containers, and common setup patterns all center on a command to run, not a special "script path" object.
This is the right model because it supports:
- package scripts
- Node entrypoints
- shell commands
- wrappers that dispatch differently per platform
Adding a `setupScriptPath` field would be less flexible and would bias the feature toward shell-only implementations.
## 2. Separate Setup From App Start
The strongest cross-product pattern is lifecycle separation:
- setup prepares the environment
- start commands run interactive services later
For Orca v1, the setup/setup command should be explicitly scoped to worktree preparation tasks such as:
- install dependencies
- copy ignored env files
- initialize submodules
- run one-time codegen
- seed local config for that worktree
It should not be positioned as the place to:
- launch long-running servers
- start watch processes
- open ports
- manage ongoing background services
That guidance should appear in the design and product copy even if Orca does not enforce it technically.
## 3. Defaults Should Be Policy, Not Just Boolean State
The initial design proposed a boolean repo default. The broader research suggests a better model:
```ts
type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default'
```
Why:
- some repos should always prompt because setup is expensive or conditional
- some repos almost always want setup
- some repos rarely need setup, but the action should remain visible
This matches the issue better than a hidden boolean because it preserves user intent at the moment of creation.
## 4. Output Visibility Matters
A toast-only model is too weak.
Setup commands fail for predictable reasons:
- missing env files
- package manager auth issues
- version mismatches
- broken scripts
The user needs a place to inspect the output. V1 does not need a full job dashboard, but it should preserve enough output to debug failures.
## Proposed Product UX
## 1. Repository Settings
Keep the current hook system, but surface setup as a first-class setup concept.
Recommended changes:
- relabel the `setup` section to `Setup Command`
- keep `archive` under lifecycle hooks
- add a repo-level `When creating a worktree` policy control with:
- `Ask every time`
- `Run by default`
- `Skip by default`
- add guidance that setup commands should be safe to rerun
Recommended helper text:
- `Runs in the new worktree after creation to prepare the environment. Prefer an idempotent command such as "pnpm worktree:setup" or "node scripts/setup-worktree.mjs". Avoid starting long-running dev servers here.`
Why:
- this makes the feature discoverable
- it nudges teams toward reliable command shapes
- it aligns with cross-platform support
## 2. Create Worktree Dialog
When an effective setup command exists for the selected repo, show a setup section:
- label: `Setup`
- checkbox: `Run setup command after creation`
- supporting text: show whether the command comes from `orca.yaml` or UI settings
- preview: show a truncated first line or command summary
Initial checkbox value and dialog behavior comes from the repo policy:
- `ask` means the dialog must require an explicit choice:
- radio/select choice: `Run setup now` or `Skip for now`
- no implicit default submit path
- the create action stays disabled until the user chooses one
- `run-by-default` means the checkbox is checked. (If triggered via a "Quick Create" flow like a command palette, it may bypass the dialog entirely).
- `skip-by-default` means the checkbox is unchecked.
Why:
- the issue explicitly wants user choice
- expensive setup work should be visible at creation time
- `ask` should mean a real decision, not an unchecked box that silently turns into skip
## 3. Setup Result Visibility (Open A Terminal And Run It)
V1 should run setup in a normal integrated terminal for the new worktree, not as a hidden background task. Because the configured setup command might be a multiline script from `orca.yaml`, it cannot simply be pasted into an interactive terminal (doing so breaks idempotency if an early line fails, as the shell will continue executing subsequent lines).
**Execution Strategy (Generated Runner Script):**
1. The Main Process generates a temporary executable script file inside the worktree (`.git/orca/setup-runner.sh` for macOS/Linux, or `.git/orca/setup-runner.cmd` for Windows).
2. The Main Process writes the resolved multiline setup command into this script. For bash/zsh, prepend `set -e` so failures halt execution immediately.
3. After the worktree is created, if setup is enabled, Orca opens and focuses a terminal for that worktree.
4. Orca starts the generated script in that terminal (e.g., `bash .git/orca/setup-runner.sh` or `cmd.exe /c .git\\orca\\setup-runner.cmd`).
5. Success, failure, and interactive prompts (like SSH keys) are handled directly in the terminal safely.
**Post-Execution UX:**
- the terminal remains a normal, fully interactive shell
- when the command finishes, the user is back at the shell prompt in that same terminal
- the user can inspect output, press `Up Arrow` to retry, or run whatever they want next
Non-goal:
- a background jobs system
- durable setup-session recovery across reloads
- custom log persistence or special transcript storage
## Proposed Data Model
Do not add `setupScriptPath`.
Keep the setup command as a string and add a run policy:
```ts
type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default'
type RepoHookSettings = {
mode: 'auto' | 'override'
setupRunPolicy?: SetupRunPolicy // Defaults to 'run-by-default' if undefined
scripts: {
setup: string
archive: string
}
}
```
Rationale:
- the command model already solves "point to a script" and more
- policy is the missing product state
- `setupRunPolicy` being optional ensures backward compatibility with existing configs (migrating gracefully by defaulting to `run-by-default`)
## Proposed IPC/API Changes
Extend the create-worktree request:
```ts
type CreateWorktreeArgs = {
repoId: string
name: string
baseBranch?: string
setupDecision?: 'inherit' | 'run' | 'skip'
}
```
And update the return type to provide the generated runner script and environment payload:
```ts
type CreateWorktreeResult = {
// ... existing fields
setup?: {
runnerScriptPath: string
envVars: Record<string, string>
}
}
```
Update `pty:spawn` in `src/main/ipc/pty.ts` to accept custom environment variables so the setup terminal can receive its context:
```ts
// Existing: ipcMain.handle('pty:spawn', (_event, args: { cols: number; rows: number; cwd?: string })
// New:
ipcMain.handle('pty:spawn', (_event, args: { cols: number; rows: number; cwd?: string, env?: Record<string, string> })
```
Behavior:
- `run` always runs setup when an effective setup command exists
- `skip` always skips setup
- `inherit` delegates resolution to the backend using repo policy
- if the resolved policy is `ask` and the caller sends `inherit` (or omits the field), the backend rejects the request with an explicit error such as `Setup decision required for this repository`
Why:
- the backend must own policy enforcement so non-dialog callers cannot accidentally bypass `ask` or `skip-by-default`
- a tri-state decision keeps compatibility for existing callers while still allowing the backend to reject ambiguous creates when the repo requires a choice
## Proposed Execution Rules
## 1. Setup Resolution
Continue using `getEffectiveHooks(repo)` for command resolution.
That means setup may still come from:
- `orca.yaml`
- UI override
- UI fallback in auto mode
## 2. Create Flow
`worktrees:create` should:
1. create the git worktree exactly as it does today
2. persist worktree metadata
3. resolve whether this create operation should run setup by combining `setupDecision` with the repo's `setupRunPolicy`
4. if setup should run, generate a temporary runner script (e.g., `.git/orca/setup-runner.sh`) containing the resolved command.
5. return the created worktree plus the path to the generated setup script and the env vars to inject into the PTY.
Crucially, **the backend owns the policy decision and script generation, but the renderer owns opening the terminal and starting the visible terminal command**.
Requirements for execution:
- **Terminal-First:** Setup must run in a visible terminal, not through `runHook()` and not as a hidden background exec.
- **Safe Execution:** The Renderer must pass the generated runner script and environment variables to the new PTY, not raw multiline text.
- **Simple Ownership:** Orca should use the existing terminal flow. Open a terminal for the worktree, then start the setup command there.
- **Best-Effort:** If the renderer reloads before or during setup, Orca does not need to recover or resume that setup run. The user can rerun it manually.
- **Interactivity:** The user is NOT blocked from interacting with the workspace. They can browse code while the terminal runs the setup in plain view.
- **No Rollback:** Setup failure must not roll back worktree creation (the git operation already succeeded).
Why:
- environment preparation is best-effort workspace setupping, not git correctness
- a terminal-first approach avoids the complexity of background process management
- using the existing terminal ownership model is much simpler than inventing setup-session infrastructure
### Terminal Behavior
Required behavior:
- after a successful create with setup enabled, Orca automatically switches focus to the new worktree and opens its terminal panel
- Orca starts the generated runner script in that terminal with the appropriate environment variables injected
- when the command exits, the terminal remains available as a normal shell
- Orca does not guarantee that an in-flight setup survives reloads or terminal closure
Why:
- this solves the actual user problem, "show me the setup and let me interact with it"
- it avoids building a second PTY lifecycle just for setup
- if setup is interrupted, retrying in a terminal is straightforward
## 3. Idempotency Requirement
The product should document a strong expectation that setup commands are idempotent.
That means rerunning the command should be safe and should not corrupt the worktree.
Examples of acceptable behavior:
- reinstall or verify dependencies
- overwrite generated files deterministically
- copy missing env templates without deleting user-edited files
Examples of risky behavior Orca should discourage in docs and copy:
- unconditional destructive deletes
- long-running foreground servers
- one-off mutations that fail or duplicate state on rerun
Why:
- users may create multiple worktrees
- users may retry after failure
- policy defaults may cause setup to run frequently
## 4. Environment Variables
Orca already provides:
- `ORCA_ROOT_PATH`
- `ORCA_WORKTREE_PATH`
- `CONDUCTOR_ROOT_PATH`
- `GHOSTX_ROOT_PATH`
These existing variables should be passed to the setup command PTY to ensure scripts have the context they need.
## 5. Execution Environment (PTY)
Shell scripts can hang indefinitely if they accidentally prompt for user input (e.g., auth prompts, `read -p`).
By executing the setup command in an integrated terminal instead of a hidden background process:
- the user can see and respond to interactive prompts natively.
- the user has full control to cancel hanging scripts via standard terminal controls (`Ctrl+C`).
- familiar, colorized output is preserved.
## Repo-Committed Config Format
For v1, keep `orca.yaml` as the repo-committed config surface.
Example:
```yaml
scripts:
setup: |
pnpm install
node scripts/setup-worktree.mjs
```
or:
```yaml
scripts:
setup: |
node scripts/setup-worktree.mjs
```
Do not add `conductor.json` parsing in this issue.
Why:
- Orca already has `orca.yaml`
- loading both config files introduces precedence ambiguity
- env compatibility gives most of the practical value
## Cross-Platform Guidance
This feature must remain compatible with macOS, Linux, and Windows.
Recommended command examples:
- `pnpm worktree:setup`
- `npm run worktree:setup`
- `node scripts/setup-worktree.mjs`
Avoid recommending only:
- `./scripts/setup-worktree.sh`
Why:
- shell-script-only examples are weaker on Windows
- package scripts and Node entrypoints are easier to keep portable
## Edge Cases
## 1. No Setup Command Configured
- create-worktree dialog shows no setup section
- create flow behaves as it does today without setup execution
## 2. Repo Changes While Dialog Is Open
- the selected repos effective setup state controls setup section visibility
- if the repo selection changes and the new repo has no setup command, hide the section
- if the source changes between YAML and UI fallback, update the source label accordingly
## 3. Expensive Or Conditional Setup
- `ask` policy keeps the choice explicit
- `skip-by-default` covers repos where setup is uncommon but still available
## 4. Setup Failure
- worktree stays created
- failure is surfaced to the user
- the user can inspect output
- no automatic deletion or rollback
## 5. Re-Run After Creation / Recovery
Because setups can fail due to transient issues (e.g., missing `.env`, VPN drops), recovery is straightforward because the user is left in a normal terminal.
- The user can simply press `Up Arrow` and `Enter` in the terminal to retry the setup command.
- We can include a "Rerun Setup" action in the Worktree context menu later as a convenient shortcut that opens a terminal for that worktree and runs the same command again.
- This leverages the idempotency requirement to give developers an easy escape hatch when setup fails.
## 6. UI State During Creation
- Git cloning takes time. During creation, the "Create" button in the dialog should show a loading spinner.
- Once creation is successful, if setup is enabled, Orca should automatically switch focus to the new worktree and immediately open its terminal to surface the setup run.
## 8. Security & Unverified Repositories
- Automatically running setup scripts is a vector for arbitrary code execution if a user clones an untrusted repository.
- Because Orca relies on standard git cloning, if the user explicitly clicks `Run setup now`, they are opting in. However, the `run-by-default` policy must be carefully considered if Orca ever adds features to auto-clone arbitrary public repos. For v1 (managing existing trusted work repositories), defaulting to `run-by-default` is acceptable, but the UI must always display the preview of the command being run.
## Alternatives Considered
## 1. Background Execution with Log Tailing
Rejected.
Reasons:
- running shell scripts in the background is fragile (hidden SSH prompts cause hanging).
- building robust cross-platform process cancellation is difficult.
- tailing text logs in Electron requires additional IPC streaming overhead.
- "preventing interaction" while a 5-minute setup runs creates a hostile UX. A terminal-first approach solves all of these cleanly.
## 2. Add A Dedicated `setupScriptPath` Field
Rejected.
Reasons:
- current command-string model already supports script paths
- a path-only field biases the feature toward shell-specific usage
- command strings cover package scripts, Node entrypoints, wrappers, and inline commands with one model
## 2. Use A Boolean Default
Rejected in favor of a policy enum.
Reasons:
- a boolean cannot express "always prompt"
- issue #238 is fundamentally about user choice at creation time
- policy better matches real repo variation
## 3. Always Auto-Run Setup Like Conductor
Rejected.
Reasons:
- the issue explicitly asks for user choice
- setup commands can be slow, conditional, or side-effectful
## 4. Parse `conductor.json`
Rejected for this issue.
Reasons:
- increases config precedence complexity
- not required to solve the feature request
- environment compatibility provides most of the reuse value
## Implementation Plan
## Main Process
Files:
- [src/shared/types.ts](../src/shared/types.ts)
- [src/main/hooks.ts](../src/main/hooks.ts)
- [src/main/ipc/worktrees.ts](../src/main/ipc/worktrees.ts)
- [src/main/ipc/pty.ts](../src/main/ipc/pty.ts)
Changes:
- add `SetupRunPolicy`
- add `setupRunPolicy?: SetupRunPolicy` to `RepoHookSettings`
- extend `worktrees:create` args with `setupDecision?: 'inherit' | 'run' | 'skip'`
- resolve effective setup behavior in `worktrees:create`, including rejecting ambiguous creates when policy is `ask`
- if setup should run, generate a temporary runner script file (e.g. `.git/orca/setup-runner.sh`) containing the resolved command with `set -e`
- return the created worktree, the generated script path, and the injected environment variables in the result payload
- update `pty:spawn` to accept custom `env` overrides
- keep hidden `runHook()` execution for archive, but do not use it for visible setup execution
## Renderer
Files:
- [src/preload/index.d.ts](../src/preload/index.d.ts)
- [src/preload/index.ts](../src/preload/index.ts)
- [src/renderer/src/store/slices/worktrees.ts](../src/renderer/src/store/slices/worktrees.ts)
- [src/renderer/src/components/sidebar/AddWorktreeDialog.tsx](../src/renderer/src/components/sidebar/AddWorktreeDialog.tsx)
- [src/renderer/src/components/settings/RepositoryPane.tsx](../src/renderer/src/components/settings/RepositoryPane.tsx)
- [src/renderer/src/components/settings/HookEditor.tsx](../src/renderer/src/components/settings/HookEditor.tsx)
Changes:
- thread `setupDecision` through preload and store
- show policy-driven setup controls in `AddWorktreeDialog` with a loading state during creation
- for `ask`, require an explicit `Run setup now` vs `Skip for now` choice before enabling create
- for `run-by-default` and `skip-by-default`, initialize the checkbox from repo policy
- expose setup policy in repository settings
- update settings copy to emphasize setup scope and idempotency
- on successful worktree creation, automatically switch focus to the new worktree
- if setup is enabled, open a terminal for the new worktree, pass the returned custom environment variables to the PTY, and start the generated runner script
## Tests
Add or extend tests for:
- `worktrees:create` skips setup when `setupDecision` is `skip`
- `worktrees:create` generates a runner script and returns path when `setupDecision` is `run`
- `worktrees:create` resolves `inherit` via repo policy
- `worktrees:create` rejects ambiguous `inherit` calls when repo policy is `ask`
- create-worktree dialog shows setup controls only when effective setup exists
- dialog requires an explicit choice when repo policy is `ask`
- dialog uses repo policy for initial state when policy is `run-by-default` or `skip-by-default`
- renderer opens a terminal and starts setup when create returns with setup enabled
- output is visible in the terminal after setup failure
## Recommendation
Implement this as an extension of the current hook system, but tighten the product model:
- frame setup as a worktree setup command
- keep the command as a string
- use a repo-level run policy enum instead of a boolean default
- keep explicit per-create user choice in the dialog, and enforce `ask` in the backend instead of trusting the renderer
- **execute the setup command via a generated runner script in a normal integrated terminal** so multiline commands execute safely, and prompts, cancellation, and output are visible
- explicitly pass standard Orca context variables (`ORCA_WORKTREE_PATH`, etc.) directly into the PTY environment
- keep v1 best-effort, if the terminal is closed or the renderer reloads, the user can rerun setup manually
- document setup as idempotent environment preparation, not app startup
## Sources
- [Conductor environment variables](https://docs.conductor.build/tips/conductor-env)
- [Conductor workspaces and branches](https://docs.conductor.build/tips/workspaces-and-branches)
- [Conductor using monorepos](https://docs.conductor.build/tips/using-monorepos)
- [GitHub Codespaces: Introduction to dev containers](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers)
- [GitHub Codespaces: Configuring prebuilds](https://docs.github.com/en/codespaces/prebuilding-your-codespaces/configuring-prebuilds)
- [containers.dev supporting tools and prebuild patterns](https://containers.dev/supporting.html)
- [containers.dev prebuild guide](https://containers.dev/guide/prebuild)
- [Gitpod tasks](https://ona.com/docs/classic/user/configure/workspaces/tasks)
- [thoughtbot: Use `bin/setup` to simplify development environment setup](https://thoughtbot.com/blog/bin-setup)
- [thoughtbot: Laptop setup for an awesome development environment](https://thoughtbot.com/blog/laptop-setup-for-an-awesome-development-environment)
- [Chris Blunt: Simplifying local environment setup with `bin/setup`](https://www.chrisblunt.com/rails-simplifying-local-environment-setup/)
- [Mesi Rendon: Working environment setupper](https://mesirendon.com/articles/working-environment-setuper/)
- [Nathan Onn: Git worktrees and setup friction in multi-agent workflows](https://www.nathanonn.com/how-i-vibe-code-with-3-ai-agents-using-git-worktrees-without-breaking-anything/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

View File

@ -12,27 +12,27 @@
"format": "oxfmt --write .",
"lint": "oxlint",
"prepare": "husky",
"test": "vitest run",
"tc:node": "tsgo --noEmit -p tsconfig.node.json",
"tc:cli": "tsgo --noEmit -p tsconfig.tc.cli.json",
"tc:web": "tsgo --noEmit -p tsconfig.tc.web.json",
"tc": "tsgo --noEmit -p tsconfig.node.json && tsgo --noEmit -p tsconfig.tc.cli.json && tsgo --noEmit -p tsconfig.tc.web.json",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:cli": "tsc --noEmit -p tsconfig.cli.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "tsc --noEmit -p tsconfig.node.json --composite false && tsc --noEmit -p tsconfig.cli.json --composite false && tsc --noEmit -p tsconfig.web.json --composite false",
"test": "vitest run --config config/vitest.config.ts",
"tc:node": "tsgo --noEmit -p config/tsconfig.node.json",
"tc:cli": "tsgo --noEmit -p config/tsconfig.tc.cli.json",
"tc:web": "tsgo --noEmit -p config/tsconfig.tc.web.json",
"tc": "tsgo --noEmit -p config/tsconfig.node.json && tsgo --noEmit -p config/tsconfig.tc.cli.json && tsgo --noEmit -p config/tsconfig.tc.web.json",
"typecheck:node": "tsc --noEmit -p config/tsconfig.node.json --composite false",
"typecheck:cli": "tsc --noEmit -p config/tsconfig.cli.json --composite false",
"typecheck:web": "tsc --noEmit -p config/tsconfig.web.json --composite false",
"typecheck": "tsc --noEmit -p config/tsconfig.node.json --composite false && tsc --noEmit -p config/tsconfig.cli.json --composite false && tsc --noEmit -p config/tsconfig.web.json --composite false",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build:cli": "tsc -p tsconfig.cli.json --outDir out --composite false --incremental false",
"build:cli": "tsc -p config/tsconfig.cli.json --outDir out --composite false --incremental false",
"build:electron-vite": "node scripts/run-electron-vite-build.mjs",
"build": "pnpm run typecheck && pnpm run build:electron-vite && pnpm run build:cli",
"postinstall": "pnpm rebuild electron && electron-builder install-app-deps",
"build:unpack": "pnpm run build && electron-builder --config electron-builder.config.cjs --dir",
"build:win": "pnpm run build && electron-builder --config electron-builder.config.cjs --win",
"build:unpack": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --dir",
"build:win": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --win",
"build:icons": "bash icon/generate.sh",
"build:mac": "pnpm run build && electron-builder --config electron-builder.config.cjs --mac",
"build:mac:release": "node scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build && ORCA_MAC_RELEASE=1 electron-builder --config electron-builder.config.cjs --mac",
"build:linux": "pnpm run build && electron-builder --config electron-builder.config.cjs --linux",
"build:mac": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --mac",
"build:mac:release": "node scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac",
"build:linux": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --linux",
"release:rc": "npm version prerelease --preid=rc && git push --follow-tags",
"release:patch": "npm version patch && git push --follow-tags",
"release:minor": "npm version minor && git push --follow-tags",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@ -227,7 +227,6 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() {
handleOpenChange,
resolvedSetupDecision,
selectedRepo,
setupConfig,
shouldWaitForSetupCheck
])

View File

@ -1,6 +1,6 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
"references": [{ "path": "./config/tsconfig.node.json" }, { "path": "./config/tsconfig.web.json" }],
"compilerOptions": {
"baseUrl": ".",
"paths": {