mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 15:02:39 +00:00
Complete Foundry refactor checklist
This commit is contained in:
parent
40bed3b0a1
commit
13fc9cb318
91 changed files with 5091 additions and 4108 deletions
|
|
@ -4,6 +4,12 @@ export type FoundryBillingPlanId = "free" | "team";
|
|||
export type FoundryBillingStatus = "active" | "trialing" | "past_due" | "scheduled_cancel";
|
||||
export type FoundryGithubInstallationStatus = "connected" | "install_required" | "reconnect_required";
|
||||
export type FoundryGithubSyncStatus = "pending" | "syncing" | "synced" | "error";
|
||||
export type FoundryGithubSyncPhase =
|
||||
| "discovering_repositories"
|
||||
| "syncing_repositories"
|
||||
| "syncing_branches"
|
||||
| "syncing_members"
|
||||
| "syncing_pull_requests";
|
||||
export type FoundryOrganizationKind = "personal" | "organization";
|
||||
export type FoundryStarterRepoStatus = "pending" | "starred" | "skipped";
|
||||
|
||||
|
|
@ -53,6 +59,10 @@ export interface FoundryGithubState {
|
|||
lastSyncAt: number | null;
|
||||
lastWebhookAt: number | null;
|
||||
lastWebhookEvent: string;
|
||||
syncGeneration?: number;
|
||||
syncPhase?: FoundryGithubSyncPhase | null;
|
||||
processedRepositoryCount?: number;
|
||||
totalRepositoryCount?: number;
|
||||
}
|
||||
|
||||
export interface FoundryOrganizationSettings {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,19 @@ export const CreateTaskInputSchema = z.object({
|
|||
});
|
||||
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
|
||||
|
||||
export const WorkspacePullRequestSummarySchema = z.object({
|
||||
number: z.number().int(),
|
||||
title: z.string().min(1),
|
||||
state: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
headRefName: z.string().min(1),
|
||||
baseRefName: z.string().min(1),
|
||||
repoFullName: z.string().min(1),
|
||||
authorLogin: z.string().nullable(),
|
||||
isDraft: z.boolean(),
|
||||
updatedAtMs: z.number().int(),
|
||||
});
|
||||
|
||||
export const TaskRecordSchema = z.object({
|
||||
organizationId: OrganizationIdSchema,
|
||||
repoId: z.string().min(1),
|
||||
|
|
@ -69,6 +82,7 @@ export const TaskRecordSchema = z.object({
|
|||
sandboxProviderId: SandboxProviderIdSchema,
|
||||
status: TaskStatusSchema,
|
||||
activeSandboxId: z.string().nullable(),
|
||||
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
|
||||
sandboxes: z.array(
|
||||
z.object({
|
||||
sandboxId: z.string().min(1),
|
||||
|
|
@ -80,12 +94,6 @@ export const TaskRecordSchema = z.object({
|
|||
updatedAt: z.number().int(),
|
||||
}),
|
||||
),
|
||||
diffStat: z.string().nullable(),
|
||||
prUrl: z.string().nullable(),
|
||||
prAuthor: z.string().nullable(),
|
||||
ciStatus: z.string().nullable(),
|
||||
reviewStatus: z.string().nullable(),
|
||||
reviewer: z.string().nullable(),
|
||||
createdAt: z.number().int(),
|
||||
updatedAt: z.number().int(),
|
||||
});
|
||||
|
|
@ -99,6 +107,7 @@ export const TaskSummarySchema = z.object({
|
|||
title: z.string().min(1).nullable(),
|
||||
status: TaskStatusSchema,
|
||||
updatedAt: z.number().int(),
|
||||
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
|
||||
});
|
||||
export type TaskSummary = z.infer<typeof TaskSummarySchema>;
|
||||
|
||||
|
|
@ -129,12 +138,8 @@ export const RepoBranchRecordSchema = z.object({
|
|||
taskId: z.string().nullable(),
|
||||
taskTitle: z.string().nullable(),
|
||||
taskStatus: TaskStatusSchema.nullable(),
|
||||
prNumber: z.number().int().nullable(),
|
||||
prState: z.string().nullable(),
|
||||
prUrl: z.string().nullable(),
|
||||
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
|
||||
ciStatus: z.string().nullable(),
|
||||
reviewStatus: z.string().nullable(),
|
||||
reviewer: z.string().nullable(),
|
||||
updatedAt: z.number().int(),
|
||||
});
|
||||
export type RepoBranchRecord = z.infer<typeof RepoBranchRecordSchema>;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * from "./app-shell.js";
|
|||
export * from "./contracts.js";
|
||||
export * from "./config.js";
|
||||
export * from "./logging.js";
|
||||
export * from "./models.js";
|
||||
export * from "./realtime-events.js";
|
||||
export * from "./workspace.js";
|
||||
export * from "./organization.js";
|
||||
|
|
|
|||
217
foundry/packages/shared/src/models.ts
Normal file
217
foundry/packages/shared/src/models.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import claudeConfig from "../../../../scripts/agent-configs/resources/claude.json" with { type: "json" };
|
||||
import codexConfig from "../../../../scripts/agent-configs/resources/codex.json" with { type: "json" };
|
||||
|
||||
export type WorkspaceAgentKind = string;
|
||||
export type WorkspaceModelId = string;
|
||||
|
||||
export interface WorkspaceModelOption {
|
||||
id: WorkspaceModelId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceModelGroup {
|
||||
provider: string;
|
||||
agentKind: WorkspaceAgentKind;
|
||||
sandboxAgentId: string;
|
||||
models: WorkspaceModelOption[];
|
||||
}
|
||||
|
||||
interface AgentConfigResource {
|
||||
defaultModel?: string;
|
||||
models?: Array<{ id?: string; name?: string }>;
|
||||
}
|
||||
|
||||
interface SandboxAgentInfoLike {
|
||||
id?: unknown;
|
||||
configOptions?: unknown;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function normalizeModelLabel(model: { id?: string; name?: string }): string {
|
||||
const name = model.name?.trim();
|
||||
if (name && name.length > 0) {
|
||||
return name;
|
||||
}
|
||||
return model.id?.trim() || "Unknown";
|
||||
}
|
||||
|
||||
function buildGroup(provider: string, agentKind: WorkspaceAgentKind, sandboxAgentId: string, config: AgentConfigResource): WorkspaceModelGroup {
|
||||
return {
|
||||
provider,
|
||||
agentKind,
|
||||
sandboxAgentId,
|
||||
models: (config.models ?? [])
|
||||
.map((model) => {
|
||||
const id = model.id?.trim();
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
label: normalizeModelLabel(model),
|
||||
};
|
||||
})
|
||||
.filter((model): model is WorkspaceModelOption => model != null),
|
||||
};
|
||||
}
|
||||
|
||||
function titleCaseIdentifier(value: string): string {
|
||||
return value
|
||||
.split(/[\s_-]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function workspaceAgentMetadata(agentId: string): { provider: string; agentKind: string } {
|
||||
const normalized = agentId.trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case "claude":
|
||||
return { provider: "Claude", agentKind: "Claude" };
|
||||
case "codex":
|
||||
return { provider: "Codex", agentKind: "Codex" };
|
||||
default:
|
||||
return {
|
||||
provider: titleCaseIdentifier(agentId),
|
||||
agentKind: titleCaseIdentifier(agentId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOptionLabel(entry: Record<string, unknown>): string | null {
|
||||
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const label = typeof entry.label === "string" ? entry.label.trim() : "";
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
const value = typeof entry.value === "string" ? entry.value.trim() : "";
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function appendSelectOptionModels(target: WorkspaceModelOption[], options: unknown): void {
|
||||
if (!Array.isArray(options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of options) {
|
||||
if (!isRecord(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = typeof entry.value === "string" ? entry.value.trim() : "";
|
||||
if (value) {
|
||||
target.push({
|
||||
id: value,
|
||||
label: normalizeOptionLabel(entry) ?? value,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
appendSelectOptionModels(target, entry.options);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAgentModels(configOptions: unknown): WorkspaceModelOption[] {
|
||||
if (!Array.isArray(configOptions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = configOptions.find((entry) => isRecord(entry) && entry.category === "model" && entry.type === "select");
|
||||
if (!isRecord(options)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const models: WorkspaceModelOption[] = [];
|
||||
appendSelectOptionModels(models, options.options);
|
||||
|
||||
const seen = new Set<string>();
|
||||
return models.filter((model) => {
|
||||
if (!model.id || seen.has(model.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(model.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function workspaceModelGroupsFromSandboxAgents(agents: SandboxAgentInfoLike[]): WorkspaceModelGroup[] {
|
||||
return agents
|
||||
.map((agent) => {
|
||||
const sandboxAgentId = typeof agent.id === "string" ? agent.id.trim() : "";
|
||||
if (!sandboxAgentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const models = normalizeAgentModels(agent.configOptions);
|
||||
if (models.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = workspaceAgentMetadata(sandboxAgentId);
|
||||
return {
|
||||
provider: metadata.provider,
|
||||
agentKind: metadata.agentKind,
|
||||
sandboxAgentId,
|
||||
models,
|
||||
} satisfies WorkspaceModelGroup;
|
||||
})
|
||||
.filter((group): group is WorkspaceModelGroup => group != null);
|
||||
}
|
||||
|
||||
export const DEFAULT_WORKSPACE_MODEL_GROUPS: WorkspaceModelGroup[] = [
|
||||
buildGroup("Claude", "Claude", "claude", claudeConfig as AgentConfigResource),
|
||||
buildGroup("Codex", "Codex", "codex", codexConfig as AgentConfigResource),
|
||||
].filter((group) => group.models.length > 0);
|
||||
|
||||
export const DEFAULT_WORKSPACE_MODEL_ID: WorkspaceModelId =
|
||||
((codexConfig as AgentConfigResource).defaultModel ?? DEFAULT_WORKSPACE_MODEL_GROUPS[0]?.models[0]?.id ?? "default").trim();
|
||||
|
||||
export function workspaceProviderAgent(
|
||||
provider: string,
|
||||
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
): WorkspaceAgentKind {
|
||||
return groups.find((group) => group.provider === provider)?.agentKind ?? provider;
|
||||
}
|
||||
|
||||
export function workspaceModelGroupForId(
|
||||
id: WorkspaceModelId,
|
||||
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
): WorkspaceModelGroup | null {
|
||||
return groups.find((group) => group.models.some((model) => model.id === id)) ?? null;
|
||||
}
|
||||
|
||||
export function workspaceModelLabel(
|
||||
id: WorkspaceModelId,
|
||||
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
): string {
|
||||
const group = workspaceModelGroupForId(id, groups);
|
||||
const model = group?.models.find((candidate) => candidate.id === id);
|
||||
return model && group ? `${group.provider} ${model.label}` : id;
|
||||
}
|
||||
|
||||
export function workspaceAgentForModel(
|
||||
id: WorkspaceModelId,
|
||||
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
): WorkspaceAgentKind {
|
||||
const group = workspaceModelGroupForId(id, groups);
|
||||
if (group) {
|
||||
return group.agentKind;
|
||||
}
|
||||
return groups[0]?.agentKind ?? "Claude";
|
||||
}
|
||||
|
||||
export function workspaceSandboxAgentIdForModel(
|
||||
id: WorkspaceModelId,
|
||||
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
|
||||
): string {
|
||||
const group = workspaceModelGroupForId(id, groups);
|
||||
return group?.sandboxAgentId ?? groups[0]?.sandboxAgentId ?? "claude";
|
||||
}
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
import type { SandboxProviderId, TaskStatus } from "./contracts.js";
|
||||
import type { WorkspaceAgentKind, WorkspaceModelGroup, WorkspaceModelId, WorkspaceModelOption } from "./models.js";
|
||||
|
||||
export type WorkspaceTaskStatus = TaskStatus | "new";
|
||||
export type WorkspaceAgentKind = "Claude" | "Codex" | "Cursor";
|
||||
export type WorkspaceModelId =
|
||||
| "claude-sonnet-4"
|
||||
| "claude-opus-4"
|
||||
| "gpt-5.3-codex"
|
||||
| "gpt-5.4"
|
||||
| "gpt-5.2-codex"
|
||||
| "gpt-5.1-codex-max"
|
||||
| "gpt-5.2"
|
||||
| "gpt-5.1-codex-mini";
|
||||
export type WorkspaceTaskStatus = TaskStatus;
|
||||
export type WorkspaceSessionStatus = "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error";
|
||||
|
||||
export type { WorkspaceAgentKind, WorkspaceModelGroup, WorkspaceModelId, WorkspaceModelOption } from "./models.js";
|
||||
|
||||
export interface WorkspaceTranscriptEvent {
|
||||
id: string;
|
||||
eventIndex: number;
|
||||
|
|
@ -132,6 +125,7 @@ export interface WorkspaceTaskSummary {
|
|||
updatedAtMs: number;
|
||||
branch: string | null;
|
||||
pullRequest: WorkspacePullRequestSummary | null;
|
||||
activeSessionId: string | null;
|
||||
/** Summary of sessions — no transcript content. */
|
||||
sessionsSummary: WorkspaceSessionSummary[];
|
||||
}
|
||||
|
|
@ -140,11 +134,6 @@ export interface WorkspaceTaskSummary {
|
|||
export interface WorkspaceTaskDetail extends WorkspaceTaskSummary {
|
||||
/** Original task prompt/instructions shown in the detail view. */
|
||||
task: string;
|
||||
/** Underlying task runtime status preserved for detail views and error handling. */
|
||||
runtimeStatus: TaskStatus;
|
||||
diffStat: string | null;
|
||||
prUrl: string | null;
|
||||
reviewStatus: string | null;
|
||||
fileChanges: WorkspaceFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
fileTree: WorkspaceFileTreeNode[];
|
||||
|
|
@ -163,9 +152,32 @@ export interface WorkspaceRepositorySummary {
|
|||
latestActivityMs: number;
|
||||
}
|
||||
|
||||
export type OrganizationGithubSyncPhase =
|
||||
| "discovering_repositories"
|
||||
| "syncing_repositories"
|
||||
| "syncing_branches"
|
||||
| "syncing_members"
|
||||
| "syncing_pull_requests";
|
||||
|
||||
export interface OrganizationGithubSummary {
|
||||
connectedAccount: string;
|
||||
installationStatus: "connected" | "install_required" | "reconnect_required";
|
||||
syncStatus: "pending" | "syncing" | "synced" | "error";
|
||||
importedRepoCount: number;
|
||||
lastSyncLabel: string;
|
||||
lastSyncAt: number | null;
|
||||
lastWebhookAt: number | null;
|
||||
lastWebhookEvent: string;
|
||||
syncGeneration: number;
|
||||
syncPhase: OrganizationGithubSyncPhase | null;
|
||||
processedRepositoryCount: number;
|
||||
totalRepositoryCount: number;
|
||||
}
|
||||
|
||||
/** Organization-level snapshot — initial fetch for the organization topic. */
|
||||
export interface OrganizationSummarySnapshot {
|
||||
organizationId: string;
|
||||
github: OrganizationGithubSummary;
|
||||
repos: WorkspaceRepositorySummary[];
|
||||
taskSummaries: WorkspaceTaskSummary[];
|
||||
}
|
||||
|
|
@ -180,11 +192,11 @@ export interface WorkspaceTask {
|
|||
repoId: string;
|
||||
title: string;
|
||||
status: WorkspaceTaskStatus;
|
||||
runtimeStatus?: TaskStatus;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
branch: string | null;
|
||||
pullRequest: WorkspacePullRequestSummary | null;
|
||||
activeSessionId?: string | null;
|
||||
sessions: WorkspaceSession[];
|
||||
fileChanges: WorkspaceFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
|
|
@ -212,16 +224,6 @@ export interface TaskWorkspaceSnapshot {
|
|||
tasks: WorkspaceTask[];
|
||||
}
|
||||
|
||||
export interface WorkspaceModelOption {
|
||||
id: WorkspaceModelId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceModelGroup {
|
||||
provider: string;
|
||||
models: WorkspaceModelOption[];
|
||||
}
|
||||
|
||||
export interface TaskWorkspaceSelectInput {
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue