mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
1021 lines
33 KiB
TypeScript
1021 lines
33 KiB
TypeScript
import type { ChatMessage, ChatRunStatus, ChatSessionMode } from './chat.js';
|
|
import type { OrchestratorWorkspace } from './workspaces.js';
|
|
import type {
|
|
ProjectContextConnectorRef,
|
|
ProjectContextMcpServerRef,
|
|
ProjectContextPluginRef,
|
|
} from './context.js';
|
|
import type { ProjectSyncIntent, ProjectSyncIntentEvent, ProjectSyncState } from './project-sync.js';
|
|
import type { TeamResourceState } from './team-resources.js';
|
|
import type { WorkspaceCollabContext } from './collab.js';
|
|
|
|
export type ProjectKind =
|
|
| 'prototype'
|
|
| 'deck'
|
|
| 'template'
|
|
| 'other'
|
|
| 'brand'
|
|
| 'image'
|
|
| 'video'
|
|
| 'audio';
|
|
|
|
/**
|
|
* `metadata.videoModel` value that marks a project as HyperFrames.
|
|
*
|
|
* HyperFrames is a local HTML-to-video renderer, so a HyperFrames project is
|
|
* stored as `kind: 'video'` (the Home surface that offers it) even though the
|
|
* artifact the user authors, edits and previews is an HTML composition. Every
|
|
* surface that has to tell HyperFrames apart from a generative video provider
|
|
* — analytics `project_kind`, the system prompt's media contract, the Home
|
|
* starters, delivery validation — keys off this exact value, so it lives here
|
|
* next to `ProjectMetadata` rather than being re-declared per consumer.
|
|
*/
|
|
export const HYPERFRAMES_VIDEO_MODEL = 'hyperframes-html';
|
|
|
|
export type MediaAspect = '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
|
|
|
|
export type ProjectPlatform =
|
|
| 'auto'
|
|
| 'responsive'
|
|
| 'web-desktop'
|
|
| 'mobile-ios'
|
|
| 'mobile-android'
|
|
| 'tablet'
|
|
| 'desktop-app';
|
|
|
|
export type AudioKind = 'music' | 'speech' | 'sfx';
|
|
|
|
export type ProjectScenarioBindingProvenance =
|
|
| 'automatic_default'
|
|
| 'explicit_user'
|
|
| 'legacy_unknown';
|
|
|
|
export type ProjectScenarioTaskProfile =
|
|
| 'prototype'
|
|
| 'ppt'
|
|
| 'marketing'
|
|
| 'hyperframes'
|
|
| 'motion-design';
|
|
|
|
/**
|
|
* Daemon-owned identity for the scenario currently pinned to a project.
|
|
*
|
|
* The snapshot and plugin ids are deliberately stored together: provenance
|
|
* is authoritative only while both still match the live project pin. Old
|
|
* rows that pre-date this contract are migrated to `legacy_unknown` and never
|
|
* acquire automatic-routing authority by inference.
|
|
*/
|
|
export interface ProjectScenarioBinding {
|
|
schemaVersion: 1;
|
|
provenance: ProjectScenarioBindingProvenance;
|
|
pluginId: string;
|
|
snapshotId: string;
|
|
taskProfile?: ProjectScenarioTaskProfile;
|
|
boundAt: number;
|
|
}
|
|
|
|
/**
|
|
* Daemon-owned automatic OD Next route selected at project creation.
|
|
*
|
|
* Unlike `ProjectScenarioBinding`, this identity is independent of an
|
|
* ordinary plugin snapshot. That lets the daemon admit OD Next for a run while
|
|
* retaining the ordinary default plugin solely as an invisible fallback.
|
|
*/
|
|
export interface ProjectStrategyBinding {
|
|
schemaVersion: 1;
|
|
provenance: 'automatic_default';
|
|
taskProfile: ProjectScenarioTaskProfile;
|
|
boundAt: number;
|
|
}
|
|
|
|
/**
|
|
* Daemon-owned identity of the official example card that seeded this project.
|
|
*
|
|
* An example card is a task-type entry point, not a plugin the user chose to
|
|
* run: it keeps the automatic OD Next route and contributes its material as a
|
|
* user-selected Skill. So this binding deliberately carries no `snapshotId`
|
|
* and is never fed into the explicit-plugin tests — naming it must not move
|
|
* the task onto the ordinary route.
|
|
*
|
|
* `pluginSource` is the exact local catalogue identity the daemon re-resolved
|
|
* the record through, and `manifestSourceDigest` pins the bytes it read. A
|
|
* later read that cannot reproduce both is stale and carries no authority.
|
|
*/
|
|
export interface ProjectExampleBinding {
|
|
schemaVersion: 1;
|
|
provenance: 'example_card';
|
|
pluginId: string;
|
|
pluginSource: string;
|
|
manifestSourceDigest: string;
|
|
boundAt: number;
|
|
}
|
|
|
|
export type ProjectDisplayStatus =
|
|
| 'not_started'
|
|
| 'queued'
|
|
| 'running'
|
|
| 'awaiting_input'
|
|
| 'succeeded'
|
|
// Terminal-but-not-really-done: the latest run stamped `succeeded` yet ended
|
|
// with unfinished declared work (a non-`completed` TodoWrite task, or a
|
|
// max_tokens truncation). Kept distinct from `succeeded` so the project pill
|
|
// never reads "Completed" for a run whose work is not actually finished
|
|
// (#1247 / #1060).
|
|
| 'incomplete'
|
|
| 'failed'
|
|
| 'canceled';
|
|
|
|
export interface ProjectStatusInfo {
|
|
value: ProjectDisplayStatus;
|
|
updatedAt?: number;
|
|
runId?: string;
|
|
}
|
|
|
|
export interface PromptTemplateMetadataSource {
|
|
repo: string;
|
|
license: string;
|
|
author?: string;
|
|
url?: string;
|
|
}
|
|
|
|
// Subset of a curated PromptTemplate kept on the project so the agent can
|
|
// reference it on every turn without re-reading the gallery file. The
|
|
// `prompt` field is the (possibly user-edited) body — when the user tunes
|
|
// it in the New Project panel before clicking Create, those edits land
|
|
// here and become authoritative for the system prompt.
|
|
export interface PromptTemplateMetadata {
|
|
id: string;
|
|
surface: 'image' | 'video';
|
|
title: string;
|
|
prompt: string;
|
|
summary?: string;
|
|
category?: string;
|
|
tags?: string[];
|
|
model?: string;
|
|
aspect?: MediaAspect;
|
|
source?: PromptTemplateMetadataSource;
|
|
}
|
|
|
|
/**
|
|
* The local Workspace/member partition from which a catalogue item was picked.
|
|
*
|
|
* This is lookup provenance only. It does not bind the project to a Workspace,
|
|
* prove current membership, or authorize a remote operation.
|
|
*/
|
|
export interface LocalCatalogScope {
|
|
workspaceId: string;
|
|
workspaceMemberId: string;
|
|
}
|
|
|
|
export interface ProjectResourceCatalogScopes {
|
|
skill?: LocalCatalogScope;
|
|
designSystem?: LocalCatalogScope;
|
|
}
|
|
|
|
export type DesignSystemReviewDecision = 'looks-good' | 'needs-work';
|
|
|
|
export type DesignSystemReviewTaskStatus = 'queued' | 'sent' | 'failed';
|
|
|
|
export interface DesignSystemReviewAgentTask {
|
|
status: DesignSystemReviewTaskStatus;
|
|
prompt: string;
|
|
queuedAt: string;
|
|
sentAt?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export interface DesignSystemReviewEntry {
|
|
decision: DesignSystemReviewDecision;
|
|
updatedAt: string;
|
|
feedback?: string;
|
|
files?: string[];
|
|
agentTask?: DesignSystemReviewAgentTask;
|
|
}
|
|
|
|
export interface ProjectMetadata {
|
|
/** Reserved admitted facts; installed only with the matching local content receipt. */
|
|
_commentFiles?: import('./comments.js').CommentFileState;
|
|
kind: ProjectKind;
|
|
// `live-artifact`: the data-backed live dashboard flow (drives the
|
|
// live-artifact skill/system-prompt path). `web-clone`: website reproduction
|
|
// projects from the Home `Website clone` card — stored as prototypes so they
|
|
// keep prototype preview behavior, but the intent routes the `example-web-clone`
|
|
// scenario plugin and splits them into their own `web_clone` analytics kind.
|
|
// `document`: resume/report/PDF projects from the Home `document` card — an
|
|
// analytics-only discriminator (no product behavior keys off it) so a created
|
|
// `other`-kind project reports `project_kind: 'document'` instead of generic
|
|
// `other`. `webgl-experience` and `worker-visualizer`: the powered-preview
|
|
// GPU / off-main-thread scenario cards — analytics-only discriminators for the
|
|
// powered-artifact chips.
|
|
intent?:
|
|
| 'live-artifact'
|
|
| 'web-clone'
|
|
| 'document'
|
|
| 'webgl-experience'
|
|
| 'worker-visualizer'
|
|
| 'marketing'
|
|
| 'hyperframes'
|
|
| 'motion-design';
|
|
fidelity?: 'wireframe' | 'high-fidelity';
|
|
speakerNotes?: boolean;
|
|
slideCount?: string;
|
|
animations?: boolean;
|
|
includeLandingPage?: boolean;
|
|
includeOsWidgets?: boolean;
|
|
templateId?: string;
|
|
templateLabel?: string;
|
|
// Template/gallery duplication provenance. Unlike examplePrompt, this path
|
|
// creates a project by copying existing files and does not launch an agent.
|
|
duplicatedFromPluginId?: string;
|
|
duplicatedFromPluginEntry?: string;
|
|
/** Primary target surface selected at project creation. */
|
|
platform?: ProjectPlatform;
|
|
// Tracks whether the visible name was typed by the user or generated by
|
|
// the create flow. First-prompt auto-naming may only replace generated
|
|
// names so named imports/templates stay stable across conversations.
|
|
nameSource?: 'generated' | 'prompt' | 'agent' | 'user';
|
|
/** Concrete delivery surfaces the artifact must account for. `responsive` is a web breakpoint target, not a native app expansion. */
|
|
platformTargets?: ProjectPlatform[];
|
|
inspirationDesignSystemIds?: string[];
|
|
importedFrom?: 'claude-design' | 'folder' | string;
|
|
entryFile?: string;
|
|
sourceFileName?: string;
|
|
// Folder-import (#597): when set, the project's files live under this
|
|
// absolute path instead of .od/projects/<id>/. OD reads and writes
|
|
// directly inside the user's folder. Stored as the realpath() result so
|
|
// symlinks can't redirect writes after import time.
|
|
baseDir?: string;
|
|
// Project library location that owns baseDir when the project was created
|
|
// under a configured project location root.
|
|
projectLocationId?: string;
|
|
// PR #974: marker stamped by the daemon's HMAC-gated import handler
|
|
// when a folder import passed the desktop-main-process trust gate.
|
|
// Only set on folder-imported projects (`baseDir` set) and only when
|
|
// the import request carried a valid `X-OD-Desktop-Import-Token`
|
|
// signed with the secret the desktop main process registered with the
|
|
// daemon at startup. The desktop `shell.openPath` IPC refuses to
|
|
// forward folder-imported projects whose metadata lacks this marker,
|
|
// so a renderer cannot launder an attacker-chosen baseDir into a
|
|
// file-manager reveal even if a future codepath inadvertently lets
|
|
// it set `baseDir` outside the trusted flow. Privileged: rejected
|
|
// by `POST /api/projects` and `PATCH /api/projects/:id`.
|
|
fromTrustedPicker?: true;
|
|
// Externally prepared scratch workspace provenance. OD may read/write
|
|
// metadata.baseDir, but source authority and writeback stay outside OD.
|
|
orchestratorWorkspace?: OrchestratorWorkspace;
|
|
// Hint stamped by the Home composer working-directory chip. It records
|
|
// where the user wanted the project to live without granting write access
|
|
// to that path; actual filesystem roots still use baseDir/import flows.
|
|
userWorkingDir?: string;
|
|
imageModel?: string;
|
|
imageAspect?: MediaAspect;
|
|
imageStyle?: string;
|
|
videoModel?: string;
|
|
videoLength?: number;
|
|
videoAspect?: MediaAspect;
|
|
audioKind?: AudioKind;
|
|
audioModel?: string;
|
|
audioDuration?: number;
|
|
voice?: string;
|
|
// Curated prompt template the user picked in the image/video tab of the
|
|
// New Project panel. Treated by the system-prompt composer as a stylistic
|
|
// and structural reference for the generation request.
|
|
promptTemplate?: PromptTemplateMetadata;
|
|
// Absolute paths to local code folders the agent can read via --add-dir.
|
|
linkedDirs?: string[];
|
|
// Batch/API-created projects can opt out of the initial discovery form so
|
|
// the first agent turn builds immediately from the submitted brief.
|
|
skipDiscoveryBrief?: boolean;
|
|
// Set when the user submits an unmodified curated example prompt from the
|
|
// gallery. Skips discovery AND requests full-quality direct generation,
|
|
// treating the curated title/brief as the complete creative brief. Honored
|
|
// by both the daemon and contracts (API/BYOK) system-prompt composers so the
|
|
// feature is not mode-dependent.
|
|
examplePrompt?: boolean;
|
|
examplePromptTitle?: string;
|
|
examplePromptBrief?: Record<string, string>;
|
|
// Plugins selected through @ mentions on Home. These are additive
|
|
// context references; the explicit "Use plugin" snapshot, when present,
|
|
// remains the primary executable plugin for the run.
|
|
contextPlugins?: ProjectContextPluginRef[];
|
|
contextMcpServers?: ProjectContextMcpServerRef[];
|
|
contextConnectors?: ProjectContextConnectorRef[];
|
|
/**
|
|
* Daemon-stamped provenance for locally selected project resources. This
|
|
* keeps the first run on the same local catalogue partition when Workspace
|
|
* identity is still loading; it is never Workspace ownership or authority.
|
|
*/
|
|
localCatalogScopes?: ProjectResourceCatalogScopes;
|
|
/** Daemon-owned, exact provenance for the scenario currently pinned here. */
|
|
scenarioBinding?: ProjectScenarioBinding;
|
|
/** Daemon-owned automatic OD Next route, independent of plugin selection. */
|
|
strategyBinding?: ProjectStrategyBinding;
|
|
/** Daemon-owned identity of the official example card that seeded this project. */
|
|
exampleBinding?: ProjectExampleBinding;
|
|
// Stored on design-system projects so the review overview can remember
|
|
// which generated sections were accepted or sent back for another pass.
|
|
designSystemReview?: Record<string, DesignSystemReviewEntry>;
|
|
// Stored on brand extraction projects so the Brands library, project list,
|
|
// and design-system picker can all point back to the same source kit.
|
|
brandId?: string;
|
|
brandSourceUrl?: string;
|
|
brandDesignSystemId?: string;
|
|
// Stored on design-system projects created from an existing project so the
|
|
// generated workspace can point back to the source evidence it duplicated.
|
|
sourceProjectId?: string;
|
|
sourceProjectName?: string;
|
|
// AI-optimize (deep enrichment) state for a programmatically-extracted DS.
|
|
// `programmatic` (or unset) = fast pass only; `ai_refined` = a user-triggered
|
|
// enrichment run completed successfully. Lets analytics compare the two
|
|
// cohorts' retention/usage (tracking spec C15 / §6).
|
|
enrichmentStatus?: 'programmatic' | 'ai_refined';
|
|
enrichmentCompletedAt?: number;
|
|
// Stamped by the daemon when it registers a local placeholder record for a
|
|
// hub-shared project whose content has NOT been materialized locally yet
|
|
// (fresh data root / opened-before-pulled). While set, the daemon refuses
|
|
// to publish the project's local directory to the resource hub — the
|
|
// recvqzaDvUU6B3 fresh-install wipe guard. Cleared by the pull flow once
|
|
// real hub content lands on disk. See the daemon's
|
|
// collab/shared-project-placeholder.ts for the invariant.
|
|
sharedProjectPlaceholderAt?: number;
|
|
}
|
|
|
|
export interface Project {
|
|
id: string;
|
|
name: string;
|
|
skillId: string | null;
|
|
designSystemId: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
status?: ProjectStatusInfo;
|
|
pendingPrompt?: string;
|
|
metadata?: ProjectMetadata;
|
|
// Plan §3.A1 / spec §11.5 — set when the project was created with a
|
|
// plugin pinned (e.g. via PluginLoopHome on the web entry). Lets the
|
|
// UI hide the in-composer plugin rail and render the active plugin
|
|
// as context on user messages instead of re-prompting the user to
|
|
// pick a plugin they already selected.
|
|
appliedPluginSnapshotId?: string;
|
|
customInstructions?: string;
|
|
/**
|
|
* The daemon-authoritative visibility from this project's
|
|
* `workspace_projects` row.
|
|
*
|
|
* This is a read projection, not project storage. It lets clients classify a
|
|
* local project even when that resource is synchronized through a non-project
|
|
* hub (for example, a Design System backing project). Absent means the reader
|
|
* did not carry workspace visibility and must be treated as "no opinion".
|
|
*/
|
|
workspaceVisibility?: ProjectVisibility;
|
|
/**
|
|
* The workspace this project belongs to — exactly one, per the 2026-07-21
|
|
* ruling that 草稿 and shared projects are both bound to a workspace.
|
|
*
|
|
* A read model, not storage: the daemon resolves it from the project's single
|
|
* `workspace_projects` row. Absent means the daemon has not bound the project
|
|
* to a workspace yet (a pre-workspace project awaiting adoption on the next
|
|
* personal-workspace read), NOT "belongs to no workspace" — so a client must
|
|
* treat absence as "no opinion" and never hide a project on the strength of a
|
|
* missing value.
|
|
*/
|
|
workspaceId?: string | null;
|
|
}
|
|
|
|
export interface ProjectTemplate {
|
|
id: string;
|
|
name: string;
|
|
sourceProjectId?: string;
|
|
files: Array<{ name: string; content: string }>;
|
|
description?: string;
|
|
createdAt: number;
|
|
}
|
|
|
|
export interface ProjectBrowserWorkspaceTab {
|
|
id: string;
|
|
insertAfter?: string | null;
|
|
label: string;
|
|
title?: string;
|
|
url?: string;
|
|
iconUrl?: string;
|
|
}
|
|
|
|
export interface ProjectTabsState {
|
|
tabs: string[];
|
|
active: string | null;
|
|
browserTabs?: ProjectBrowserWorkspaceTab[];
|
|
hasSavedState?: boolean;
|
|
updatedAt?: number;
|
|
}
|
|
|
|
export interface Conversation {
|
|
id: string;
|
|
projectId: string;
|
|
title: string | null;
|
|
sessionMode?: ChatSessionMode;
|
|
messageCount?: number;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
totalDurationMs?: number;
|
|
latestRun?: {
|
|
status: ChatRunStatus;
|
|
startedAt?: number;
|
|
endedAt?: number;
|
|
durationMs?: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Exact local catalogue identity of an official example card, as claimed by
|
|
* the client. Both fields are required: the daemon looks the record up by
|
|
* `source` — the same lookup `/api/plugins/:id/apply-local` performs — and
|
|
* rejects the request when the resolved record's id disagrees with `pluginId`.
|
|
*/
|
|
export interface CreateProjectExampleReference {
|
|
pluginId: string;
|
|
source: string;
|
|
}
|
|
|
|
export interface CreateProjectRequest {
|
|
name: string;
|
|
/** Optional project library location id. Omit or use `default` for .od/projects. */
|
|
projectLocationId?: string;
|
|
skillId?: string | null;
|
|
/** Local lookup provenance captured when the Skill was selected. */
|
|
skillCatalogScope?: LocalCatalogScope;
|
|
designSystemId?: string | null;
|
|
/** Local lookup provenance captured when the Design System was selected. */
|
|
designSystemCatalogScope?: LocalCatalogScope;
|
|
pendingPrompt?: string;
|
|
metadata?: ProjectMetadata;
|
|
pluginId?: string;
|
|
/** Exact identity of the local catalogue record selected by the caller. */
|
|
pluginSource?: string;
|
|
appliedPluginSnapshotId?: string;
|
|
pluginInputs?: Record<string, unknown>;
|
|
/** Product-owned automatic OD Next route. The daemon validates and stamps it. */
|
|
automaticStrategyTaskProfile?: ProjectScenarioTaskProfile;
|
|
/**
|
|
* The official example card the user picked under a task type.
|
|
*
|
|
* A claim about an identity, never content: the daemon re-resolves the
|
|
* record through the local catalogue and reads the example's material from
|
|
* disk. Unlike `pluginId`/`appliedPluginSnapshotId` this does not make the
|
|
* project an explicit plugin pin, so the automatic OD Next route stays in
|
|
* force and the example travels as a user-selected Skill instead of
|
|
* replacing the strategy.
|
|
*/
|
|
exampleReference?: CreateProjectExampleReference;
|
|
/** Session mode for the default conversation seeded with the project. */
|
|
conversationMode?: ChatSessionMode;
|
|
customInstructions?: string;
|
|
/** Persisted to metadata.skipDiscoveryBrief for automated project runs. */
|
|
skipDiscoveryBrief?: boolean;
|
|
}
|
|
|
|
export interface UpdateProjectRequest {
|
|
name?: string;
|
|
skillId?: string | null;
|
|
designSystemId?: string | null;
|
|
pendingPrompt?: string | null;
|
|
metadata?: ProjectMetadata | null;
|
|
customInstructions?: string | null;
|
|
}
|
|
|
|
export interface ProjectsResponse {
|
|
projects: Project[];
|
|
}
|
|
|
|
export interface ProjectResponse {
|
|
project: Project;
|
|
}
|
|
|
|
export interface RestoreProjectAutomaticScenarioRequest {
|
|
/** Compare-and-swap guard for the project pin the caller inspected. */
|
|
expectedCurrentSnapshotId: string | null;
|
|
}
|
|
|
|
export interface RestoreProjectAutomaticScenarioResponse extends ProjectResponse {
|
|
scenarioBinding?: ProjectScenarioBinding;
|
|
strategyBinding?: ProjectStrategyBinding;
|
|
changed: boolean;
|
|
}
|
|
|
|
export type ProjectDesignTokenSuggestionProp =
|
|
| 'color'
|
|
| 'backgroundColor'
|
|
| 'borderColor'
|
|
| 'fontFamily'
|
|
| 'fontSize'
|
|
| 'fontWeight'
|
|
| 'lineHeight'
|
|
| 'letterSpacing'
|
|
| 'width'
|
|
| 'height'
|
|
| 'gap'
|
|
| 'padding'
|
|
| 'margin'
|
|
| 'borderRadius'
|
|
| 'borderWidth';
|
|
|
|
export interface ProjectDesignTokenSuggestionQuery {
|
|
file?: string;
|
|
targetId?: string;
|
|
props?: ProjectDesignTokenSuggestionProp[];
|
|
values?: Partial<Record<ProjectDesignTokenSuggestionProp, string>>;
|
|
}
|
|
|
|
export interface ProjectDesignTokenSuggestion {
|
|
prop: ProjectDesignTokenSuggestionProp;
|
|
token: string;
|
|
value: string;
|
|
sourceFile: string;
|
|
line: number;
|
|
matchReason: string;
|
|
score: number;
|
|
}
|
|
|
|
export interface ProjectDesignTokenSuggestionsResponse {
|
|
projectId: string;
|
|
query: ProjectDesignTokenSuggestionQuery;
|
|
suggestions: ProjectDesignTokenSuggestion[];
|
|
}
|
|
|
|
// Response body for `GET /api/projects/:id`. Carries the same `project`
|
|
// payload as `ProjectResponse` plus a derived `resolvedDir` so the web
|
|
// client can address the on-disk working directory directly (e.g. for
|
|
// `shell.openPath` from the desktop bridge). For folder-imported projects
|
|
// `resolvedDir === metadata.baseDir`; for native projects it is
|
|
// `path.join(<daemon projects root>, project.id)`. Computed server-side via
|
|
// `resolveProjectDir(...)` so the web client never reconstructs the path.
|
|
export interface ProjectDetailResponse extends ProjectResponse {
|
|
resolvedDir: string;
|
|
}
|
|
|
|
export type ProjectVisibility = 'personal' | 'team';
|
|
|
|
/**
|
|
* Daemon-authoritative workspace and billing scope for one persisted project.
|
|
*
|
|
* `visibility` answers whether the project itself is a private draft or shared
|
|
* with the team. It is deliberately independent from `kind`: a private draft
|
|
* may still belong to a team workspace and therefore use that workspace's
|
|
* wallet. The tagged union prevents clients from treating every non-null
|
|
* workspace id as a team-billing scope.
|
|
*/
|
|
export type ProjectWorkspaceScope =
|
|
| {
|
|
kind: 'unbound';
|
|
projectId: string;
|
|
workspaceId: null;
|
|
context: null;
|
|
}
|
|
| {
|
|
kind: 'unavailable';
|
|
projectId: string;
|
|
workspaceId: string;
|
|
visibility: ProjectVisibility;
|
|
context: null;
|
|
}
|
|
| {
|
|
kind: 'personal';
|
|
projectId: string;
|
|
workspaceId: string;
|
|
visibility: ProjectVisibility;
|
|
context: WorkspaceCollabContext & { workspaceType: 'personal' };
|
|
}
|
|
| {
|
|
kind: 'team';
|
|
projectId: string;
|
|
workspaceId: string;
|
|
visibility: ProjectVisibility;
|
|
context: WorkspaceCollabContext & { workspaceType: 'team' };
|
|
};
|
|
|
|
/** GET /api/projects/:id/workspace-scope. */
|
|
export interface ProjectWorkspaceScopeResponse {
|
|
scope: ProjectWorkspaceScope;
|
|
}
|
|
|
|
// Local D-lane placeholder until the B-owned CurrentWorkspaceContext is
|
|
// imported into open-design. The route adapter keeps this replaceable.
|
|
export type WorkspaceProjectRole = 'owner' | 'admin' | 'member';
|
|
|
|
// C owns project sync orchestration. D exposes this on its read model and emits
|
|
// intent metadata when visibility changes, but it does not upload or mirror
|
|
// project content directly.
|
|
export type WorkspaceProjectSyncIntentEvent = ProjectSyncIntentEvent;
|
|
|
|
export type WorkspaceProjectSyncIntent = ProjectSyncIntent;
|
|
|
|
export type ProjectDisabledReason =
|
|
| 'workspace_locked'
|
|
| 'workspace_deleted'
|
|
| 'permission_denied'
|
|
| 'sync_pending'
|
|
| 'resource_frozen';
|
|
|
|
export interface ProjectAccessFlags {
|
|
canOpen: boolean;
|
|
canRename: boolean;
|
|
canDelete: boolean;
|
|
canDuplicate: boolean;
|
|
canMoveToTeam: boolean;
|
|
canMoveToPersonal: boolean;
|
|
canExport: boolean;
|
|
canSendTo: boolean;
|
|
canRestoreVersion: boolean;
|
|
disabledReason?: ProjectDisabledReason;
|
|
}
|
|
|
|
export interface WorkspaceProjectSummary {
|
|
id: string;
|
|
name: string;
|
|
workspaceId: string;
|
|
visibility: ProjectVisibility;
|
|
resourceState: TeamResourceState;
|
|
createdByWorkspaceMemberId: string | null;
|
|
updatedByWorkspaceMemberId?: string | null;
|
|
/**
|
|
* E resource-hub mapping seam. When present, this is Spec E's
|
|
* ResourceRecord.id for a `kind: 'project'` cloud resource. Personal projects
|
|
* are local-only and normally keep this null; team-visible projects receive
|
|
* the cloud resource through C's orchestration of E's upload/version/mirror
|
|
* mechanism.
|
|
*/
|
|
resourceHubResourceId?: string | null;
|
|
/** Set when a previously shared cloud resource should be tombstoned. */
|
|
cloudTombstonedAt?: number | null;
|
|
currentUserAccess: ProjectAccessFlags;
|
|
syncState?: ProjectSyncState;
|
|
pendingSyncIntent?: WorkspaceProjectSyncIntent;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
metadata?: ProjectMetadata;
|
|
project: Project;
|
|
}
|
|
|
|
export interface WorkspaceProjectsResponse {
|
|
projects: WorkspaceProjectSummary[];
|
|
}
|
|
|
|
export interface MoveWorkspaceProjectRequest {
|
|
visibility: ProjectVisibility;
|
|
}
|
|
|
|
export interface BatchDeleteWorkspaceProjectsRequest {
|
|
projectIds: string[];
|
|
}
|
|
|
|
export interface BatchMoveWorkspaceProjectsRequest {
|
|
projectIds: string[];
|
|
visibility: ProjectVisibility;
|
|
}
|
|
|
|
export interface CreateProjectResponse extends ProjectResponse {
|
|
conversationId?: string;
|
|
appliedPluginSnapshotId?: string;
|
|
}
|
|
|
|
export interface CreateDesignSystemProjectFromProjectRequest {
|
|
name?: string;
|
|
pendingPrompt?: string;
|
|
}
|
|
|
|
export interface CreateDesignSystemProjectFromProjectResponse extends ProjectResponse {
|
|
conversationId: string;
|
|
designSystemId: string;
|
|
copiedFiles: string[];
|
|
}
|
|
|
|
export interface DuplicateProjectRequest {
|
|
name?: string;
|
|
}
|
|
|
|
export interface DuplicateProjectResponse extends ProjectResponse {
|
|
conversationId: string;
|
|
copiedFiles: string[];
|
|
}
|
|
|
|
export interface ProjectLocation {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
builtIn?: boolean;
|
|
}
|
|
|
|
export interface ProjectLocationsResponse {
|
|
locations: ProjectLocation[];
|
|
removedProjectIds?: string[];
|
|
}
|
|
|
|
export interface UpdateProjectLocationsRequest {
|
|
locations: Array<{ id?: string; name?: string; path: string }>;
|
|
}
|
|
|
|
export interface ProjectManifest {
|
|
schemaVersion: 1;
|
|
id: string;
|
|
name: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
skillId?: string | null;
|
|
designSystemId?: string | null;
|
|
}
|
|
|
|
export interface ScanProjectLocationsResponse {
|
|
scanned: number;
|
|
imported: Project[];
|
|
existing: string[];
|
|
skipped: Array<{ path: string; reason: string }>;
|
|
}
|
|
|
|
// POST /api/import/folder — create a project rooted at an existing local
|
|
// folder. The submitted baseDir is stored as the project's metadata.baseDir
|
|
// (after realpath canonicalization) and OD reads/writes directly inside it.
|
|
// The user owns version control; OD does not snapshot or copy.
|
|
export interface ImportFolderRequest {
|
|
baseDir: string;
|
|
name?: string;
|
|
skillId?: string | null;
|
|
designSystemId?: string | null;
|
|
orchestratorWorkspace?: OrchestratorWorkspace;
|
|
}
|
|
|
|
export interface ImportFolderResponse {
|
|
project: Project;
|
|
conversationId: string;
|
|
entryFile: string | null;
|
|
}
|
|
|
|
export interface ReplaceProjectWorkingDirRequest {
|
|
baseDir: string;
|
|
orchestratorWorkspace?: OrchestratorWorkspace;
|
|
}
|
|
|
|
export interface ReplaceProjectWorkingDirResponse {
|
|
project: Project;
|
|
baseDir: string;
|
|
entryFile: string | null;
|
|
}
|
|
|
|
export interface ConversationsResponse {
|
|
conversations: Conversation[];
|
|
}
|
|
|
|
export interface ConversationResponse {
|
|
conversation: Conversation;
|
|
}
|
|
|
|
export interface CreateConversationRequest {
|
|
title?: string | null;
|
|
sessionMode?: ChatSessionMode;
|
|
/**
|
|
* Seed the new conversation with another conversation's context by copying
|
|
* its messages. The source must belong to the same project; a missing or
|
|
* foreign id is ignored and an empty conversation is created. Powers the
|
|
* "Side Chat" launcher, which forks the current chat's context into a new
|
|
* conversation.
|
|
*/
|
|
seedFromConversationId?: string | null;
|
|
/**
|
|
* When paired with `seedFromConversationId`, copy only source messages up to
|
|
* and including this message. Used by the chat "Fork" action so the new
|
|
* conversation resumes from a specific assistant turn instead of inheriting
|
|
* future follow-ups from the source conversation.
|
|
*/
|
|
forkAfterMessageId?: string | null;
|
|
/**
|
|
* One compact client-side message used only when `forkAfterMessageId` is not
|
|
* present in the source conversation's persisted history. The daemon appends
|
|
* it to the persisted prefix, which preserves an errored/unpersisted final
|
|
* assistant turn without making normal forks upload the entire transcript.
|
|
*/
|
|
forkFallbackMessage?: ChatMessage;
|
|
/**
|
|
* The persisted message immediately before `forkFallbackMessage`, or null
|
|
* when the fallback is the first message. The daemon cuts persisted history
|
|
* at this boundary before appending the fallback, so later turns cannot leak
|
|
* into a fork from an older unpersisted assistant message.
|
|
*/
|
|
forkFallbackPredecessorMessageId?: string | null;
|
|
/**
|
|
* Client-supplied snapshot of the messages to seed the fork with, in order,
|
|
* up to and including the fork point. When present, the daemon copies these
|
|
* instead of reading the source conversation from the database by id. This
|
|
* makes "Fork" resilient to a fork point that was never persisted — e.g. an
|
|
* assistant turn whose run errored or had its connection reset before the
|
|
* message reached the database. Without it, such a fork would 404 on
|
|
* `forkAfterMessageId` and silently fail. When absent, the daemon falls back
|
|
* to copying from `seedFromConversationId` (the original Side Chat path).
|
|
*
|
|
* @deprecated Retained for older clients. New clients should first fork from
|
|
* persisted history and use `forkFallbackMessage` only after a missing fork
|
|
* point response.
|
|
*/
|
|
seedMessages?: ChatMessage[];
|
|
}
|
|
|
|
export interface UpdateConversationRequest {
|
|
title?: string | null;
|
|
sessionMode?: ChatSessionMode;
|
|
}
|
|
|
|
export interface MessagesResponse {
|
|
messages: ChatMessage[];
|
|
}
|
|
|
|
export type DeployProviderId = 'vercel-self' | 'cloudflare-pages';
|
|
export type DeploymentStatus =
|
|
| 'deploying'
|
|
| 'preparing-link'
|
|
| 'ready'
|
|
| 'link-delayed'
|
|
| 'protected'
|
|
| 'failed';
|
|
|
|
export interface CloudflarePagesConfigHints {
|
|
lastZoneId?: string;
|
|
lastZoneName?: string;
|
|
lastDomainPrefix?: string;
|
|
}
|
|
|
|
export interface CloudflarePagesZoneInfo {
|
|
id: string;
|
|
name: string;
|
|
status?: string;
|
|
type?: string;
|
|
}
|
|
|
|
export interface CloudflarePagesZonesResponse {
|
|
zones: CloudflarePagesZoneInfo[];
|
|
cloudflarePages?: CloudflarePagesConfigHints;
|
|
}
|
|
|
|
export interface CloudflarePagesDeploySelection {
|
|
zoneId: string;
|
|
zoneName: string;
|
|
domainPrefix: string;
|
|
}
|
|
|
|
export type DeploymentLinkStatus =
|
|
| 'ready'
|
|
| 'link-delayed'
|
|
| 'protected'
|
|
| 'failed';
|
|
|
|
export interface DeploymentLinkInfo {
|
|
url: string;
|
|
status: DeploymentLinkStatus;
|
|
statusMessage?: string;
|
|
reachableAt?: number;
|
|
}
|
|
|
|
export type CloudflarePagesDnsStatus =
|
|
| 'skipped'
|
|
| 'created'
|
|
| 'reused'
|
|
| 'unmarked'
|
|
| 'patched'
|
|
| 'conflict'
|
|
| 'failed';
|
|
|
|
export type CloudflarePagesDomainStatus =
|
|
| 'skipped'
|
|
| 'pending'
|
|
| 'active'
|
|
| 'conflict'
|
|
| 'failed';
|
|
|
|
export type CloudflarePagesCustomDomainStatus =
|
|
| 'pending'
|
|
| 'ready'
|
|
| 'conflict'
|
|
| 'failed';
|
|
|
|
export type CloudflarePagesDnsOwnership = 'marked' | 'unmarked' | 'external';
|
|
|
|
export interface CloudflarePagesCustomDomainInfo {
|
|
hostname: string;
|
|
url: string;
|
|
zoneId: string;
|
|
zoneName: string;
|
|
domainPrefix: string;
|
|
status: CloudflarePagesCustomDomainStatus;
|
|
statusMessage?: string;
|
|
errorCode?: string;
|
|
errorMessage?: string;
|
|
dnsStatus?: CloudflarePagesDnsStatus;
|
|
dnsRecordId?: string;
|
|
dnsOwnership?: CloudflarePagesDnsOwnership;
|
|
domainStatus?: CloudflarePagesDomainStatus;
|
|
pagesDomainStatus?: string;
|
|
validationData?: unknown;
|
|
verificationData?: unknown;
|
|
}
|
|
|
|
export interface CloudflarePagesDeploymentInfo {
|
|
projectName: string;
|
|
pagesDev: DeploymentLinkInfo;
|
|
customDomain?: CloudflarePagesCustomDomainInfo;
|
|
}
|
|
|
|
export interface DeployConfigResponse {
|
|
providerId: DeployProviderId;
|
|
configured: boolean;
|
|
tokenMask: string;
|
|
teamId: string;
|
|
teamSlug: string;
|
|
accountId?: string;
|
|
projectName?: string;
|
|
cloudflarePages?: CloudflarePagesConfigHints;
|
|
target: 'preview' | 'production';
|
|
}
|
|
|
|
export interface UpdateDeployConfigRequest {
|
|
providerId?: DeployProviderId;
|
|
token?: string;
|
|
teamId?: string;
|
|
teamSlug?: string;
|
|
accountId?: string;
|
|
projectName?: string;
|
|
cloudflarePages?: CloudflarePagesConfigHints;
|
|
}
|
|
|
|
export interface DeploymentInfo {
|
|
id: string;
|
|
projectId: string;
|
|
fileName: string;
|
|
providerId: DeployProviderId;
|
|
url: string;
|
|
deploymentId?: string;
|
|
deploymentCount: number;
|
|
target: 'preview' | 'production';
|
|
status: DeploymentStatus;
|
|
statusMessage?: string;
|
|
reachableAt?: number;
|
|
cloudflarePages?: CloudflarePagesDeploymentInfo;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface ProjectDeploymentsResponse {
|
|
deployments: DeploymentInfo[];
|
|
}
|
|
|
|
export interface DeployProjectFileRequest {
|
|
fileName: string;
|
|
providerId?: DeployProviderId;
|
|
cloudflarePages?: CloudflarePagesDeploySelection;
|
|
target?: 'preview' | 'production';
|
|
}
|
|
|
|
export interface DeployProjectFileResponse extends DeploymentInfo {}
|
|
|
|
export interface CheckDeploymentLinkResponse extends DeploymentInfo {}
|
|
|
|
// Preflight inspects the file set that would be uploaded for a deploy
|
|
// without sending anything to the provider. Lets the UI show file count,
|
|
// total size, and warnings before the user pays the network round-trip.
|
|
|
|
export type DeployPreflightWarningCode =
|
|
| 'broken-reference'
|
|
| 'invalid-reference'
|
|
| 'large-asset'
|
|
| 'large-bundle'
|
|
| 'large-html'
|
|
| 'external-script'
|
|
| 'external-stylesheet'
|
|
| 'no-doctype'
|
|
| 'no-viewport'
|
|
/** The entry would collide with the project's root index.html; deploy is refused with ENTRY_INDEX_CONFLICT. */
|
|
| 'entry-index-conflict';
|
|
|
|
export interface DeployPreflightWarning {
|
|
code: DeployPreflightWarningCode;
|
|
message: string;
|
|
path?: string;
|
|
url?: string;
|
|
size?: number;
|
|
}
|
|
|
|
export interface DeployPreflightFile {
|
|
path: string;
|
|
size: number;
|
|
mime: string;
|
|
sourcePath: string;
|
|
}
|
|
|
|
export interface DeployPreflightRequest {
|
|
fileName: string;
|
|
providerId?: DeployProviderId;
|
|
}
|
|
|
|
export interface DeployPreflightResponse {
|
|
providerId: DeployProviderId;
|
|
entry: string;
|
|
files: DeployPreflightFile[];
|
|
totalFiles: number;
|
|
totalBytes: number;
|
|
warnings: DeployPreflightWarning[];
|
|
}
|