mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 18:03:56 +00:00
Fix Foundry UI bugs: org names, sessions, and repo selection (#250)
* Fix Foundry auth: migrate to Better Auth adapter, fix access token retrieval - Remove @ts-nocheck from better-auth.ts, auth-user/index.ts, app-shell.ts and fix all type errors - Fix getAccessTokenForSession: read GitHub token directly from account record instead of calling Better Auth's internal /get-access-token endpoint which returns 403 on server-side calls - Re-implement workspaceAuth helper functions (workspaceAuthColumn, normalizeAuthValue, workspaceAuthClause, workspaceAuthWhere) that were accidentally deleted - Remove all retry logic (withRetries, isRetryableAppActorError) - Implement CORS origin allowlist from configured environment - Document cachedAppWorkspace singleton pattern - Add inline org sync fallback in buildAppSnapshot for post-OAuth flow - Add no-retry rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add Foundry dev panel from fix-git-data branch Port the dev panel component that was left out when PR #243 was replaced by PR #247. Adapted to remove runtime/mock-debug references that don't exist on the current branch. - Toggle with Shift+D, persists visibility to localStorage - Shows context, session, GitHub sync status sections - Dev-only (import.meta.env.DEV) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add full Docker image defaults, fix actor deadlocks, and improve dev experience - Add Dockerfile.full and --all flag to install-agent CLI for pre-built images - Centralize Docker image constant (FULL_IMAGE) pinned to 0.3.1-full - Remove examples/shared/Dockerfile{,.dev} and daytona snapshot example - Expand Docker docs with full runnable Dockerfile - Fix self-deadlock in createWorkbenchSession (fire-and-forget provisioning) - Audit and convert 12 task actions from wait:true to wait:false - Add bun --hot for dev backend hot reload - Remove --force from pnpm install in dev Dockerfile for faster startup - Add env_file support to compose.dev.yaml for automatic credential loading - Add mock frontend compose config and dev panel - Update CLAUDE.md with wait:true policy and dev environment setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * WIP: async action fixes and interest manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Foundry UI bugs: org names, hanging sessions, and wrong repo creation - Fix org display name using GitHub description instead of name field - Fix createWorkbenchSession hanging when sandbox is provisioning - Fix auto-session creation retry storm on errors - Fix task creation using wrong repo due to React state race conditions - Remove Bun hot-reload from backend Dockerfile (causes port drift) - Add GitHub sync/install status to dev panel Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58c54156f1
commit
d8b8b49f37
88 changed files with 9252 additions and 1933 deletions
379
foundry/packages/frontend/src/components/dev-panel.tsx
Normal file
379
foundry/packages/frontend/src/components/dev-panel.tsx
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { useFoundryTokens } from "../app/theme";
|
||||
import { isMockFrontendClient } from "../lib/env";
|
||||
import type { FoundryOrganization, TaskWorkbenchSnapshot, WorkbenchTask } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
interface DevPanelProps {
|
||||
workspaceId: string;
|
||||
snapshot: TaskWorkbenchSnapshot;
|
||||
organization?: FoundryOrganization | null;
|
||||
}
|
||||
|
||||
interface TopicInfo {
|
||||
label: string;
|
||||
key: string;
|
||||
listenerCount: number;
|
||||
hasConnection: boolean;
|
||||
lastRefresh: number | null;
|
||||
}
|
||||
|
||||
function timeAgo(ts: number | null): string {
|
||||
if (!ts) return "never";
|
||||
const seconds = Math.floor((Date.now() - ts) / 1000);
|
||||
if (seconds < 5) return "now";
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
return `${Math.floor(minutes / 60)}h`;
|
||||
}
|
||||
|
||||
function taskStatusLabel(task: WorkbenchTask): string {
|
||||
if (task.status === "archived") return "archived";
|
||||
const hasRunning = task.tabs?.some((tab) => tab.status === "running");
|
||||
if (hasRunning) return "running";
|
||||
return task.status ?? "idle";
|
||||
}
|
||||
|
||||
function statusColor(status: string, t: ReturnType<typeof useFoundryTokens>): string {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return t.statusSuccess;
|
||||
case "archived":
|
||||
return t.textMuted;
|
||||
case "error":
|
||||
case "failed":
|
||||
return t.statusError;
|
||||
default:
|
||||
return t.textTertiary;
|
||||
}
|
||||
}
|
||||
|
||||
function syncStatusColor(status: string, t: ReturnType<typeof useFoundryTokens>): string {
|
||||
switch (status) {
|
||||
case "synced":
|
||||
return t.statusSuccess;
|
||||
case "syncing":
|
||||
case "pending":
|
||||
return t.statusWarning;
|
||||
case "error":
|
||||
return t.statusError;
|
||||
default:
|
||||
return t.textMuted;
|
||||
}
|
||||
}
|
||||
|
||||
function installStatusColor(status: string, t: ReturnType<typeof useFoundryTokens>): string {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return t.statusSuccess;
|
||||
case "install_required":
|
||||
return t.statusWarning;
|
||||
case "reconnect_required":
|
||||
return t.statusError;
|
||||
default:
|
||||
return t.textMuted;
|
||||
}
|
||||
}
|
||||
|
||||
export const DevPanel = memo(function DevPanel({ workspaceId, snapshot, organization }: DevPanelProps) {
|
||||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
const [now, setNow] = useState(Date.now());
|
||||
|
||||
// Tick every 2s to keep relative timestamps fresh
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const topics = useMemo((): TopicInfo[] => {
|
||||
const items: TopicInfo[] = [];
|
||||
|
||||
// Workbench subscription topic
|
||||
items.push({
|
||||
label: "Workbench",
|
||||
key: `ws:${workspaceId}`,
|
||||
listenerCount: 1,
|
||||
hasConnection: true,
|
||||
lastRefresh: now,
|
||||
});
|
||||
|
||||
// Per-task tab subscriptions
|
||||
for (const task of snapshot.tasks ?? []) {
|
||||
if (task.status === "archived") continue;
|
||||
for (const tab of task.tabs ?? []) {
|
||||
items.push({
|
||||
label: `Tab/${task.title?.slice(0, 16) || task.id.slice(0, 8)}/${tab.sessionName.slice(0, 10)}`,
|
||||
key: `${workspaceId}:${task.id}:${tab.id}`,
|
||||
listenerCount: 1,
|
||||
hasConnection: tab.status === "running",
|
||||
lastRefresh: tab.status === "running" ? now : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [workspaceId, snapshot, now]);
|
||||
|
||||
const tasks = snapshot.tasks ?? [];
|
||||
const repos = snapshot.repos ?? [];
|
||||
const projects = snapshot.projects ?? [];
|
||||
|
||||
const mono = css({
|
||||
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace",
|
||||
fontSize: "10px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
position: "fixed",
|
||||
bottom: "8px",
|
||||
right: "8px",
|
||||
width: "320px",
|
||||
maxHeight: "50vh",
|
||||
zIndex: 99999,
|
||||
backgroundColor: t.surfaceElevated,
|
||||
border: `1px solid ${t.borderMedium}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: t.shadow,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
})}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "4px 8px",
|
||||
borderBottom: `1px solid ${t.borderSubtle}`,
|
||||
backgroundColor: t.surfaceTertiary,
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
color: t.textSecondary,
|
||||
letterSpacing: "0.5px",
|
||||
textTransform: "uppercase",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
})}
|
||||
>
|
||||
Dev
|
||||
{isMockFrontendClient && <span className={css({ fontSize: "8px", fontWeight: 600, color: t.statusWarning, letterSpacing: "0.3px" })}>MOCK</span>}
|
||||
</span>
|
||||
<span className={css({ fontSize: "9px", color: t.textMuted })}>Shift+D</span>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className={css({ overflowY: "auto", padding: "6px" })}>
|
||||
{/* Interest Topics */}
|
||||
<Section label="Interest Topics" t={t} css={css}>
|
||||
{topics.map((topic) => (
|
||||
<div
|
||||
key={topic.key}
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "2px 0",
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: topic.hasConnection ? t.statusSuccess : t.textMuted,
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ fontSize: "10px", color: t.textPrimary, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{topic.label}
|
||||
</span>
|
||||
<span className={`${mono} ${css({ color: t.textMuted })}`}>{topic.key.length > 24 ? `...${topic.key.slice(-20)}` : topic.key}</span>
|
||||
<span className={`${mono} ${css({ color: t.textTertiary })}`}>{timeAgo(topic.lastRefresh)}</span>
|
||||
</div>
|
||||
))}
|
||||
{topics.length === 0 && <span className={css({ fontSize: "10px", color: t.textMuted })}>No active subscriptions</span>}
|
||||
</Section>
|
||||
|
||||
{/* Snapshot Summary */}
|
||||
<Section label="Snapshot" t={t} css={css}>
|
||||
<div className={css({ display: "flex", gap: "10px", fontSize: "10px" })}>
|
||||
<Stat label="repos" value={repos.length} t={t} css={css} />
|
||||
<Stat label="projects" value={projects.length} t={t} css={css} />
|
||||
<Stat label="tasks" value={tasks.length} t={t} css={css} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Tasks */}
|
||||
{tasks.length > 0 && (
|
||||
<Section label="Tasks" t={t} css={css}>
|
||||
{tasks.slice(0, 10).map((task) => {
|
||||
const status = taskStatusLabel(task);
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "1px 0",
|
||||
fontSize: "10px",
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: statusColor(status, t),
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: t.textPrimary, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{task.title || task.id.slice(0, 12)}
|
||||
</span>
|
||||
<span className={`${mono} ${css({ color: statusColor(status, t) })}`}>{status}</span>
|
||||
<span className={`${mono} ${css({ color: t.textMuted })}`}>{task.tabs?.length ?? 0} tabs</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* GitHub */}
|
||||
{organization && (
|
||||
<Section label="GitHub" t={t} css={css}>
|
||||
<div className={css({ display: "flex", flexDirection: "column", gap: "3px", fontSize: "10px" })}>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
|
||||
<span
|
||||
className={css({
|
||||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: installStatusColor(organization.github.installationStatus, t),
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: t.textPrimary, flex: 1 })}>App</span>
|
||||
<span className={`${mono} ${css({ color: installStatusColor(organization.github.installationStatus, t) })}`}>
|
||||
{organization.github.installationStatus.replace(/_/g, " ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
|
||||
<span
|
||||
className={css({
|
||||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: syncStatusColor(organization.github.syncStatus, t),
|
||||
flexShrink: 0,
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: t.textPrimary, flex: 1 })}>Sync</span>
|
||||
<span className={`${mono} ${css({ color: syncStatusColor(organization.github.syncStatus, t) })}`}>{organization.github.syncStatus}</span>
|
||||
</div>
|
||||
<div className={css({ display: "flex", gap: "10px", marginTop: "2px" })}>
|
||||
<Stat label="repos imported" value={organization.github.importedRepoCount} t={t} css={css} />
|
||||
</div>
|
||||
{organization.github.connectedAccount && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted, marginTop: "1px" })}`}>@{organization.github.connectedAccount}</div>
|
||||
)}
|
||||
{organization.github.lastSyncLabel && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted })}`}>last sync: {organization.github.lastSyncLabel}</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Workspace */}
|
||||
<Section label="Workspace" t={t} css={css}>
|
||||
<div className={`${mono} ${css({ color: t.textTertiary })}`}>{workspaceId}</div>
|
||||
{organization && (
|
||||
<div className={`${mono} ${css({ color: t.textMuted, marginTop: "2px" })}`}>
|
||||
org: {organization.settings.displayName} ({organization.kind})
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function Section({
|
||||
label,
|
||||
t,
|
||||
css: cssFn,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
t: ReturnType<typeof useFoundryTokens>;
|
||||
css: ReturnType<typeof useStyletron>[0];
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cssFn({ marginBottom: "6px" })}>
|
||||
<div
|
||||
className={cssFn({
|
||||
fontSize: "9px",
|
||||
fontWeight: 600,
|
||||
color: t.textMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.5px",
|
||||
marginBottom: "2px",
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({
|
||||
label,
|
||||
value,
|
||||
t,
|
||||
css: cssFn,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
t: ReturnType<typeof useFoundryTokens>;
|
||||
css: ReturnType<typeof useStyletron>[0];
|
||||
}) {
|
||||
return (
|
||||
<span>
|
||||
<span className={cssFn({ fontWeight: 600, color: t.textPrimary })}>{value}</span>
|
||||
<span className={cssFn({ color: t.textTertiary, marginLeft: "2px" })}>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDevPanel() {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey && e.key === "D" && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
||||
e.preventDefault();
|
||||
setVisible((prev) => !prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return visible;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue