@xfey/tutti 0.1.53 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/artifacts/index.d.ts +3 -3
  2. package/dist/artifacts/index.js +3 -3
  3. package/dist/artifacts/live-manifest.d.ts +23 -0
  4. package/dist/artifacts/live-manifest.js +115 -0
  5. package/dist/artifacts/preview-runtime.d.ts +34 -36
  6. package/dist/artifacts/preview-runtime.js +215 -273
  7. package/dist/artifacts/static-preview.d.ts +2 -1
  8. package/dist/artifacts/static-preview.js +19 -21
  9. package/dist/control-plane/index.d.ts +1 -0
  10. package/dist/control-plane/index.js +13 -2
  11. package/dist/control-plane/types.d.ts +4 -0
  12. package/dist/server-shell/cli/host-server-runtime.js +20 -0
  13. package/dist/server-shell/http/routes/project-api/artifacts-routes.js +43 -110
  14. package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.d.ts +5 -8
  15. package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.js +5 -33
  16. package/dist/server-shell/http/routes/project-api/openapi.d.ts +17 -8
  17. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -2
  18. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +7 -88
  19. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +3 -48
  20. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  21. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  22. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +1 -10
  23. package/package.json +1 -1
  24. package/web/assets/{homepage-motion-scene-F4ibPvI9.js → homepage-motion-scene-C96Na8TV.js} +1 -1
  25. package/web/assets/{index-B1mBtL9x.js → index-CM41hrQd.js} +3 -3
  26. package/web/assets/index-nWy1QP97.css +1 -0
  27. package/web/assets/tutti_avatar-_0Yc-cMJ.png +0 -0
  28. package/web/index.html +2 -2
  29. package/dist/artifacts/manifest.d.ts +0 -41
  30. package/dist/artifacts/manifest.js +0 -169
  31. package/web/assets/index-OFlcQ3eS.css +0 -1
  32. package/web/assets/tutti_avatar-BBhaZGi3.png +0 -0
@@ -1,9 +1,9 @@
1
- import { posix } from "node:path";
1
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, posix, relative, resolve } from "node:path";
2
3
  import { VIEWER_HARD_READ_LIMIT_BYTES } from "../workspace-ops/constants.js";
3
4
  import { WorkspaceOpsError } from "../workspace-ops/errors.js";
4
- import { entryKind, lsPath, readBlob, requireFileEntry } from "../workspace-ops/viewer-git.js";
5
5
  import { requireNormalizedViewerPath } from "../workspace-ops/viewer-paths.js";
