@xfey/tutti 0.1.53 → 0.1.55

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 (63) hide show
  1. package/README.md +5 -2
  2. package/desktop-assets/README.md +13 -0
  3. package/desktop-assets/Tutti-square.png +0 -0
  4. package/desktop-assets/Tutti.icns +0 -0
  5. package/desktop-assets/macos-handler.applescript +24 -0
  6. package/dist/artifacts/index.d.ts +3 -3
  7. package/dist/artifacts/index.js +3 -3
  8. package/dist/artifacts/live-manifest.d.ts +23 -0
  9. package/dist/artifacts/live-manifest.js +115 -0
  10. package/dist/artifacts/preview-runtime.d.ts +34 -36
  11. package/dist/artifacts/preview-runtime.js +215 -273
  12. package/dist/artifacts/static-preview.d.ts +2 -1
  13. package/dist/artifacts/static-preview.js +19 -21
  14. package/dist/control-plane/index.d.ts +1 -0
  15. package/dist/control-plane/index.js +13 -2
  16. package/dist/control-plane/types.d.ts +4 -0
  17. package/dist/server-shell/cli/args.d.ts +7 -0
  18. package/dist/server-shell/cli/args.js +32 -0
  19. package/dist/server-shell/cli/cli.js +76 -1
  20. package/dist/server-shell/cli/errors.d.ts +1 -1
  21. package/dist/server-shell/cli/host-server-runtime.js +20 -0
  22. package/dist/server-shell/cli/version.d.ts +6 -0
  23. package/dist/server-shell/cli/version.js +11 -2
  24. package/dist/server-shell/desktop-integration/index.d.ts +5 -0
  25. package/dist/server-shell/desktop-integration/index.js +5 -0
  26. package/dist/server-shell/desktop-integration/linux.d.ts +12 -0
  27. package/dist/server-shell/desktop-integration/linux.js +186 -0
  28. package/dist/server-shell/desktop-integration/macos.d.ts +9 -0
  29. package/dist/server-shell/desktop-integration/macos.js +138 -0
  30. package/dist/server-shell/desktop-integration/manager.d.ts +40 -0
  31. package/dist/server-shell/desktop-integration/manager.js +223 -0
  32. package/dist/server-shell/desktop-integration/platform.d.ts +23 -0
  33. package/dist/server-shell/desktop-integration/platform.js +21 -0
  34. package/dist/server-shell/desktop-integration/protocol.d.ts +9 -0
  35. package/dist/server-shell/desktop-integration/protocol.js +28 -0
  36. package/dist/server-shell/desktop-integration/record.d.ts +17 -0
  37. package/dist/server-shell/desktop-integration/record.js +74 -0
  38. package/dist/server-shell/http/routes/project-api/artifacts-routes.js +43 -110
  39. package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.d.ts +5 -8
  40. package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.js +5 -33
  41. package/dist/server-shell/http/routes/project-api/openapi.d.ts +17 -8
  42. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -2
  43. package/dist/server-shell/local-console/invocation-context.d.ts +7 -0
  44. package/dist/server-shell/local-console/invocation-context.js +8 -9
  45. package/dist/server-shell/local-console/managed-console.d.ts +1 -0
  46. package/dist/server-shell/local-console/managed-console.js +10 -5
  47. package/dist/server-shell/local-console/project-service.js +1 -1
  48. package/dist/server-shell/local-console/server.js +23 -6
  49. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +7 -88
  50. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +3 -48
  51. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  52. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  53. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +1 -10
  54. package/package.json +2 -1
  55. package/web/assets/{homepage-motion-scene-F4ibPvI9.js → homepage-motion-scene-D0-wLq3D.js} +1 -1
  56. package/web/assets/{index-B1mBtL9x.js → index-BAsLSZ5U.js} +14 -14
  57. package/web/assets/index-DMaiDZ9k.css +1 -0
  58. package/web/assets/tutti_avatar-_0Yc-cMJ.png +0 -0
  59. package/web/index.html +2 -2
  60. package/dist/artifacts/manifest.d.ts +0 -41
  61. package/dist/artifacts/manifest.js +0 -169
  62. package/web/assets/index-OFlcQ3eS.css +0 -1
  63. package/web/assets/tutti_avatar-BBhaZGi3.png +0 -0
