Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# Save Current Tab as New Config — Product Spec
|
||||
|
||||
Linear: [APP-3704](https://linear.app/warpdotdev/issue/APP-3704/save-current-tab-as-a-new-config)
|
||||
Figma: [House of Agents — node 7123-32864](https://figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7123-32864&m=dev)
|
||||
|
||||
## Summary
|
||||
|
||||
Add a "Save as new config" item to the tab right-click context menu that snapshots the current tab's pane layout (splits, working directories, focus, and color) and writes it as a new tab config TOML file in `~/.warp/tab_configs/`.
|
||||
|
||||
## Problem
|
||||
|
||||
Users can create complex pane layouts interactively (splits, different working directories per pane, tab color) but have no way to persist that layout as a reusable tab config. The only path today is to hand-author TOML from scratch, which requires understanding the schema and manually transcribing the layout.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let users save their current tab's pane layout as a new tab config TOML with one click.
|
||||
- Preserve the full spatial structure: splits, working directories, focus state, and tab color.
|
||||
- Open the generated file in the user's editor so they can rename it and customize further.
|
||||
- The saved config appears immediately in the `+` tab menu (the filesystem watcher picks it up).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **"Save and update config"** (overwriting an existing tab config the tab was opened from). This requires tracking the source config path per tab and is a separate ticket.
|
||||
- **"Combine panes into single tab"** — shown greyed out in the Figma, separate feature.
|
||||
- Saving non-terminal pane content (notebooks, code panes, settings panes). These are not replayable from a TOML config.
|
||||
- Parameterization. The saved config has no `[params]` section; the user can add params manually.
|
||||
|
||||
## Figma
|
||||
|
||||
Figma: [House of Agents — node 7123-32864](https://figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7123-32864&m=dev)
|
||||
|
||||
The updated tab right-click menu adds a new section between the "close" section and the "color" section:
|
||||
- **Save as new config** — saves the current tab as a new tab config TOML.
|
||||
|
||||
Note: The Figma also shows "Save and update config" and "Combine panes into single tab" in this section. Those are out of scope for this ticket (see Non-goals).
|
||||
|
||||
## User Experience
|
||||
|
||||
### Triggering the save
|
||||
|
||||
1. User right-clicks a tab.
|
||||
2. The context menu includes "Save as new config" (between the close-tab section and the color section). This item is only visible when the `TabConfigs` feature flag is enabled.
|
||||
3. User clicks "Save as new config".
|
||||
|
||||
### What happens
|
||||
|
||||
1. Warp snapshots the tab's pane tree: split directions, each pane's current working directory, focus state, and tab color.
|
||||
2. Warp writes a new `.toml` file to `~/.warp/tab_configs/` with an auto-generated filename (`my_tab_config.toml`, or `my_tab_config_1.toml` if the name is taken).
|
||||
3. Warp opens the file in the user's configured editor (same behavior as "Create new tab config...").
|
||||
4. The filesystem watcher picks up the new file, and it appears in the `+` tab menu as "New Tab: My Tab Config".
|
||||
|
||||
### TOML structure
|
||||
|
||||
The saved config uses the flat `[[panes]]` schema from APP-3575. Example for a two-pane horizontal split:
|
||||
|
||||
```toml
|
||||
name = "My Tab Config"
|
||||
color = "blue"
|
||||
|
||||
[[panes]]
|
||||
id = "p1"
|
||||
split = "horizontal"
|
||||
children = ["p2", "p3"]
|
||||
|
||||
[[panes]]
|
||||
id = "p2"
|
||||
type = "terminal"
|
||||
cwd = "/Users/me/project-a"
|
||||
is_focused = true
|
||||
|
||||
[[panes]]
|
||||
id = "p3"
|
||||
type = "terminal"
|
||||
cwd = "/Users/me/project-b"
|
||||
```
|
||||
|
||||
### Non-terminal pane handling
|
||||
|
||||
When the tab contains non-terminal panes (notebook, code, settings, etc.), those panes are replaced with an empty terminal pane in the saved config. This preserves the spatial layout (splits are maintained) while substituting content that cannot be replayed from TOML. The replacement terminal pane has no `cwd` set (omitted from TOML; defaults to the user's home directory when opened).
|
||||
|
||||
### Tab title and color
|
||||
|
||||
- If the tab has a custom title, it is saved as the `title` field.
|
||||
- If the tab has a color, it is saved as the `color` field.
|
||||
- The config `name` is always `"My Tab Config"` — the user can rename it in the file.
|
||||
|
||||
### Error handling
|
||||
|
||||
- If writing the file fails (permissions, disk full), a warning is logged and no file is opened. No toast or modal is shown — this is a low-probability error.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Right-clicking a tab with `FeatureFlag::TabConfigs` enabled shows "Save as new config" in the context menu.
|
||||
2. Clicking "Save as new config" on a single-pane terminal tab writes a valid TOML with one `[[panes]]` entry and the correct `cwd`.
|
||||
3. Clicking "Save as new config" on a two-pane horizontal split writes a TOML with a split root and two leaf children with correct cwds.
|
||||
4. Clicking "Save as new config" on a tab with a blue color writes `color = "blue"` in the TOML.
|
||||
5. The focus state is preserved: the focused pane has `is_focused = true`; unfocused panes omit the field entirely.
|
||||
6. A tab with a non-terminal pane (e.g., a notebook pane in a split) produces a TOML that replaces the notebook with a terminal pane, preserving the split.
|
||||
7. The written TOML file round-trips: `toml::from_str::<TabConfig>(toml::to_string_pretty(&config))` produces the same `TabConfig`.
|
||||
8. The file appears in the `+` menu after saving.
|
||||
9. Opening the saved config from the `+` menu reproduces the original pane layout.
|
||||
10. The menu item does not appear when `FeatureFlag::TabConfigs` is off.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Unit tests:** `tab_config_from_pane_snapshot` for single pane, split, 2x2 grid, non-terminal replacement, focus handling, and TOML round-trip.
|
||||
- **Manual testing:** Right-click → "Save as new config" on various tab layouts, verify the TOML, open the config from the `+` menu, confirm the layout matches.
|
||||
|
||||
## Open Questions
|
||||
|
||||
(None outstanding.)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Save Current Tab as New Config — Tech Spec
|
||||
|
||||
Product spec: `specs/APP-3704/PRODUCT.md`
|
||||
|
||||
## Problem
|
||||
|
||||
There is no code path to convert a live tab's pane tree into a `TabConfig` TOML. The existing `PaneTemplateType::try_from(PaneNodeSnapshot)` in `launch_configs/launch_config.rs` converts snapshots to the launch config format but drops non-terminal panes entirely and produces a `PaneTemplateType`, not a `TabConfig`.
|
||||
|
||||
## Relevant Code
|
||||
|
||||
- `app/src/tab_configs/session_config.rs:174` — `tab_config_from_pane_snapshot` (new)
|
||||
- `app/src/tab_configs/session_config.rs:194` — `snapshot_to_flat_panes` helper (new)
|
||||
- `app/src/tab_configs/session_config.rs:144` — `write_tab_config` (updated signature)
|
||||
- `app/src/tab_configs/tab_config.rs` — `TabConfig`, `TabConfigPaneNode`, `TabConfigPaneType`
|
||||
- `app/src/pane_group/mod.rs:2029` — `PaneGroup::snapshot()` returns `PaneNodeSnapshot`
|
||||
- `app/src/app_state.rs` — `PaneNodeSnapshot`, `LeafContents`, `TerminalPaneSnapshot`, `BranchSnapshot`
|
||||
- `app/src/tab.rs:340` — `save_config_menu_items` (new)
|
||||
- `app/src/workspace/action.rs:561` — `WorkspaceAction::SaveCurrentTabAsNewConfig`
|
||||
- `app/src/workspace/view.rs:4969` — `save_current_tab_as_new_config` handler (new)
|
||||
- `app/src/workspace/view.rs:16831` — action dispatch
|
||||
- `app/src/user_config/mod.rs` — `find_unused_toml_path`, `tab_configs_dir`
|
||||
|
||||
## Current State
|
||||
|
||||
**Snapshot infrastructure:** `PaneGroup::snapshot()` recursively walks the pane tree and produces a `PaneNodeSnapshot` with `Branch` and `Leaf` variants. Leaf snapshots carry `LeafContents` which is `Terminal(TerminalPaneSnapshot)`, `Notebook(...)`, `Code(...)`, etc. `TerminalPaneSnapshot` includes `cwd: Option<String>` and `is_active: bool`.
|
||||
|
||||
**Tab config serialization:** `TabConfig` derives both `Serialize` and `Deserialize` (added in APP-3575). `write_tab_config` in `session_config.rs` serializes via `toml::to_string_pretty` and writes to disk.
|
||||
|
||||
**Tab right-click menu:** Built in `TabData::menu_items()` with four sections: session sharing, modify tab, close tab, and color. Each section is a separate method returning `Vec<MenuItem<WorkspaceAction>>`.
|
||||
|
||||
**Open-in-editor pattern:** `create_and_open_new_tab_config` writes a file, resolves the editor target via `EditorSettings`, and calls `open_file_with_target`.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `tab_config_from_pane_snapshot` and `snapshot_to_flat_panes`
|
||||
|
||||
Added to `app/src/tab_configs/session_config.rs`. Public function `tab_config_from_pane_snapshot` accepts `&PaneNodeSnapshot`, optional title, and optional color. It delegates to a private recursive helper `snapshot_to_flat_panes` that walks the tree with a `&mut usize` counter for ID generation (`"p1"`, `"p2"`, …).
|
||||
|
||||
Branch nodes: reserve their ID, recurse into children to collect child IDs, then insert the split node before the children in the flat list (root-first ordering via `panes.insert(insert_pos, ...)`).
|
||||
|
||||
Terminal leaves: extract `cwd` from `TerminalPaneSnapshot.cwd`, set `pane_type = Terminal`, set `is_focused = Some(true)` only when focused (otherwise `None` — omitted from TOML).
|
||||
|
||||
Non-terminal leaves: produce a terminal pane with no `cwd`, preserving the split layout.
|
||||
|
||||
Returns `TabConfig { name: "My Tab Config", title: custom_title, color, panes, params: HashMap::new() }`.
|
||||
|
||||
### 2. `write_tab_config` generalized
|
||||
|
||||
Signature changed to accept `base_name: &str` instead of hardcoding `"startup_config"`. Call site in `handle_session_config_completed` updated to pass `"startup_config"`. The new save handler passes `"my_tab_config"`.
|
||||
|
||||
### 3. `WorkspaceAction::SaveCurrentTabAsNewConfig(usize)`
|
||||
|
||||
Added at `workspace/action.rs:561`. Returns `false` in `should_save_app_state_on_action` — writing a config file doesn't change workspace state.
|
||||
|
||||
### 4. `save_config_menu_items` in `TabData`
|
||||
|
||||
Added at `tab.rs:340`. Static method gated behind `FeatureFlag::TabConfigs`. Returns a single "Save as new config" item dispatching `SaveCurrentTabAsNewConfig(index)`. Inserted in `menu_items()` between the close-tab section and the color section.
|
||||
|
||||
### 5. `save_current_tab_as_new_config` workspace handler
|
||||
|
||||
Added at `workspace/view.rs:4969`. Gated behind `#[cfg(feature = "local_fs")]` with a no-op stub for non-local-fs builds. Inline-imports `tab_config_from_pane_snapshot` and `write_tab_config` to match the existing inline import pattern used by `handle_session_config_completed`. Snapshots the tab's pane group, extracts custom title and color, builds the `TabConfig`, writes TOML to `~/.warp/tab_configs/my_tab_config.toml` (collision-safe), and opens the file in the user's editor via `resolve_file_target` + `open_file_with_target`.
|
||||
|
||||
Action dispatch wired at `workspace/view.rs:16831`.
|
||||
|
||||
## End-to-End Flow
|
||||
|
||||
1. User right-clicks a tab → `TabData::menu_items()` runs, includes "Save as new config" (flag-gated).
|
||||
2. User clicks → `WorkspaceAction::SaveCurrentTabAsNewConfig(tab_index)` dispatched.
|
||||
3. Workspace handler snapshots the tab's `PaneGroup` via `snapshot()`.
|
||||
4. `tab_config_from_pane_snapshot` converts the `PaneNodeSnapshot` tree into a flat `TabConfig`.
|
||||
5. `write_tab_config` serializes to TOML and writes to `~/.warp/tab_configs/my_tab_config.toml`.
|
||||
6. The file is opened in the user's configured editor.
|
||||
7. The filesystem watcher detects the new file and adds it to the `+` menu.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
**Non-terminal pane replacement:** Replacing non-terminal panes with empty terminals is lossy — the user might not expect an empty pane when reopening the config. Mitigated by opening the file in the editor immediately, so the user can see and adjust the result.
|
||||
|
||||
**Snapshot timing:** `PaneGroup::snapshot()` is synchronous and captures the current state at call time. If a shell is still initializing, `cwd` might be empty. This is the same risk as the existing launch config save flow, which is acceptable.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### `tab_config_from_pane_snapshot` (unit tests in `session_config_tests.rs`)
|
||||
|
||||
- `snapshot_single_terminal_pane` — single leaf produces correct cwd, `is_focused = true`, ID `"p1"`.
|
||||
- `snapshot_two_pane_horizontal_split` — split root `"p1"` + two leaf children `"p2"` and `"p3"` with correct cwds.
|
||||
- `snapshot_2x2_grid` — 3 split nodes + 4 leaf nodes = 7 total panes, root is `"p1"`.
|
||||
- `snapshot_non_terminal_leaf_replaced_with_terminal` — notebook pane replaced with `pane_type = Terminal`, `cwd = None`.
|
||||
- `snapshot_preserves_custom_title_and_color` — title and color propagated to `TabConfig`.
|
||||
- `snapshot_round_trip_toml` — `toml::to_string_pretty` → `toml::from_str` produces matching `TabConfig`.
|
||||
|
||||
### `write_tab_config` with custom base name
|
||||
|
||||
- `write_tab_config_custom_base_name` — base name `"my_tab_config"` writes to `my_tab_config.toml`.
|
||||
- Existing `write_tab_config_creates_file_with_correct_naming` tests updated for new `base_name` parameter.
|
||||
|
||||
### Manual verification
|
||||
|
||||
- Build and run Warp, create a split layout, right-click → "Save as new config", verify the TOML, open from `+` menu.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- **"Save and update config":** Track which tab config a tab was opened from (store path on `TabData` or `PaneGroup`), and overwrite on save.
|
||||
- **Agent/Cloud pane handling:** Currently all panes save as `type = "terminal"`. Once agent/cloud panes are more common, detect them from the snapshot and save `type = "agent"` or `type = "cloud"`.
|
||||
Reference in New Issue
Block a user