103 lines
10 KiB
Markdown
103 lines
10 KiB
Markdown
# 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.
|