@@ -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;
@@ -1,14 +1,21 @@
1
1
  export declare const LOCAL_CONSOLE_INVOCATION_ENV_KEYS: readonly ["DBUS_SESSION_BUS_ADDRESS", "DISPLAY", "LANG", "LC_ALL", "LC_CTYPE", "PATH", "TUTTI_HOST_ENDPOINT_HOST", "TUTTI_HOST_ENDPOINT_PORT", "TUTTI_RELAY_URL", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP"];
2
2
  export type LocalConsoleInvocationEnvironment = Partial<Record<(typeof LOCAL_CONSOLE_INVOCATION_ENV_KEYS)[number], string>>;
3
3
  export type LocalConsoleInvocationContext = {
4
+ source: "terminal";
4
5
  currentDirectory: string;
5
6
  environment: LocalConsoleInvocationEnvironment;
7
+ } | {
8
+ source: "desktop";
9
+ environment: LocalConsoleInvocationEnvironment;
6
10
  };
7
11
  export declare function collectLocalConsoleInvocationEnvironment(env: NodeJS.ProcessEnv): LocalConsoleInvocationEnvironment;
8
12
  export declare function createLocalConsoleInvocationContext(options: {
9
13
  cwd: string;
10
14
  env: NodeJS.ProcessEnv;
11
15
  }): LocalConsoleInvocationContext;
16
+ export declare function createDesktopLocalConsoleInvocationContext(options: {
17
+ env: NodeJS.ProcessEnv;
18
+ }): LocalConsoleInvocationContext;
12
19
  export declare function createLocalConsoleServiceEnvironment(options: {
13
20
  env: NodeJS.ProcessEnv;
14
21
  tuttiHome: string;
@@ -24,21 +24,20 @@ export function collectLocalConsoleInvocationEnvironment(env) {
24
24
  }
25
25
  export function createLocalConsoleInvocationContext(options) {
26
26
  return {
27
+ source: "terminal",
27
28
  currentDirectory: resolve(options.cwd),
28
29
  environment: collectLocalConsoleInvocationEnvironment(options.env),
29
30
  };
30
31
  }
32
+ export function createDesktopLocalConsoleInvocationContext(options) {
33
+ return {
34
+ source: "desktop",
35
+ environment: collectLocalConsoleInvocationEnvironment(options.env),
36
+ };
37
+ }
31
38
  export function createLocalConsoleServiceEnvironment(options) {
32
39
  const environment = { TUTTI_HOME: options.tuttiHome };
33
- for (const key of [
34
- "HOME",
35
- "LANG",
36
- "LC_ALL",
37
- "LC_CTYPE",
38
- "LOGNAME",
39
- "TMPDIR",
40
- "USER",
41
- ]) {
40
+ for (const key of ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME", "TMPDIR", "USER"]) {
42
41
  const value = options.env[key];
43
42
  if (value !== undefined && value !== "") {
44
43
  environment[key] = value;
@@ -24,6 +24,7 @@ export declare function ensureLocalConsole(options?: {
24
24
  cwd?: string;
25
25
  env?: NodeJS.ProcessEnv;
26
26
  fetchImpl?: FetchLike;
27
+ source?: "terminal" | "desktop";
27
28
  }): Promise<{
28
29
  url: string;
29
30
  endpoint: LocalConsoleRuntimeEndpoint;
@@ -3,7 +3,7 @@ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, stat
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveTuttiHome } from "../../providers/openai/index.js";
5
5
  import { readCliVersion } from "../cli/version.js";
6
- import { createLocalConsoleInvocationContext, createLocalConsoleServiceEnvironment, } from "./invocation-context.js";
6
+ import { createLocalConsoleInvocationContext, createDesktopLocalConsoleInvocationContext, createLocalConsoleServiceEnvironment, } from "./invocation-context.js";
7
7
  import { withLocalConsoleLifecycleLock } from "./lifecycle-lock.js";
8
8
  import { deleteLocalConsoleRuntimeEndpoint, readLocalConsoleRuntimeEndpoint, } from "./runtime-endpoint.js";
9
9
  import { LOCAL_CONSOLE_API_BASE } from "./server.js";
@@ -208,7 +208,6 @@ async function ensureConsoleEndpointUnlocked(options) {
208
208
  }
209
209
  }
210
210
  async function issueBrowserAccess(options) {
211
- const context = createLocalConsoleInvocationContext({ cwd: options.cwd, env: options.env });
212
211
  const response = await options.fetchImpl(`${options.endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
213
212
  method: "POST",
214
213
  headers: {
@@ -217,8 +216,11 @@ async function issueBrowserAccess(options) {
217
216
  "content-type": "application/json",
218
217
  },
219
218
  body: JSON.stringify({
220
- current_directory: context.currentDirectory,
221
- environment: context.environment,
219
+ source: options.context.source,
220
+ ...(options.context.source === "terminal"
221
+ ? { current_directory: options.context.currentDirectory }
222
+ : {}),
223
+ environment: options.context.environment,
222
224
  }),
223
225
  signal: AbortSignal.timeout(3_000),
224
226
  });
@@ -240,9 +242,12 @@ export async function ensureLocalConsole(options = {}) {
240
242
  tuttiHome,
241
243
  run: async () => await ensureConsoleEndpointUnlocked({ cwd, env, tuttiHome, fetchImpl }),
242
244
  });
245
+ const context = options.source === "desktop"
246
+ ? createDesktopLocalConsoleInvocationContext({ env })
247
+ : createLocalConsoleInvocationContext({ cwd, env });
243
248
  return {
244
249
  endpoint,
245
- url: await issueBrowserAccess({ endpoint, cwd, env, fetchImpl }),
250
+ url: await issueBrowserAccess({ endpoint, context, fetchImpl }),
246
251
  };
247
252
  }
248
253
  export async function readLocalConsoleServiceStatus(options = {}) {
@@ -164,7 +164,7 @@ export class LocalConsoleProjectService {
164
164
  }
165
165
  #operationOptions(context) {
166
166
  return {
167
- cwd: context.currentDirectory,
167
+ cwd: context.source === "terminal" ? context.currentDirectory : this.#tuttiHome,
168
168
  env: createLocalConsoleOperationEnvironment({
169
169
  serviceEnvironment: this.#serviceEnvironment,
170
170
  invocationEnvironment: context.environment,
@@ -159,9 +159,10 @@ export function createLocalConsoleServer(options) {
159
159
  schema: {
160
160
  body: {
161
161
  type: "object",
162
- required: ["current_directory"],
162
+ required: ["source"],
163
163
  additionalProperties: false,
164
164
  properties: {
165
+ source: { type: "string", enum: ["terminal", "desktop"] },
165
166
  current_directory: { type: "string", minLength: 1, maxLength: 4096 },
166
167
  environment: {
167
168
  type: "object",
@@ -176,10 +177,21 @@ export function createLocalConsoleServer(options) {
176
177
  },
177
178
  }, (request) => {
178
179
  requireControl(request);
179
- const access = sessions.issueAccessToken({
180
- currentDirectory: resolve(request.body.current_directory),
181
- environment: request.body.environment ?? {},
182
- });
180
+ if ((request.body.source === "terminal" && request.body.current_directory === undefined) ||
181
+ (request.body.source === "desktop" && request.body.current_directory !== undefined)) {
182
+ throw new LocalConsoleHttpError(400, "bad_request", "Local Console invocation context is invalid.");
183
+ }
184
+ const context = request.body.source === "terminal"
185
+ ? {
186
+ source: "terminal",
187
+ currentDirectory: resolve(request.body.current_directory),
188
+ environment: request.body.environment ?? {},
189
+ }
190
+ : {
191
+ source: "desktop",
192
+ environment: request.body.environment ?? {},
193
+ };
194
+ const access = sessions.issueAccessToken(context);
183
195
  return { access_token: access.token, expires_at: access.expires_at };
184
196
  });
185
197
  app.post(`${LOCAL_CONSOLE_API_BASE}/session`, {
@@ -212,7 +224,12 @@ export function createLocalConsoleServer(options) {
212
224
  const session = requireSession(request);
213
225
  return {
214
226
  csrf_token: session.csrfToken,
215
- current_directory: session.context.currentDirectory,
227
+ invocation: session.context.source === "terminal"
228
+ ? {
229
+ source: "terminal",
230
+ current_directory: session.context.currentDirectory,
231
+ }
232
+ : { source: "desktop" },
216
233
  platform: process.platform,
217
234
  folder_picker: detectLocalFolderPicker(process.platform, session.context.environment).kind,
218
235
  };