Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# APP-3736: Allow specifying tab-close behavior in tab configs
|
||||
|
||||
Linear: [APP-3736](https://linear.app/warpdotdev/issue/APP-3736/allow-for-specifying-closing-of-tab-behavior)
|
||||
|
||||
## Summary
|
||||
|
||||
Add an optional top-level close hook to tab configs so a tab instance opened from a config can run best-effort cleanup behavior when the tab is explicitly closed. The primary use case is worktree cleanup: deleting the worktree directory on close, with an optional second step that also deletes the worktree branch.
|
||||
|
||||
## Problem
|
||||
|
||||
Tab configs can already create a worktree when a tab opens, but they cannot declare what should happen when that tab is later closed. Users who use tab configs as ephemeral worktree environments have to remember to manually clean up the worktree directory and branch. The current template and bundled `tab-configs` skill teach creation-time worktree commands, but not cleanup.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let tab config authors opt into tab-close behavior on a per-config basis.
|
||||
- Make worktree cleanup a first-class documented tab-config pattern.
|
||||
- Support both cleanup variants:
|
||||
- remove the worktree only
|
||||
- remove the worktree and delete the branch
|
||||
- Use the actual resolved values for the specific tab instance at close time, including params and any auto-generated worktree branch name.
|
||||
- Update the default template comments and bundled `tab-configs` skill examples to show the new close behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Per-pane close hooks.
|
||||
- Guaranteeing cleanup when Warp crashes, is force-quit, or loses power.
|
||||
- Guaranteeing cleanup after app restore or undo-close. Close behavior is resolved for the live tab instance when the tab opens and is not persisted into workspace snapshots.
|
||||
- Managing worktrees created outside the tab config flow.
|
||||
- New confirmation UI or modal flow for close cleanup.
|
||||
- Automatically inferring cleanup commands from open-time commands; config authors must declare close behavior explicitly.
|
||||
|
||||
## Figma / design references
|
||||
|
||||
Figma: none provided.
|
||||
|
||||
## User experience
|
||||
|
||||
### Authoring the config
|
||||
|
||||
Tab configs may optionally include a top-level `[on_close]` table.
|
||||
|
||||
- `on_close` applies to the tab as a whole, not to individual panes.
|
||||
- `[on_close]` may specify:
|
||||
- `directory` (optional): working directory used for close-time commands.
|
||||
- `commands` (required when `[on_close]` is present): ordered shell commands to run when the tab closes.
|
||||
- `directory` and `commands` support the same template variables that tab configs already support for `title`, pane `directory`, and pane `commands`.
|
||||
- Close-time template expansion uses the values that were used when the tab instance was opened. Warp must not re-prompt for params when the tab closes.
|
||||
- Template rendering follows the same quoting rules as open-time config rendering: `directory` receives unquoted values so paths remain valid, and `commands` receive shell-quoted values.
|
||||
|
||||
### Triggering close behavior
|
||||
|
||||
- Close behavior runs when the user explicitly closes a tab instance opened from a tab config, such as from the close button, tab context menu, or keyboard shortcut.
|
||||
- If a config does not define `[on_close]`, closing behaves exactly as it does today.
|
||||
- Close behavior runs once per closing tab instance, even if the tab contains multiple panes.
|
||||
- There is no additional confirmation prompt at close time.
|
||||
- Close cleanup starts asynchronously during tab close, and the tab is removed immediately without waiting for cleanup commands to finish.
|
||||
|
||||
### Command execution semantics
|
||||
|
||||
- Warp runs `on_close.commands` in order.
|
||||
- If a command fails, Warp still closes the tab. Failures are best-effort and do not block close.
|
||||
- If a command fails, Warp stops running the remaining close commands for that tab instance, logs the failure, and shows a persistent non-blocking error toast.
|
||||
- If Warp cannot access local shell state for cleanup, close commands are skipped, the tab still closes, and Warp shows a persistent non-blocking error toast.
|
||||
- For worktree configs, the common pattern is to set `on_close.directory` to the repo root so cleanup does not depend on the tab's live shell state.
|
||||
|
||||
### Worktree example: remove the worktree, keep the branch
|
||||
|
||||
```toml
|
||||
name = "New Worktree"
|
||||
title = "{{worktree_branch_name}}"
|
||||
|
||||
[[panes]]
|
||||
id = "main"
|
||||
type = "terminal"
|
||||
directory = "{{repo}}"
|
||||
commands = [
|
||||
"git worktree add -b {{worktree_branch_name}} ../{{worktree_branch_name}} {{branch}}",
|
||||
"cd ../{{worktree_branch_name}}",
|
||||
]
|
||||
|
||||
[on_close]
|
||||
directory = "{{repo}}"
|
||||
commands = [
|
||||
"git worktree remove ../{{worktree_branch_name}}",
|
||||
]
|
||||
```
|
||||
|
||||
Closing a tab opened from this config removes the worktree directory but leaves the branch in place.
|
||||
|
||||
### Worktree example: remove the worktree and delete the branch
|
||||
|
||||
```toml
|
||||
name = "Ephemeral Worktree"
|
||||
title = "{{worktree_branch_name}}"
|
||||
|
||||
[[panes]]
|
||||
id = "main"
|
||||
type = "terminal"
|
||||
directory = "{{repo}}"
|
||||
commands = [
|
||||
"git worktree add -b {{worktree_branch_name}} ../{{worktree_branch_name}} {{branch}}",
|
||||
"cd ../{{worktree_branch_name}}",
|
||||
]
|
||||
|
||||
[on_close]
|
||||
directory = "{{repo}}"
|
||||
commands = [
|
||||
"git worktree remove ../{{worktree_branch_name}}",
|
||||
"git branch -D {{worktree_branch_name}}",
|
||||
]
|
||||
```
|
||||
|
||||
Closing a tab opened from this config first removes the worktree and then deletes the associated branch.
|
||||
|
||||
### Autogenerated worktree names
|
||||
|
||||
If a worktree config relies on an auto-generated branch name, close behavior uses the resolved branch name for that tab instance. Authors can reference that runtime value in `[on_close]` with `{{autogenerated_branch_name}}`, matching the open-time placeholder used by autogenerate worktree configs.
|
||||
|
||||
### Template and skill updates
|
||||
|
||||
- `app/resources/tab_configs/new_tab_config_template.toml` updates its commented worktree example to show close cleanup.
|
||||
- `resources/bundled/skills/tab-configs/SKILL.md` updates its schema reference and examples to include `[on_close]`.
|
||||
- Both documentation surfaces should show both worktree variants:
|
||||
- remove the worktree only
|
||||
- remove the worktree and delete the branch
|
||||
- The branch-deleting example must clearly read as destructive and opt-in.
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. A tab config without `[on_close]` closes with no behavior change from today.
|
||||
2. A tab config with `[on_close]` runs its close commands once when the user explicitly closes the tab.
|
||||
3. Close-time commands use the resolved values from the tab instance that is closing; Warp does not reopen the param modal.
|
||||
4. A worktree config can remove only the worktree on close while leaving the branch untouched.
|
||||
5. A worktree config can remove the worktree and then delete the branch on close.
|
||||
6. If the worktree removal step fails, Warp still closes the tab immediately, shows a persistent error toast, logs the cleanup failure, and does not run later branch-deletion commands.
|
||||
7. The default tab config template includes a commented example of close cleanup for worktrees.
|
||||
8. The bundled `tab-configs` skill documentation describes `[on_close]` and includes both worktree cleanup variants.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests for parsing `[on_close]`, rendering its templated `directory` and `commands`, and preserving per-tab resolved values.
|
||||
- Unit or integration tests that verify close commands run once per tab close, stop after the first failure, and surface a persistent error toast.
|
||||
- Manual verification with a real git repo:
|
||||
- open a config that creates a worktree and removes only the worktree on close
|
||||
- open a config that creates a worktree and removes both worktree and branch on close
|
||||
- verify the correct repo, worktree, and branch state after each close
|
||||
- Manual verification that tabs still close immediately if a cleanup command fails, and that the failure is shown in a persistent toast and logged rather than blocking tab close.
|
||||
- Manual verification that close cleanup is skipped gracefully if local shell state is unavailable and that a persistent toast is shown.
|
||||
- Manual verification that the updated template and bundled skill examples are internally consistent and produce valid TOML.
|
||||
|
||||
## Open questions
|
||||
|
||||
(None outstanding.)
|
||||
@@ -0,0 +1,102 @@
|
||||
# APP-3736: Allow specifying tab-close behavior in tab configs — Tech Spec
|
||||
Product spec: `specs/APP-3736/PRODUCT.md`
|
||||
## Problem
|
||||
Tab configs can declare how to open a tab, including pane layout, startup commands, and templated params, but there was no declarative way to run cleanup when a tab created from a config is later closed. This was especially painful for worktree configs, which can create a worktree on open but previously left users to manually remove the worktree directory and branch.
|
||||
## Relevant Code
|
||||
- `app/src/tab_configs/tab_config.rs` — `TabConfigOnClose`, `ResolvedTabCloseBehavior`, `TabConfig::on_close`, `render_tab_close_behavior`, `build_template_contexts`
|
||||
- `app/src/tab_configs/mod.rs` — exports `render_tab_close_behavior` and `ResolvedTabCloseBehavior`
|
||||
- `app/src/tab.rs` — stores `resolved_tab_close_behavior` on `TabData`
|
||||
- `app/src/workspace/view.rs` — resolves close behavior when opening a tab config, runs cleanup during tab close, and executes close commands with `LocalCommandExecutor`
|
||||
- `app/src/tab_configs/session_config.rs` — initializes `on_close: None` for generated configs that do not define close behavior
|
||||
- `app/src/tab_configs/tab_config_tests.rs` — parser and template-rendering coverage for `[on_close]`
|
||||
## Current State
|
||||
`TabConfig` already supports templated titles, pane `directory`, pane startup commands, and params. Rendering uses two template contexts: unquoted values for paths/titles and shell-quoted values for commands.
|
||||
`TabData` is the in-memory state container for each tab. Before this change, it tracked pane group state, colors, mouse state, and telemetry flags, but not any tab-config-specific close behavior.
|
||||
Tab close flows already funnel through `Workspace::close_tabs`, which optionally shows an unsaved-state confirmation dialog, cancels tab renaming, removes tabs in reverse index order, and emits telemetry once tabs are actually closed.
|
||||
## Changes
|
||||
### 1. Tab-config schema and resolved close behavior
|
||||
`TabConfig` now has an optional top-level `on_close: Option<TabConfigOnClose>`.
|
||||
`TabConfigOnClose` mirrors the close-time command contract:
|
||||
- `directory: Option<String>` — optional working directory for cleanup commands
|
||||
- `commands: Vec<String>` — ordered commands to run on tab close
|
||||
`ResolvedTabCloseBehavior` stores the fully rendered cleanup plan for one opened tab instance:
|
||||
- `directory: Option<PathBuf>`
|
||||
- `commands: Vec<String>`
|
||||
The resolved form is intentionally separate from `TabConfigOnClose` so close-time execution can use the exact values chosen when that tab was opened, without re-reading config files or re-prompting for params.
|
||||
### 2. Shared template-context builder
|
||||
`build_template_contexts` now centralizes construction of both template contexts used by tab config rendering:
|
||||
- `unquoted_context`: raw param values, plus `autogenerated_branch_name` when provided
|
||||
- `quoted_context`: shell-quoted param values, plus a shell-quoted `autogenerated_branch_name` when provided
|
||||
`render_tab_config` and `render_tab_close_behavior` both use this helper so open-time and close-time template substitution follow the same quoting rules:
|
||||
- titles and pane `directory` use unquoted values
|
||||
- commands use shell-quoted values
|
||||
This avoids duplicating context setup and preserves existing shell-quoting behavior for command templates with spaces.
|
||||
### 3. Render and attach close behavior when opening tab configs
|
||||
`Workspace::open_tab_config_with_params` now resolves both:
|
||||
- the tab title + pane layout via `render_tab_config`
|
||||
- the optional close behavior via `render_tab_close_behavior`
|
||||
After calling `add_tab_with_pane_layout`, the method writes the resolved close behavior into `self.tabs[self.active_tab_index].resolved_tab_close_behavior`. It also applies the tab color from the config as before.
|
||||
This means each tab instance carries its own rendered cleanup commands, including manual param values and the generated branch name from `maybe_generate_worktree_name`.
|
||||
### 4. Store close behavior on tab state
|
||||
`TabData` has a new field:
|
||||
- `resolved_tab_close_behavior: Option<ResolvedTabCloseBehavior>`
|
||||
`TabData::new` initializes it to `None`, so ordinary tabs and tabs not opened from configs have no close hook and keep the existing close behavior.
|
||||
The field is stored only in memory on the live `TabData`; it is not serialized into workspace snapshots.
|
||||
### 5. Run cleanup from the existing tab-close flow
|
||||
`Workspace::close_tabs` now collects each closing tab's `resolved_tab_close_behavior` before removing any tabs:
|
||||
1. Build `tab_indices_vec`
|
||||
2. Show the existing unsaved-state confirmation dialog if needed
|
||||
3. Cancel any in-progress tab rename
|
||||
4. Take (consume) each selected tab's resolved close behavior and mark `cleanup_was_run = true` on the tab
|
||||
5. Call `run_tab_close_cleanup` for each collected behavior
|
||||
6. Remove tabs in reverse index order as before
|
||||
Collecting the close behaviors first avoids depending on tab indices after tabs have been removed.
|
||||
### 6. Async best-effort cleanup execution on native local-tty builds
|
||||
On native builds with `local_tty`, `run_tab_close_cleanup` reads the current `LocalShellState` and spawns `execute_tab_close_behavior` with:
|
||||
- shell type
|
||||
- shell path
|
||||
- PATH from the active local shell environment
|
||||
If `LocalShellState` is unavailable, cleanup is skipped and a warning is logged.
|
||||
`execute_tab_close_behavior` constructs a `LocalCommandExecutor` and executes the resolved cleanup commands sequentially. For each command, it passes:
|
||||
- the resolved `directory`, if present
|
||||
- environment variables containing `HOME` and `PATH` when available
|
||||
- `ExecuteCommandOptions { run_command_in_same_shell_as_session: true }`
|
||||
If a command exits successfully, execution continues to the next command. If a command returns a non-zero status, execution stops and returns an error containing the failed command and command output. The spawned callback logs that error with `log::warn!`.
|
||||
The workspace does not await this task before removing the tab, so cleanup is intentionally best-effort and non-blocking from the user's perspective.
|
||||
If cleanup fails, or if `LocalShellState` is unavailable, `run_tab_close_cleanup` also adds a persistent `DismissibleToast::error(...)` to the workspace toast stack so the failure is visible without blocking tab close.
|
||||
On builds without native local-tty support, `run_tab_close_cleanup` is a no-op stub.
|
||||
### 7. Default `on_close` value for generated configs
|
||||
`build_tab_config` and `tab_config_from_pane_snapshot` now initialize `on_close: None` so generated startup/session configs and saved-from-live-tab configs preserve current behavior unless a user explicitly edits the TOML to add `[on_close]`.
|
||||
### 8. Tests
|
||||
`tab_config_tests.rs` adds coverage for:
|
||||
- parsing `[on_close]` in a worktree config
|
||||
- `render_tab_close_behavior` substituting manual params
|
||||
- `render_tab_close_behavior` substituting `autogenerated_branch_name`
|
||||
- shell-quoting command params with spaces while leaving pane `directory` unquoted
|
||||
Existing render tests continue to cover open-time title/layout rendering and fallback behavior for invalid pane trees.
|
||||
## End-to-End Flow
|
||||
1. User selects a tab config from the new-session menu.
|
||||
2. If the config has params, the params modal collects values; otherwise defaults are used. If any pane sets `worktree_name_autogenerated = true`, `maybe_generate_worktree_name` creates a unique branch name.
|
||||
3. `open_tab_config_with_params` renders the pane template, title, and optional close behavior, opens the tab, and stores the resolved close behavior on the new `TabData`.
|
||||
4. User later closes that tab.
|
||||
5. `close_tabs` gathers each tab's resolved close behavior and calls `run_tab_close_cleanup` before removing the tabs.
|
||||
6. On native local-tty builds, cleanup commands run asynchronously through `LocalCommandExecutor`. Tabs disappear immediately; failures are logged and stop any remaining cleanup commands for that tab.
|
||||
## Risks and Limitations
|
||||
- **Single-fire cleanup:** `resolved_tab_close_behavior` is consumed (`.take()`n) before the tab is removed, so re-closing an undo-restored tab does not fire cleanup again. Tabs restored from workspace snapshots also do not recover a prior close hook because the field is not serialized.
|
||||
- **Undo-close CWD fallback:** when a tab with `cleanup_was_run = true` is restored via undo-close, `restore_closed_tab` iterates its terminal panes and calls `cd_home_if_dir_missing` on each. If the CWD no longer exists (e.g., the worktree was removed by cleanup), the shell falls back to the home directory.
|
||||
- **Race between cleanup and undo-close:** cleanup runs asynchronously via `ctx.spawn`, so it may not have finished by the time the user triggers undo-close. In that case, the CWD still exists at restoration time and the fallback is a no-op. If the user then runs commands, the directory may disappear mid-session once cleanup finishes.
|
||||
- **Best-effort async cleanup:** Because tab removal does not wait for cleanup completion, users can immediately create another worktree or branch with the same name and race against the cleanup task.
|
||||
- **Non-blocking failure toast only:** Cleanup failures show a persistent toast and log a warning, but there is still no blocking retry/undo flow in the close path.
|
||||
- **Depends on local shell availability:** If `LocalShellState` is unavailable, cleanup is skipped.
|
||||
- **Command environment is intentionally minimal:** Cleanup gets `HOME` and `PATH`, but not a full clone of the pane's process environment.
|
||||
## Testing and Validation
|
||||
- Run targeted tab-config tests covering parser and rendering behavior, especially `[on_close]` and shell-quoting cases.
|
||||
- Run targeted workspace tests for the close-cleanup failure path to verify persistent error toasts are shown.
|
||||
- Manually verify a config that removes only a worktree on close.
|
||||
- Manually verify a config that removes a worktree and then deletes the branch on close.
|
||||
- Manually verify that a failing cleanup command does not block tab close, shows a persistent error toast, and prevents later cleanup commands from running.
|
||||
- Manually verify that tabs opened from configs without `[on_close]` keep existing close behavior.
|
||||
- Run `cargo fmt` and a Rust test/lint pass appropriate for the touched modules before sending the branch for review.
|
||||
## Follow-ups
|
||||
- Persist close behavior in tab/workspace snapshots if restored tabs should continue running `[on_close]`.
|
||||
- Consider generating `[on_close]` automatically in `build_worktree_config_toml` if default worktree configs should clean themselves up without manual edits.
|
||||
Reference in New Issue
Block a user