feat(split): clickable file paths and image paste in Pane B

- Pane B registers the primary pane's file-path/URL link provider on
  its own terminal, so a path an agent prints there opens the file
  preview or log viewer for Pane B's session (it was plain text).
- Ctrl+V/Cmd+V in Pane B goes through the primary pane's paste trap,
  aimed at Pane B: a pasted image uploads to Pane B's session and its
  path is typed there; text keeps its bracketed-paste markers. xterm's
  default handled text only.

Tests pin both the tile wiring and the targeted link provider itself
(registered on the target terminal, opening against the target's
session at click time, the primary's tap-path provider untouched).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 10:20:25 +02:00
parent a54ad81684
commit fbd69e62aa
3 changed files with 215 additions and 4 deletions
+44 -1
View File
@@ -90,7 +90,10 @@ class FakeTerminal {
onData(cb: (data: string) => void) {
this.dataCb = cb;
}
attachCustomKeyEventHandler() {}
keyHandler: ((ev: Record<string, unknown>) => boolean) | null = null;
attachCustomKeyEventHandler(fn: (ev: Record<string, unknown>) => boolean) {
this.keyHandler = fn;
}
registerLinkProvider() {}
writes: string[] = [];
write(data: string, cb?: () => void) {
@@ -177,6 +180,8 @@ function makeApp(): App {
app._updateConnectionIndicator = vi.fn();
app.markIdleAlertSeen = vi.fn();
app.showToast = vi.fn();
// The key handler's chord gates read the shortcut registry, which reads these.
app.loadAppSettingsFromStorage = () => ({});
app._ws = null;
app._wsSessionId = null;
app._estimateReplayRows = (text: string) => text.split('\n').length;
@@ -597,3 +602,41 @@ describe('TerminalTile geometry (#464: the pane and its PTY never disagree)', ()
expect(term.resizes.length).toBe(before);
});
});
describe('TerminalTile links and paste follow THIS pane', () => {
it('registers the shared link provider on its own terminal, resolving its own session', async () => {
const app = makeApp();
const register = vi.fn();
app.registerFilePathLinkProvider = register;
const { term } = await connectTile(app);
expect(register).toHaveBeenCalledTimes(1);
const target = register.mock.calls[0][0] as { terminal: unknown; getSessionId: () => string };
expect(target.terminal).toBe(term);
expect(target.getSessionId()).toBe('s-tile');
});
it("routes Ctrl+V into the paste trap with this pane's terminal and session", async () => {
const app = makeApp();
const paste = vi.fn();
app._handleImagePaste = paste;
const { term } = await connectTile(app);
const handled = term.keyHandler!({ type: 'keydown', key: 'v', ctrlKey: true, code: 'KeyV' });
expect(handled).toBe(false);
expect(paste).toHaveBeenCalledWith({ terminal: term, sessionId: 's-tile' });
});
it('leaves Ctrl+Shift+V (voice input) out of the paste trap', async () => {
const app = makeApp();
const paste = vi.fn();
app._handleImagePaste = paste;
const { term } = await connectTile(app);
term.keyHandler!({ type: 'keydown', key: 'V', ctrlKey: true, shiftKey: true, code: 'KeyV' });
expect(paste).not.toHaveBeenCalled();
});
});