6
- import { readArtifactDeclaration } from "./manifest.js";
6
+ import { readLiveArtifactDeclaration, } from "./live-manifest.js";
7
7
  export const ARTIFACT_PREVIEW_CSP = [
8
8
  "default-src 'none'",
9
9
  "script-src 'self' https: 'unsafe-inline' 'unsafe-eval' blob:",
@@ -139,10 +139,7 @@ function requireDeclaredStaticArtifact(declaration) {
139
139
  return declaration;
140
140
  }
141
141
  export function readArtifactStaticPreview(options) {
142
- const declaration = requireDeclaredStaticArtifact(readArtifactDeclaration({
143
- workspaceRoot: options.workspaceRoot,
144
- ...(options.now === undefined ? {} : { now: options.now }),
145
- }));
142
+ const declaration = requireDeclaredStaticArtifact(options.declaration ?? readLiveArtifactDeclaration({ workspaceRoot: options.workspaceRoot }));
146
143
  const runtime = declaration.runtime;
147
144
  if (runtime.kind !== "static") {
148
145
  throw new WorkspaceOpsError("validation_failed", "Artifact is not static");
@@ -150,30 +147,31 @@ export function readArtifactStaticPreview(options) {
150
147
  const requestedPath = requireSafeArtifactAssetPath(decodePath(options.path));
151
148
  const relativePath = requestedPath === "" ? runtime.entry_path : requestedPath;
152
149
  const repoPath = runtime.root_path === "" ? relativePath : posix.join(runtime.root_path, relativePath);
153
- const entry = lsPath(options.workspaceRoot, repoPath);
154
- if (entry === null) {
155
- throw new WorkspaceOpsError("not_found", "Artifact preview file was not found");
156
- }
157
- if (entryKind(entry) !== "file") {
158
- throw new WorkspaceOpsError("validation_failed", "Artifact preview path is not a file");
150
+ let filePath;
151
+ let entrySize;
152
+ try {
153
+ const workspaceRealPath = realpathSync.native(options.workspaceRoot);
154
+ filePath = realpathSync.native(resolve(workspaceRealPath, repoPath));
155
+ const relativePath = relative(workspaceRealPath, filePath);
156
+ if ((relativePath !== "" && (relativePath.startsWith("..") || isAbsolute(relativePath))) ||
157
+ !lstatSync(filePath).isFile()) {
158
+ throw new Error("Artifact preview path is not a contained file");
159
+ }
160
+ entrySize = lstatSync(filePath).size;
159
161
  }
160
- if (entry.size === undefined || !Number.isSafeInteger(entry.size)) {
161
- throw new WorkspaceOpsError("git_failed", "Artifact preview file size is unavailable");
162
+ catch {
163
+ throw new WorkspaceOpsError("not_found", "Artifact preview file was not found");
162
164
  }
163
- if (entry.size > VIEWER_HARD_READ_LIMIT_BYTES) {
165
+ if (entrySize > VIEWER_HARD_READ_LIMIT_BYTES) {
164
166
  throw new WorkspaceOpsError("validation_failed", "Artifact preview file is too large");
165
167
  }
166
- const fileEntry = requireFileEntry({
167
- workspaceRoot: options.workspaceRoot,
168
- path: repoPath,
169
- });
170
168
  const mediaType = mediaTypeFor(repoPath);
171
169
  return {
172
170
  media_type: mediaType,
173
171
  content: rewriteArtifactPreviewResourceUrls({
174
172
  mediaType,
175
173
  resourcePath: relativePath,
176
- content: readBlob(options.workspaceRoot, fileEntry.oid, fileEntry.size),
174
+ content: readFileSync(filePath),
177
175
  }),
178
176
  };
179
177
  }
@@ -15,6 +15,7 @@ export declare class Phase5ControlPlane {
15
15
  private readonly readActiveApproval;
16
16
  private readonly logger;
17
17
  private readonly projectContext;
18
+ private readonly runLifecycle;
18
19
  private readonly now;
19
20
  private readonly scratchpadAutoRefreshScheduler;
20
21
  private lastRecoveryConclusion;
@@ -27,6 +27,7 @@ export class Phase5ControlPlane {
27
27
  readActiveApproval;
28
28
  logger;
29
29
  projectContext;
30
+ runLifecycle;
30
31
  now;
31
32
  scratchpadAutoRefreshScheduler;
32
33
  lastRecoveryConclusion = null;
@@ -39,6 +40,7 @@ export class Phase5ControlPlane {
39
40
  this.readActiveApproval = options.readActiveApproval;
40
41
  this.logger = options.logger;
41
42
  this.projectContext = options.projectContext;
43
+ this.runLifecycle = options.runLifecycle;
42
44
  this.now = options.now ?? (() => new Date());
43
45
  this.engine = new ProcedureEngine({ now: this.now });
44
46
  this.runEngine = new RunEngine({ now: this.now });
@@ -203,7 +205,7 @@ export class Phase5ControlPlane {
203
205
  }
204
206
  }
205
207
  startRunForTask(input) {
206
- return startControlPlaneRunForTask({
208
+ const result = startControlPlaneRunForTask({
207
209
  runEngine: this.runEngine,
208
210
  store: this.store,
209
211
  run: input,
@@ -211,10 +213,19 @@ export class Phase5ControlPlane {
211
213
  resolveWorkflowRunner: this.resolveWorkflowRunner,
212
214
  logger: this.logger,
213
215
  now: this.now,
214
- publishRunTransition: (state) => this.publishRunTransition(state),
216
+ publishRunTransition: (state) => {
217
+ this.publishRunTransition(state);
218
+ if (state.transition_kind === "terminal") {
219
+ this.runLifecycle?.settled();
220
+ }
221
+ },
215
222
  publishTaskRunStarted: (task, activityRef) => this.publishTaskRunStarted(task, activityRef),
216
223
  publishRunResultRecorded: (recorded) => this.publishRunResultRecorded(recorded),
217
224
  });
225
+ if (result.kind === "accepted") {
226
+ this.runLifecycle?.started();
227
+ }
228
+ return result;
218
229
  }
219
230
  readActiveRun() {
220
231
  const runStatus = this.runEngine.getExecutionStatus();
@@ -19,6 +19,10 @@ export type Phase5ControlPlaneOptions = {
19
19
  workspaceRoot: string;
20
20
  projectName: string;
21
21
  };
22
+ runLifecycle?: {
23
+ started: () => void;
24
+ settled: () => void;
25
+ };
22
26
  now?: () => Date;
23
27
  };
24
28
  export type ControlPlaneLogger = Pick<RuntimeLogger, "info">;
@@ -2,6 +2,7 @@ import { basename, join } from "node:path";
2
2
  import { serializeRuntimeLogLine } from "@tutti/shared/utils";
3
3
  import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
4
4
  import { ApprovalRequestManager } from "../../approvals/index.js";
5
+ import { ArtifactPreviewManager } from "../../artifacts/index.js";
5
6
  import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
6
7
  import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
7
8
  import { Phase5ControlPlane } from "../../control-plane/index.js";
@@ -208,12 +209,30 @@ export async function startForegroundHostServer(options) {
208
209
  events: workspaceEvents,
209
210
  now,
210
211
  });
212
+ const artifactPreviewManager = new ArtifactPreviewManager({
213
+ onStatusChange: () => {
214
+ workspaceEvents.publish({
215
+ event_type: "query.invalidate",
216
+ payload: {
217
+ reason: "bulk_state_change",
218
+ message: "Artifacts changed.",
219
+ },
220
+ invalidates: [{ kind: "artifacts" }],
221
+ });
222
+ },
223
+ });
211
224
  const controlPlane = new Phase5ControlPlane({
212
225
  store,
213
226
  events: workspaceEvents,
214
227
  now,
215
228
  logger: controlPlaneLogger,
216
229
  projectContext,
230
+ runLifecycle: {
231
+ started: () => artifactPreviewManager.beginWorkspaceUpdate(),
232
+ settled: () => artifactPreviewManager.settleWorkspaceUpdate({
233
+ workspaceRoot: options.preparation.workspace_root,
234
+ }),
235
+ },
217
236
  readActiveApproval: () => approvalManager.getActiveApproval(),
218
237
  readMainlineStatus: () => ({
219
238
  mainline_clean: readHostRepoProjection(options.preparation.workspace_root).mainline_clean,
@@ -362,6 +381,7 @@ export async function startForegroundHostServer(options) {
362
381
  : { refreshJoinUrl: options.refreshRelayJoinUrl }),
363
382
  },
364
383
  trustedMetadataStore,
384
+ artifactPreviewManager,
365
385
  store,
366
386
  events: workspaceEvents,
367
387
  controlPlane,
@@ -1,9 +1,8 @@
1
- import { HeartbeatArtifactPreviewBodySchema, HeartbeatArtifactPreviewResponseSchema, GetArtifactsResponseSchema, StartArtifactPreviewBodySchema, StartArtifactPreviewResponseSchema, StopArtifactPreviewBodySchema, StopArtifactPreviewResponseSchema, } from "@tutti/shared/schemas/api";
2
- import { ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_CSP, ArtifactPreviewManager, isArtifactPreviewHttpMethod, readArtifactProjection, readArtifactStaticPreview, } from "../../../../artifacts/index.js";
1
+ import { GetArtifactsResponseSchema, } from "@tutti/shared/schemas/api";
2
+ import { ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_CSP, ArtifactPreviewManager, isArtifactPreviewHttpMethod, } from "../../../../artifacts/index.js";
3
3
  import { WorkspaceOpsError } from "../../../../workspace-ops/index.js";
4
- import { executeIdempotentCommand } from "../../../command-idempotency/index.js";
5
4
  import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
6
- import { requireHostProjectSession, requireProjectStore, requireWorkspaceRoot, } from "./session-requirements.js";
5
+ import { requireHostProjectSession, requireWorkspaceRoot, } from "./session-requirements.js";
7
6
  function artifactInvalidates() {
8
7
  return [{ kind: "artifacts" }];
9
8
  }
@@ -32,27 +31,27 @@ function previewRequestBody(body) {
32
31
  }
33
32
  return Buffer.from(JSON.stringify(body), "utf8");
34
33
  }
35
- function requireCurrentArtifactManifestMarker(request, manifestBlobOid) {
36
- const marker = new URL(request.url, "http://artifact.invalid").searchParams.get("v");
37
- if (marker !== null && marker !== manifestBlobOid) {
38
- throw new WorkspaceOpsError("not_found", "Artifact preview manifest is stale");
34
+ function requireCurrentArtifactGeneration(request, previewGeneration) {
35
+ const marker = new URL(request.url, "http://artifact.invalid").searchParams.get("g");
36
+ if (marker !== previewGeneration) {
37
+ throw new WorkspaceOpsError("not_found", "Artifact preview generation is stale");
39
38
  }
40
39
  }
41
40
  export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
42
- const artifactPreviewManager = new ArtifactPreviewManager({
43
- ...options.artifactPreview,
44
- ...(options.now === undefined ? {} : { now: options.now }),
45
- onStatusChange: () => {
46
- workspaceEvents.publish({
47
- event_type: "query.invalidate",
48
- payload: {
49
- reason: "bulk_state_change",
50
- message: "Artifacts changed.",
51
- },
52
- invalidates: artifactInvalidates(),
53
- });
54
- },
55
- });
41
+ const artifactPreviewManager = options.artifactPreviewManager ??
42
+ new ArtifactPreviewManager({
43
+ ...options.artifactPreview,
44
+ onStatusChange: () => {
45
+ workspaceEvents.publish({
46
+ event_type: "query.invalidate",
47
+ payload: {
48
+ reason: "bulk_state_change",
49
+ message: "Artifacts changed.",
50
+ },
51
+ invalidates: artifactInvalidates(),
52
+ });
53
+ },
54
+ });
56
55
  app.addHook("onClose", async () => {
57
56
  await artifactPreviewManager.stopAll();
58
57
  });
@@ -68,6 +67,23 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
68
67
  workspaceRoot: requireWorkspaceRoot(options),
69
68
  });
70
69
  });
70
+ app.post(`${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/lease`, {
71
+ schema: {
72
+ response: {
73
+ 204: { type: "null" },
74
+ },
75
+ },
76
+ }, (request, reply) => {
77
+ if (request.body !== undefined) {
78
+ throw new WorkspaceOpsError("bad_request", "Artifact preview lease does not accept a request body");
79
+ }
80
+ const session = requireHostProjectSession(request, options);
81
+ artifactPreviewManager.renewLease({
82
+ viewerId: session.client_session.client_session_id,
83
+ workspaceRoot: requireWorkspaceRoot(options),
84
+ });
85
+ void reply.code(204).send();
86
+ });
71
87
  app.all("/artifacts/preview/*", {
72
88
  bodyLimit: ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES,
73
89
  }, async (request, reply) => {
@@ -81,12 +97,9 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
81
97
  return;
82
98
  }
83
99
  const workspaceRoot = requireWorkspaceRoot(options);
84
- const projection = readArtifactProjection({
85
- workspaceRoot,
86
- ...(options.now === undefined ? {} : { now: options.now }),
87
- });
100
+ const projection = artifactPreviewManager.readProjection();
88
101
  if (projection.artifact.status === "declared") {
89
- requireCurrentArtifactManifestMarker(request, projection.artifact.manifest_blob_oid);
102
+ requireCurrentArtifactGeneration(request, projection.artifact.preview_generation);
90
103
  }
91
104
  if (projection.artifact.status === "declared" &&
92
105
  projection.artifact.artifact.kind === "server") {
@@ -118,10 +131,11 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
118
131
  .send("Static Artifact previews only support GET and HEAD");
119
132
  return;
120
133
  }
121
- const preview = readArtifactStaticPreview({
134
+ const queryIndex = request.url.indexOf("?");
135
+ const preview = artifactPreviewManager.readStaticPreview({
122
136
  workspaceRoot,
123
137
  path: request.params["*"],
124
- ...(options.now === undefined ? {} : { now: options.now }),
138
+ ...(queryIndex < 0 ? {} : { query: request.url.slice(queryIndex + 1) }),
125
139
  });
126
140
  void reply
127
141
  .header("content-type", preview.media_type)
@@ -130,86 +144,5 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
130
144
  .header("cache-control", "private, max-age=60")
131
145
  .send(preview.content);
132
146
  });
133
- app.post(`${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/start`, {
134
- schema: {
135
- body: StartArtifactPreviewBodySchema,
136
- response: {
137
- 200: StartArtifactPreviewResponseSchema,
138
- },
139
- },
140
- }, (request) => {
141
- requireHostProjectSession(request, options);
142
- const store = requireProjectStore(options);
143
- const invalidates = artifactInvalidates();
144
- const execution = executeIdempotentCommand({
145
- db: store.db,
146
- command_id: request.body.command_id,
147
- command_name: "artifacts.preview.start",
148
- target_ref: "artifacts:active",
149
- payload: request.body.payload,
150
- ...(options.now === undefined ? {} : { now: options.now }),
151
- execute: () => artifactPreviewManager.start({ workspaceRoot: requireWorkspaceRoot(options) }),
152
- });
153
- return {
154
- command_id: execution.command_id,
155
- disposition: execution.disposition,
156
- ...(execution.result === undefined ? {} : { result: execution.result }),
157
- invalidates,
158
- };
159
- });
160
- app.post(`${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/heartbeat`, {
161
- schema: {
162
- body: HeartbeatArtifactPreviewBodySchema,
163
- response: {
164
- 200: HeartbeatArtifactPreviewResponseSchema,
165
- },
166
- },
167
- }, (request) => {
168
- const session = requireHostProjectSession(request, options);
169
- const store = requireProjectStore(options);
170
- const invalidates = artifactInvalidates();
171
- const execution = executeIdempotentCommand({
172
- db: store.db,
173
- command_id: request.body.command_id,
174
- command_name: "artifacts.preview.heartbeat",
175
- target_ref: `artifacts:active:${session.client_session.client_session_id}`,
176
- payload: request.body.payload,
177
- ...(options.now === undefined ? {} : { now: options.now }),
178
- execute: () => artifactPreviewManager.heartbeat({ workspaceRoot: requireWorkspaceRoot(options) }),
179
- });
180
- return {
181
- command_id: execution.command_id,
182
- disposition: execution.disposition,
183
- ...(execution.result === undefined ? {} : { result: execution.result }),
184
- invalidates,
185
- };
186
- });
187
- app.post(`${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/stop`, {
188
- schema: {
189
- body: StopArtifactPreviewBodySchema,
190
- response: {
191
- 200: StopArtifactPreviewResponseSchema,
192
- },
193
- },
194
- }, (request) => {
195
- requireHostProjectSession(request, options);
196
- const store = requireProjectStore(options);
197
- const invalidates = artifactInvalidates();
198
- const execution = executeIdempotentCommand({
199
- db: store.db,
200
- command_id: request.body.command_id,
201
- command_name: "artifacts.preview.stop",
202
- target_ref: "artifacts:active",
203
- payload: request.body.payload,
204
- ...(options.now === undefined ? {} : { now: options.now }),
205
- execute: () => artifactPreviewManager.stop({ workspaceRoot: requireWorkspaceRoot(options) }),
206
- });
207
- return {
208
- command_id: execution.command_id,
209
- disposition: execution.disposition,
210
- ...(execution.result === undefined ? {} : { result: execution.result }),
211
- invalidates,
212
- };
213
- });
214
147
  }
215
148
  //# sourceMappingURL=artifacts-routes.js.map
@@ -11,41 +11,38 @@ export declare const HOST_PROJECT_ARTIFACTS_OPENAPI_ROUTES: ({
11
11
  status: import("@sinclair/typebox").TLiteral<"not_declared">;
12
12
  }>, import("@sinclair/typebox").TObject<{
13
13
  status: import("@sinclair/typebox").TLiteral<"invalid">;
14
- manifest_path: import("@sinclair/typebox").TLiteral<"tutti.artifact.json">;
15
14
  error: import("@sinclair/typebox").TObject<{
16
15
  code: import("@sinclair/typebox").TString;
17
16
  message: import("@sinclair/typebox").TString;
18
17
  }>;
19
18
  }>, import("@sinclair/typebox").TObject<{
20
19
  status: import("@sinclair/typebox").TLiteral<"declared">;
21
- manifest_path: import("@sinclair/typebox").TLiteral<"tutti.artifact.json">;
22
- manifest_blob_oid: import("@sinclair/typebox").TString;
20
+ preview_generation: import("@sinclair/typebox").TString;
23
21
  artifact: import("@sinclair/typebox").TObject<{
24
22
  title: import("@sinclair/typebox").TString;
25
23
  kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"static">, import("@sinclair/typebox").TLiteral<"server">]>;
26
24
  network: import("@sinclair/typebox").TBoolean;
27
25
  }>;
28
26
  preview: import("@sinclair/typebox").TObject<{
29
- status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ready">, import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"stopped">, import("@sinclair/typebox").TLiteral<"failed">]>;
27
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ready">, import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"idle">, import("@sinclair/typebox").TLiteral<"failed">]>;
30
28
  url: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
31
29
  error_message: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
32
30
  }>;
33
31
  }>]>;
34
32
  }>;
35
33
  };
34
+ 204?: never;
36
35
  };
37
- requestBody?: never;
38
36
  } | {
39
37
  method: "post";
40
38
  path: string;
41
39
  summary: string;
42
40
  tags: string[];
43
- requestBody: import("@sinclair/typebox").TSchema;
44
41
  responses: {
45
- 200: {
42
+ 204: {
46
43
  description: string;
47
- schema: import("@sinclair/typebox").TSchema;
48
44
  };
45
+ 200?: never;
49
46
  };
50
47
  })[];
51
48
  //# sourceMappingURL=openapi-artifacts-routes.d.ts.map
@@ -1,4 +1,4 @@
1
- import { GetArtifactsResponseSchema, HeartbeatArtifactPreviewBodySchema, HeartbeatArtifactPreviewResponseSchema, StartArtifactPreviewBodySchema, StartArtifactPreviewResponseSchema, StopArtifactPreviewBodySchema, StopArtifactPreviewResponseSchema, } from "@tutti/shared/schemas/api";
1
+ import { GetArtifactsResponseSchema, } from "@tutti/shared/schemas/api";
2
2
  import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
3
3
  export const HOST_PROJECT_ARTIFACTS_OPENAPI_ROUTES = [
4
4
  {
@@ -15,40 +15,12 @@ export const HOST_PROJECT_ARTIFACTS_OPENAPI_ROUTES = [
15
15
  },
16
16
  {
17
17
  method: "post",
18
- path: `${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/start`,
19
- summary: "Start or reuse the current server artifact preview.",
18
+ path: `${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/lease`,
19
+ summary: "Acquire or renew the current viewer's Artifact preview lease.",
20
20
  tags: ["artifacts"],
21
- requestBody: StartArtifactPreviewBodySchema,
22
21
  responses: {
23
- 200: {
24
- description: "Artifact preview start disposition.",
25
- schema: StartArtifactPreviewResponseSchema,
26
- },
27
- },
28
- },
29
- {
30
- method: "post",
31
- path: `${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/heartbeat`,
32
- summary: "Record a viewer lease heartbeat for the current artifact preview.",
33
- tags: ["artifacts"],
34
- requestBody: HeartbeatArtifactPreviewBodySchema,
35
- responses: {
36
- 200: {
37
- description: "Artifact preview heartbeat disposition.",
38
- schema: HeartbeatArtifactPreviewResponseSchema,
39
- },
40
- },
41
- },
42
- {
43
- method: "post",
44
- path: `${HOST_PROJECT_API_BASE_PATH}/artifacts/preview/stop`,
45
- summary: "Stop the current server artifact preview.",
46
- tags: ["artifacts"],
47
- requestBody: StopArtifactPreviewBodySchema,
48
- responses: {
49
- 200: {
50
- description: "Artifact preview stop disposition.",
51
- schema: StopArtifactPreviewResponseSchema,
22
+ 204: {
23
+ description: "Artifact preview lease renewed.",
52
24
  },
53
25
  },
54
26
  },
@@ -124,41 +124,38 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
124
124
  status: import("@sinclair/typebox").TLiteral<"not_declared">;
125
125
  }>, import("@sinclair/typebox").TObject<{
126
126
  status: import("@sinclair/typebox").TLiteral<"invalid">;
127
- manifest_path: import("@sinclair/typebox").TLiteral<"tutti.artifact.json">;
128
127
  error: import("@sinclair/typebox").TObject<{
129
128
  code: import("@sinclair/typebox").TString;
130
129
  message: import("@sinclair/typebox").TString;
131
130
  }>;
132
131
  }>, import("@sinclair/typebox").TObject<{
133
132
  status: import("@sinclair/typebox").TLiteral<"declared">;
134
- manifest_path: import("@sinclair/typebox").TLiteral<"tutti.artifact.json">;
135
- manifest_blob_oid: import("@sinclair/typebox").TString;
133
+ preview_generation: import("@sinclair/typebox").TString;
136
134
  artifact: import("@sinclair/typebox").TObject<{
137
135
  title: import("@sinclair/typebox").TString;
138
136
  kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"static">, import("@sinclair/typebox").TLiteral<"server">]>;
139
137
  network: import("@sinclair/typebox").TBoolean;
140
138
  }>;
141
139
  preview: import("@sinclair/typebox").TObject<{
142
- status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ready">, import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"stopped">, import("@sinclair/typebox").TLiteral<"failed">]>;
140
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ready">, import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"idle">, import("@sinclair/typebox").TLiteral<"failed">]>;
143
141
  url: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
144
142
  error_message: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
145
143
  }>;
146
144
  }>]>;
147
145
  }>;
148
146
  };
147
+ 204?: never;
149
148
  };
150
- requestBody?: never;
151
149
  } | {
152
150
  method: "post";
153
151
  path: string;
154
152
  summary: string;
155
153
  tags: string[];
156
- requestBody: import("@sinclair/typebox").TSchema;
157
154
  responses: {
158
- 200: {
155
+ 204: {
159
156
  description: string;
160
- schema: import("@sinclair/typebox").TSchema;
161
157
  };
158
+ 200?: never;
162
159
  };
163
160
  } | {
164
161
  method: "get";
@@ -1867,6 +1864,18 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
1867
1864
  };
1868
1865
  };
1869
1866
  requestBody?: never;
1867
+ } | {
1868
+ method: "post";
1869
+ path: string;
1870
+ summary: string;
1871
+ tags: string[];
1872
+ requestBody: import("@sinclair/typebox").TSchema;
1873
+ responses: {
1874
+ 200: {
1875
+ description: string;
1876
+ schema: import("@sinclair/typebox").TSchema;
1877
+ };
1878
+ };
1870
1879
  } | {
1871
1880
  method: "get";
1872
1881
  path: string;
@@ -1,7 +1,7 @@
1
1
  import type { ReadOnlyChatAssistantHandle } from "../../../../chat-assistant/index.js";
2
2
  import type { Phase5ControlPlane } from "../../../../control-plane/index.js";
3
3
  import type { ApprovalRequestManager } from "../../../../approvals/index.js";
4
- import type { ArtifactPreviewManagerOptions } from "../../../../artifacts/index.js";
4
+ import type { ArtifactPreviewManager, ArtifactPreviewManagerOptions } from "../../../../artifacts/index.js";
5
5
  import type { HostProjectStore } from "../../../../store/index.js";
6
6
  import type { ProjectId, RelayProjectRef } from "@tutti/shared/ids";
7
7
  import type { AccountAvatarProjection } from "@tutti/shared/domain";
@@ -73,7 +73,8 @@ export type HostProjectApiCapabilities = {
73
73
  clarification: boolean;
74
74
  };
75
75
  export type HostProjectApiRouteOptions = {
76
- artifactPreview?: Omit<ArtifactPreviewManagerOptions, "now" | "onStatusChange">;
76
+ artifactPreview?: Omit<ArtifactPreviewManagerOptions, "onStatusChange">;
77
+ artifactPreviewManager?: ArtifactPreviewManager;
77
78
  project: {
78
79
  project_id: ProjectId;
79
80
  display_name: string;