diff --git a/docs/superpowers/plans/2026-03-24-sftp-tabs.md b/docs/superpowers/plans/2026-03-24-sftp-tabs.md new file mode 100644 index 0000000..bc7df57 --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-sftp-tabs.md @@ -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([]) +const activeTabId = ref(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" +``` diff --git a/docs/superpowers/specs/2026-03-24-sftp-tabs-design.md b/docs/superpowers/specs/2026-03-24-sftp-tabs-design.md new file mode 100644 index 0000000..df255c6 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-sftp-tabs-design.md @@ -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`.