mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 22:03:48 +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
|
|
@ -172,6 +172,23 @@ export const RepoOverviewSchema = z.object({
|
|||
baseRef: z.string().nullable(),
|
||||
stackAvailable: z.boolean(),
|
||||
fetchedAt: z.number().int(),
|
||||
branchSyncAt: z.number().int().nullable(),
|
||||
prSyncAt: z.number().int().nullable(),
|
||||
branchSyncStatus: z.enum(["pending", "syncing", "synced", "error"]),
|
||||
prSyncStatus: z.enum(["pending", "syncing", "synced", "error"]),
|
||||
repoActionJobs: z.array(
|
||||
z.object({
|
||||
jobId: z.string().min(1),
|
||||
action: z.enum(["sync_repo", "restack_repo", "restack_subtree", "rebase_branch", "reparent_branch"]),
|
||||
branchName: z.string().nullable(),
|
||||
parentBranch: z.string().nullable(),
|
||||
status: z.enum(["queued", "running", "completed", "error"]),
|
||||
message: z.string().min(1),
|
||||
createdAt: z.number().int(),
|
||||
updatedAt: z.number().int(),
|
||||
completedAt: z.number().int().nullable(),
|
||||
}),
|
||||
),
|
||||
branches: z.array(RepoBranchRecordSchema),
|
||||
});
|
||||
export type RepoOverview = z.infer<typeof RepoOverviewSchema>;
|
||||
|
|
@ -189,8 +206,10 @@ export const RepoStackActionInputSchema = z.object({
|
|||
export type RepoStackActionInput = z.infer<typeof RepoStackActionInputSchema>;
|
||||
|
||||
export const RepoStackActionResultSchema = z.object({
|
||||
jobId: z.string().min(1).nullable().optional(),
|
||||
action: RepoStackActionSchema,
|
||||
executed: z.boolean(),
|
||||
status: z.enum(["queued", "running", "completed", "error"]).optional(),
|
||||
message: z.string().min(1),
|
||||
at: z.number().int(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ export * from "./app-shell.js";
|
|||
export * from "./contracts.js";
|
||||
export * from "./config.js";
|
||||
export * from "./logging.js";
|
||||
export * from "./realtime-events.js";
|
||||
export * from "./workbench.js";
|
||||
export * from "./workspace.js";
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ export interface FoundryLoggerOptions {
|
|||
service: string;
|
||||
bindings?: Record<string, unknown>;
|
||||
level?: string;
|
||||
format?: "json" | "logfmt";
|
||||
}
|
||||
|
||||
type ProcessLike = {
|
||||
env?: Record<string, string | undefined>;
|
||||
stdout?: {
|
||||
write?: (chunk: string) => unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function resolveEnvVar(name: string): string | undefined {
|
||||
|
|
@ -28,6 +32,116 @@ function isBrowserRuntime(): boolean {
|
|||
return typeof window !== "undefined" && typeof document !== "undefined";
|
||||
}
|
||||
|
||||
function serializeLogValue(value: unknown): string | number | boolean | null {
|
||||
if (value === undefined || value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (value instanceof Error) {
|
||||
return JSON.stringify({
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return "[unserializable]";
|
||||
}
|
||||
}
|
||||
|
||||
function formatLogfmtValue(value: string | number | boolean | null): string {
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
const raw = value ?? "null";
|
||||
if (raw.length > 0 && !/[\s="\\]/.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return `"${raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
||||
}
|
||||
|
||||
function formatLogfmtLine(record: Record<string, unknown>): string {
|
||||
return Object.entries(record)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => `${key}=${formatLogfmtValue(serializeLogValue(value))}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function stringifyMessagePart(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
const serialized = serializeLogValue(value);
|
||||
return typeof serialized === "string" ? serialized : String(serialized);
|
||||
}
|
||||
|
||||
function buildLogRecord(level: string, bindings: Record<string, unknown>, args: Parameters<Logger["info"]>): Record<string, unknown> {
|
||||
const record: Record<string, unknown> = {
|
||||
time: new Date().toISOString(),
|
||||
level,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(bindings)) {
|
||||
if (key !== "time" && key !== "level" && key !== "msg" && value !== undefined) {
|
||||
record[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const [first, ...rest] = args;
|
||||
if (first && typeof first === "object") {
|
||||
if (first instanceof Error) {
|
||||
record.err = {
|
||||
name: first.name,
|
||||
message: first.message,
|
||||
stack: first.stack,
|
||||
};
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(first)) {
|
||||
if (key !== "time" && key !== "level" && key !== "msg" && value !== undefined) {
|
||||
record[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
record.msg = rest.map(stringifyMessagePart).join(" ");
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
record.msg = [first, ...rest].map(stringifyMessagePart).join(" ");
|
||||
return record;
|
||||
}
|
||||
|
||||
function writeLogfmtLine(line: string): void {
|
||||
const processLike = (globalThis as { process?: ProcessLike }).process;
|
||||
if (processLike?.stdout?.write) {
|
||||
processLike.stdout.write(`${line}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
export function createFoundryLogger(options: FoundryLoggerOptions): Logger {
|
||||
const browser = isBrowserRuntime();
|
||||
const loggerOptions: LoggerOptions = {
|
||||
|
|
@ -44,6 +158,15 @@ export function createFoundryLogger(options: FoundryLoggerOptions): Logger {
|
|||
};
|
||||
} else {
|
||||
loggerOptions.timestamp = pino.stdTimeFunctions.isoTime;
|
||||
if (options.format === "logfmt") {
|
||||
loggerOptions.hooks = {
|
||||
logMethod(this: Logger, args, _method, level) {
|
||||
const levelLabel = this.levels.labels[level] ?? "info";
|
||||
const record = buildLogRecord(levelLabel, this.bindings(), args);
|
||||
writeLogfmtLine(formatLogfmtLine(record));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return pino(loggerOptions);
|
||||
|
|
|
|||
36
foundry/packages/shared/src/realtime-events.ts
Normal file
36
foundry/packages/shared/src/realtime-events.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { FoundryAppSnapshot } from "./app-shell.js";
|
||||
import type { WorkbenchRepoSummary, WorkbenchSessionDetail, WorkbenchTaskDetail, WorkbenchTaskSummary } from "./workbench.js";
|
||||
|
||||
export interface SandboxProcessSnapshot {
|
||||
id: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
createdAtMs: number;
|
||||
cwd?: string | null;
|
||||
exitCode?: number | null;
|
||||
exitedAtMs?: number | null;
|
||||
interactive: boolean;
|
||||
pid?: number | null;
|
||||
status: "running" | "exited";
|
||||
tty: boolean;
|
||||
}
|
||||
|
||||
/** Workspace-level events broadcast by the workspace actor. */
|
||||
export type WorkspaceEvent =
|
||||
| { type: "taskSummaryUpdated"; taskSummary: WorkbenchTaskSummary }
|
||||
| { type: "taskRemoved"; taskId: string }
|
||||
| { type: "repoAdded"; repo: WorkbenchRepoSummary }
|
||||
| { type: "repoUpdated"; repo: WorkbenchRepoSummary }
|
||||
| { type: "repoRemoved"; repoId: string };
|
||||
|
||||
/** Task-level events broadcast by the task actor. */
|
||||
export type TaskEvent = { type: "taskDetailUpdated"; detail: WorkbenchTaskDetail };
|
||||
|
||||
/** Session-level events broadcast by the task actor and filtered by sessionId on the client. */
|
||||
export type SessionEvent = { type: "sessionUpdated"; session: WorkbenchSessionDetail };
|
||||
|
||||
/** App-level events broadcast by the app workspace actor. */
|
||||
export type AppEvent = { type: "appUpdated"; snapshot: FoundryAppSnapshot };
|
||||
|
||||
/** Sandbox process events broadcast by the sandbox instance actor. */
|
||||
export type SandboxProcessesEvent = { type: "processesUpdated"; processes: SandboxProcessSnapshot[] };
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import type { AgentType, ProviderId, TaskStatus } from "./contracts.js";
|
||||
|
||||
export type WorkbenchTaskStatus = "running" | "idle" | "new" | "archived";
|
||||
export type WorkbenchAgentKind = "Claude" | "Codex" | "Cursor";
|
||||
export type WorkbenchModelId = "claude-sonnet-4" | "claude-opus-4" | "gpt-4o" | "o3";
|
||||
|
|
@ -18,7 +20,8 @@ export interface WorkbenchComposerDraft {
|
|||
updatedAtMs: number | null;
|
||||
}
|
||||
|
||||
export interface WorkbenchAgentTab {
|
||||
/** Session metadata without transcript content. */
|
||||
export interface WorkbenchSessionSummary {
|
||||
id: string;
|
||||
sessionId: string | null;
|
||||
sessionName: string;
|
||||
|
|
@ -28,6 +31,21 @@ export interface WorkbenchAgentTab {
|
|||
thinkingSinceMs: number | null;
|
||||
unread: boolean;
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
/** Full session content — only fetched when viewing a specific session tab. */
|
||||
export interface WorkbenchSessionDetail {
|
||||
/** Stable UI tab id used for the session topic key and routing. */
|
||||
sessionId: string;
|
||||
tabId: string;
|
||||
sandboxSessionId: string | null;
|
||||
sessionName: string;
|
||||
agent: WorkbenchAgentKind;
|
||||
model: WorkbenchModelId;
|
||||
status: "running" | "idle" | "error";
|
||||
thinkingSinceMs: number | null;
|
||||
unread: boolean;
|
||||
created: boolean;
|
||||
draft: WorkbenchComposerDraft;
|
||||
transcript: WorkbenchTranscriptEvent[];
|
||||
}
|
||||
|
|
@ -76,6 +94,73 @@ export interface WorkbenchPullRequestSummary {
|
|||
status: "draft" | "ready";
|
||||
}
|
||||
|
||||
export interface WorkbenchSandboxSummary {
|
||||
providerId: ProviderId;
|
||||
sandboxId: string;
|
||||
cwd: string | null;
|
||||
}
|
||||
|
||||
/** Sidebar-level task data. Materialized in the workspace actor's SQLite. */
|
||||
export interface WorkbenchTaskSummary {
|
||||
id: string;
|
||||
repoId: string;
|
||||
title: string;
|
||||
status: WorkbenchTaskStatus;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
branch: string | null;
|
||||
pullRequest: WorkbenchPullRequestSummary | null;
|
||||
/** Summary of sessions — no transcript content. */
|
||||
sessionsSummary: WorkbenchSessionSummary[];
|
||||
}
|
||||
|
||||
/** Full task detail — only fetched when viewing a specific task. */
|
||||
export interface WorkbenchTaskDetail extends WorkbenchTaskSummary {
|
||||
/** Original task prompt/instructions shown in the detail view. */
|
||||
task: string;
|
||||
/** Agent choice used when creating new sandbox sessions for this task. */
|
||||
agentType: AgentType | null;
|
||||
/** Underlying task runtime status preserved for detail views and error handling. */
|
||||
runtimeStatus: TaskStatus;
|
||||
statusMessage: string | null;
|
||||
activeSessionId: string | null;
|
||||
diffStat: string | null;
|
||||
prUrl: string | null;
|
||||
reviewStatus: string | null;
|
||||
fileChanges: WorkbenchFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
fileTree: WorkbenchFileTreeNode[];
|
||||
minutesUsed: number;
|
||||
/** Sandbox info for this task. */
|
||||
sandboxes: WorkbenchSandboxSummary[];
|
||||
activeSandboxId: string | null;
|
||||
}
|
||||
|
||||
/** Repo-level summary for workspace sidebar. */
|
||||
export interface WorkbenchRepoSummary {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
|
||||
taskCount: number;
|
||||
latestActivityMs: number;
|
||||
}
|
||||
|
||||
/** Workspace-level snapshot — initial fetch for the workspace topic. */
|
||||
export interface WorkspaceSummarySnapshot {
|
||||
workspaceId: string;
|
||||
repos: WorkbenchRepoSummary[];
|
||||
taskSummaries: WorkbenchTaskSummary[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated compatibility aliases for older mock/view-model code.
|
||||
* New code should use the summary/detail/topic-specific types above.
|
||||
*/
|
||||
export interface WorkbenchAgentTab extends WorkbenchSessionSummary {
|
||||
draft: WorkbenchComposerDraft;
|
||||
transcript: WorkbenchTranscriptEvent[];
|
||||
}
|
||||
|
||||
export interface WorkbenchTask {
|
||||
id: string;
|
||||
repoId: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue