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
2 changes: 2 additions & 0 deletions docs/specs/transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ VS Code-only workbench chord mirroring uses `dormouse:runWorkbenchCommand` from
| Direction | Message | Source type | Contract |
| --- | --- | --- | --- |
| Webview → host | `dormouse:openExternal` | `WebviewMessage` | Request the host to open a user-confirmed external URI from an OSC 8 hyperlink. Hosts must revalidate and reject malformed, control-character-bearing, or blocked pseudo-scheme targets (`javascript:`, `data:`, `blob:`, `about:`). |
| Webview → host | `pty:getOpenPorts` | `WebviewMessage` | Request the TCP listening ports opened by a PTY's shell process **and all of its descendant subprocesses**. The host resolves them from the PTY's root pid and replies with `pty:openPorts`. Source of truth: `getOpenPortsForPid()` in `standalone/sidecar/pty-core.js` (the VS Code extension loads it through the `lib/pty-core.cjs` shim). |
| Host → webview | `pty:openPorts` | `ExtensionMessage` | Reply to `pty:getOpenPorts`: `ports: OpenPort[]` (`{ protocol, family, address, port, pid, processName }`), de-duplicated by `(family, address, port)` and sorted by port. Empty array when the PTY is gone or enumeration fails. |
| Host → webview | `pty:data` | `ExtensionMessage` | PTY output after state-driving supported OSC sequences have been parsed/stripped; `OSC 8` hyperlinks are preserved for xterm.js and routed only to the owning router. |
| Host → webview | `pty:replay` | `ExtensionMessage` | Buffered raw output since spawn; the webview parses semantic OSCs during replay reconstruction without triggering alerts. |
| Host → webview | `dormouse:newTerminal` | `ExtensionMessage` | Payload may include `shell`, `args`, display `name`, `replaceUntouched`, and `announce`; the webview replaces the selected untouched terminal in-place only when `replaceUntouched` is true, otherwise it spawns a new pane. |
Expand Down
24 changes: 24 additions & 0 deletions lib/src/lib/platform/fake-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,30 @@ describe('FakePtyAdapter', () => {
expect(dataEvents).toEqual([{ id: 't1', data: 'after' }]);
});

it('clears configured open ports on kill', async () => {
const { adapter } = createAdapter();
const ports = [
{
protocol: 'tcp' as const,
family: 'IPv4' as const,
address: '127.0.0.1',
port: 5173,
pid: 1234,
processName: 'vite',
},
];
adapter.spawnPty('t1');
adapter.setOpenPorts('t1', ports);

await expect(adapter.getOpenPorts('t1')).resolves.toEqual(ports);

adapter.killPty('t1');
await expect(adapter.getOpenPorts('t1')).resolves.toEqual([]);

adapter.spawnPty('t1');
await expect(adapter.getOpenPorts('t1')).resolves.toEqual([]);
});

