Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ new version heading in the same commit.

## [Unreleased]

## [0.192.0] — 2026-07-14
### Changed
- **The browser terminal now selects and clicks like a web page — no more Option-drag, and links work.**
claude's fullscreen TUI turns on mouse tracking, which made xterm hand every click/drag to the app: a
plain drag wouldn't select (you had to hold Option), and links couldn't be clicked. But claude only ever
uses the *wheel* (it runs with `DISABLE_MOUSE_CLICKS`) — so our first-party `<Xterm>` client now strips
just the mouse-*tracking* DECSET modes (1000–1003) out of the PTY output stream before xterm sees them
(native drag-select + clickable links come back), and forwards **only the wheel** back to claude as SGR
events so its conversation still scrolls. Uses documented xterm API only (no internals), so it survives
xterm upgrades. (`web/src/Xterm.tsx` `stripMouseTracking` + the custom wheel handler.)
- **Links no longer need a scheme to be clickable.** Replaced the stock `WebLinksAddon` (full `https://…`
only) with custom link providers that also match bare domains (`example.com`), subdomains, `www.…`,
`localhost:PORT`/`127.0.0.1:PORT`, and OSC-8 hyperlinks — opening the right target in a new tab — while
deliberately *not* underlining `file.tsx:42`-style tokens. (`web/src/Xterm.tsx` `makeLinkProvider`.)
- Test bed: `scripts/mouse-tui.mjs` reproduces claude's mouse-reporting condition (plain bash never did),
and `TERMBED_CMD=…` points the test bed at it — so selection/links/wheel can be verified against a real
mouse-mode app. (`scripts/termbed.mjs`, `web/src/termbed.tsx`.)

## [0.191.0] — 2026-07-14
### Added
- **Chat — a plain-language window onto a claude-code run for non-technical teammates.** A new **Chat**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.191.0",
"version": "0.192.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
62 changes: 62 additions & 0 deletions scripts/mouse-tui.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env node
// A tiny fullscreen TUI that reproduces the ONE condition plain bash never did in the test bed: an app
// that turns on mouse tracking (like claude's TUI). It goes to the alternate screen, enables mouse
// reporting (1000 + SGR 1006 — exactly what makes xterm disable selection), prints a few links WITHOUT a
// scheme, and echoes every wheel event it receives back onto a status line. So in the browser terminal
// you can verify, against a real mouse-reporting app:
// • plain drag SELECTS (no Option) — our output-stream mouse-tracking stripper
// • a bare domain / localhost:port is CLICKABLE — our custom link providers
// • the WHEEL still reaches the app — our custom wheel forwarder (the counter ticks)
//
// Run it inside the test bed: TERMBED_CMD='node ../scripts/mouse-tui.mjs' node scripts/termbed.mjs
// (or just `node scripts/mouse-tui.mjs` in any terminal to eyeball the escapes). Ctrl-C / q to quit.
const out = process.stdout
const w = (s) => out.write(s)

const ALT_ON = '\x1b[?1049h', ALT_OFF = '\x1b[?1049l'
const MOUSE_ON = '\x1b[?1000h\x1b[?1006h', MOUSE_OFF = '\x1b[?1006l\x1b[?1000l'
const HIDE = '\x1b[?25l', SHOW = '\x1b[?25h'
const home = (r, c) => `\x1b[${r};${c}H`
const clear = '\x1b[2J'

let wheels = 0, lastEvt = '—'

function draw() {
w(clear + home(1, 1))
w('\x1b[1;38;5;39m mouse-tui \x1b[0m — reproduces claude\'s mouse-reporting TUI\r\n')
w('\x1b[90m mouse tracking is ON (1000+1006), so without the fix xterm would refuse to select\x1b[0m\r\n\r\n')
w(' Links to click (none carry a scheme except the first):\r\n')
w(' 1. full url https://example.com/path?q=1\r\n')
w(' 2. bare domain example.com\r\n')
w(' 3. subdomain docs.github.io/xterm\r\n')
w(' 4. localhost localhost:5199/foo\r\n')
w(' 5. www www.google.com\r\n')
w(' 6. not a link Component.tsx:42 (should NOT underline)\r\n\r\n')
w(' Drag across any line above → it should highlight and land on your clipboard.\r\n\r\n')
w(`\x1b[7m WHEEL received: ${wheels} last event: ${lastEvt} \x1b[0m\r\n`)
w('\r\n \x1b[90mscroll over the pane to tick the counter · press q or Ctrl-C to quit\x1b[0m')
}

function cleanup(code = 0) {
w(MOUSE_OFF + SHOW + ALT_OFF)
try { process.stdin.setRawMode(false) } catch { /* not a tty */ }
process.exit(code)
}

w(ALT_ON + HIDE + MOUSE_ON)
draw()

try { process.stdin.setRawMode(true) } catch { /* not a tty — still readable */ }
process.stdin.resume()
process.stdin.on('data', (buf) => {
const s = buf.toString('latin1')
if (s === 'q' || s === '\x03') return cleanup(0) // q or Ctrl-C
// SGR mouse: ESC [ < btn ; col ; row (M|m). Wheel up = 64, down = 65 (bit 0x40 set).
const m = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(s)
if (m) {
const btn = Number(m[1])
if (btn === 64 || btn === 65) { wheels++; lastEvt = `${btn === 64 ? 'up' : 'down'} @ ${m[2]},${m[3]}`; draw() }
}
})
process.on('SIGINT', () => cleanup(0))
process.on('SIGTERM', () => cleanup(0))
8 changes: 6 additions & 2 deletions scripts/termbed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ for (const bin of ['tmux', 'ttyd']) {
// host's tmux config. (A `set -g mouse on` there would hand wheel + drag to tmux copy-mode — tmux would
// scroll its own history and copy-on-select, clearing the highlight — masking the real client behaviour.)
const shell = process.env.SHELL || '/bin/bash'
spawnSync('tmux', ['-f', '/dev/null', '-S', SOCK, 'new-session', '-d', '-s', SESSION, shell], { stdio: 'ignore' })
// TERMBED_CMD lets you point the session at a mouse-reporting TUI instead of a bare shell — e.g.
// `TERMBED_CMD='node ../scripts/mouse-tui.mjs' node scripts/termbed.mjs` to reproduce claude's mouse
// mode and verify native drag-select + clickable links + wheel forwarding. Default: a login shell.
const prog = process.env.TERMBED_CMD ? ['sh', '-c', process.env.TERMBED_CMD] : [shell]
spawnSync('tmux', ['-f', '/dev/null', '-S', SOCK, 'new-session', '-d', '-s', SESSION, ...prog], { stdio: 'ignore' })
for (const opt of [
['set', '-g', 'set-clipboard', 'on'], // forward OSC 52 copy-on-select → our client's OSC 52 handler
['set', '-g', 'allow-passthrough', 'on'],
Expand All @@ -55,7 +59,7 @@ for (const opt of [
const ttyd = spawn('ttyd', [
'-p', String(TTYD_PORT), '-i', '127.0.0.1', '-b', '/pty', '-W',
'-t', 'disableLeaveAlert=true',
'tmux', '-f', '/dev/null', '-u', '-S', SOCK, 'new-session', '-A', '-s', SESSION, shell,
'tmux', '-f', '/dev/null', '-u', '-S', SOCK, 'new-session', '-A', '-s', SESSION, ...prog,
], { stdio: 'inherit' })
ttyd.on('error', (e) => { console.error('✗ ttyd failed:', e.message); cleanup(1) })

Expand Down
Loading
Loading