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:
@@ -7,8 +7,9 @@
|
||||
* (`forkSession: true` → `claude --resume <id> --fork-session`), leaving the
|
||||
* original untouched, and is offered even while the original has a live pane. A
|
||||
* session that still has a live pane offers that pane instead. A resume
|
||||
* starts in the workspace's first free pane slot (`POST /api/lanes/:id/start
|
||||
* {slot, resumeSessionId}`, panes re-read first) and opens it — offered only
|
||||
* starts in the workspace's first free pane slot, picked by the server
|
||||
* (`POST /api/lanes/:id/start {slot: "auto", resumeSessionId}`, panes re-read
|
||||
* first) and opens it — offered only
|
||||
* for a session whose folder IS the workspace's, because the pane starts in
|
||||
* the workspace's folder and Claude finds a session by the folder it ran in.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -22,7 +23,7 @@ import { api, type RunHandle } from "../../lib/api";
|
||||
import type { Lane, Session } from "../../lib/types";
|
||||
import { useLanes } from "../../hooks/useLanes";
|
||||
import { ConfirmModal } from "../ConfirmModal";
|
||||
import { freeSlot, paneRunId } from "./WorkspaceHome";
|
||||
import { startInFreeTabDetailed } from "./startSession";
|
||||
import { liveRunFor, sessionWorktreeName } from "./WorkspaceSessionList";
|
||||
|
||||
/** The workspace of this tool whose folder holds `cwd` most deeply — the
|
||||
@@ -81,8 +82,8 @@ export function SessionResumeButton({
|
||||
(same(session.cwd, lane.cwd) ||
|
||||
sessionWorktreeName(lane, { cwd: session.cwd } as Session) !== null);
|
||||
if (!run && !resumable) return null;
|
||||
const open = (runId: string) =>
|
||||
navigate(`/workspace?lane=${lane.id}&run=${encodeURIComponent(runId)}`);
|
||||
const open = (runId: string, laneId = lane.id) =>
|
||||
navigate(`/workspace?lane=${laneId}&run=${encodeURIComponent(runId)}`);
|
||||
|
||||
const resume = async (fork: boolean) => {
|
||||
// Busy first: the Confirm button is disabled only through it, and the
|
||||
@@ -99,17 +100,12 @@ export function SessionResumeButton({
|
||||
setRuns(fresh);
|
||||
const live = liveRunFor(lane, fresh, session.id);
|
||||
if (live && !fork) return open(live.id);
|
||||
const slot = freeSlot(lane.id, fresh);
|
||||
if (slot === null) {
|
||||
setError(t("home.allBusy"));
|
||||
return;
|
||||
}
|
||||
const r = await api.lanes.action(lane.id, "start", {
|
||||
slot,
|
||||
// A pane of another (nested) workspace may hold it: open it there.
|
||||
const r = await startInFreeTabDetailed(lane.id, {
|
||||
resumeSessionId: session.id,
|
||||
...(fork ? { forkSession: true } : {}),
|
||||
});
|
||||
open(r.run_id ?? paneRunId(lane.id, slot));
|
||||
open(r.runId, r.laneId);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user