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:
@@ -168,11 +168,17 @@ export function WorkspaceTree() {
|
||||
}
|
||||
};
|
||||
|
||||
const onMenuAction = (lane: Lane, action: WorkspaceMenuAction) => {
|
||||
const onMenuAction = (lane: Lane, action: WorkspaceMenuAction, runId?: string) => {
|
||||
if (action === "rename") setRenamingId(lane.id);
|
||||
else if (action === "new-here") openAdd(lane.source_repo || lane.cwd);
|
||||
else if (action === "reset" || action === "remove") setDestructive({ lane, action });
|
||||
else if (action === "started") select(lane.id);
|
||||
// Start opens the session it started, as the home's menu does.
|
||||
else if (action === "started")
|
||||
navigate(
|
||||
runId
|
||||
? `/workspace?lane=${lane.id}&run=${encodeURIComponent(runId)}`
|
||||
: `/workspace?lane=${lane.id}`
|
||||
);
|
||||
};
|
||||
|
||||
const confirmDestructive = async (body?: { expect?: unknown; force?: boolean }) => {
|
||||
@@ -192,7 +198,8 @@ export function WorkspaceTree() {
|
||||
}
|
||||
};
|
||||
|
||||
const headBtn = "rounded p-1 text-fg-muted hover:bg-surface-3 hover:text-fg-primary";
|
||||
// ≥24px targets (WCAG 2.5.8); each carries an aria-label.
|
||||
const headBtn = "btn-icon";
|
||||
return (
|
||||
<section data-testid="workspace-tree" className="mt-3 border-t border-border pt-3">
|
||||
<div className="mb-1 flex items-center gap-1 px-2">
|
||||
@@ -205,7 +212,7 @@ export function WorkspaceTree() {
|
||||
onChange={(e) => writeProjectTreePrefs({ tool: e.target.value || null })}
|
||||
aria-label={t("tree.toolFilter")}
|
||||
title={t("tree.toolFilter")}
|
||||
className="max-w-[6.5rem] rounded border border-border bg-surface-1 px-1 py-0.5 text-[11px] text-fg-secondary"
|
||||
className="select max-w-[6.5rem] !rounded !py-0.5 !pl-1 !pr-5 !text-[11px] text-fg-secondary"
|
||||
>
|
||||
<option value="">{t("tree.allTools")}</option>
|
||||
{tools.map((id) => (
|
||||
@@ -307,7 +314,7 @@ export function WorkspaceTree() {
|
||||
x={menu.x}
|
||||
y={menu.y}
|
||||
onClose={() => setMenu(null)}
|
||||
onAction={(a) => onMenuAction(menu.lane, a)}
|
||||
onAction={(a, runId) => onMenuAction(menu.lane, a, runId)}
|
||||
/>
|
||||
)}
|
||||
{destructive && (
|
||||
|
||||
Reference in New Issue
Block a user