it('clears input handlers on reset', () => {
const { adapter, dataEvents } = createAdapter();
const handled: string[] = [];
Expand Down
15 changes: 14 additions & 1 deletion lib/src/lib/platform/fake-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlertStateDetail, PlatformAdapter, PtyInfo } from './types';
import type { AlertStateDetail, OpenPort, PlatformAdapter, PtyInfo } from './types';
import { AlertManager, type SessionStatus } from '../alert-manager';
import { normalizeExternalUri } from '../external-links';
import {
Expand Down Expand Up @@ -45,6 +45,7 @@ export class FakePtyAdapter implements PlatformAdapter {
private scenarioMap = new Map<string, FakeScenario>();
private inputHandlers = new Map<string, (data: string) => void>();
private protocolParsers = new Map<string, TerminalProtocolParser>();
private openPortsMap = new Map<string, OpenPort[]>();
private alertManager = new AlertManager();

constructor() {
Expand Down Expand Up @@ -91,6 +92,7 @@ export class FakePtyAdapter implements PlatformAdapter {
this.spawnHandlers.clear();
this.inputHandlers.clear();
this.protocolParsers.clear();
this.openPortsMap.clear();
this.alertManager.dispose();
this.alertManager = new AlertManager();
this.alertManager.onStateChange((id, state) => {
Expand Down Expand Up @@ -158,6 +160,7 @@ export class FakePtyAdapter implements PlatformAdapter {
this.terminalSizes.delete(id);
this.inputHandlers.delete(id);
this.protocolParsers.delete(id);
this.openPortsMap.delete(id);
this.alertManager.onExit(id, 0);
for (const handler of this.exitHandlers) {
handler({ id, exitCode: 0 });
Expand All @@ -183,6 +186,16 @@ export class FakePtyAdapter implements PlatformAdapter {
async getCwd(_id: string): Promise<string | null> { return null; }
async getScrollback(_id: string): Promise<string | null> { return null; }

/** Ports the playground/tests want a given terminal to report. */
setOpenPorts(id: string, ports: OpenPort[]): void {
this.openPortsMap.set(id, ports);
}

async getOpenPorts(id: string): Promise<OpenPort[]> {
if (!this.terminals.has(id)) return [];
return this.openPortsMap.get(id) ?? [];
}

getPtySize(id: string): FakePtySize {
return this.terminalSizes.get(id) ?? DEFAULT_PTY_SIZE;
}
Expand Down
27 changes: 27 additions & 0 deletions lib/src/lib/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ export interface PtyInfo {
exitCode?: number;
}

/**
* A TCP socket in the LISTEN state opened by a terminal's shell process or any
* of its descendant subprocesses. `address` is the bind interface — `0.0.0.0`
* / `::` mean all interfaces, `127.0.0.1` / `::1` mean loopback-only.
*/
export interface OpenPort {
protocol: 'tcp';
family: 'IPv4' | 'IPv6';
address: string;
port: number;
pid: number;
processName?: string;
}

/**
* End-to-end budget for `getOpenPorts()` at every transport boundary
* (webview → host adapter, host → pty-host child, Tauri command → sidecar) and
* for the per-subprocess execs inside `getOpenPortsForPid()` (lsof, PowerShell,
* `Get-NetTCPConnection`, `netstat`). Wider than the 1 s cwd query because
* enumeration shells out on macOS/Windows; tight enough to fail visibly rather
* than hang a pane header. Mirrored as `OPEN_PORT_TIMEOUT_MS` in
* `standalone/sidecar/pty-core.js` and `standalone/src-tauri/src/lib.rs`.
*/
export const OPEN_PORT_TIMEOUT_MS = 3000;

export type AlertStateDetail = { id: string } & AlertState;

export interface PlatformAdapter {
Expand All @@ -26,6 +51,8 @@ export interface PlatformAdapter {
// PTY queries
getCwd(id: string): Promise<string | null>;
getScrollback(id: string): Promise<string | null>;
/** TCP listening ports opened by this terminal's process tree (shell + descendants). */
getOpenPorts(id: string): Promise<OpenPort[]>;

// Clipboard support for file references and raw images.
readClipboardFilePaths(): Promise<string[] | null>;
Expand Down
12 changes: 11 additions & 1 deletion lib/src/lib/platform/vscode-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AlertStateDetail, PlatformAdapter, PtyInfo } from './types';
import type { AlertStateDetail, OpenPort, PlatformAdapter, PtyInfo } from './types';
import { OPEN_PORT_TIMEOUT_MS } from './types';
import { setDefaultShellOpts } from '../shell-defaults';
import {
collectTerminalSemanticEvents,
Expand Down Expand Up @@ -162,6 +163,15 @@ export class VSCodeAdapter implements PlatformAdapter {
return this.requestResponse('pty:getScrollback', 'pty:scrollback', { id }, (msg) => msg.data);
}

async getOpenPorts(id: string): Promise<OpenPort[]> {
const result = await this.requestResponse<OpenPort[]>(
'pty:getOpenPorts', 'pty:openPorts', { id },
(msg) => msg.ports as OpenPort[],
OPEN_PORT_TIMEOUT_MS,
);
return result ?? [];
}

readClipboardFilePaths(): Promise<string[] | null> {
return this.requestResponse<string[] | null>(
'clipboard:readFiles', 'clipboard:files', {},
Expand Down
1 change: 1 addition & 0 deletions standalone/sidecar/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ rl.on('line', (line) => {
case 'pty:kill': mgr.kill(data.id); break;
case 'pty:requestInit': mgr.list(); break;
case 'pty:getCwd': mgr.getCwd(data.id, data.requestId); break;
case 'pty:getOpenPorts': mgr.getOpenPorts(data.id, data.requestId); break;
case 'pty:getScrollback': mgr.getScrollback(data.id, data.requestId); break;
case 'pty:getShells': mgr.getShells(data.requestId); break;
case 'pty:gracefulKillAll': mgr.gracefulKillAll(data.timeout); break;
Expand Down
3 changes: 3 additions & 0 deletions standalone/sidecar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"private": true,
"version": "0.1.0",
"main": "main.js",
"scripts": {
"test": "node --test"
},
"dependencies": {
"node-pty": "1.2.0-beta.13"
}
Expand Down
Loading
Loading