refactor(terminal): seams for a second terminal pane (input socket map, targeted links, copy, paste)

No behavior change. Prepares the split pane's second terminal (and later
grid tiles) to share what today only the primary terminal has:

- _inputSocketFor/_registerInputSocket/_unregisterInputSocket: the
  exactly-once input queue, its ACK handling and the redelivery sweep now
  deliver over any registered socket bound to a session, not only
  this._ws. ACKs are routed by the receiving socket's session; silence is
  judged per socket; a stale handle cannot unregister its replacement.
- registerFilePathLinkProvider, cleanedTerminalSelection,
  copyTerminalSelection and _handleImagePaste take an optional target
  terminal and session (defaults: the primary pane).
- _focusedPane() is the one place to ask which pane the keyboard is in
  (primary only, for now); _forEachTile() replaces the _splitPane special
  cases in the font, family, weight, skin and resize paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 08:55:05 +02:00
parent f1537a7887
commit ead3d34411
7 changed files with 473 additions and 63 deletions
+12 -6
View File
@@ -50,8 +50,13 @@ Object.assign(CodemanApp.prototype, {
// Called from customKeyEventHandler in terminal-ui.js on Ctrl+V keydown.
// Creates a hidden paste trap, lets the browser paste into it, then inspects
// the result for images. Works on plain HTTP (no Clipboard API needed).
_handleImagePaste() {
// `target` names the terminal the Ctrl+V came from and its session; both
// default to the primary pane. A second terminal (the split pane) passes its
// own, so text pastes into THAT xterm and images upload to THAT session.
_handleImagePaste(target = {}) {
const self = this;
const terminal = target.terminal || this.terminal;
const sessionId = target.sessionId || this.activeSessionId;
// Create a hidden contenteditable div to receive the paste
const trap = document.createElement('div');
@@ -93,11 +98,11 @@ Object.assign(CodemanApp.prototype, {
setTimeout(function() {
if (trap.parentNode) trap.parentNode.removeChild(trap);
// Refocus the terminal
if (self.terminal) self.terminal.focus();
if (terminal) terminal.focus();
}, 0);
if (imageFiles.length > 0) {
self._uploadAndInsertImages(imageFiles);
self._uploadAndInsertImages(imageFiles, { sessionId: sessionId });
} else {
// No image -- route text through xterm's paste() so bracketed-paste
// markers (CSI 200~ ... CSI 201~) survive when the inner application
@@ -106,7 +111,7 @@ Object.assign(CodemanApp.prototype, {
// indistinguishable from typed input, weakening the CLI's
// prompt-injection defenses.
var text = e.clipboardData ? e.clipboardData.getData('text/plain') : '';
if (text && self.terminal) self.terminal.paste(text);
if (text && terminal) terminal.paste(text);
}
});
@@ -126,9 +131,10 @@ Object.assign(CodemanApp.prototype, {
/** Upload a batch and normally insert its paths into the active terminal.
* The prompt composer passes `{ insert: false }` so it can put those paths
* into its textarea instead. Returns successful paths in selection order. */
* into its textarea instead. `options.sessionId` names the session to upload
* to (default: the active one). Returns successful paths in selection order. */
async _uploadAndInsertImages(fileList, options = {}) {
const sessionId = this.activeSessionId;
const sessionId = options.sessionId || this.activeSessionId;
if (!sessionId) return [];
let files = Array.from(fileList || []);