feat(workspace): no double resume, server-picked tabs, terminal a11y

- A resume of a session a live pane already holds is refused (409
  ESESSIONLIVE with error.run_id) and the client opens that pane, pasting
  any typed message there; catalog tools refuse resumeSessionId
  (EUNSUPPORTED) instead of silently starting fresh.
- POST /api/lanes/:id/start takes slot: "auto": the server picks the free
  tab under the lane lock (components/lanes/startSession.ts on the client,
  kept out of WorkspaceHome so the sidebar does not pull react-markdown
  into the main bundle).
- Unsaved whole-file edits are guarded on every exit (Back, another file,
  tab switch, close, lane/session change, beforeunload).
- Closing an AI tab asks first; hiding a pane has its own icon. Tabs and
  pane headers show the session name, a state dot, rename by double-click.
- Terminal: Ctrl+M toggles Tab-moves-focus (no keyboard trap), Cmd+F /
  Ctrl+Shift+F find bar, clickable links (noopener), a write-only OSC 52
  clipboard, a Reconnecting overlay, debounced resizes, optional screen
  reader mode. Alt+1-4 / Alt+\ / Alt+W focus, split and hide panes.
- Changes panel: draggable splitter, wrapping git bar, history paging,
  disarming discard/undo/abort, inline diff comments pasted only into a
  visible pane; drag MIME guard on sidebar rows; IME-safe Enter; Show
  more in the sidebar and Recents; idle dots readable with sr-only text.
- Adds @xterm/addon-clipboard, @xterm/addon-search, @xterm/addon-web-links.
This commit is contained in:
2026-10-05 13:22:12 +07:00
parent 6715fbd971
commit efa417488a
43 changed files with 2252 additions and 507 deletions
+42 -4
View File
@@ -74,7 +74,7 @@
"changes.commentOn": "Comment on {{at}} — pasted into the pane you used last; nothing is sent until you press Enter there",
"changes.commentSend": "Paste into pane",
"changes.commentCancel": "Cancel",
"changes.commentNoPane": "No pane of this workspace is open — open its session first",
"changes.commentNoPane": "No terminal of this workspace is on screen — open its session (or narrow the Changes panel) first",
"changes.layout.split": "Side-by-side",
"changes.layout.unified": "Unified",
"changes.loading": "Reading the working copy…",
@@ -390,7 +390,7 @@
"panes.split.bottom": "Split down",
"panes.full": "4 panes is the most a workspace can show",
"panes.newTerminal": "New terminal",
"panes.close": "Close pane (its session keeps running as a tab)",
"panes.close": "Hide pane (its session keeps running as a tab)",
"panes.resize": "Resize panes",
"tabs.home": "Workspace home",
"sessionMenu.open": "Open",
@@ -401,9 +401,47 @@
"panes.show": "Show {{name}} here",
"panes.splitNew.right": "Split right with a new terminal",
"panes.splitNew.bottom": "Split down with a new terminal",
"panes.closeShort": "Close pane",
"panes.closeShort": "Hide pane",
"changes.copyPath": "Copy relative path",
"changes.viewCommit": "View commit",
"changes.copySha": "Copy commit SHA",
"changes.copySubject": "Copy commit message"
"changes.copySubject": "Copy commit message",
"replay.live": "This session is open in a tab — continue it there.",
"replay.noResume": "{{tool}} cannot resume a session by id, so this conversation is read-only here.",
"replay.newSession": "Start a new session",
"tabs.state.awaiting": "Waiting for you",
"tabs.state.error": "Error",
"tabs.state.running": "Running",
"tabs.renameHint": "Double-click to rename",
"tabs.stopTitle": "Stop “{{name}}”?",
"tabs.stopBody": "Closing the tab stops its session, even mid-turn. It can be resumed later from Recents.",
"tabs.stopConfirm": "Stop session",
"changes.loadMore": "Load more commits",
"changes.commentPasted": "Pasted into tab {{tab}} — press Enter there to send it",
"changes.splitter": "Resize the changes panel",
"tree.archiveLiveTitle": "Archive a live session?",
"tree.archiveLiveBody": "Its tab keeps running; archiving only hides it from this list. Unarchive it from the archived fold.",
"tree.state.awaiting": "waiting for you",
"tree.state.error": "error",
"tree.state.live": "live",
"tree.state.idle": "ended",
"tree.showMore_one": "Show {{count}} more",
"tree.showMore_other": "Show {{count}} more",
"home.contextPanel": "Workspace context",
"row.state.needsYou": "needs you",
"row.state.error": "error",
"row.state.running": "running",
"row.state.active": "active",
"row.state.idle": "idle",
"menu.stopTitle_one": "Stop the running session?",
"menu.stopTitle_other": "Stop {{count}} running sessions?",
"menu.stopBody": "Every tab of this workspace stops, even mid-turn. Its sessions can be resumed later from Recents.",
"gitBar.done.fetch": "Fetched",
"gitBar.done.pull": "Pulled",
"gitBar.done.push": "Pushed",
"gitBar.done.publish": "Published",
"runtime.listening": "listening",
"runtime.notListening": "not listening",
"replay.pasted": "That session was already open in a tab — your message is pasted there; press Enter to send it.",
"replay.notPasted": "That session is open in a tab, but your message could not be pasted there: {{text}}"
}
+12
View File
@@ -148,5 +148,17 @@
"imagesOnly": "Only images can be dropped here",
"remove": "Remove image",
"view": "View image"
},
"terminal": {
"ended": "[session ended, exit code {{code}}]",
"reconnecting": "Reconnecting… keystrokes are not reaching the session until the terminal is back.",
"keyboardHint": "Terminal. Press Ctrl+M to toggle whether Tab moves focus out of the terminal; Ctrl+Shift+F (Cmd+F on macOS) finds text.",
"tabMoves": "Tab moves focus — Ctrl+M to give Tab back to the terminal",
"selectHint": "Shift+drag to select text in the browser; a plain drag selects in tmux and copies on release.",
"dismissHint": "Dismiss",
"find": "Find in terminal",
"findPrev": "Previous match",
"findNext": "Next match",
"findClose": "Close find"
}
}