From bf5241e87b9ddb687cbd11d24432d774e79117c8 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Tue, 30 Jun 2026 14:08:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(v0.6):=20remove=20standalone=20Manage=20pa?= =?UTF-8?q?ge=20=E2=80=94=20manage=20sessions=20on=20the=20Sessions=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Sessions chooser already shows every running session with a live thumbnail and Open + New session, so fold delete into it and drop the separate page: - add a Kill ✕ button to each session card (DELETE /live-sessions/:id + refresh), reusing makePreviewCard's extraActions and the existing .mg-kill style - remove public/manage.html + public/manage.ts and the esbuild manage entry - remove the 🗂 toolbar button (main.ts) and the 🗂 Manage header link (launcher.ts) Backend DELETE/preview routes stay (used by the launcher kill, thumbnails, and the project pages). Frontend + build-config only. Verified: full suite 453 green, web typecheck clean, build:web emits only main (no manage.* artifacts), and in-browser the Sessions cards have Open + Kill (2→1 on kill), no Manage entry, and GET /manage.html → 404. --- docs/PROGRESS_LOG.md | 14 +++++ package.json | 4 +- public/launcher.ts | 18 ++++-- public/main.ts | 12 +--- public/manage.html | 15 ----- public/manage.ts | 135 ------------------------------------------- 6 files changed, 32 insertions(+), 166 deletions(-) delete mode 100644 public/manage.html delete mode 100644 public/manage.ts diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md index 74867c6..f50ff17 100644 --- a/docs/PROGRESS_LOG.md +++ b/docs/PROGRESS_LOG.md @@ -146,6 +146,20 @@ - **commit**: ======================================================= --> +### 2026-06-30 · v0.6 删除 manage 独立页 — Sessions 选择器即会话管理 + +- **状态**: `[x]` DONE。**453 测试全绿** · web typecheck 干净 · `build:web` OK(仅 main,无 manage 产物)· 真浏览器验证通过。 +- **需求**(用户): manage 页面不要了——在 **Sessions 选择器**这里直接管理 session(添加 + 删除)。 +- **改动**: + - **删** `public/manage.html` + `public/manage.ts`(及 build 产物);`package.json` 的 `build:web`/`dev:web` 去掉 `public/manage.ts` 入口。 + - **删** `public/main.ts` 工具栏 🗂 按钮(原跳 `/manage.html`);**删** `public/launcher.ts` 头部 "🗂 Manage" 链接。 + - **加** `launcher.ts` 每张 session 卡 **Kill ✕**(`makePreviewCard` 的 `extraActions`,复用 manage 的 `killOne`:`DELETE /live-sessions/:id` → refresh)。`.mg-kill` 样式已存在,直接复用。 + - 后端 DELETE/preview 路由保留(仍由 launcher kill + 缩略图 + projects 复用);注释里的 "manage page" 字样属历史,无害。 +- **结果**: Sessions 视图 = 完整会话管理——**+New session**(加)· **Open ↗**(进)· **Kill ✕**(删),配实时缩略图。bulk「Kill all/detached」未迁移(如需可加)。 +- **验证**: `npx tsc -p tsconfig.web.json` 干净;`npx vitest run` 453/453;`build:web` 仅出 main(无 manage.*);真浏览器:Sessions 卡含 Open+Kill、无 Manage 入口、点 Kill 2→1、`GET /manage.html`→404。 +- **遗留**: manage-only 死 CSS(`#manage-root`/`.mg-header`/`.mg-bar`/`.mg-title`/`.mg-sub`/`.mg-btn`)留作可选清理;README 若提 manage 页待更新。 +- **commit**: (本次提交) + ### 2026-06-30 · v0.6 项目网格卡片 — 直接关掉 session - **状态**: `[x]` DONE。51 projects-panel 测试全绿(+1)· web typecheck 干净 · `build:web` OK · 真浏览器验证通过。 diff --git a/package.json b/package.json index 7fe0148..6c641e2 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "start": "tsx src/server.ts", "dev": "tsx watch src/server.ts", "build": "tsc -p tsconfig.json", - "build:web": "esbuild public/main.ts public/manage.ts --bundle --format=esm --outdir=public/build --sourcemap", - "dev:web": "esbuild public/main.ts public/manage.ts --bundle --format=esm --outdir=public/build --sourcemap --watch", + "build:web": "esbuild public/main.ts --bundle --format=esm --outdir=public/build --sourcemap", + "dev:web": "esbuild public/main.ts --bundle --format=esm --outdir=public/build --sourcemap --watch", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.web.json", "test": "vitest run", "test:watch": "vitest", diff --git a/public/launcher.ts b/public/launcher.ts index c7bb677..5cdf297 100644 --- a/public/launcher.ts +++ b/public/launcher.ts @@ -45,9 +45,7 @@ export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher const actions = el('div', 'launcher-actions') const newBtn = el('button', 'launcher-new', '+ New session') newBtn.addEventListener('click', () => hooks.onNew()) - const manage = el('a', 'mg-btn', '🗂 Manage') as HTMLAnchorElement - manage.href = '/manage.html' - actions.append(newBtn, manage) + actions.append(newBtn) head.append(sub, actions) root.append(head) @@ -57,8 +55,20 @@ export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher const cards = new Map() let timer: ReturnType | null = null + /** Kill a session (DELETE /live-sessions/:id) then refresh the grid. */ + async function killOne(id: string): Promise { + await fetch(`/live-sessions/${encodeURIComponent(id)}`, { method: 'DELETE' }).catch(() => {}) + void refresh() + } + function makeCard(s: LiveSessionInfo): PreviewCard { - return makePreviewCard(s, { onOpen: (id) => hooks.onOpen(id) }) + const kill = el('button', 'mg-kill', 'Kill ✕') + kill.title = 'Kill this session' + kill.addEventListener('click', () => void killOne(s.id)) + return makePreviewCard(s, { + onOpen: (id) => hooks.onOpen(id), + extraActions: () => [kill], + }) } async function refresh(): Promise { diff --git a/public/main.ts b/public/main.ts index 195f831..3c8efdd 100644 --- a/public/main.ts +++ b/public/main.ts @@ -76,16 +76,8 @@ mountHistory(toolbar, { mountShortcuts(toolbar) mountShareSession(toolbar, () => app.activeSessionId()) -// Session manager (standalone page) — manage/kill the host's running sessions. -const manageBtn = document.createElement('button') -manageBtn.className = 'toolbtn' -manageBtn.textContent = '🗂' -manageBtn.title = 'Manage sessions (open / kill)' -manageBtn.setAttribute('aria-label', 'Manage sessions') -manageBtn.addEventListener('click', () => { - location.href = '/manage.html' -}) -toolbar.appendChild(manageBtn) +// Session management lives on the home Sessions chooser now (open / kill / +// new), so the standalone /manage.html page was removed. mountQrConnect(toolbar) diff --git a/public/manage.html b/public/manage.html deleted file mode 100644 index bee033c..0000000 --- a/public/manage.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Session Manager — Web Terminal - - - - - -
- - - diff --git a/public/manage.ts b/public/manage.ts deleted file mode 100644 index fd50f29..0000000 --- a/public/manage.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * public/manage.ts — standalone Session Manager page (/manage.html). - * - * A full-page grid of the host's running sessions, each with a LIVE preview - * thumbnail (a read-only xterm rendering the session's current screen, scaled - * down like a screenshot) so you can see what each one is doing at a glance. - * Open one (?join=), kill one, or bulk-kill. Auto-refreshes. - * - * Previews use GET /live-sessions/:id/preview (the scrollback tail) — they do - * NOT open a WS / attach, so they don't inflate watcher counts or keep sessions - * alive. The card + preview plumbing is shared with the launcher via - * public/preview-grid.ts (DRY). - */ - -import '@xterm/xterm/css/xterm.css' -import type { LiveSessionInfo } from '../src/types.js' -import { - el, - relTime, - makePreviewCard, - updatePreviewCard, - loadPreviewInto, - fitThumb, - fetchLiveSessions, - type PreviewCard, -} from './preview-grid.js' - -const REFRESH_MS = 4000 -const THUMB_W = 360 // card thumbnail width in px -const THUMB_MAX_H = 220 - -const cards = new Map() -let busy = false - -async function killOne(id: string): Promise { - await fetch(`/live-sessions/${id}`, { method: 'DELETE' }).catch(() => {}) - void render() -} - -async function killBulk(detachedOnly: boolean): Promise { - const label = detachedOnly ? 'all DETACHED sessions (no device watching)' : 'ALL sessions' - if (!confirm(`Kill ${label}? Running shells/Claude will be terminated.`)) return - await fetch(`/live-sessions${detachedOnly ? '?detached=1' : ''}`, { method: 'DELETE' }).catch(() => {}) - void render() -} - -function makeCard(s: LiveSessionInfo): PreviewCard { - const kill = el('button', 'mg-kill', 'Kill ✕') - kill.addEventListener('click', () => void killOne(s.id)) - return makePreviewCard(s, { - onOpen: (id) => { - location.href = `/?join=${id}` - }, - openHref: (id) => `/?join=${id}`, - extraActions: () => [kill], - }) -} - -function updateCard(card: PreviewCard, s: LiveSessionInfo): void { - updatePreviewCard(card, s) - card.meta.textContent = `${s.cwd ?? 'unknown dir'} · ${s.cols}×${s.rows} · ${relTime(s.createdAt)} old · ${s.id.slice(0, 8)}` -} - -const root = document.getElementById('manage-root') -if (!root) throw new Error('#manage-root not found') - -let grid: HTMLElement | null = null -let countEl: HTMLElement | null = null - -function ensureChrome(): void { - if (grid) return - const header = el('div', 'mg-header') - header.append(el('div', 'mg-title', 'Session Manager')) - countEl = el('div', 'mg-sub', '') - header.append(countEl) - - const bar = el('div', 'mg-bar') - const back = el('a', 'mg-btn', '← Back to terminal') as HTMLAnchorElement - back.href = '/' - const refresh = el('button', 'mg-btn', '↻ Refresh') - refresh.addEventListener('click', () => void render()) - const killDetached = el('button', 'mg-btn warn', 'Kill detached') - killDetached.addEventListener('click', () => void killBulk(true)) - const killAll = el('button', 'mg-btn danger', 'Kill all') - killAll.addEventListener('click', () => void killBulk(false)) - bar.append(back, refresh, killDetached, killAll) - header.append(bar) - - grid = el('div', 'mg-grid') - root!.append(header, grid) -} - -async function render(): Promise { - if (busy) return - busy = true - ensureChrome() - - const sessions = await fetchLiveSessions() - if (countEl) countEl.textContent = `${sessions.length} session(s) running on the host` - - const seen = new Set() - for (const s of sessions) { - seen.add(s.id) - let card = cards.get(s.id) - if (!card) { - card = makeCard(s) - cards.set(s.id, card) - grid!.append(card.el) - } - updateCard(card, s) - void loadPreviewInto(s.id, card, THUMB_W, THUMB_MAX_H) - } - // Drop cards for sessions that are gone. - for (const [id, card] of cards) { - if (!seen.has(id)) { - card.term.dispose() - card.el.remove() - cards.delete(id) - } - } - - if (sessions.length === 0 && grid && grid.querySelector('.mg-empty') === null) { - grid.append(el('div', 'mg-empty', 'No sessions running. Open the terminal to start one.')) - } else { - grid?.querySelector('.mg-empty')?.remove() - } - busy = false -} - -void render() -setInterval(() => void render(), REFRESH_MS) -// Re-scale thumbnails if the window resizes. -window.addEventListener('resize', () => { - for (const card of cards.values()) fitThumb(card, THUMB_W, THUMB_MAX_H) -})