docs: 添加 sftp 标签页规格与计划文档

This commit is contained in:
liumangmang
2026-03-24 17:52:28 +08:00
parent 43207e24bf
commit 93cc13ddd0
2 changed files with 327 additions and 0 deletions

View 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"
```

View 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`.