feat(v0.2): tab title = current folder (from OSC title)

- title-util.ts: folderFromTitle() parses 'user@host:~/path' → last folder
  segment (home stays ~); pure, dependency-free, 7 unit tests
- terminal-session.ts: onTitleChange → folderFromTitle so the tab auto-names
  to the cwd folder and updates live on cd
- Browser-verified: ~ → cd project → 'web-terminal' → cd docs → 'docs' → cd ~ → '~'.
  198 tests green, typecheck + build ok.
This commit is contained in:
Yaojia Wang
2026-06-17 15:27:01 +02:00
parent 459a27eda8
commit 4091283773
3 changed files with 61 additions and 1 deletions

View File

@@ -11,6 +11,7 @@
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import type { ClientMessage, ServerMessage } from '../src/types.js'
import { folderFromTitle } from './title-util.js'
const RESET = '\x1b[0m'
const BOLD = '\x1b[1m'
@@ -94,7 +95,8 @@ export class TerminalSession {
this.term.open(this.el)
this.term.onData((data) => this.send(data))
this.term.onTitleChange((title) => this.onTitle?.(title))
// Tab title = current folder, derived from the shell's OSC title.
this.term.onTitleChange((title) => this.onTitle?.(folderFromTitle(title) ?? title))
this.resizeObserver = new ResizeObserver(() => this.scheduleResize())
this.resizeObserver.observe(this.el)

25
public/title-util.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* public/title-util.ts — derive a short tab title from a terminal OSC title.
*
* Pure, dependency-free (no xterm/DOM) so it's unit-testable in node.
*/
/**
* Derive the **current folder** from the terminal's OSC title.
*
* Most shells emit titles like `user@host:~/path/to/dir` (zsh/bash default).
* Take the part after the last ':' (the path), then its last segment (the
* folder name). `~` (home) stays `~`. Returns null for an empty title; falls
* back to the raw-ish basename when there's no `user@host:` shape (e.g. a
* running command title).
*/
export function folderFromTitle(title: string): string | null {
let s = title.trim()
if (!s) return null
const colon = s.lastIndexOf(':')
if (colon !== -1) s = s.slice(colon + 1).trim()
s = s.replace(/\/+$/, '') // strip trailing slash
const slash = s.lastIndexOf('/')
if (slash !== -1) s = s.slice(slash + 1)
return s || null
}