From 73a1c1afc1ba47934047830526d2e020730b146c Mon Sep 17 00:00:00 2001 From: Hally Maschine Date: Tue, 31 Mar 2026 16:49:37 -0700 Subject: [PATCH] cmd/sgai: add pending-response shortcut --- cmd/sgai/webapp/src/App.tsx | 51 +++- .../webapp/src/__tests__/AppShortcut.test.tsx | 275 ++++++++++++++++++ cmd/sgai/webapp/src/lib/pending-response.ts | 37 +++ cmd/sgai/webapp/src/pages/Dashboard.tsx | 32 +- 4 files changed, 364 insertions(+), 31 deletions(-) create mode 100644 cmd/sgai/webapp/src/__tests__/AppShortcut.test.tsx create mode 100644 cmd/sgai/webapp/src/lib/pending-response.ts diff --git a/cmd/sgai/webapp/src/App.tsx b/cmd/sgai/webapp/src/App.tsx index 18385c0..803076b 100644 --- a/cmd/sgai/webapp/src/App.tsx +++ b/cmd/sgai/webapp/src/App.tsx @@ -1,8 +1,56 @@ -import { Outlet } from "react-router"; +import { useEffect, useMemo } from "react"; +import { Outlet, useNavigate } from "react-router"; import { ConnectionStatusBanner } from "./components/ConnectionStatusBanner"; import { NotificationPermissionBar } from "./components/NotificationPermissionBar"; import { TooltipProvider } from "./components/ui/tooltip"; import { useNotifications } from "./hooks/useNotifications"; +import { useFactoryState } from "./lib/factory-state"; +import { getFirstPendingResponseTarget } from "./lib/pending-response"; +import { buildWorkspacePath } from "./lib/workspace-identity"; + +function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof Element)) { + return false; + } + + return Boolean( + target.closest("input, textarea, select, .monaco-editor") + || target.closest("[contenteditable]:not([contenteditable='false'])"), + ); +} + +function PendingResponseShortcut() { + const navigate = useNavigate(); + const { workspaces } = useFactoryState(); + const firstPendingResponseTarget = useMemo( + () => getFirstPendingResponseTarget(workspaces), + [workspaces], + ); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.defaultPrevented + || event.key.toLowerCase() !== "i" + || !(event.metaKey || event.ctrlKey) + || event.altKey + || event.shiftKey + || isEditableTarget(event.target) + || !firstPendingResponseTarget + ) { + return; + } + + event.preventDefault(); + navigate(buildWorkspacePath(firstPendingResponseTarget, "respond")); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [firstPendingResponseTarget, navigate]); + + return null; +} export function App() { useNotifications(); @@ -11,6 +59,7 @@ export function App() { +
diff --git a/cmd/sgai/webapp/src/__tests__/AppShortcut.test.tsx b/cmd/sgai/webapp/src/__tests__/AppShortcut.test.tsx new file mode 100644 index 0000000..5b45da9 --- /dev/null +++ b/cmd/sgai/webapp/src/__tests__/AppShortcut.test.tsx @@ -0,0 +1,275 @@ +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route, Routes, useLocation } from "react-router"; +import type { ReactNode } from "react"; +import * as factoryStateModule from "@/lib/factory-state"; +import type { ApiForkEntry, ApiWorkspaceEntry } from "@/types"; +import { App } from "../App"; + +function createMockFork(overrides: Partial = {}): ApiForkEntry { + return { + name: "pending-fork", + dir: "/workspaces/pending-fork", + running: false, + needsInput: false, + inProgress: false, + pinned: false, + title: "", + ...overrides, + }; +} + +function createMockWorkspace(overrides: Partial = {}): ApiWorkspaceEntry { + return { + name: "workspace", + dir: "/workspaces/workspace", + running: false, + needsInput: false, + inProgress: false, + pinned: false, + isRoot: false, + isFork: false, + title: "", + computedTitle: "", + status: "", + badgeClass: "", + badgeText: "", + hasSgai: true, + hasEditedGoal: false, + interactiveAuto: false, + continuousMode: false, + currentAgent: "", + currentModel: "", + task: "", + goalContent: "", + rawGoalContent: "", + pmContent: "", + hasProjectMgmt: false, + svgHash: "", + totalExecTime: "", + latestProgress: "", + humanMessage: "", + agentSequence: [], + cost: { + totalCost: 0, + dollars: { + input: 0, + output: 0, + reasoning: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + totalTokens: { + input: 0, + output: 0, + reasoning: 0, + cacheRead: 0, + cacheWrite: 0, + }, + byAgent: [], + }, + events: [], + messages: [], + projectTodos: [], + agentTodos: [], + log: [], + ...overrides, + }; +} + +function RouteProbe({ children }: { children: ReactNode }) { + const location = useLocation(); + + return ( +
+
{location.pathname}
+ {children} +
+ ); +} + +function renderApp(initialRoute = "/") { + return render( + + + }> + + + + )} + /> + + +