Files
headquarter/apps/web/src/components/start-tool-fab.tsx
T
alex a0cfbbc2d2 fix: container mount permissions, terminal shift, ESC capture
Bug 1 — in-container repo mounting:
- docker-compose.yml: added /data/working-copies:/data/working-copies mount
  to API container so workspace dirs are visible on host filesystem
- Dockerfile: create /data/working-copies dir in image

Bug 2 — /home/user not writable:
- workspace_manager.py: chmod 777 workspace dirs + 666 files after clone
  and after sync, so any container user can write
- manifest_compiler.py: explicit mkdir + chown + chmod 755 for home dir
  in generated Dockerfile

Bug 3 — terminal text shifts left on typing:
- terminal.tsx: removed manual term.refresh() after fit (caused reflow)
- Track lastSentCols/lastSentRows and only send resize when dimensions
  actually changed, preventing resize feedback loops

Bug 4 — ESC key captured by terminal:
- terminal.tsx: attachCustomKeyEventHandler allows ESC to propagate to
  browser when not in alternate buffer (vim/tmux), so modals/navigation
  work; ESC still sent to PTY when in vim/tmux alternate screen

Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces (9 passed)
2026-06-01 22:36:31 +02:00

119 lines
3.0 KiB
TypeScript

/** Floating action button to start a tool from any page. */
import { useState } from "react";
import { Icon } from "./icon";
import { ToolStarter } from "./tool-starter";
import type { Workspace } from "../types/workspace";
import { listAllWorkspaces } from "../api/workspaces";
export function StartToolFAB() {
const [open, setOpen] = useState(false);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [workspacesLoading, setWorkspacesLoading] = useState(false);
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
null,
);
const handleOpen = async () => {
setOpen(true);
setWorkspacesLoading(true);
try {
const data = await listAllWorkspaces();
setWorkspaces(data);
} catch {
// ignore
} finally {
setWorkspacesLoading(false);
}
};
const handleClose = () => {
setOpen(false);
setSelectedWorkspace(null);
};
return (
<>
<button
className="start-tool-fab"
onClick={handleOpen}
title="Start a new tool"
type="button"
aria-label="Start a new tool"
>
<Icon name="play" size="md" />
</button>
{open && (
<div className="modal-overlay" onClick={handleClose}>
<div
className="modal-content start-tool-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>Start Tool</h3>
<button
className="ghost-button small"
onClick={handleClose}
type="button"
aria-label="Close"
>
<Icon name="close" size="sm" />
</button>
</div>
{workspacesLoading ? (
<p className="muted">Loading workspaces...</p>
) : workspaces.length === 0 ? (
<p className="muted">
No workspaces yet.{" "}
<a href="/workspaces">Create a workspace first</a>.
</p>
) : !selectedWorkspace ? (
<div className="form-group">
<label htmlFor="fab-workspace-select">Select a workspace</label>
<select
id="fab-workspace-select"
value=""
onChange={(e) => {
const ws = workspaces.find((w) => w.id === e.target.value);
if (ws) setSelectedWorkspace(ws);
}}
>
<option value="">Choose a workspace...</option>
{workspaces.map((ws) => (
<option key={ws.id} value={ws.id}>
{ws.project_name} / {ws.repo_name} / {ws.name}
</option>
))}
</select>
</div>
) : (
<>
<div className="tool-starter-header">
<h4>
{selectedWorkspace.project_name} /{" "}
{selectedWorkspace.repo_name} / {selectedWorkspace.name}
</h4>
<button
className="ghost-button small"
onClick={() => setSelectedWorkspace(null)}
type="button"
>
Change
</button>
</div>
<ToolStarter
workspace={selectedWorkspace}
onStarted={handleClose}
onCancel={() => setSelectedWorkspace(null)}
/>
</>
)}
</div>
</div>
)}
</>
);
}