Merge remote-tracking branch 'origin/main' into test-dev-webhooks-flow

# Conflicts:
#	factory/packages/backend/src/actors/project/actions.ts
#	factory/packages/backend/src/actors/workspace/actions.ts
#	factory/packages/frontend/src/components/mock-layout.tsx
This commit is contained in:
Nathan Flurry 2026-03-11 11:14:04 -07:00
commit c8a095b69f
302 changed files with 11419 additions and 9952 deletions

View file

@ -2,53 +2,60 @@ import { z } from "zod";
export const AgentEnumSchema = z.enum(["claude", "codex"]);
export const NotifyBackendSchema = z.enum([
"openclaw",
"macos-osascript",
"linux-notify-send",
"terminal"
]);
export const NotifyBackendSchema = z.enum(["openclaw", "macos-osascript", "linux-notify-send", "terminal"]);
export const ConfigSchema = z.object({
theme: z.string().min(1).optional(),
auto_submit: z.boolean().default(false),
default_agent: AgentEnumSchema.default("codex"),
model: z.object({
provider: z.string(),
model: z.string()
}).optional(),
model: z
.object({
provider: z.string(),
model: z.string(),
})
.optional(),
notify: z.array(NotifyBackendSchema).default(["terminal"]),
workspace: z.object({
default: z.string().min(1).default("default")
}).default({ default: "default" }),
backend: z.object({
host: z.string().default("127.0.0.1"),
port: z.number().int().min(1).max(65535).default(7741),
dbPath: z.string().default("~/.local/share/openhandoff/handoff.db"),
opencode_poll_interval: z.number().default(2),
github_poll_interval: z.number().default(30),
backup_interval_secs: z.number().default(3600),
backup_retention_days: z.number().default(7)
}).default({
host: "127.0.0.1",
port: 7741,
dbPath: "~/.local/share/openhandoff/handoff.db",
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7
}),
providers: z.object({
local: z.object({
rootDir: z.string().optional(),
sandboxAgentPort: z.number().int().min(1).max(65535).optional(),
}).default({}),
daytona: z.object({
endpoint: z.string().optional(),
apiKey: z.string().optional(),
image: z.string().default("ubuntu:24.04")
}).default({ image: "ubuntu:24.04" })
}).default({ local: {}, daytona: { image: "ubuntu:24.04" } })
workspace: z
.object({
default: z.string().min(1).default("default"),
})
.default({ default: "default" }),
backend: z
.object({
host: z.string().default("127.0.0.1"),
port: z.number().int().min(1).max(65535).default(7741),
dbPath: z.string().default("~/.local/share/openhandoff/handoff.db"),
opencode_poll_interval: z.number().default(2),
github_poll_interval: z.number().default(30),
backup_interval_secs: z.number().default(3600),
backup_retention_days: z.number().default(7),
})
.default({
host: "127.0.0.1",
port: 7741,
dbPath: "~/.local/share/openhandoff/handoff.db",
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7,
}),
providers: z
.object({
local: z
.object({
rootDir: z.string().optional(),
sandboxAgentPort: z.number().int().min(1).max(65535).optional(),
})
.default({}),
daytona: z
.object({
endpoint: z.string().optional(),
apiKey: z.string().optional(),
image: z.string().default("ubuntu:24.04"),
})
.default({ image: "ubuntu:24.04" }),
})
.default({ local: {}, daytona: { image: "ubuntu:24.04" } }),
});
export type AppConfig = z.infer<typeof ConfigSchema>;

View file

