feat: allow multiple terminal tabs per connection
This commit is contained in:
101
docs/superpowers/plans/2026-03-26-terminal-multi-tabs.md
Normal file
101
docs/superpowers/plans/2026-03-26-terminal-multi-tabs.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Terminal Multi Tabs Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Allow the connection list to open multiple terminal tabs for the same connection, with each click creating a fresh terminal session.
|
||||
|
||||
**Architecture:** Keep the existing `/terminal` workspace and Pinia-driven tab model, but change terminal tab creation from connection-deduplicated to always-new. Add lightweight title numbering so repeated tabs for the same connection remain distinguishable without changing routing or terminal widget lifecycle.
|
||||
|
||||
**Tech Stack:** Vue 3, TypeScript, Pinia, Vue Router, xterm.js, Vite, vue-tsc
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `frontend/src/stores/terminalTabs.ts`
|
||||
- Change tab creation semantics and add repeated-title numbering.
|
||||
- Modify: `frontend/src/views/ConnectionsView.vue`
|
||||
- Use the new always-open terminal action from the connection list.
|
||||
- Verify: `frontend/src/views/TerminalWorkspaceView.vue`
|
||||
- Confirm current per-tab widget mounting already supports independent sessions.
|
||||
- Verify: `frontend/src/layouts/MainLayout.vue`
|
||||
- Confirm terminal sidebar tab rendering needs no structure change beyond new titles.
|
||||
- Reference: `docs/superpowers/specs/2026-03-26-terminal-multi-tabs-design.md`
|
||||
- Source of truth for behavior and acceptance criteria.
|
||||
|
||||
### Task 1: Change Terminal Tabs Store to Always Create New Tabs
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/stores/terminalTabs.ts`
|
||||
- Reference: `frontend/src/stores/sftpTabs.ts`
|
||||
|
||||
- [ ] **Step 1: Inspect current tab store behavior**
|
||||
|
||||
Read `frontend/src/stores/terminalTabs.ts` and confirm the existing dedup-by-connection behavior.
|
||||
|
||||
- [ ] **Step 2: Add repeated-title generation**
|
||||
|
||||
Add a helper that counts currently open tabs for the same `connectionId` and returns:
|
||||
|
||||
```ts
|
||||
function getTabTitle(connection: Connection) {
|
||||
const sameConnectionCount = tabs.value.filter(t => t.connectionId === connection.id).length
|
||||
|
||||
if (sameConnectionCount === 0) {
|
||||
return connection.name
|
||||
}
|
||||
|
||||
return `${connection.name} (${sameConnectionCount + 1})`
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace dedup open logic with always-new creation**
|
||||
|
||||
Change the open action so it always creates a new `TerminalTab` and activates it immediately.
|
||||
|
||||
- [ ] **Step 4: Keep activate/close behavior unchanged**
|
||||
|
||||
Do not change tab activation and close-neighbor behavior except what is necessary to support the new open logic.
|
||||
|
||||
### Task 2: Wire the Connection List to the New Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/ConnectionsView.vue`
|
||||
- Use: `frontend/src/stores/terminalTabs.ts`
|
||||
|
||||
- [ ] **Step 1: Rename the method usage to match semantics**
|
||||
|
||||
Update `openTerminal(conn)` to call the new always-open store action.
|
||||
|
||||
- [ ] **Step 2: Preserve routing behavior**
|
||||
|
||||
Keep `router.push('/terminal')` unchanged after opening the tab.
|
||||
|
||||
### Task 3: Verify Integration and Build
|
||||
|
||||
**Files:**
|
||||
- Verify: `frontend/src/layouts/MainLayout.vue`
|
||||
- Verify: `frontend/src/views/TerminalWorkspaceView.vue`
|
||||
|
||||
- [ ] **Step 1: Confirm sidebar rendering already keys by `tab.id`**
|
||||
|
||||
No structural code changes should be required if repeated tabs render correctly with distinct titles.
|
||||
|
||||
- [ ] **Step 2: Run final frontend build**
|
||||
|
||||
Run in `frontend/`:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: `vue-tsc -b` and `vite build` both pass.
|
||||
|
||||
- [ ] **Step 3: Manual runtime verification**
|
||||
|
||||
Check:
|
||||
|
||||
1. Same connection opens multiple terminal tabs.
|
||||
2. Repeated tabs are titled distinctly.
|
||||
3. Each tab remains an independent terminal session.
|
||||
4. Close/switch behavior still works.
|
||||
@@ -0,0 +1,97 @@
|
||||
# 2026-03-26 Terminal Multi Tabs Design
|
||||
|
||||
## Background
|
||||
|
||||
The connection list already opens terminal sessions into the shared terminal workspace at `/terminal`, and the sidebar already renders terminal tabs. However, the current store deduplicates tabs by `connectionId`, so clicking `终端` for the same connection only focuses the existing tab instead of opening a new shell session.
|
||||
|
||||
## Goal
|
||||
|
||||
- Allow users to open multiple terminal tabs from the connection list for the same connection.
|
||||
- Keep each terminal tab as an independent shell session.
|
||||
- Preserve the current terminal workspace route and sidebar interaction model.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No tab persistence across refresh.
|
||||
- No route change from `/terminal` to per-tab URLs.
|
||||
- No generic tab-system refactor shared with SFTP.
|
||||
- No terminal session restore or reconnect workflow.
|
||||
|
||||
## Current State
|
||||
|
||||
- `frontend/src/stores/terminalTabs.ts` manages terminal tabs.
|
||||
- `openOrFocus(connection)` reuses an existing tab when `connectionId` matches.
|
||||
- `frontend/src/views/ConnectionsView.vue` uses that method before routing to `/terminal`.
|
||||
- `frontend/src/views/TerminalWorkspaceView.vue` already mounts one `TerminalWidget` per tab keyed by `tab.id`, so multiple tabs can coexist as long as the store allows them.
|
||||
|
||||
## Selected Approach
|
||||
|
||||
Keep the existing terminal workspace architecture and change only the terminal tab creation semantics: clicking `终端` always creates a new tab, even when the same connection already has open tabs.
|
||||
|
||||
### 1. Terminal tab creation becomes always-new
|
||||
|
||||
Update `frontend/src/stores/terminalTabs.ts` so the terminal action always creates a new tab entry:
|
||||
|
||||
- remove the `connectionId` dedup behavior from the open action
|
||||
- generate a fresh `tab.id` every time
|
||||
- activate the newly created tab immediately
|
||||
|
||||
This preserves the current in-memory tab lifecycle while enabling multiple concurrent sessions to the same host.
|
||||
|
||||
### 2. Distinguishable tab titles for repeated connections
|
||||
|
||||
When the same connection is opened multiple times, sidebar labels must remain distinguishable.
|
||||
|
||||
Recommended title strategy:
|
||||
|
||||
- first tab: `Connection Name`
|
||||
- second tab: `Connection Name (2)`
|
||||
- third tab: `Connection Name (3)`
|
||||
|
||||
The sequence is computed from currently open tabs for the same `connectionId`. No persistence is needed.
|
||||
|
||||
### 3. Connections entry behavior
|
||||
|
||||
Update `frontend/src/views/ConnectionsView.vue` so clicking `终端` calls the always-new tab action and routes to `/terminal`.
|
||||
|
||||
Result:
|
||||
|
||||
- each click from the connection list opens a fresh shell session
|
||||
- users can intentionally keep several terminals open for the same host
|
||||
|
||||
### 4. Terminal workspace behavior stays unchanged
|
||||
|
||||
`frontend/src/views/TerminalWorkspaceView.vue` and `frontend/src/components/TerminalWidget.vue` do not need architectural changes:
|
||||
|
||||
- the workspace already loops through tabs by `tab.id`
|
||||
- each active tab renders its own `TerminalWidget`
|
||||
- each widget maintains an independent xterm instance and WebSocket connection
|
||||
|
||||
This matches the desired UX: multiple tabs for the same connection are separate terminal sessions rather than alternate views of one shared session.
|
||||
|
||||
## Behavior Kept Unchanged
|
||||
|
||||
- Terminal route remains `/terminal`.
|
||||
- Sidebar terminal tab section remains the primary tab switcher.
|
||||
- Closing and activating adjacent terminal tabs continues to follow the current logic.
|
||||
- SFTP tab behavior remains unchanged.
|
||||
- Existing slate/cyan visual language remains unchanged.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Clicking `终端` for the same connection multiple times creates multiple terminal tabs.
|
||||
- Each new terminal tab becomes the active tab.
|
||||
- Repeated tabs for the same connection are visually distinguishable in the sidebar.
|
||||
- Switching between tabs preserves each tab's own terminal session output.
|
||||
- Closing a terminal tab keeps current close/activate behavior intact.
|
||||
- Opening tabs for different connections continues to work.
|
||||
|
||||
## Verification
|
||||
|
||||
- Run `npm run build` in `frontend/`.
|
||||
- Manual verification:
|
||||
1. Click `终端` on the same connection three times and confirm three tabs appear.
|
||||
2. Confirm the sidebar shows distinguishable titles for repeated tabs.
|
||||
3. Type different commands in different tabs for the same connection and confirm each tab keeps its own output.
|
||||
4. Close active and inactive terminal tabs and confirm focus changes remain correct.
|
||||
5. Re-check that opening terminal tabs for different connections still works.
|
||||
Reference in New Issue
Block a user