Improve Foundry auth and task flows (#240)

This commit is contained in:
Nathan Flurry 2026-03-11 18:13:31 -07:00 committed by GitHub
parent d75e8c31d1
commit dbc2ff0682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 621 additions and 137 deletions

View file

@ -25,7 +25,7 @@ import {
type ModelId,
} from "./mock-layout/view-model";
import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app";
import { taskWorkbenchClient } from "../lib/workbench";
import { getTaskWorkbenchClient } from "../lib/workbench";
function firstAgentTabId(task: Task): string | null {
return task.tabs[0]?.id ?? null;
@ -61,6 +61,7 @@ function sanitizeActiveTabId(task: Task, tabId: string | null | undefined, openD
}
const TranscriptPanel = memo(function TranscriptPanel({
taskWorkbenchClient,
task,
activeTabId,
lastAgentTabId,
@ -70,6 +71,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
onSetLastAgentTabId,
onSetOpenDiffs,
}: {
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
task: Task;
activeTabId: string | null;
lastAgentTabId: string | null;
@ -858,6 +860,7 @@ function MockWorkspaceOrgBar() {
export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: MockLayoutProps) {
const navigate = useNavigate();
const taskWorkbenchClient = useMemo(() => getTaskWorkbenchClient(workspaceId), [workspaceId]);
const viewModel = useSyncExternalStore(
taskWorkbenchClient.subscribe.bind(taskWorkbenchClient),
taskWorkbenchClient.getSnapshot.bind(taskWorkbenchClient),
@ -887,10 +890,12 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
const [activeTabIdByTask, setActiveTabIdByTask] = useState<Record<string, string | null>>({});
const [lastAgentTabIdByTask, setLastAgentTabIdByTask] = useState<Record<string, string | null>>({});
const [openDiffsByTask, setOpenDiffsByTask] = useState<Record<string, string[]>>({});
const [selectedNewTaskRepoId, setSelectedNewTaskRepoId] = useState("");
const [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH));
const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH));
const leftWidthRef = useRef(leftWidth);
const rightWidthRef = useRef(rightWidth);
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
useEffect(() => {
leftWidthRef.current = leftWidth;
@ -1001,9 +1006,49 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
});
}, [activeTask, lastAgentTabIdByTask, selectedSessionId, syncRouteSession]);
useEffect(() => {
if (selectedNewTaskRepoId && viewModel.repos.some((repo) => repo.id === selectedNewTaskRepoId)) {
return;
}
const fallbackRepoId =
activeTask?.repoId && viewModel.repos.some((repo) => repo.id === activeTask.repoId) ? activeTask.repoId : (viewModel.repos[0]?.id ?? "");
if (fallbackRepoId !== selectedNewTaskRepoId) {
setSelectedNewTaskRepoId(fallbackRepoId);
}
}, [activeTask?.repoId, selectedNewTaskRepoId, viewModel.repos]);
useEffect(() => {
if (!activeTask) {
return;
}
if (activeTask.tabs.length > 0) {
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
return;
}
if (selectedSessionId) {
return;
}
if (autoCreatingSessionForTaskRef.current.has(activeTask.id)) {
return;
}
autoCreatingSessionForTaskRef.current.add(activeTask.id);
void (async () => {
try {
const { tabId } = await taskWorkbenchClient.addTab({ taskId: activeTask.id });
syncRouteSession(activeTask.id, tabId, true);
} catch (error) {
console.error("failed to auto-create workbench session", error);
} finally {
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
}
})();
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
const createTask = useCallback(() => {
void (async () => {
const repoId = activeTask?.repoId ?? viewModel.repos[0]?.id ?? "";
const repoId = selectedNewTaskRepoId;
if (!repoId) {
throw new Error("Cannot create a task without an available repo");
}
@ -1023,7 +1068,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
search: { sessionId: tabId ?? undefined },
});
})();
}, [activeTask?.repoId, navigate, viewModel.repos, workspaceId]);
}, [navigate, selectedNewTaskRepoId, workspaceId]);
const openDiffTab = useCallback(
(path: string) => {
@ -1158,9 +1203,12 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<Sidebar
projects={projects}
newTaskRepos={viewModel.repos}
selectedNewTaskRepoId={selectedNewTaskRepoId}
activeId=""
onSelect={selectTask}
onCreate={createTask}
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
onMarkUnread={markTaskUnread}
onRenameTask={renameTask}
onRenameBranch={renameBranch}
@ -1190,10 +1238,32 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first task</h2>
<p style={{ margin: 0, opacity: 0.75 }}>
{viewModel.repos.length > 0
? "Start from the sidebar to create a task on the first available repo."
: "No repos are available in this workspace yet."}
{viewModel.repos.length > 0 ? "Choose a repo, then create a task." : "No repos are available in this workspace yet."}
</p>
{viewModel.repos.length > 0 ? (
<label style={{ display: "flex", flexDirection: "column", gap: "6px", textAlign: "left" }}>
<span style={{ fontSize: "11px", fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase", opacity: 0.7 }}>Repo</span>
<select
value={selectedNewTaskRepoId}
onChange={(event) => {
setSelectedNewTaskRepoId(event.currentTarget.value);
}}
style={{
borderRadius: "10px",
border: "1px solid rgba(255,255,255,0.12)",
padding: "10px 12px",
background: "rgba(255,255,255,0.05)",
color: "#f4f4f5",
}}
>
{viewModel.repos.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.label}
</option>
))}
</select>
</label>
) : null}
<button
type="button"
onClick={createTask}
@ -1231,9 +1301,12 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<Sidebar
projects={projects}
newTaskRepos={viewModel.repos}
selectedNewTaskRepoId={selectedNewTaskRepoId}
activeId={activeTask.id}
onSelect={selectTask}
onCreate={createTask}
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
onMarkUnread={markTaskUnread}
onRenameTask={renameTask}
onRenameBranch={renameBranch}
@ -1243,6 +1316,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
<PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} />
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
<TranscriptPanel
taskWorkbenchClient={taskWorkbenchClient}
task={activeTask}
activeTabId={activeTabId}
lastAgentTabId={lastAgentTabId}

