docs: 添加 sftp 标签页规格与计划文档
This commit is contained in:
201
docs/superpowers/plans/2026-03-24-sftp-tabs.md
Normal file
201
docs/superpowers/plans/2026-03-24-sftp-tabs.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# SFTP Sidebar 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:** Add non-duplicated, session-scoped SFTP tabs in the sidebar so users can return to file sessions without losing tab entries.
|
||||||
|
|
||||||
|
**Architecture:** Introduce a dedicated Pinia store (`sftpTabs`) parallel to `terminalTabs`, then wire it into `ConnectionsView`, `MainLayout`, and `SftpView`. Keep `/sftp/:id` as the single SFTP route and use route-driven synchronization so tab switching updates the view correctly without stale state.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3, TypeScript, Pinia, Vue Router, Vite, vue-tsc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Create: `frontend/src/stores/sftpTabs.ts`
|
||||||
|
- Owns SFTP tab state and actions (`openOrFocus`, `activate`, `close`).
|
||||||
|
- Modify: `frontend/src/views/ConnectionsView.vue`
|
||||||
|
- Registers/focuses SFTP tab before routing to `/sftp/:id`.
|
||||||
|
- Modify: `frontend/src/layouts/MainLayout.vue`
|
||||||
|
- Renders SFTP sidebar tabs, handles click/close navigation behavior.
|
||||||
|
- Modify: `frontend/src/views/SftpView.vue`
|
||||||
|
- Keeps route param and SFTP tab state in sync via watcher.
|
||||||
|
- Verify: `frontend/src/router/index.ts`
|
||||||
|
- Route shape remains unchanged (`/sftp/:id`).
|
||||||
|
- Verify: `docs/superpowers/specs/2026-03-24-sftp-tabs-design.md`
|
||||||
|
- Source of truth for requirements and acceptance criteria.
|
||||||
|
|
||||||
|
### Task 1: Add Dedicated SFTP Tabs Store
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/src/stores/sftpTabs.ts`
|
||||||
|
- Reference: `frontend/src/stores/terminalTabs.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check current store pattern and workspace status**
|
||||||
|
|
||||||
|
Run `git status --short` and read `frontend/src/stores/terminalTabs.ts` to mirror established naming/style patterns.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `SftpTab` model and store state**
|
||||||
|
|
||||||
|
Create `sftpTabs` store with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface SftpTab {
|
||||||
|
id: string
|
||||||
|
connectionId: number
|
||||||
|
title: string
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
State and computed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tabs = ref<SftpTab[]>([])
|
||||||
|
const activeTabId = ref<string | null>(null)
|
||||||
|
const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) || null)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement open/activate/close logic with dedup**
|
||||||
|
|
||||||
|
Implement actions analogous to terminal tabs:
|
||||||
|
|
||||||
|
- `openOrFocus(connection)` reuses existing tab by `connectionId`
|
||||||
|
- `activate(tabId)` flips active flags and updates `activeTabId`
|
||||||
|
- `close(tabId)` removes tab and activates neighbor when needed
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build-check after new store**
|
||||||
|
|
||||||
|
Run in `frontend/`: `npm run build`
|
||||||
|
|
||||||
|
Expected: build succeeds and no type errors in new store.
|
||||||
|
|
||||||
|
### Task 2: Register SFTP Tabs from Connections Entry
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/views/ConnectionsView.vue`
|
||||||
|
- Use: `frontend/src/stores/sftpTabs.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add store import and instance**
|
||||||
|
|
||||||
|
Import `useSftpTabsStore` and initialize `const sftpTabsStore = useSftpTabsStore()`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update `openSftp(conn)` behavior**
|
||||||
|
|
||||||
|
Before routing, call:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
sftpTabsStore.openOrFocus(conn)
|
||||||
|
router.push(`/sftp/${conn.id}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build-check for integration safety**
|
||||||
|
|
||||||
|
Run in `frontend/`: `npm run build`
|
||||||
|
|
||||||
|
Expected: build succeeds and `ConnectionsView` typing remains valid.
|
||||||
|
|
||||||
|
### Task 3: Render and Control SFTP Tabs in Sidebar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/layouts/MainLayout.vue`
|
||||||
|
- Use: `frontend/src/stores/sftpTabs.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add SFTP store wiring and computed values**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- `const sftpTabsStore = useSftpTabsStore()`
|
||||||
|
- `const sftpTabs = computed(() => sftpTabsStore.tabs)`
|
||||||
|
- route helper for SFTP context (for active styling and close-navigation logic)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add SFTP tab click and close handlers**
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
- click handler: activate tab + `router.push(`/sftp/${tab.connectionId}`)` + close sidebar
|
||||||
|
- close handler:
|
||||||
|
- always close in store
|
||||||
|
- only navigate when current route is the closed tab's route
|
||||||
|
- if tabs remain, navigate to active tab route
|
||||||
|
- if no tabs remain, navigate `/connections`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Render `文件` sidebar section near terminal section**
|
||||||
|
|
||||||
|
Add a section matching current visual style conventions:
|
||||||
|
|
||||||
|
- title row with `FolderOpen` icon and text `文件`
|
||||||
|
- list items for `sftpTabs`
|
||||||
|
- close button per item
|
||||||
|
- active class aligned with current route context
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build-check and inspect no terminal regressions**
|
||||||
|
|
||||||
|
Run in `frontend/`: `npm run build`
|
||||||
|
|
||||||
|
Expected: build succeeds; no type/template errors in `MainLayout`.
|
||||||
|
|
||||||
|
### Task 4: Route-Driven Sync in SFTP View
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/views/SftpView.vue`
|
||||||
|
- Use: `frontend/src/stores/sftpTabs.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add SFTP tabs store and route-param watcher**
|
||||||
|
|
||||||
|
Add watcher on `route.params.id` with `{ immediate: true }` so first load and tab switching share one code path.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Consolidate initialization into watcher path**
|
||||||
|
|
||||||
|
Use watcher flow:
|
||||||
|
|
||||||
|
- parse and validate id; if invalid, show user-visible error feedback, do not create/open any SFTP tab, and navigate to `/connections`
|
||||||
|
- ensure connections loaded (fetch when needed)
|
||||||
|
- resolve connection; if missing, show user-visible error and route back to `/connections`
|
||||||
|
- call `sftpTabsStore.openOrFocus(connection)`
|
||||||
|
- refresh connection-bound SFTP view state for current route id
|
||||||
|
|
||||||
|
Avoid duplicate request/init logic split across both watcher and old mount flow.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Keep existing file-management behavior unchanged**
|
||||||
|
|
||||||
|
Do not alter existing upload/download/delete/core SFTP operations; only route/tab synchronization behavior should change.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build-check after route-sync changes**
|
||||||
|
|
||||||
|
Run in `frontend/`: `npm run build`
|
||||||
|
|
||||||
|
Expected: build succeeds and SftpView compiles cleanly.
|
||||||
|
|
||||||
|
### Task 5: End-to-End Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Verify runtime behavior in app UI
|
||||||
|
- Verify `frontend/src/router/index.ts` unchanged for route shape
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run final build verification**
|
||||||
|
|
||||||
|
Run in `frontend/`: `npm run build`
|
||||||
|
|
||||||
|
Expected: `vue-tsc -b` and Vite build both pass.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Manual behavior checks**
|
||||||
|
|
||||||
|
Verify all acceptance criteria:
|
||||||
|
|
||||||
|
1. Same-connection `文件` clicks do not create duplicate tabs.
|
||||||
|
2. Multiple SFTP tabs can be opened and switched.
|
||||||
|
3. Navigating away (for example back to connections) keeps SFTP tabs in sidebar during current session.
|
||||||
|
4. Closing active/non-active tabs follows designed route behavior.
|
||||||
|
5. Switching `/sftp/:id` between tabs updates header/file list without stale previous-connection state.
|
||||||
|
6. Invalid/nonexistent `/sftp/:id` creates no tab, shows visible feedback, and routes back to `/connections`.
|
||||||
|
7. Terminal tabs continue to open/switch/close normally.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit if user requests**
|
||||||
|
|
||||||
|
If the user asks to commit, keep commit focused:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/stores/sftpTabs.ts frontend/src/views/ConnectionsView.vue frontend/src/layouts/MainLayout.vue frontend/src/views/SftpView.vue
|
||||||
|
git commit -m "feat: add sidebar tabs for sftp sessions"
|
||||||
|
```
|
||||||
126
docs/superpowers/specs/2026-03-24-sftp-tabs-design.md
Normal file
126
docs/superpowers/specs/2026-03-24-sftp-tabs-design.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# 2026-03-24 SFTP Tabs Design
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The current connection list provides both `终端` and `文件` actions. `终端` has persistent sidebar tabs, but `文件` (SFTP) opens a route directly and has no sidebar tab lifecycle. Users lose the quick return path after navigating away, which feels inconsistent.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
- Add sidebar tabs for SFTP sessions near the existing terminal tabs.
|
||||||
|
- Ensure SFTP tabs are unique per connection (no duplicates).
|
||||||
|
- Keep tab state only for the current in-memory session.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No persistence across full page refresh (no localStorage/sessionStorage).
|
||||||
|
- No refactor that merges terminal and SFTP tab models into one generic tab system.
|
||||||
|
- No route restructuring; keep `/sftp/:id` as the SFTP route entry.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
- Terminal tabs are managed by `frontend/src/stores/terminalTabs.ts` and rendered in `frontend/src/layouts/MainLayout.vue`.
|
||||||
|
- `ConnectionsView` opens terminal via `terminalTabs.openOrFocus(conn)` and then routes to `/terminal`.
|
||||||
|
- `ConnectionsView` opens SFTP by routing directly to `/sftp/:id`, with no tab store.
|
||||||
|
- `SftpView` currently has no tab registration step.
|
||||||
|
|
||||||
|
## Selected Approach
|
||||||
|
|
||||||
|
Introduce a dedicated SFTP tab store and render a new sidebar tab section in `MainLayout`, following the same interaction model as terminal tabs while keeping route behavior centered on `/sftp/:id`.
|
||||||
|
|
||||||
|
### 1. New store: `sftpTabs`
|
||||||
|
|
||||||
|
Create `frontend/src/stores/sftpTabs.ts` with a model parallel to terminal tabs:
|
||||||
|
|
||||||
|
- `tabs: SftpTab[]`
|
||||||
|
- `activeTabId: string | null`
|
||||||
|
- `activeTab` computed
|
||||||
|
- `openOrFocus(connection)`
|
||||||
|
- `activate(tabId)`
|
||||||
|
- `close(tabId)`
|
||||||
|
|
||||||
|
Each tab includes:
|
||||||
|
|
||||||
|
- `id: string`
|
||||||
|
- `connectionId: number`
|
||||||
|
- `title: string`
|
||||||
|
- `active: boolean`
|
||||||
|
|
||||||
|
Dedup rule:
|
||||||
|
|
||||||
|
- `openOrFocus(connection)` must reuse an existing tab when `connectionId` matches.
|
||||||
|
|
||||||
|
### 2. Connection list behavior
|
||||||
|
|
||||||
|
Update `frontend/src/views/ConnectionsView.vue`:
|
||||||
|
|
||||||
|
- In `openSftp(conn)`, call `sftpTabs.openOrFocus(conn)` before `router.push(`/sftp/${conn.id}`)`.
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
- Clicking `文件` on the same connection repeatedly does not create duplicate tabs.
|
||||||
|
|
||||||
|
### 3. Sidebar rendering and controls
|
||||||
|
|
||||||
|
Update `frontend/src/layouts/MainLayout.vue`:
|
||||||
|
|
||||||
|
- Import and use `useSftpTabsStore`.
|
||||||
|
- Add computed `sftpTabs`.
|
||||||
|
- Add a new sidebar section (near terminal tabs) titled `文件`.
|
||||||
|
- Render each tab as a clickable item with close button.
|
||||||
|
|
||||||
|
Interactions:
|
||||||
|
|
||||||
|
- Click tab: activate and navigate to `/sftp/:connectionId`.
|
||||||
|
- Close tab:
|
||||||
|
- Always remove the tab from store.
|
||||||
|
- Only trigger route navigation when the current route is the closed tab's route (`/sftp/:connectionId`):
|
||||||
|
- If another SFTP tab remains, navigate to `/sftp/:newActiveConnectionId`.
|
||||||
|
- If no SFTP tabs remain, navigate to `/connections`.
|
||||||
|
- If closing a non-active tab, or closing from a non-SFTP route, remove only (no route change).
|
||||||
|
|
||||||
|
Highlighting:
|
||||||
|
|
||||||
|
- Keep active style tied to both tab active state and current SFTP route context.
|
||||||
|
|
||||||
|
### 4. Route-entry consistency
|
||||||
|
|
||||||
|
Update `frontend/src/views/SftpView.vue`:
|
||||||
|
|
||||||
|
- Import and use `useSftpTabsStore`.
|
||||||
|
- Watch `route.params.id` with `{ immediate: true }` so logic runs on first load and on `/sftp/:id` param changes (tab-to-tab switching).
|
||||||
|
- On each `id` change:
|
||||||
|
- Parse `id` to number; if invalid, navigate to `/connections`.
|
||||||
|
- Ensure connections are loaded (fetch when needed).
|
||||||
|
- Resolve the matching connection; if not found, show error feedback and navigate to `/connections`.
|
||||||
|
- Call `sftpTabs.openOrFocus(connection)` and then refresh SFTP view state for that connection.
|
||||||
|
- Consolidate route-driven initialization into this watcher (avoid a separate `onMounted` path-init flow for the same concern) so first load and param switching use one code path and do not trigger duplicate requests.
|
||||||
|
|
||||||
|
This keeps behavior consistent for direct navigation and sidebar tab switching, without duplicate tabs.
|
||||||
|
|
||||||
|
## Behavior Kept Unchanged
|
||||||
|
|
||||||
|
- Existing terminal tab logic and terminal workspace lifecycle.
|
||||||
|
- Existing SFTP route path (`/sftp/:id`) and core file-management interactions remain unchanged, except for tab registration/sync and invalid-or-missing connection route handling described above.
|
||||||
|
- Authentication and router guards.
|
||||||
|
- UI visual language (slate/cyan styling).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `文件` opens/activates an SFTP tab in sidebar.
|
||||||
|
- Repeatedly opening `文件` for the same connection does not create duplicate tabs.
|
||||||
|
- SFTP tabs remain available when navigating back to connections or other pages within the same session.
|
||||||
|
- Closing active/non-active SFTP tabs follows the navigation rules above.
|
||||||
|
- Terminal tabs continue working exactly as before.
|
||||||
|
- Switching between existing SFTP tabs (for example `/sftp/1` and `/sftp/2`) updates connection header and file list correctly, without stale data from the previous connection.
|
||||||
|
- Direct navigation to an invalid or nonexistent `/sftp/:id` does not create a tab and returns the user to `/connections` with visible feedback.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Run `npm run build` in `frontend/`.
|
||||||
|
- Manual verification:
|
||||||
|
1. Open SFTP for one connection multiple times and confirm single tab.
|
||||||
|
2. Open SFTP for multiple connections and switch between tabs.
|
||||||
|
3. Navigate away and return via sidebar SFTP tabs.
|
||||||
|
4. Close active and inactive SFTP tabs and verify resulting route behavior.
|
||||||
|
5. Re-check terminal tab open/switch/close behavior for regressions.
|
||||||
|
6. Open an invalid/nonexistent `/sftp/:id` and verify no tab is created, visible error feedback appears, and navigation returns to `/connections`.
|
||||||
Reference in New Issue
Block a user