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
4 changes: 3 additions & 1 deletion docs/specs/terminal-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ Source of truth: `context` in `standalone/sidecar/pty-core.js`; `terminalContext

**Must make diagnostic text drag-selectable**, including detail-dialog errors, without focusing the helper. Copy routing follows `docs/specs/mouse-and-clipboard.md` → "Terminal context input".

**Must drain the helper's queued xterm writes and verify a single autorun command echo before placement snapshots.**

**Must suppress xterm's auto-revealed scrollbar in visual snapshots**, while retaining terminal scrolling and layout.

**Must fit every control inside the panel at its minimum width, label included.** Port action overflow follows `docs/specs/layout.md` → "Header context menu". The gallery's play check measures each button against the panel and against its own box.

Source of truth: `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`; `lib/src/stories/TerminalContext.stories.tsx` supplies sample output; `lib/src/stories/Wall.stories.tsx` exercises the live helper. `lib/src/stories/HelperPlacement.stories.tsx` checks rendered placement and real xterm input/focus retention; the gallery checks narrow controls and always-visible details. `visualSnapshot` in `lib/.storybook/preview.ts` suppresses scrollbar paint.
Source of truth: `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`; `lib/src/stories/TerminalContext.stories.tsx` supplies sample output; `lib/src/stories/Wall.stories.tsx` exercises the live helper. `lib/src/stories/HelperPlacement.stories.tsx` checks rendered placement and real xterm input/focus retention; the gallery checks narrow controls and always-visible details. `visualSnapshot` in `lib/.storybook/preview.ts` suppresses scrollbar paint. Tests: `lib/src/lib/platform/fake-adapter-helper.test.ts`.

The Window-host workspace picker follows `docs/specs/layout.md` → Moving Surfaces between Workspaces.

Expand Down
40 changes: 40 additions & 0 deletions lib/src/lib/platform/fake-adapter-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @vitest-environment jsdom
import { afterEach, expect, it, vi } from 'vitest';
import { Terminal } from '@xterm/xterm';
import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes';
import { FakePtyAdapter } from './fake-adapter';
import { removeTerminalPaneState } from '../terminal-state-store';

const ID = 'helper-reflow';
afterEach(() => { vi.restoreAllMocks(); removeTerminalPaneState(ID); });

it('preserves one command echo when the helper resizes between PTY chunks', async () => {
const adapter = new FakePtyAdapter();
const terminal = new Terminal({ cols: 33, rows: 7, allowProposedApi: true });
terminal.loadAddon(new UnicodeGraphemesAddon());
adapter.onPtyData(({ data }) => terminal.write(data));
const drain = () => new Promise<void>(resolve => terminal.write('', resolve));
try {
adapter.spawnPty(ID, { helper: { parentId: 'source', command: 'git status' } });
await Promise.resolve();
await drain();
// Make xterm yield after ten small writes: the echo, before its CRLF.
let clockReads = 0;
const clock = vi.spyOn(performance, 'now').mockImplementation(() => Math.floor(++clockReads / 11) * 20);
const resized = new Promise<void>(resolve => {
const listener = terminal.onWriteParsed(() => {
listener.dispose();
terminal.resize(26, 7);
resolve();
});
});
adapter.writePty(ID, 'git status\r');
await resized;
clock.mockRestore();
await drain();
const text = Array.from({ length: terminal.buffer.active.length }, (_, i) =>
terminal.buffer.active.getLine(i)?.translateToString(true) ?? '').join('');
expect(text.match(/git status/g)).toHaveLength(1);
expect(text.match(/On branch main/g)).toHaveLength(1);
} finally { terminal.dispose(); adapter.shutdown(); }
});
26 changes: 16 additions & 10 deletions lib/src/lib/platform/fake-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,23 +223,29 @@ export class FakePtyAdapter implements PlatformAdapter {
private startHelperShell(id: string): void {
const helper = this.helpers.get(id)!;
let input = '';
const prompt = () => this.sendOutput(id, `\x1b]633;A\x07${helper.cwd} ❯ \x1b]633;B\x07`);
const prompt = () => `\x1b]633;A\x07${helper.cwd} ❯ \x1b]633;B\x07`;
this.inputHandlers.set(id, data => {
if (data === '\x03') { helper.busy = false; input = ''; this.sendOutput(id, '^C\r\n\x1b]633;D;130\x07'); prompt(); return; }
if (data === '\x03') { helper.busy = false; input = ''; this.sendOutput(id, '^C\r\n\x1b]633;D;130\x07' + prompt()); return; }
if (helper.busy) return;
// Submit the echo, command output, and returned prompt as one PTY chunk.
// xterm 6.1.0-beta.304 can yield after parsing the echo, then replay that
// parsed prefix when a resize flushes the remaining queue. Keep the CRLF
// in the same write as the echo (fake-adapter-helper.test.ts).
let output = '';
for (const char of data) {
if (char === '\r' || char === '\n') {
const command = input; input = '';
this.sendOutput(id, `\r\n\x1b]633;E;${command}\x07\x1b]633;C\x07`);
if (/^(sleep|nano|vim)\b/.test(command)) { helper.busy = true; this.sendOutput(id, 'Demo process running. Ctrl+C stops it.\r\n'); continue; }
const output = command === 'git status' ? 'On branch main\r\nnothing to commit, working tree clean' : command.startsWith('echo ') ? command.slice(5) : command === 'pwd' ? helper.cwd : command ? `Demo shell: ${command}` : '';
if (output) this.sendOutput(id, output + '\r\n');
this.sendOutput(id, '\x1b]633;D;0\x07'); prompt();
} else if (char === '\x7f') { if (input) { input = input.slice(0, -1); this.sendOutput(id, '\b \b'); } }
else { input += char; this.sendOutput(id, char); }
output += `\r\n\x1b]633;E;${command}\x07\x1b]633;C\x07`;
if (/^(sleep|nano|vim)\b/.test(command)) { helper.busy = true; output += 'Demo process running. Ctrl+C stops it.\r\n'; continue; }
const result = command === 'git status' ? 'On branch main\r\nnothing to commit, working tree clean' : command.startsWith('echo ') ? command.slice(5) : command === 'pwd' ? helper.cwd : command ? `Demo shell: ${command}` : '';
if (result) output += result + '\r\n';
output += '\x1b]633;D;0\x07' + prompt();
} else if (char === '\x7f') { if (input) { input = input.slice(0, -1); output += '\b \b'; } }
else { input += char; output += char; }
}
if (output) this.sendOutput(id, output);
});
queueMicrotask(prompt);
queueMicrotask(() => this.sendOutput(id, prompt()));
}

private resolveScenario(id: string): FakeScenario | null {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/stories/HelperPlacement.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ async function openContext() {
void flushTerminal(helper.id).then(() => { written = true; });
await waitFor(() => expect(written).toBe(true), { timeout: 4000 });
await settleTerminals();
const input = terminal(helper.id);
const text = Array.from({ length: input.buffer.active.length }, (_, i) =>
input.buffer.active.getLine(i)?.translateToString(true) ?? '').join('');
expect(text.match(/git status/g)).toHaveLength(1);
}
function expectedSide({ layout, zoomed, cursor, sourceAtEnd }: Props) {
// Alone in the Wall, the helper avoids the cursor; beside a neighbor, it takes the neighbor's side.
Expand Down
Loading