Files
galaxy/specs/APP-4115/TECH.md
T

102 lines
8.0 KiB
Markdown

# TECH — Don't auto-add indexed repos to Directory tab colors
See also: `PRODUCT.md` in this directory.
Linear: [APP-4115](https://linear.app/warpdotdev/issue/APP-4115/dont-auto-add-indexed-repos-including-worktrees-to-directory-tab).
## Problem
Two coupled code changes are needed to deliver the product behavior:
1. Stop the `CodebaseIndexManager`-driven subscription that auto-populates `TabSettings.directory_tab_colors` with newly indexed paths.
2. Make the `Add directory color` control conditional: render the existing searchable `FilterableDropdown` when candidate repos exist, and render a plain `ActionButton` that opens the folder picker when the candidate set is empty.
## Relevant code
- `app/src/workspace/view.rs:2775-2790` — the subscription that calls `DirectoryTabColors::merge_new_paths` on every `CodebaseIndexManagerEvent::SyncStateUpdated`.
- `app/src/workspace/tab_settings.rs:168-243``DirectoryTabColor` / `DirectoryTabColors`, including `with_color` and `color_for_directory`.
- `app/src/settings_view/appearance_page.rs:1371-1378` — construction of `DirectoryColorAddPicker`.
- `app/src/settings_view/appearance_page.rs:2416-2425``handle_directory_color_add_picker_event`.
- `app/src/settings_view/appearance_page.rs:4632-4671` — helper functions for adding a directory tab color directly and opening the folder picker.
- `app/src/settings_view/appearance_page.rs:4674-4815``DirectoryTabColorsWidget`, delete buttons, and rendering of the colors list.
- `app/src/settings_view/directory_color_add_picker.rs` — candidate computation, conditional rendering, and footer/button event emission.
- `app/src/view_components/filterable_dropdown.rs` — existing searchable dropdown behavior and pinned footer support.
- `app/src/ai/persisted_workspace.rs:714-721``PersistedWorkspace::workspaces()` iterator.
- `crates/ai/src/index/full_source_code_embedding/manager.rs:556-558``get_codebase_paths`.
- `crates/warp_util/src/path.rs:85-114``user_friendly_path` helper used in the visible list.
## Current state
- On startup and on every indexing sync-state update, the workspace view reads all `CodebaseIndexManager` paths and calls `DirectoryTabColors::merge_new_paths`, which inserts each canonical path into `directory_tab_colors` as `Unassigned` if not already present.
- `DirectoryColorAddPicker` already wraps `FilterableDropdown` and computes the correct candidate set for the searchable known-repo flow.
- The X button on each row dispatches `RemoveDefaultDirectoryTabColor`, which writes `DirectoryTabColor::Suppressed` rather than deleting the key.
## Proposed changes
### 1. Remove the auto-add subscription
Delete the entire `if FeatureFlag::DirectoryTabColors.is_enabled() { ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), …) }` block at `app/src/workspace/view.rs:2775-2790`. No replacement subscription is needed.
Delete `DirectoryTabColors::merge_new_paths` and any tests or helpers whose only purpose was validating the old auto-add behavior.
### 2. Keep `FilterableDropdown` and add a fallback button
Keep `app/src/settings_view/directory_color_add_picker.rs` centered on the existing `FilterableDropdown` implementation. Add a second child view for the plain button and track whether the dropdown currently has any candidate rows.
```rust path=null start=null
pub struct DirectoryColorAddPicker {
button: ViewHandle<ActionButton>,
dropdown: ViewHandle<FilterableDropdown<DirectoryColorAddPickerAction>>,
footer_mouse_state: MouseStateHandle,
has_dropdown_items: bool,
}
```
`DirectoryColorAddPicker::new`:
- Creates the fallback `ActionButton::new("Add directory color", SecondaryTheme).with_icon(Icon::Plus)` and wires it to dispatch `DirectoryColorAddPickerAction::AddNewDirectory`.
- Creates the existing `FilterableDropdown`, keeps its current searchable behavior, and preserves the pinned `+ Add directory…` footer.
- Subscribes to `CodebaseIndexManager`, `PersistedWorkspace`, and `TabSettings` so the candidate list refreshes when any source changes.
`refresh_items`:
- Computes the candidate set using the existing pure helper.
- Converts candidates into `DropdownItem<DirectoryColorAddPickerAction>` values and passes them to `dropdown.set_items(items, ctx)`.
- Sets `has_dropdown_items = !items.is_empty()`.
- If the list becomes empty, closes the dropdown so the view can switch cleanly to the fallback button.
`render`:
- If `has_dropdown_items` is true, render `ChildView::new(&self.dropdown)`.
- Otherwise, render `ChildView::new(&self.button)`.
`handle_action`:
- `Select(path)` emits `DirectoryColorAddPickerEvent::Selected(path)` and lets `FilterableDropdown` close itself after dispatch.
- `AddNewDirectory` closes the dropdown if needed and emits `DirectoryColorAddPickerEvent::RequestAddFromFilePicker`.
### 3. Candidate-set helper
Keep `compute_candidate_paths` as the pure helper that:
- unions indexed and persisted paths,
- canonicalizes keys the same way `DirectoryTabColors::with_color` does,
- dedupes by canonical key,
- filters out entries already present with a non-`Suppressed` color,
- keeps `Suppressed` entries re-addable,
- filters out missing paths,
- sorts by canonical key.
### 4. Appearance page wiring
No appearance-page architectural change is needed beyond the existing `DirectoryColorAddPicker` integration:
- `build_page` continues to construct the picker and subscribe to its events.
- `handle_directory_color_add_picker_event` continues to map `Selected(path)` to `add_directory_tab_color_path(path, ctx)` and `RequestAddFromFilePicker` to `open_directory_tab_color_folder_picker(ctx)`.
- The existing rebuild of `directory_tab_color_delete_buttons` and `color_picker_dot_states` in `handle_tab_settings_event` remains the source of truth for the visible list.
### 5. `Suppressed` behavior
No special-case behavior is needed beyond the existing helper and appearance-page add path:
- If a candidate row corresponds to a `Suppressed` key, selecting it transitions that entry back to `Unassigned` through `with_color(path, DirectoryTabColor::Unassigned)`.
- If the user removes a row and it still exists in the indexed/persisted candidate set, it will reappear in the dropdown.
## End-to-end flow
### Candidate repos exist
1. `DirectoryColorAddPicker` renders the searchable dropdown.
2. The user opens it, filters if needed, and either selects a row or clicks the pinned footer.
3. Row selection emits `Selected(path)`.
4. Footer click emits `RequestAddFromFilePicker`.
### No candidate repos exist
1. `DirectoryColorAddPicker` renders the plain `Add directory color` button.
2. Clicking the button emits `RequestAddFromFilePicker` immediately.
3. The native folder picker opens, and on success the selected path is added through the existing appearance-page helper.
### Indexing sync state updates
1. `CodebaseIndexManagerEvent::SyncStateUpdated` fires.
2. `directory_tab_colors` is not modified.
3. `DirectoryColorAddPicker` refreshes its candidate list and may switch between dropdown and button depending on whether any rows remain.
## Risks and mitigations
- **The control changes shape when the candidate count crosses zero.** This is intentional. The dropdown only adds value when there are known repos to show.
- **`Suppressed` semantics become less obvious.** Keep the existing `Suppressed` write path unchanged so prefix-shadowing behavior remains stable.
- **Stale dropdown state when candidates disappear.** Close the dropdown in `refresh_items` whenever the candidate list becomes empty.
## Testing and validation
- Unit tests on `compute_candidate_paths` covering dedupe, `Suppressed`, missing-path filtering, worktree inclusion, and sort order.
- Regression test confirming that `CodebaseIndexManagerEvent::SyncStateUpdated` no longer mutates `directory_tab_colors`.
- Manual validation of both control states:
1. Dropdown shown when candidates exist.
2. Fallback button shown when no candidates exist.
3. Dropdown row selection adds an `Unassigned` entry.
4. Dropdown footer opens the folder picker.
5. Fallback button opens the folder picker.
- `cargo check` and `cargo fmt --check`.
- `verify-ui-change-in-cloud` for the final UI check.
## Follow-ups
None.