-
-
-
+
OrcaBot
Agentic AI Coding Agent Orchestration on the Web
diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico
deleted file mode 100644
index 718d6fea..00000000
Binary files a/frontend/src/app/favicon.ico and /dev/null differ
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index e20b8c02..137bc8b0 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -11,9 +11,9 @@
/* Foreground/text colors */
--foreground: #111827;
- --foreground-muted: #4b5563;
- --foreground-subtle: #6b7280;
- --foreground-disabled: #9ca3af;
+ --foreground-muted: #374151;
+ --foreground-subtle: #4b5563;
+ --foreground-disabled: #6b7280;
/* Border colors */
--border: #e5e7eb;
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index e74afd33..de2fda35 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -18,8 +18,14 @@ export const metadata: Metadata = {
description:
"A collaborative workspace for Agentic AI coding agent orchestration on the web.",
icons: {
- icon: "/favicon.ico",
+ icon: [
+ { url: "/favicon.ico", sizes: "any" },
+ { url: "/favicon.svg", type: "image/svg+xml" },
+ { url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
+ ],
+ apple: "/apple-touch-icon.png",
},
+ manifest: "/site.webmanifest",
};
export default function RootLayout({
diff --git a/frontend/src/components/blocks/TerminalBlock.test.tsx b/frontend/src/components/blocks/TerminalBlock.test.tsx
index b038081c..4ff7a031 100644
--- a/frontend/src/components/blocks/TerminalBlock.test.tsx
+++ b/frontend/src/components/blocks/TerminalBlock.test.tsx
@@ -115,7 +115,7 @@ describe("TerminalBlock owner controls", () => {
it("shows control button for the owner", () => {
renderTerminal("owner-1", "owner-1");
expect(screen.getByText("Owner: Owner")).toBeTruthy();
- expect(screen.getByText("Take Control")).toBeTruthy();
+ expect(screen.queryByText("Take Control")).toBeNull();
});
it("hides control button for non-owners", () => {
diff --git a/frontend/src/components/blocks/TerminalBlock.tsx b/frontend/src/components/blocks/TerminalBlock.tsx
index c739532f..e685f970 100644
--- a/frontend/src/components/blocks/TerminalBlock.tsx
+++ b/frontend/src/components/blocks/TerminalBlock.tsx
@@ -142,6 +142,7 @@ export function TerminalBlock({
const [session, setSession] = React.useState(
data.session || null
);
+ const autoControlRequestedRef = React.useRef(false);
const createdBrowserUrlsRef = React.useRef>(new Set());
const outputBufferRef = React.useRef("");
@@ -167,6 +168,7 @@ export function TerminalBlock({
setFileEntries({});
setFileLoading({});
setFileError(null);
+ autoControlRequestedRef.current = false;
}, [session?.id]);
const [isCreatingSession, setIsCreatingSession] = React.useState(false);
const [sessionError, setSessionError] = React.useState(null);
@@ -541,9 +543,9 @@ export function TerminalBlock({
const blockX = positionAbsoluteX * zoom + (overlay?.viewport.x ?? 0);
const blockY = positionAbsoluteY * zoom + (overlay?.viewport.y ?? 0);
- // Selected or dragging terminal gets high z-index, others use tracked order
+ // Terminals always sit above other blocks; order is tracked by z-index map.
const baseZIndex = overlay?.getZIndex(id) ?? 0;
- const zIndex = selected || dragging ? 9999 : baseZIndex;
+ const zIndex = baseZIndex + 10000;
// Track z-order so last-selected/dragged stays on top
const prevSelectedRef = React.useRef(false);
@@ -640,12 +642,25 @@ export function TerminalBlock({
);
terminalRef.current?.write("\r\n");
- // NOTE: Do NOT auto-take control here.
- // Turn-taking requires explicit user action - control must be requested/granted.
- // The server will broadcast who has control via the control_state message.
+ // Control is requested separately once connected.
}
}, [isConnected, session, isReady]);
+ React.useEffect(() => {
+ if (!isConnected) {
+ autoControlRequestedRef.current = false;
+ return;
+ }
+ if (!session || !isOwner || isAgentRunning || turnTaking.isController) {
+ return;
+ }
+ if (autoControlRequestedRef.current) {
+ return;
+ }
+ autoControlRequestedRef.current = true;
+ terminalActions.takeControl();
+ }, [isConnected, session, isOwner, isAgentRunning, turnTaking.isController, terminalActions]);
+
React.useEffect(() => {
if (!session || !terminalRef.current) {
return;
@@ -1119,16 +1134,14 @@ export function TerminalBlock({
)}
{/* Input blocked overlay (when connected but can't type) */}
- {session && isConnected && !canType && isOwner && (
+ {session && isConnected && !canType && (isAgentRunning || !isOwner) && (
{isAgentRunning
? "Agent is running"
- : isOwner
- ? "Click below to request control"
- : "View-only session"}
+ : "View-only session"}
@@ -1157,19 +1170,6 @@ export function TerminalBlock({
)}
- {session && isConnected && !turnTaking.isController && !isAgentRunning && isOwner && (
-
- )}
-
{session && isConnected && turnTaking.isController && (
diff --git a/frontend/src/components/canvas/Canvas.tsx b/frontend/src/components/canvas/Canvas.tsx
index 184ce162..e5a0c452 100644
--- a/frontend/src/components/canvas/Canvas.tsx
+++ b/frontend/src/components/canvas/Canvas.tsx
@@ -15,6 +15,7 @@ import {
type EdgeChange,
type OnNodesChange,
type OnNodeDrag,
+ type OnNodeDragStart,
type NodeChange,
type ReactFlowInstance,
} from "@xyflow/react";
@@ -115,28 +116,23 @@ export function Canvas({
const [viewport, setViewport] = React.useState({ x: 0, y: 0, zoom: 1 });
const { zIndexVersion, bringToFront, getZIndex } = useTerminalZIndex();
const terminalRefs = React.useRef