Files
web-terminal/public/launcher.ts
Yaojia Wang bf5241e87b 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.
2026-06-30 14:08:00 +02:00

133 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* public/launcher.ts — the home "session chooser" shown when no tab is open.
*
* Opening the app no longer auto-creates or auto-restores tabs. Instead this
* start screen lists the host's running sessions as live preview thumbnails
* (read-only xterm, same as the manage page) so the user picks which to open —
* or starts a new one. Sessions persist server-side, so opening one replays its
* full scrollback. The thumbnail card + preview plumbing is shared with the
* manage page via public/preview-grid.ts (DRY).
*/
import type { LiveSessionInfo } from '../src/types.js'
import {
el,
relTime,
makePreviewCard,
updatePreviewCard,
loadPreviewInto,
fetchLiveSessions,
type PreviewCard,
} from './preview-grid.js'
export interface LauncherHooks {
onOpen: (id: string) => void
onNew: () => void
}
const THUMB_W = 320
const THUMB_MAX_H = 200
const REFRESH_MS = 5000
export interface Launcher {
setVisible(v: boolean): void
refresh(): void
}
export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher {
const root = el('div', 'launcher')
root.style.display = 'none'
host.appendChild(root)
const head = el('div', 'launcher-head')
head.append(el('div', 'launcher-title', 'Your sessions'))
const sub = el('div', 'launcher-sub', '')
const actions = el('div', 'launcher-actions')
const newBtn = el('button', 'launcher-new', ' New session')
newBtn.addEventListener('click', () => hooks.onNew())
actions.append(newBtn)
head.append(sub, actions)
root.append(head)
const grid = el('div', 'mg-grid')
root.append(grid)
const cards = new Map<string, PreviewCard>()
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 {
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> {
const sessions = await fetchLiveSessions()
sub.textContent = sessions.length
? `${sessions.length} running on this host — pick one to open`
: 'No sessions running yet'
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)
}
updatePreviewCard(card, s)
card.meta.textContent = `${s.cwd ?? 'unknown dir'} · ${s.cols}×${s.rows} · ${relTime(s.createdAt)} old`
void loadPreviewInto(s.id, card, THUMB_W, THUMB_MAX_H)
}
for (const [id, card] of cards) {
if (!seen.has(id)) {
card.term.dispose()
card.el.remove()
cards.delete(id)
}
}
grid.querySelector('.mg-empty')?.remove()
if (sessions.length === 0) {
grid.append(el('div', 'mg-empty', 'No running sessions. Click “New session” to start one.'))
}
}
return {
setVisible(v: boolean): void {
root.style.display = v ? 'block' : 'none'
if (v) {
void refresh()
if (timer === null) {
timer = setInterval(() => {
if (root.style.display !== 'none') void refresh()
}, REFRESH_MS)
}
} else {
if (timer !== null) {
clearInterval(timer)
timer = null
}
for (const card of cards.values()) card.term.dispose()
cards.clear()
grid.replaceChildren()
}
},
refresh(): void {
void refresh()
},
}
}