feat(v0.2): tab status dot, activity (unread) indicator, drag-reorder
- terminal-session.ts: track SessionStatus (connecting/connected/reconnecting/ exited) + onStatus callback; FIX: wire onTitleChange (onTitle was in opts but never bound — auto-title was dead) - tabs.ts: per-tab status dot (color = connection state), unread flag set on background output + cleared on activate, HTML5 drag-to-reorder (preserves active tab, persisted) - style.css: .tab-dot colors, .unread ring, .dragging/.drag-over feedback - Browser-verified: green dot connected, unread ring after bg output, reorder AAA|BBB→BBB|AAA persists across reload. 191 tests green, typechecks + build ok.
This commit is contained in:
@@ -34,6 +34,9 @@ function buildWsUrl(): string {
|
||||
return `${scheme}://${location.host}/term`
|
||||
}
|
||||
|
||||
/** Connection state, surfaced as a colored status dot on the tab. */
|
||||
export type SessionStatus = 'connecting' | 'connected' | 'reconnecting' | 'exited'
|
||||
|
||||
export interface TerminalSessionOpts {
|
||||
/** Existing session to re-attach to, or null to spawn a fresh one. */
|
||||
sessionId: string | null
|
||||
@@ -43,6 +46,8 @@ export interface TerminalSessionOpts {
|
||||
onActivity?: () => void
|
||||
/** Optional: fired when the terminal title changes (OSC 0/2 — cwd/process). */
|
||||
onTitle?: (title: string) => void
|
||||
/** Optional: fired when connection status changes (for the tab status dot). */
|
||||
onStatus?: (status: SessionStatus) => void
|
||||
}
|
||||
|
||||
export class TerminalSession {
|
||||
@@ -53,6 +58,9 @@ export class TerminalSession {
|
||||
private readonly resizeObserver: ResizeObserver
|
||||
private readonly onSessionId: (id: string) => void
|
||||
private readonly onActivity: (() => void) | undefined
|
||||
private readonly onTitle: ((title: string) => void) | undefined
|
||||
private readonly onStatus: ((status: SessionStatus) => void) | undefined
|
||||
private statusValue: SessionStatus = 'connecting'
|
||||
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null
|
||||
@@ -69,6 +77,8 @@ export class TerminalSession {
|
||||
this.sessionId = opts.sessionId
|
||||
this.onSessionId = opts.onSessionId
|
||||
this.onActivity = opts.onActivity
|
||||
this.onTitle = opts.onTitle
|
||||
this.onStatus = opts.onStatus
|
||||
|
||||
this.el = document.createElement('div')
|
||||
this.el.className = 'term-pane'
|
||||
@@ -84,6 +94,7 @@ export class TerminalSession {
|
||||
this.term.open(this.el)
|
||||
|
||||
this.term.onData((data) => this.send(data))
|
||||
this.term.onTitleChange((title) => this.onTitle?.(title))
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => this.scheduleResize())
|
||||
this.resizeObserver.observe(this.el)
|
||||
@@ -94,6 +105,16 @@ export class TerminalSession {
|
||||
return this.sessionId
|
||||
}
|
||||
|
||||
/** Current connection status (for the tab status dot). */
|
||||
get status(): SessionStatus {
|
||||
return this.statusValue
|
||||
}
|
||||
|
||||
private setStatus(s: SessionStatus): void {
|
||||
this.statusValue = s
|
||||
this.onStatus?.(s)
|
||||
}
|
||||
|
||||
/** fit() only when the pane has real dimensions (display:none → NaN, §9). */
|
||||
private safefit(): { cols: number; rows: number } | null {
|
||||
try {
|
||||
@@ -129,6 +150,7 @@ export class TerminalSession {
|
||||
connect(): void {
|
||||
if (this.disposed || this.isConnecting) return
|
||||
this.isConnecting = true
|
||||
this.setStatus('connecting')
|
||||
this.term.write(statusLine(`${YELLOW}Connecting…${RESET}`))
|
||||
|
||||
const socket = new WebSocket(buildWsUrl())
|
||||
@@ -136,6 +158,7 @@ export class TerminalSession {
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
this.reconnectDelay = 1000
|
||||
this.setStatus('connected')
|
||||
this.term.write(statusLine(`${GREEN}${BOLD}Connected${RESET}`))
|
||||
socket.send(buildMessage({ type: 'attach', sessionId: this.sessionId }))
|
||||
})
|
||||
@@ -176,6 +199,7 @@ export class TerminalSession {
|
||||
break
|
||||
}
|
||||
case 'exit': {
|
||||
this.setStatus('exited')
|
||||
const reason = msg.reason ? ` (${msg.reason})` : ''
|
||||
this.term.write(
|
||||
statusLine(
|
||||
@@ -200,6 +224,7 @@ export class TerminalSession {
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer !== null) return
|
||||
this.setStatus('reconnecting')
|
||||
const delay = this.reconnectDelay
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000)
|
||||
this.term.write(
|
||||
|
||||
Reference in New Issue
Block a user