feat(v0.6): remove standalone Manage page — manage sessions on the Sessions view
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.
This commit is contained in:
@@ -146,6 +146,20 @@
|
|||||||
- **commit**: <hash 或 N/A>
|
- **commit**: <hash 或 N/A>
|
||||||
======================================================= -->
|
======================================================= -->
|
||||||
|
|
||||||
|
### 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
|
### 2026-06-30 · v0.6 项目网格卡片 — 直接关掉 session
|
||||||
|
|
||||||
- **状态**: `[x]` DONE。51 projects-panel 测试全绿(+1)· web typecheck 干净 · `build:web` OK · 真浏览器验证通过。
|
- **状态**: `[x]` DONE。51 projects-panel 测试全绿(+1)· web typecheck 干净 · `build:web` OK · 真浏览器验证通过。
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"start": "tsx src/server.ts",
|
"start": "tsx src/server.ts",
|
||||||
"dev": "tsx watch src/server.ts",
|
"dev": "tsx watch src/server.ts",
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"build:web": "esbuild public/main.ts public/manage.ts --bundle --format=esm --outdir=public/build --sourcemap",
|
"build:web": "esbuild public/main.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",
|
"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",
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.web.json",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher
|
|||||||
const actions = el('div', 'launcher-actions')
|
const actions = el('div', 'launcher-actions')
|
||||||
const newBtn = el('button', 'launcher-new', '+ New session')
|
const newBtn = el('button', 'launcher-new', '+ New session')
|
||||||
newBtn.addEventListener('click', () => hooks.onNew())
|
newBtn.addEventListener('click', () => hooks.onNew())
|
||||||
const manage = el('a', 'mg-btn', '🗂 Manage') as HTMLAnchorElement
|
actions.append(newBtn)
|
||||||
manage.href = '/manage.html'
|
|
||||||
actions.append(newBtn, manage)
|
|
||||||
head.append(sub, actions)
|
head.append(sub, actions)
|
||||||
root.append(head)
|
root.append(head)
|
||||||
|
|
||||||
@@ -57,8 +55,20 @@ export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher
|
|||||||
const cards = new Map<string, PreviewCard>()
|
const cards = new Map<string, PreviewCard>()
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
/** Kill a session (DELETE /live-sessions/:id) then refresh the grid. */
|
||||||
|
async function killOne(id: string): Promise<void> {
|
||||||
|
await fetch(`/live-sessions/${encodeURIComponent(id)}`, { method: 'DELETE' }).catch(() => {})
|
||||||
|
void refresh()
|
||||||
|
}
|
||||||
|
|
||||||
function makeCard(s: LiveSessionInfo): PreviewCard {
|
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<void> {
|
async function refresh(): Promise<void> {
|
||||||
|
|||||||
@@ -76,16 +76,8 @@ mountHistory(toolbar, {
|
|||||||
mountShortcuts(toolbar)
|
mountShortcuts(toolbar)
|
||||||
mountShareSession(toolbar, () => app.activeSessionId())
|
mountShareSession(toolbar, () => app.activeSessionId())
|
||||||
|
|
||||||
// Session manager (standalone page) — manage/kill the host's running sessions.
|
// Session management lives on the home Sessions chooser now (open / kill /
|
||||||
const manageBtn = document.createElement('button')
|
// new), so the standalone /manage.html page was removed.
|
||||||
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)
|
|
||||||
|
|
||||||
mountQrConnect(toolbar)
|
mountQrConnect(toolbar)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
||||||
<title>Session Manager — Web Terminal</title>
|
|
||||||
<meta name="theme-color" content="#0e0f13">
|
|
||||||
<link rel="stylesheet" href="./build/manage.css">
|
|
||||||
<link rel="stylesheet" href="./style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="manage-root"></div>
|
|
||||||
<script type="module" src="./build/manage.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
135
public/manage.ts
135
public/manage.ts
@@ -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=<id>), 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<string, PreviewCard>()
|
|
||||||
let busy = false
|
|
||||||
|
|
||||||
async function killOne(id: string): Promise<void> {
|
|
||||||
await fetch(`/live-sessions/${id}`, { method: 'DELETE' }).catch(() => {})
|
|
||||||
void render()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function killBulk(detachedOnly: boolean): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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<string>()
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user