View file

@ -24,18 +24,24 @@ function projectIconColor(label: string): string {
export const Sidebar = memo(function Sidebar({
projects,
newTaskRepos,
selectedNewTaskRepoId,
activeId,
onSelect,
onCreate,
onSelectNewTaskRepo,
onMarkUnread,
onRenameTask,
onRenameBranch,
onReorderProjects,
}: {
projects: ProjectSection[];
newTaskRepos: Array<{ id: string; label: string }>;
selectedNewTaskRepoId: string;
activeId: string;
onSelect: (id: string) => void;
onCreate: () => void;
onSelectNewTaskRepo: (repoId: string) => void;
onMarkUnread: (id: string) => void;
onRenameTask: (id: string) => void;
onRenameBranch: (id: string) => void;
@ -68,28 +74,69 @@ export const Sidebar = memo(function Sidebar({
<div
role="button"
tabIndex={0}
onClick={onCreate}
aria-disabled={newTaskRepos.length === 0}
onClick={() => {
if (newTaskRepos.length === 0) {
return;
}
onCreate();
}}
onKeyDown={(event) => {
if (newTaskRepos.length === 0) {
return;
}
if (event.key === "Enter" || event.key === " ") onCreate();
}}
className={css({
width: "26px",
height: "26px",
borderRadius: "8px",
backgroundColor: "rgba(255, 255, 255, 0.12)",
backgroundColor: newTaskRepos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.06)",
color: "#e4e4e7",
cursor: "pointer",
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "background 200ms ease",
flexShrink: 0,
":hover": { backgroundColor: "rgba(255, 255, 255, 0.20)" },
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
":hover": newTaskRepos.length > 0 ? { backgroundColor: "rgba(255, 255, 255, 0.20)" } : undefined,
})}
>
<Plus size={14} style={{ display: "block" }} />
</div>
</PanelHeaderBar>
<div className={css({ padding: "0 8px 8px", display: "flex", flexDirection: "column", gap: "6px" })}>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ textTransform: "uppercase", letterSpacing: "0.04em" }}>
Repo
</LabelXSmall>
<select
value={selectedNewTaskRepoId}
disabled={newTaskRepos.length === 0}
onChange={(event) => {
onSelectNewTaskRepo(event.currentTarget.value);
}}
className={css({
width: "100%",
borderRadius: "8px",
border: "1px solid rgba(255, 255, 255, 0.10)",
backgroundColor: "rgba(255, 255, 255, 0.05)",
color: "#f4f4f5",
fontSize: "12px",
padding: "8px 10px",
outline: "none",
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
})}
>
{newTaskRepos.length === 0 ? <option value="">No repos available</option> : null}
{newTaskRepos.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.label}
</option>
))}
</select>
</div>
<ScrollBody>
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
{projects.map((project, projectIndex) => {