@ -1,6 +1,10 @@
import { z } from "zod";
export const WorkspaceIdSchema = z.string().min(1).max(64).regex(/^[a-zA-Z0-9._-]+$/);
export const WorkspaceIdSchema = z
.string()
.min(1)
.max(64)
.regex(/^[a-zA-Z0-9._-]+$/);
export type WorkspaceId = z.infer<typeof WorkspaceIdSchema>;
export const ProviderIdSchema = z.enum(["daytona", "local"]);
@ -36,7 +40,7 @@ export const HandoffStatusSchema = z.enum([
"kill_destroy_sandbox",
"kill_finalize",
"killed",
"error"
"error",
]);
export type HandoffStatus = z.infer<typeof HandoffStatusSchema>;
@ -64,7 +68,7 @@ export const CreateHandoffInputSchema = z.object({
initialPrompt: z.string().optional(),
providerId: ProviderIdSchema.optional(),
agentType: AgentTypeSchema.optional(),
onBranch: z.string().trim().min(1).optional()
onBranch: z.string().trim().min(1).optional(),
});
export type CreateHandoffInput = z.infer<typeof CreateHandoffInputSchema>;
@ -90,7 +94,7 @@ export const HandoffRecordSchema = z.object({
cwd: z.string().nullable(),
createdAt: z.number().int(),
updatedAt: z.number().int(),
})
}),
),
agentType: z.string().nullable(),
prSubmitted: z.boolean(),
@ -104,7 +108,7 @@ export const HandoffRecordSchema = z.object({
hasUnpushed: z.string().nullable(),
parentBranch: z.string().nullable(),
createdAt: z.number().int(),
updatedAt: z.number().int()
updatedAt: z.number().int(),
});
export type HandoffRecord = z.infer<typeof HandoffRecordSchema>;
@ -115,13 +119,13 @@ export const HandoffSummarySchema = z.object({
branchName: z.string().min(1).nullable(),
title: z.string().min(1).nullable(),
status: HandoffStatusSchema,
updatedAt: z.number().int()
updatedAt: z.number().int(),
});
export type HandoffSummary = z.infer<typeof HandoffSummarySchema>;
export const HandoffActionInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
handoffId: z.string().min(1)
handoffId: z.string().min(1),
});
export type HandoffActionInput = z.infer<typeof HandoffActionInputSchema>;
@ -129,13 +133,13 @@ export const SwitchResultSchema = z.object({
workspaceId: WorkspaceIdSchema,
handoffId: z.string().min(1),
providerId: ProviderIdSchema,
switchTarget: z.string().min(1)
switchTarget: z.string().min(1),
});
export type SwitchResult = z.infer<typeof SwitchResultSchema>;
export const ListHandoffsInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
repoId: RepoIdSchema.optional()
repoId: RepoIdSchema.optional(),
});
export type ListHandoffsInput = z.infer<typeof ListHandoffsInputSchema>;
@ -158,7 +162,7 @@ export const RepoBranchRecordSchema = z.object({
reviewer: z.string().nullable(),
firstSeenAt: z.number().int().nullable(),
lastSeenAt: z.number().int().nullable(),
updatedAt: z.number().int()
updatedAt: z.number().int(),
});
export type RepoBranchRecord = z.infer<typeof RepoBranchRecordSchema>;
@ -169,17 +173,11 @@ export const RepoOverviewSchema = z.object({
baseRef: z.string().nullable(),
stackAvailable: z.boolean(),
fetchedAt: z.number().int(),
branches: z.array(RepoBranchRecordSchema)
branches: z.array(RepoBranchRecordSchema),
});
export type RepoOverview = z.infer<typeof RepoOverviewSchema>;
export const RepoStackActionSchema = z.enum([
"sync_repo",
"restack_repo",
"restack_subtree",
"rebase_branch",
"reparent_branch"
]);
export const RepoStackActionSchema = z.enum(["sync_repo", "restack_repo", "restack_subtree", "rebase_branch", "reparent_branch"]);
export type RepoStackAction = z.infer<typeof RepoStackActionSchema>;
export const RepoStackActionInputSchema = z.object({
@ -187,7 +185,7 @@ export const RepoStackActionInputSchema = z.object({
repoId: RepoIdSchema,
action: RepoStackActionSchema,
branchName: z.string().trim().min(1).optional(),
parentBranch: z.string().trim().min(1).optional()
parentBranch: z.string().trim().min(1).optional(),
});
export type RepoStackActionInput = z.infer<typeof RepoStackActionInputSchema>;
@ -195,20 +193,31 @@ export const RepoStackActionResultSchema = z.object({
action: RepoStackActionSchema,
executed: z.boolean(),
message: z.string().min(1),
at: z.number().int()
at: z.number().int(),
});
export type RepoStackActionResult = z.infer<typeof RepoStackActionResultSchema>;
export const WorkspaceUseInputSchema = z.object({
workspaceId: WorkspaceIdSchema
workspaceId: WorkspaceIdSchema,
});
export type WorkspaceUseInput = z.infer<typeof WorkspaceUseInputSchema>;
export const StarSandboxAgentRepoInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
});
export type StarSandboxAgentRepoInput = z.infer<typeof StarSandboxAgentRepoInputSchema>;
export const StarSandboxAgentRepoResultSchema = z.object({
repo: z.string().min(1),
starredAt: z.number().int(),
});
export type StarSandboxAgentRepoResult = z.infer<typeof StarSandboxAgentRepoResultSchema>;
export const HistoryQueryInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
limit: z.number().int().positive().max(500).optional(),
branch: z.string().min(1).optional(),
handoffId: z.string().min(1).optional()
handoffId: z.string().min(1).optional(),
});
export type HistoryQueryInput = z.infer<typeof HistoryQueryInputSchema>;
@ -220,14 +229,14 @@ export const HistoryEventSchema = z.object({
branchName: z.string().nullable(),
kind: z.string().min(1),
payloadJson: z.string().min(1),
createdAt: z.number().int()
createdAt: z.number().int(),
});
export type HistoryEvent = z.infer<typeof HistoryEventSchema>;
export const PruneInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
dryRun: z.boolean(),
yes: z.boolean()
yes: z.boolean(),
});
export type PruneInput = z.infer<typeof PruneInputSchema>;
@ -235,19 +244,19 @@ export const KillInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
handoffId: z.string().min(1),
deleteBranch: z.boolean(),
abandon: z.boolean()
abandon: z.boolean(),
});
export type KillInput = z.infer<typeof KillInputSchema>;
export const StatuslineInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
format: z.enum(["table", "claude-code"])
format: z.enum(["table", "claude-code"]),
});
export type StatuslineInput = z.infer<typeof StatuslineInputSchema>;
export const ListInputSchema = z.object({
workspaceId: WorkspaceIdSchema,
format: z.enum(["table", "json"]),
full: z.boolean()
full: z.boolean(),
});
export type ListInput = z.infer<typeof ListInputSchema>;

View file

@ -1,9 +1,6 @@
import type { AppConfig } from "./config.js";
export function resolveWorkspaceId(
flagWorkspace: string | undefined,
config: AppConfig
): string {
export function resolveWorkspaceId(flagWorkspace: string | undefined, config: AppConfig): string {
if (flagWorkspace && flagWorkspace.trim().length > 0) {
return flagWorkspace.trim();
}

View file

@ -12,11 +12,11 @@ const cfg: AppConfig = ConfigSchema.parse({
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7
backup_retention_days: 7,
},
providers: {
daytona: { image: "ubuntu:24.04" }
}
daytona: { image: "ubuntu:24.04" },
},
});
describe("resolveWorkspaceId", () => {
@ -31,7 +31,7 @@ describe("resolveWorkspaceId", () => {
it("falls back to literal default when config value is empty", () => {
const empty = {
...cfg,
workspace: { default: "" }
workspace: { default: "" },
} as AppConfig;
expect(resolveWorkspaceId(undefined, empty)).toBe("default");