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
+21 -3
View File
@@ -2,7 +2,8 @@
* @file panePaste.test.ts
* @description Pins where a pasted diff comment lands: the workspace's pane
* focused last, else any of its panes, never another workspace's (lane 1 is
* not lane 12), and nowhere once the pane unmounted.
* not lane 12), nowhere once the pane unmounted, and never into a pane that
* is not on screen (a hidden console behind a full-width Changes panel).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -16,7 +17,7 @@ describe("panePaste", () => {
const off12 = reg("agenthub-lane-12");
const off0 = reg("agenthub-lane-1");
const off2 = reg("agenthub-lane-1-2");
expect(pasteToLane(1, "a")).toBe(true);
expect(pasteToLane(1, "a")).toBe("agenthub-lane-1");
expect(got["agenthub-lane-1"]).toEqual(["a"]);
notePaneFocus("agenthub-lane-1-2");
pasteToLane(1, "b");
@@ -27,7 +28,24 @@ describe("panePaste", () => {
expect(got["agenthub-lane-12"]).toBeUndefined();
off0();
off2();
expect(pasteToLane(1, "d")).toBe(false);
expect(pasteToLane(1, "d")).toBeNull();
off12();
});
it("never pastes into a pane that is not visible", () => {
const got: string[] = [];
let shown = false;
const off = registerPane(
"agenthub-lane-5",
(t) => got.push(t),
() => shown
);
notePaneFocus("agenthub-lane-5");
expect(pasteToLane(5, "x")).toBeNull();
expect(got).toEqual([]);
shown = true;
expect(pasteToLane(5, "y")).toBe("agenthub-lane-5");
expect(got).toEqual(["y"]);
off();
});
});