import * as Schema from "effect/Schema"; import { RepositoryIdentity, ThreadEnvMode } from "./environment.ts"; import { ModelSelection } from "./modelSelection.ts"; import { CommandId, IsoDateTime, NonNegativeInt, PositiveInt, ProjectId, TrimmedNonEmptyString, TrimmedString, } from "./baseSchemas.ts"; const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200; const PROJECT_SEARCH_CONTENTS_MAX_LIMIT = 500; const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512; const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512; export const ProjectScriptIcon = Schema.Literals([ "play", "test", "lint", "configure", "build", "debug", ]); export type ProjectScriptIcon = typeof ProjectScriptIcon.Type; export const ProjectScript = Schema.Struct({ id: TrimmedNonEmptyString, name: TrimmedNonEmptyString, command: TrimmedNonEmptyString, icon: ProjectScriptIcon, runOnWorktreeCreate: Schema.Boolean, previewUrl: Schema.optional(TrimmedNonEmptyString), autoOpenPreview: Schema.optional(Schema.Boolean), }); export type ProjectScript = typeof ProjectScript.Type; export const ProjectIconColor = Schema.Literals([ "gray", "red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose", ]); export type ProjectIconColor = typeof ProjectIconColor.Type; const ProjectLucideIconName = TrimmedNonEmptyString.check( Schema.isMaxLength(64), Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), ); const ProjectEmoji = TrimmedNonEmptyString.check(Schema.isMaxLength(32)); export const ProjectIconOverride = Schema.Union([ Schema.Struct({ kind: Schema.Literal("lucide"), name: ProjectLucideIconName, color: ProjectIconColor, }), Schema.Struct({ kind: Schema.Literal("emoji"), emoji: ProjectEmoji, }), ]); export type ProjectIconOverride = typeof ProjectIconOverride.Type; export const Project = Schema.Struct({ id: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), faviconPath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), defaultModelSelection: Schema.NullOr(ModelSelection), defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), // Opt-in because background sync performs network I/O and may move the checkout. autoPull: Schema.optional(Schema.Boolean), scripts: Schema.Array(ProjectScript), createdAt: IsoDateTime, updatedAt: IsoDateTime, deletedAt: Schema.NullOr(IsoDateTime), }); export type Project = typeof Project.Type; export const ProjectSnapshot = Schema.Struct({ projects: Schema.Array(Project), updatedAt: IsoDateTime, }); export type ProjectSnapshot = typeof ProjectSnapshot.Type; export const ProjectChange = Schema.Union([ Schema.Struct({ type: Schema.Literal("project.upserted"), project: Project }), Schema.Struct({ type: Schema.Literal("project.deleted"), projectId: ProjectId, deletedAt: IsoDateTime, }), ]); export type ProjectChange = typeof ProjectChange.Type; export const ProjectMutation = Schema.Union([ Schema.Struct({ type: Schema.Literal("project.create"), commandId: CommandId, projectId: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, createWorkspaceRootIfMissing: Schema.optional(Schema.Boolean), defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)), scripts: Schema.optional(Schema.Array(ProjectScript)), }), Schema.Struct({ type: Schema.Literal("project.update"), commandId: CommandId, projectId: ProjectId, title: Schema.optional(TrimmedNonEmptyString), workspaceRoot: Schema.optional(TrimmedNonEmptyString), defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)), autoPull: Schema.optional(Schema.Boolean), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), faviconPath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), scripts: Schema.optional(Schema.Array(ProjectScript)), }), Schema.Struct({ type: Schema.Literal("project.delete"), commandId: CommandId, projectId: ProjectId, force: Schema.optional(Schema.Boolean), }), ]); export type ProjectMutation = typeof ProjectMutation.Type; export class ProjectMutationError extends Schema.TaggedErrorClass()( "ProjectMutationError", { commandId: CommandId, message: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) {} export const ProjectEntryKind = Schema.Literals(["file", "directory"]); export type ProjectEntryKind = typeof ProjectEntryKind.Type; export const ProjectSearchEntriesInput = Schema.Struct({ cwd: TrimmedNonEmptyString, // An empty query is a bounded browse: the index returns frecency-ordered // entries, which the file picker uses for its initial results. query: TrimmedString.check(Schema.isMaxLength(256)), limit: PositiveInt.check(Schema.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT)), kind: Schema.optional(ProjectEntryKind), imageOnly: Schema.optional(Schema.Boolean), }); export type ProjectSearchEntriesInput = typeof ProjectSearchEntriesInput.Type; export const ProjectEntry = Schema.Struct({ path: TrimmedNonEmptyString, kind: ProjectEntryKind, }); export type ProjectEntry = typeof ProjectEntry.Type; export const ProjectSearchEntriesResult = Schema.Struct({ entries: Schema.Array(ProjectEntry), truncated: Schema.Boolean, }); export type ProjectSearchEntriesResult = typeof ProjectSearchEntriesResult.Type; export const ProjectSearchContentsInput = Schema.Struct({ cwd: TrimmedNonEmptyString, // Whitespace is significant in content queries (" foo", regex trailing // spaces), so the query is deliberately not trimmed on the wire. query: Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(256)), limit: PositiveInt.check(Schema.isLessThanOrEqualTo(PROJECT_SEARCH_CONTENTS_MAX_LIMIT)), caseSensitive: Schema.Boolean, wholeWord: Schema.Boolean, useRegex: Schema.Boolean, }); export type ProjectSearchContentsInput = typeof ProjectSearchContentsInput.Type; export const ProjectContentMatchRange = Schema.Struct({ start: NonNegativeInt, end: NonNegativeInt, }); export type ProjectContentMatchRange = typeof ProjectContentMatchRange.Type; export const ProjectContentMatch = Schema.Struct({ path: TrimmedNonEmptyString, lineNumber: PositiveInt, lineContent: Schema.String, matchRanges: Schema.Array(ProjectContentMatchRange), }); export type ProjectContentMatch = typeof ProjectContentMatch.Type; export const ProjectSearchContentsResult = Schema.Struct({ matches: Schema.Array(ProjectContentMatch), truncated: Schema.Boolean, regexFallbackError: Schema.optional(Schema.String), }); export type ProjectSearchContentsResult = typeof ProjectSearchContentsResult.Type; export const ProjectListEntriesInput = Schema.Struct({ cwd: TrimmedNonEmptyString, }); export type ProjectListEntriesInput = typeof ProjectListEntriesInput.Type; export const ProjectListEntriesResult = Schema.Struct({ entries: Schema.Array(ProjectEntry), truncated: Schema.Boolean, }); export type ProjectListEntriesResult = typeof ProjectListEntriesResult.Type; export const ProjectEntriesFailure = Schema.Literals([ "workspace_root_not_found", "workspace_root_create_failed", "workspace_root_stat_failed", "workspace_root_not_directory", "search_index_create_failed", "search_index_scan_timed_out", "search_index_search_failed", ]); export type ProjectEntriesFailure = typeof ProjectEntriesFailure.Type; type ProjectEntriesFailureContext = { readonly failure: ProjectEntriesFailure; readonly normalizedCwd?: string; readonly timeout?: string; readonly detail?: string; readonly cause?: unknown; }; function decodedProjectErrorMessage(props: object): string | undefined { if (!("message" in props)) return undefined; return typeof props.message === "string" ? props.message : undefined; } export class ProjectSearchEntriesError extends Schema.TaggedErrorClass()( "ProjectSearchEntriesError", { cwd: Schema.optional(TrimmedNonEmptyString), queryLength: Schema.optional(NonNegativeInt), limit: Schema.optional(PositiveInt), failure: Schema.optional(ProjectEntriesFailure), normalizedCwd: Schema.optional(TrimmedNonEmptyString), timeout: Schema.optional(TrimmedNonEmptyString), detail: Schema.optional(TrimmedNonEmptyString), message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) { // The structured fields are optional on the wire so newer peers can decode legacy message-only // failures. New application code must provide them through this constructor. // @effect-diagnostics-next-line overriddenSchemaConstructor:off constructor( props: ProjectEntriesFailureContext & { readonly cwd: string; readonly queryLength: number; readonly limit: number; }, ) { super({ ...props, message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`, } as any); } } export class ProjectSearchContentsError extends Schema.TaggedErrorClass()( "ProjectSearchContentsError", { cwd: Schema.optional(TrimmedNonEmptyString), queryLength: Schema.optional(NonNegativeInt), limit: Schema.optional(PositiveInt), failure: Schema.optional(ProjectEntriesFailure), normalizedCwd: Schema.optional(TrimmedNonEmptyString), timeout: Schema.optional(TrimmedNonEmptyString), detail: Schema.optional(TrimmedNonEmptyString), message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) { // @effect-diagnostics-next-line overriddenSchemaConstructor:off constructor( props: ProjectEntriesFailureContext & { readonly cwd: string; readonly queryLength: number; readonly limit: number; }, ) { super({ ...props, message: decodedProjectErrorMessage(props) ?? `Failed to search workspace contents in '${props.cwd}'.`, } as any); } } export class ProjectListEntriesError extends Schema.TaggedErrorClass()( "ProjectListEntriesError", { cwd: Schema.optional(TrimmedNonEmptyString), failure: Schema.optional(ProjectEntriesFailure), normalizedCwd: Schema.optional(TrimmedNonEmptyString), timeout: Schema.optional(TrimmedNonEmptyString), detail: Schema.optional(TrimmedNonEmptyString), message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) { // @effect-diagnostics-next-line overriddenSchemaConstructor:off constructor(props: ProjectEntriesFailureContext & { readonly cwd: string }) { super({ ...props, message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`, } as any); } } export const ProjectReadFileInput = Schema.Struct({ cwd: TrimmedNonEmptyString, // Workspace-relative, or an absolute host path for a file outside the // workspace. Only workspace-relative paths can be written back. relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH)), }); export type ProjectReadFileInput = typeof ProjectReadFileInput.Type; export const ProjectReadFileResult = Schema.Struct({ relativePath: TrimmedNonEmptyString, contents: Schema.String, byteLength: NonNegativeInt, truncated: Schema.Boolean, }); export type ProjectReadFileResult = typeof ProjectReadFileResult.Type; export const ProjectFileFailure = Schema.Literals([ "workspace_path_outside_root", "resolved_path_outside_root", "path_not_file", "binary_file", "operation_failed", ]); export type ProjectFileFailure = typeof ProjectFileFailure.Type; export const ProjectFileOperation = Schema.Literals([ "realpath-workspace-root", "realpath-target", "open", "stat", "read", "close", "make-directory", "write-file", ]); export type ProjectFileOperation = typeof ProjectFileOperation.Type; type ProjectFileFailureContext = { readonly cwd: string; readonly relativePath: string; readonly failure: ProjectFileFailure; readonly resolvedPath?: string; readonly resolvedWorkspaceRoot?: string; readonly operation?: ProjectFileOperation; readonly operationPath?: string; readonly cause?: unknown; }; export class ProjectReadFileError extends Schema.TaggedErrorClass()( "ProjectReadFileError", { cwd: Schema.optional(TrimmedNonEmptyString), relativePath: Schema.optional(TrimmedNonEmptyString), failure: Schema.optional(ProjectFileFailure), resolvedPath: Schema.optional(TrimmedNonEmptyString), resolvedWorkspaceRoot: Schema.optional(TrimmedNonEmptyString), operation: Schema.optional(ProjectFileOperation), operationPath: Schema.optional(TrimmedNonEmptyString), message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) { // @effect-diagnostics-next-line overriddenSchemaConstructor:off constructor(props: ProjectFileFailureContext) { super({ ...props, message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`, } as any); } } export const ProjectWriteFileInput = Schema.Struct({ cwd: TrimmedNonEmptyString, relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)), contents: Schema.String, }); export type ProjectWriteFileInput = typeof ProjectWriteFileInput.Type; export const ProjectWriteFileResult = Schema.Struct({ relativePath: TrimmedNonEmptyString, }); export type ProjectWriteFileResult = typeof ProjectWriteFileResult.Type; export class ProjectWriteFileError extends Schema.TaggedErrorClass()( "ProjectWriteFileError", { cwd: Schema.optional(TrimmedNonEmptyString), relativePath: Schema.optional(TrimmedNonEmptyString), failure: Schema.optional(ProjectFileFailure), resolvedPath: Schema.optional(TrimmedNonEmptyString), resolvedWorkspaceRoot: Schema.optional(TrimmedNonEmptyString), operation: Schema.optional(ProjectFileOperation), operationPath: Schema.optional(TrimmedNonEmptyString), message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) { // @effect-diagnostics-next-line overriddenSchemaConstructor:off constructor(props: ProjectFileFailureContext) { super({ ...props, message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`, } as any); } }