@agent-native/core 0.157.8 → 0.157.10

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 (154) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
  3. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +261 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
  5. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +75 -31
  6. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
  7. package/corpus/templates/analytics/server/plugins/db.ts +139 -0
  8. package/corpus/templates/slides/AGENTS.md +14 -10
  9. package/corpus/templates/slides/actions/export-pptx.ts +1311 -121
  10. package/corpus/templates/slides/actions/import-file.ts +28 -1
  11. package/corpus/templates/slides/actions/import-pptx.ts +69 -42
  12. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
  13. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
  14. package/corpus/templates/slides/app/global.css +19 -1
  15. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
  16. package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
  17. package/corpus/templates/slides/app/lib/export-pptx-client.ts +601 -30
  18. package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
  19. package/corpus/templates/slides/server/handlers/import/html-converter.ts +1240 -22
  20. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +329 -64
  21. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
  22. package/corpus/templates/slides/server/lib/source-import.ts +10 -1
  23. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  24. package/dist/agent/harness/migrations.d.ts +4 -0
  25. package/dist/agent/harness/migrations.js +60 -0
  26. package/dist/agent/production-agent.d.ts +2 -0
  27. package/dist/agent/production-agent.js +15 -0
  28. package/dist/agent/run-loop-with-resume.d.ts +1 -0
  29. package/dist/agent/run-loop-with-resume.js +44 -12
  30. package/dist/agent/run-migrations.d.ts +8 -0
  31. package/dist/agent/run-migrations.js +61 -0
  32. package/dist/automations/service.d.ts +6 -0
  33. package/dist/automations/service.js +35 -0
  34. package/dist/chat-threads/migrations.d.ts +1 -0
  35. package/dist/chat-threads/migrations.js +1 -0
  36. package/dist/chat-threads/schema-migrations.d.ts +8 -0
  37. package/dist/chat-threads/schema-migrations.js +60 -0
  38. package/dist/cli/code-agent-connector.d.ts +4 -1
  39. package/dist/cli/code-agent-connector.js +154 -23
  40. package/dist/cli/code-agent-executor.js +255 -13
  41. package/dist/cli/code-agent-runs.d.ts +1 -0
  42. package/dist/cli/code-agent-runs.js +2 -1
  43. package/dist/cli/portal-workspace.d.ts +49 -0
  44. package/dist/cli/portal-workspace.js +320 -0
  45. package/dist/client/AssistantChat.d.ts +1 -1
  46. package/dist/client/AssistantChat.js +43 -61
  47. package/dist/client/ConnectBuilderCard.js +33 -5
  48. package/dist/client/agent-chat-adapter.js +4 -0
  49. package/dist/client/analytics-platform.d.ts +2 -0
  50. package/dist/client/analytics-platform.js +12 -0
  51. package/dist/client/analytics.d.ts +3 -0
  52. package/dist/client/analytics.js +10 -1
  53. package/dist/client/chat/index.d.ts +1 -0
  54. package/dist/client/chat/index.js +1 -0
  55. package/dist/client/chat/message-components.d.ts +2 -0
  56. package/dist/client/chat/message-components.js +11 -1
  57. package/dist/client/chat/run-recovery.d.ts +2 -1
  58. package/dist/client/chat/run-recovery.js +2 -2
  59. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  60. package/dist/client/chat-first/apps-rail.js +11 -11
  61. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  62. package/dist/client/chat-first/surface-tabs.js +38 -36
  63. package/dist/client/chat-first/types.d.ts +5 -0
  64. package/dist/client/chat-first.d.ts +1 -1
  65. package/dist/client/chat-first.js +4 -4
  66. package/dist/client/desktop-local-code-change.d.ts +10 -0
  67. package/dist/client/desktop-local-code-change.js +16 -0
  68. package/dist/client/index.d.ts +1 -0
  69. package/dist/client/index.js +1 -0
  70. package/dist/client/observability/ThumbsFeedback.js +53 -26
  71. package/dist/client/observability/useObservability.d.ts +1 -1
  72. package/dist/client/observability/useObservability.js +6 -3
  73. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  74. package/dist/client/terminal/AgentTerminal.js +38 -3
  75. package/dist/client/terminal/index.d.ts +1 -1
  76. package/dist/client/terminal/index.js +1 -1
  77. package/dist/client/track.js +9 -1
  78. package/dist/client/use-action.js +3 -0
  79. package/dist/collab/awareness.d.ts +2 -2
  80. package/dist/deploy/build.js +1 -1
  81. package/dist/feature-flags/a2a-action-route.js +4 -2
  82. package/dist/feature-flags/permissions.d.ts +2 -0
  83. package/dist/feature-flags/permissions.js +6 -1
  84. package/dist/ingestion/pptx.d.ts +92 -1
  85. package/dist/ingestion/pptx.js +1180 -115
  86. package/dist/integrations/index.d.ts +3 -3
  87. package/dist/integrations/index.js +2 -2
  88. package/dist/integrations/plugin.js +95 -6
  89. package/dist/integrations/remote-commands-store.d.ts +5 -0
  90. package/dist/integrations/remote-commands-store.js +63 -22
  91. package/dist/integrations/remote-devices-store.d.ts +2 -1
  92. package/dist/integrations/remote-devices-store.js +46 -0
  93. package/dist/integrations/remote-types.d.ts +20 -0
  94. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  95. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  96. package/dist/jobs/frontmatter.d.ts +16 -0
  97. package/dist/jobs/frontmatter.js +53 -0
  98. package/dist/jobs/remote-execution.d.ts +42 -0
  99. package/dist/jobs/remote-execution.js +287 -0
  100. package/dist/jobs/scheduler.js +102 -3
  101. package/dist/jobs/tools.js +50 -3
  102. package/dist/localization/core-messages/ar-SA.js +4 -0
  103. package/dist/localization/core-messages/de-DE.js +4 -0
  104. package/dist/localization/core-messages/en-US.d.ts +4 -0
  105. package/dist/localization/core-messages/en-US.js +4 -0
  106. package/dist/localization/core-messages/es-ES.js +4 -0
  107. package/dist/localization/core-messages/fr-FR.js +4 -0
  108. package/dist/localization/core-messages/hi-IN.js +4 -0
  109. package/dist/localization/core-messages/ja-JP.js +4 -0
  110. package/dist/localization/core-messages/ko-KR.js +4 -0
  111. package/dist/localization/core-messages/pt-BR.js +4 -0
  112. package/dist/localization/core-messages/zh-CN.js +4 -0
  113. package/dist/localization/core-messages/zh-TW.js +4 -0
  114. package/dist/localization/core-messages.d.ts +4 -0
  115. package/dist/notifications/routes.d.ts +3 -3
  116. package/dist/observability/feedback.js +40 -16
  117. package/dist/observability/routes.d.ts +6 -6
  118. package/dist/observability/routes.js +15 -3
  119. package/dist/observability/store.js +2 -0
  120. package/dist/observability/traces.js +5 -1
  121. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  122. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  123. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/secrets/routes.d.ts +9 -9
  126. package/dist/server/action-routes.d.ts +7 -5
  127. package/dist/server/action-routes.js +13 -6
  128. package/dist/server/agent-chat-plugin.js +6 -0
  129. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  130. package/dist/server/agent-run-context.d.ts +1 -0
  131. package/dist/server/agent-run-context.js +9 -0
  132. package/dist/server/better-auth-instance.js +4 -0
  133. package/dist/server/core-routes-plugin.js +8 -1
  134. package/dist/server/realtime-voice.js +2 -1
  135. package/dist/server/release-migrations.js +16 -0
  136. package/dist/server/request-context.d.ts +2 -0
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/dist/shared/analytics-platform.d.ts +6 -0
  139. package/dist/shared/analytics-platform.js +14 -0
  140. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  141. package/dist/shared/mcp-embed-headers.js +1 -1
  142. package/dist/styles/agent-native.css +83 -103
  143. package/dist/terminal/cli-registry.js +5 -0
  144. package/dist/tracking/registry.js +9 -1
  145. package/dist/triggers/actions/list-automations.d.ts +3 -0
  146. package/dist/triggers/actions/list-automations.js +3 -0
  147. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  148. package/dist/triggers/actions/manage-automation.js +14 -2
  149. package/dist/triggers/actions.js +82 -7
  150. package/dist/usage/migrations.d.ts +4 -0
  151. package/dist/usage/migrations.js +45 -0
  152. package/docs/content/template-content-developers.mdx +48 -0
  153. package/docs/content/toolkit-ui.mdx +57 -0
  154. package/package.json +3 -3
@@ -0,0 +1,49 @@
1
+ export interface PortalHandoff {
2
+ schemaVersion: 1;
3
+ handoffId: string;
4
+ branch: string;
5
+ remoteRef: string;
6
+ commit: string;
7
+ sourceBranch?: string;
8
+ sourceDirty: boolean;
9
+ createdCommit: boolean;
10
+ repositoryName: string;
11
+ remoteName: string;
12
+ envPolicy: "load-local";
13
+ }
14
+ export interface PortalEnvironment {
15
+ values: Record<string, string>;
16
+ files: string[];
17
+ root: string;
18
+ }
19
+ export interface PortalWorkspace {
20
+ handoff: PortalHandoff;
21
+ repositoryPath: string;
22
+ workspacePath: string;
23
+ environmentRoot: string;
24
+ environmentFiles: string[];
25
+ reused: boolean;
26
+ }
27
+ export interface PortalGitResult {
28
+ status: number | null;
29
+ stdout?: string;
30
+ stderr?: string;
31
+ error?: Error;
32
+ }
33
+ export type RunPortalGit = (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => PortalGitResult | Promise<PortalGitResult>;
34
+ export declare function createPortalHandoff(input: {
35
+ sourcePath: string;
36
+ remoteName?: string;
37
+ handoffId?: string;
38
+ now?: Date;
39
+ runGit?: RunPortalGit;
40
+ }): Promise<PortalHandoff>;
41
+ export declare function preparePortalWorkspace(input: {
42
+ handoff: unknown;
43
+ workspacePath?: string;
44
+ worktreeRoot?: string;
45
+ runGit?: RunPortalGit;
46
+ }): Promise<PortalWorkspace>;
47
+ export declare function parsePortalHandoff(value: unknown): PortalHandoff;
48
+ export declare function listPortalEnvironmentFiles(root: string, mode?: string): string[];
49
+ export declare function loadPortalEnvironment(root: string, mode?: string): PortalEnvironment;
@@ -0,0 +1,320 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { codeAgentStoreRoot } from "./code-agent-runs.js";
8
+ const execFile = promisify(execFileCallback);
9
+ const MAX_GIT_OUTPUT_BYTES = 1_000_000;
10
+ const PORTAL_ENV_FILES = [".env", ".env.local"];
11
+ const PORTAL_PROTECTED_ENV_KEYS = new Set([
12
+ "AGENT_NATIVE_CODE_AGENTS_HOME",
13
+ "AGENT_NATIVE_CODE_AGENT_PERMISSION_MODE",
14
+ "AGENT_NATIVE_COMPUTER_BRIDGE_TOKEN",
15
+ "AGENT_NATIVE_COMPUTER_BRIDGE_URL",
16
+ "AGENT_NATIVE_REMOTE_DEVICE_PATH",
17
+ "ELECTRON_RUN_AS_NODE",
18
+ "HOME",
19
+ "NODE_OPTIONS",
20
+ "PATH",
21
+ "PWD",
22
+ "SHELL",
23
+ ]);
24
+ const runPortalGit = async (args, cwd, env) => {
25
+ try {
26
+ const result = await execFile("git", args, {
27
+ cwd,
28
+ encoding: "utf8",
29
+ env: {
30
+ ...process.env,
31
+ GIT_TERMINAL_PROMPT: "0",
32
+ ...env,
33
+ },
34
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
35
+ });
36
+ return {
37
+ status: 0,
38
+ stdout: String(result.stdout),
39
+ stderr: String(result.stderr),
40
+ };
41
+ }
42
+ catch (error) {
43
+ const candidate = error;
44
+ return {
45
+ status: typeof candidate.code === "number" ? candidate.code : 1,
46
+ stdout: candidate.stdout ? String(candidate.stdout) : undefined,
47
+ stderr: candidate.stderr ? String(candidate.stderr) : undefined,
48
+ error: error instanceof Error ? error : new Error(String(error)),
49
+ };
50
+ }
51
+ };
52
+ export async function createPortalHandoff(input) {
53
+ const executeGit = input.runGit ?? runPortalGit;
54
+ const sourceCandidate = path.resolve(input.sourcePath);
55
+ const remoteName = input.remoteName ?? "origin";
56
+ assertSafeRemoteName(remoteName);
57
+ const handoffId = input.handoffId ?? crypto.randomUUID();
58
+ assertSafeHandoffId(handoffId);
59
+ const repositoryPath = await requiredGitOutput(executeGit, ["rev-parse", "--show-toplevel"], sourceCandidate, "Selected folder is not a Git repository. Choose a Git folder to use Portal.");
60
+ const head = await requiredGitOutput(executeGit, ["rev-parse", "--verify", "HEAD"], repositoryPath, "Portal could not determine the current Git commit.");
61
+ const status = await executeGit(["status", "--porcelain", "--untracked-files=all"], repositoryPath);
62
+ if (status.status !== 0) {
63
+ throw gitFailure(status, "Portal could not inspect the local Git working tree.");
64
+ }
65
+ const remoteUrl = await requiredGitOutput(executeGit, ["remote", "get-url", remoteName], repositoryPath, `Portal could not find the ${remoteName} Git remote.`);
66
+ if (!remoteUrl) {
67
+ throw new Error(`Portal could not find the ${remoteName} Git remote.`);
68
+ }
69
+ const branchResult = await executeGit(["branch", "--show-current"], repositoryPath);
70
+ const sourceBranch = branchResult.status === 0
71
+ ? branchResult.stdout?.trim() || undefined
72
+ : undefined;
73
+ const sourceDirty = Boolean(status.stdout?.trim());
74
+ let commit = head;
75
+ let createdCommit = false;
76
+ if (sourceDirty) {
77
+ const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "agent-native-portal-index-"));
78
+ const indexPath = path.join(tempRoot, "index");
79
+ try {
80
+ const indexEnv = { GIT_INDEX_FILE: indexPath };
81
+ const added = await executeGit(["add", "--all", "--", "."], repositoryPath, indexEnv);
82
+ if (added.status !== 0) {
83
+ throw gitFailure(added, "Portal could not snapshot local code.");
84
+ }
85
+ const tree = await requiredGitOutput(executeGit, ["write-tree"], repositoryPath, "Portal could not write the local code snapshot.", indexEnv);
86
+ const authorName = (await optionalGitOutput(executeGit, ["config", "--get", "user.name"], repositoryPath)) ?? "Agent Native Portal";
87
+ const authorEmail = (await optionalGitOutput(executeGit, ["config", "--get", "user.email"], repositoryPath)) ?? "portal@agent-native.local";
88
+ const committed = await requiredGitOutput(executeGit, ["commit-tree", tree, "-p", head, "-m", `Portal handoff ${handoffId}`], repositoryPath, "Portal could not create the local code snapshot.", {
89
+ ...indexEnv,
90
+ GIT_AUTHOR_NAME: authorName,
91
+ GIT_AUTHOR_EMAIL: authorEmail,
92
+ GIT_COMMITTER_NAME: authorName,
93
+ GIT_COMMITTER_EMAIL: authorEmail,
94
+ });
95
+ commit = committed;
96
+ createdCommit = true;
97
+ }
98
+ finally {
99
+ await fs.promises.rm(tempRoot, { recursive: true, force: true });
100
+ }
101
+ }
102
+ const branch = portalBranchName(input.now ?? new Date(), handoffId);
103
+ const pushed = await executeGit(["push", "--no-verify", remoteName, `${commit}:refs/heads/${branch}`], repositoryPath);
104
+ if (pushed.status !== 0) {
105
+ throw gitFailure(pushed, "Portal could not push the code snapshot to the Git remote.");
106
+ }
107
+ return {
108
+ schemaVersion: 1,
109
+ handoffId,
110
+ branch,
111
+ remoteRef: `refs/heads/${branch}`,
112
+ commit,
113
+ sourceBranch,
114
+ sourceDirty,
115
+ createdCommit,
116
+ repositoryName: path.basename(repositoryPath),
117
+ remoteName,
118
+ envPolicy: "load-local",
119
+ };
120
+ }
121
+ export async function preparePortalWorkspace(input) {
122
+ const executeGit = input.runGit ?? runPortalGit;
123
+ const handoff = parsePortalHandoff(input.handoff);
124
+ const configuredWorkspace = input.workspacePath?.trim();
125
+ if (!configuredWorkspace) {
126
+ throw new Error("Portal needs a configured workspacePath on the paired computer.");
127
+ }
128
+ const repositoryPath = await requiredGitOutput(executeGit, ["rev-parse", "--show-toplevel"], path.resolve(configuredWorkspace), "Portal could not find a Git repository on the paired computer.");
129
+ const trackingRef = `refs/remotes/${handoff.remoteName}/${handoff.branch}`;
130
+ const fetched = await executeGit([
131
+ "fetch",
132
+ "--no-tags",
133
+ "--no-prune",
134
+ handoff.remoteName,
135
+ `${handoff.remoteRef}:${trackingRef}`,
136
+ ], repositoryPath);
137
+ if (fetched.status !== 0) {
138
+ throw gitFailure(fetched, "Portal could not fetch the code snapshot on the paired computer.");
139
+ }
140
+ const verified = await executeGit(["cat-file", "-e", `${handoff.commit}^{commit}`], repositoryPath);
141
+ if (verified.status !== 0) {
142
+ throw gitFailure(verified, "Portal fetched the branch but could not verify its commit.");
143
+ }
144
+ const worktreeRoot = path.resolve(input.worktreeRoot ?? path.join(codeAgentStoreRoot(), "portals"));
145
+ await fs.promises.mkdir(worktreeRoot, { recursive: true });
146
+ const baseName = `${handoff.handoffId}-${handoff.commit.slice(0, 8)}`;
147
+ let workspacePath = path.join(worktreeRoot, baseName);
148
+ let reused = false;
149
+ if (fs.existsSync(workspacePath)) {
150
+ const existingHead = await executeGit(["rev-parse", "HEAD"], workspacePath);
151
+ const existingStatus = await executeGit(["status", "--porcelain", "--untracked-files=all"], workspacePath);
152
+ if (existingHead.status === 0 &&
153
+ existingHead.stdout?.trim() === handoff.commit &&
154
+ existingStatus.status === 0 &&
155
+ !existingStatus.stdout?.trim()) {
156
+ reused = true;
157
+ }
158
+ else {
159
+ workspacePath = path.join(worktreeRoot, `${baseName}-${crypto.randomUUID().slice(0, 8)}`);
160
+ }
161
+ }
162
+ if (!reused) {
163
+ const created = await executeGit(["worktree", "add", "--detach", workspacePath, handoff.commit], repositoryPath);
164
+ if (created.status !== 0) {
165
+ throw gitFailure(created, "Portal could not create the isolated workspace on the paired computer.");
166
+ }
167
+ }
168
+ return {
169
+ handoff,
170
+ repositoryPath,
171
+ workspacePath,
172
+ environmentRoot: repositoryPath,
173
+ environmentFiles: listPortalEnvironmentFiles(repositoryPath),
174
+ reused,
175
+ };
176
+ }
177
+ export function parsePortalHandoff(value) {
178
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
179
+ throw new Error("Portal handoff metadata is missing.");
180
+ }
181
+ const record = value;
182
+ const handoffId = stringField(record.handoffId);
183
+ const branch = stringField(record.branch);
184
+ const remoteRef = stringField(record.remoteRef);
185
+ const commit = stringField(record.commit);
186
+ const repositoryName = stringField(record.repositoryName);
187
+ const remoteName = stringField(record.remoteName);
188
+ if (!handoffId || !branch || !remoteRef || !commit || !repositoryName) {
189
+ throw new Error("Portal handoff metadata is incomplete.");
190
+ }
191
+ assertSafeHandoffId(handoffId);
192
+ assertSafeBranch(branch);
193
+ assertSafeRemoteName(remoteName || "");
194
+ if (remoteRef !== `refs/heads/${branch}`) {
195
+ throw new Error("Portal handoff metadata has an invalid remote ref.");
196
+ }
197
+ if (!/^[a-f0-9]{7,64}$/i.test(commit)) {
198
+ throw new Error("Portal handoff metadata has an invalid commit.");
199
+ }
200
+ if (record.schemaVersion !== 1 || record.envPolicy !== "load-local") {
201
+ throw new Error("Portal handoff metadata has an unsupported version.");
202
+ }
203
+ if (typeof record.sourceDirty !== "boolean" ||
204
+ typeof record.createdCommit !== "boolean") {
205
+ throw new Error("Portal handoff metadata has invalid snapshot state.");
206
+ }
207
+ const sourceBranch = stringField(record.sourceBranch);
208
+ return {
209
+ schemaVersion: 1,
210
+ handoffId,
211
+ branch,
212
+ remoteRef,
213
+ commit,
214
+ sourceBranch,
215
+ sourceDirty: record.sourceDirty,
216
+ createdCommit: record.createdCommit,
217
+ repositoryName,
218
+ remoteName: remoteName || "origin",
219
+ envPolicy: "load-local",
220
+ };
221
+ }
222
+ export function listPortalEnvironmentFiles(root, mode = process.env.NODE_ENV || "development") {
223
+ const normalizedMode = mode.trim().replace(/[^A-Za-z0-9_-]/g, "");
224
+ const names = [
225
+ ...PORTAL_ENV_FILES,
226
+ ...(normalizedMode ? [`.env.${normalizedMode}`] : []),
227
+ ...(normalizedMode ? [`.env.${normalizedMode}.local`] : []),
228
+ ];
229
+ return [...new Set(names)].filter((name) => {
230
+ try {
231
+ return fs.statSync(path.join(root, name)).isFile();
232
+ // coercion-ok: A missing local env file is an intentional absent value.
233
+ }
234
+ catch {
235
+ return false;
236
+ }
237
+ });
238
+ }
239
+ export function loadPortalEnvironment(root, mode = process.env.NODE_ENV || "development") {
240
+ const resolvedRoot = path.resolve(root);
241
+ const files = listPortalEnvironmentFiles(resolvedRoot, mode);
242
+ const values = {};
243
+ for (const name of files) {
244
+ const filePath = path.join(resolvedRoot, name);
245
+ let content;
246
+ try {
247
+ content = fs.readFileSync(filePath, "utf8");
248
+ }
249
+ catch (error) {
250
+ throw new Error(`Portal could not read ${name} on the paired computer: ${error instanceof Error ? error.message : String(error)}`);
251
+ }
252
+ for (const line of content.split(/\r?\n/)) {
253
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
254
+ if (!match || PORTAL_PROTECTED_ENV_KEYS.has(match[1]))
255
+ continue;
256
+ values[match[1]] = parsePortalEnvValue(match[2]);
257
+ }
258
+ }
259
+ return { values, files, root: resolvedRoot };
260
+ }
261
+ function parsePortalEnvValue(value) {
262
+ const trimmed = value.trim();
263
+ if (trimmed.length >= 2 &&
264
+ ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
265
+ (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
266
+ return trimmed.slice(1, -1);
267
+ }
268
+ const comment = trimmed.indexOf(" #");
269
+ return comment >= 0 ? trimmed.slice(0, comment).trimEnd() : trimmed;
270
+ }
271
+ function portalBranchName(now, handoffId) {
272
+ const timestamp = now.toISOString().replace(/\D/g, "").slice(0, 14);
273
+ const suffix = handoffId.slice(0, 8).replace(/[-.]+$/g, "") || "handoff";
274
+ return `portal/${timestamp}-${suffix}`;
275
+ }
276
+ function stringField(value) {
277
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
278
+ }
279
+ function assertSafeHandoffId(value) {
280
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(value)) {
281
+ throw new Error("Portal handoff id is invalid.");
282
+ }
283
+ }
284
+ function assertSafeBranch(value) {
285
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/.test(value) ||
286
+ value.includes("..") ||
287
+ value.includes("//") ||
288
+ value.endsWith("/") ||
289
+ value.endsWith(".")) {
290
+ throw new Error("Portal branch is invalid.");
291
+ }
292
+ }
293
+ function assertSafeRemoteName(value) {
294
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(value)) {
295
+ throw new Error("Portal Git remote name is invalid.");
296
+ }
297
+ }
298
+ async function requiredGitOutput(executeGit, args, cwd, fallback, env) {
299
+ const result = await executeGit(args, cwd, env);
300
+ if (result.status !== 0 || !result.stdout?.trim()) {
301
+ throw gitFailure(result, fallback);
302
+ }
303
+ return result.stdout.trim();
304
+ }
305
+ async function optionalGitOutput(executeGit, args, cwd) {
306
+ const result = await executeGit(args, cwd);
307
+ return result.status === 0 ? result.stdout?.trim() || undefined : undefined;
308
+ }
309
+ function gitFailure(result, fallback) {
310
+ const detail = sanitizeGitError(result.stderr?.trim() || result.error?.message);
311
+ return new Error(detail ? `${fallback} ${detail}` : fallback);
312
+ }
313
+ function sanitizeGitError(value) {
314
+ if (!value)
315
+ return undefined;
316
+ return value
317
+ .replace(/https?:\/\/[^\s/@]+(?::[^\s/@]*)?@/gi, "https://REDACTED@")
318
+ .replace(/\s+/g, " ")
319
+ .slice(0, 800);
320
+ }
@@ -318,7 +318,7 @@ export interface AssistantChatProps {
318
318
  composerAreaClassName?: string;
319
319
  /** Placeholder for the shared composer in its normal idle state. */
320
320
  composerPlaceholder?: string;
321
- /** Sidebar uses a compact setup CTA above the composer; page chat keeps the default below-composer CTA. */
321
+ /** Controls the compactness of the provider setup panel attached above the composer. */
322
322
  missingApiKeySetupLayout?: BuilderSetupCardLayout;
323
323
  /** Visual density for the shared composer shell. */
324
324
  composerLayoutVariant?: AgentComposerLayoutVariant;
@@ -22,13 +22,12 @@ import { readAssistantChatComposerDraft, writeAssistantChatComposerDraft, } from
22
22
  import { TextStreamingContext } from "./chat/markdown-renderer.js";
23
23
  import { CheckpointContext, MessageActionsContext, assistantMessageRunId, UserMessage, AssistantMessage, SelectionAttachedPill, RunningActivityStatus, displayableUserMessageText, isHiddenUserMessage, ServerRunActiveContext, UserStoppedRunContext, } from "./chat/message-components.js";
24
24
  import { repoHasAssistantMessage, getRepoMessages, getRepoMessage, shouldImportServerThreadData, dedupeRepoMessagesById, dropEmptyAssistantMessages, withLastAssistantRunDuration, } from "./chat/repo-helpers.js";
25
- import { BuilderSetupCard, BuilderSetupContent, LoopLimitContinueCard, RunErrorRecoveryCard, PlanModeCallout, getLoopLimitMetadata, getRunErrorMetadata, getRequestModeMetadata, runErrorKey, } from "./chat/run-recovery.js";
25
+ import { BuilderSetupCard, LoopLimitContinueCard, RunErrorRecoveryCard, PlanModeCallout, getLoopLimitMetadata, getRunErrorMetadata, getRequestModeMetadata, runErrorKey, } from "./chat/run-recovery.js";
26
26
  import { createAgentChatRuntimeAdapter, } from "./chat/runtime.js";
27
27
  import { ChatRunningContext, ChatRunningRunIdContext, ChatRunningTurnIdContext, ChatRunDurationContext, SuppressInlineOpenAppContext, ApprovalContext, ReconnectStreamMessage, } from "./chat/tool-call-display.js";
28
28
  import { useAgentChatLifecycleTracking } from "./chat/use-agent-chat-lifecycle-tracking.js";
29
29
  import { useReconnectReaderOwner } from "./chat/use-reconnect-reader-owner.js";
30
30
  import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, useMessageScroller, } from "./components/ui/message-scroller.js";
31
- import { Popover, PopoverContent, PopoverTrigger, } from "./components/ui/popover.js";
32
31
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./components/ui/tooltip.js";
33
32
  import { AgentComposerFrame, TiptapComposer, } from "./composer/index.js";
34
33
  import { useAgentDynamicSuggestionsResult, } from "./dynamic-suggestions.js";
@@ -1501,15 +1500,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1501
1500
  agentEngineConfigured.state === "missing" &&
1502
1501
  missingApiKey;
1503
1502
  const isComposerDisabled = composerDisabled;
1504
- const [missingKeySetupOpen, setMissingKeySetupOpen] = useState(false);
1505
- const requestMissingKeySetup = useCallback(() => {
1506
- setMissingKeySetupOpen(true);
1507
- }, []);
1508
- useEffect(() => {
1509
- if (agentEngineConfigured.state !== "configured")
1510
- return;
1511
- setMissingKeySetupOpen(false);
1512
- }, [agentEngineConfigured.state]);
1513
1503
  const [authError, setAuthError] = useState(null);
1514
1504
  const [authSessionAvailable, setAuthSessionAvailable] = useState(false);
1515
1505
  const [queuedMessages, setQueuedMessages] = useState([]);
@@ -2644,6 +2634,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2644
2634
  })();
2645
2635
  return () => {
2646
2636
  cancelled = true;
2637
+ // React StrictMode replays effects without resetting refs. Let the
2638
+ // replay start the restore again after cancelling this attempt.
2639
+ hasRestoredRef.current = false;
2647
2640
  };
2648
2641
  }
2649
2642
  else if (threadId && isNewThread) {
@@ -2737,6 +2730,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2737
2730
  })();
2738
2731
  return () => {
2739
2732
  cancelled = true;
2733
+ hasRestoredRef.current = false;
2740
2734
  };
2741
2735
  }
2742
2736
  else {
@@ -2968,7 +2962,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2968
2962
  // Nudge the shared hook to re-check after a Builder connect.
2969
2963
  const handleBuilderConnected = useCallback(() => {
2970
2964
  focusComposerAfterConnectRef.current = true;
2971
- setMissingKeySetupOpen(false);
2972
2965
  window.dispatchEvent(new Event("agent-engine:configured-changed"));
2973
2966
  }, []);
2974
2967
  useEffect(() => {
@@ -3611,9 +3604,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3611
3604
  const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false, submitMessageId, approvedToolCalls, continuationTurnId) => {
3612
3605
  if (isAgentChatSubmitCancelled(submitMessageId))
3613
3606
  return;
3614
- if (engineSetupRequired) {
3615
- requestMissingKeySetup();
3616
- }
3617
3607
  if (!preserveReconnectAutoRecoveryBudget) {
3618
3608
  reconnectAutoRecoveryCountRef.current = 0;
3619
3609
  }
@@ -3826,7 +3816,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3826
3816
  markOptimisticRunning,
3827
3817
  engineSetupRequired,
3828
3818
  appendThreadMessage,
3829
- requestMissingKeySetup,
3830
3819
  selectedEffort,
3831
3820
  selectedEngine,
3832
3821
  selectedModel,
@@ -4032,7 +4021,10 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4032
4021
  const latestAssistantWasPlan = latestMessageRole === "assistant" &&
4033
4022
  getRequestModeMetadata(latestMessage) === "plan";
4034
4023
  const showMissingKeySetup = engineSetupRequired && !authError;
4035
- const showInlineMissingKeySetup = showMissingKeySetup && missingApiKeySetupLayout === "sidebar";
4024
+ const [missingKeyBouncePulse, setMissingKeyBouncePulse] = useState(0);
4025
+ const bounceMissingKeySetup = useCallback(() => {
4026
+ setMissingKeyBouncePulse((pulse) => pulse + 1);
4027
+ }, []);
4036
4028
  const showPlanModeCallout = execMode === "plan" &&
4037
4029
  !planModeDisabled &&
4038
4030
  !isComposerDisabled &&
@@ -4121,7 +4113,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4121
4113
  (guidedQuestions && guidedQuestions.length > 0) ||
4122
4114
  visibleComposerContextItems.length > 0 ||
4123
4115
  showPlanModeCallout ||
4124
- showInlineMissingKeySetup);
4116
+ showMissingKeySetup);
4125
4117
  const approvalResolutionScope = threadId ?? tabId ?? "default";
4126
4118
  const [approvalResolutionState, setApprovalResolutionState] = useState(() => ({
4127
4119
  scope: approvalResolutionScope,
@@ -4220,7 +4212,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4220
4212
  t("agentChat.empty.prompt") }), emptyStateAddon, resolvedSuggestions &&
4221
4213
  resolvedSuggestions.length > 0 ? (_jsx("div", { className: "flex w-full max-w-[280px] flex-col gap-1", children: resolvedSuggestions.map((suggestion) => (_jsx("button", { onClick: () => {
4222
4214
  if (engineSetupRequired) {
4223
- requestMissingKeySetup();
4224
4215
  return;
4225
4216
  }
4226
4217
  void addToQueue(suggestion);
@@ -4283,48 +4274,39 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4283
4274
  ? { submitLabel: guidedQuestionsSubmitLabel }
4284
4275
  : {}), className: "h-auto items-stretch justify-stretch bg-transparent" }) })), composerError && (_jsxs("div", { role: "alert", className: "shrink-0 mx-3 mb-1.5 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive", children: [_jsx(IconAlertTriangle, { className: "h-3.5 w-3.5 mt-0.5 shrink-0" }), _jsx("span", { className: "flex-1 leading-snug", children: composerError }), _jsx("button", { type: "button", "aria-label": t("agentChat.common.dismissError"), onClick: () => setComposerError(null), className: "shrink-0 opacity-70 hover:opacity-100", children: _jsx(IconX, { className: "h-3 w-3" }) })] })), _jsxs("div", { className: "agent-composer-stack", "data-agent-composer-adjacent-ui": hasComposerAccessoryAboveStack
4285
4276
  ? "true"
4286
- : undefined, children: [_jsx(SelectionAttachedPill, {}), showPlanModeCallout && (_jsx(PlanModeCallout, { canImplementPlan: canImplementPlan, onImplementPlan: handleImplementPlan, onSwitchToAct: handleSwitchToAct })), showInlineMissingKeySetup ? (_jsx(BuilderSetupCard, { fullWidth: true, layout: "sidebar", onConnected: handleBuilderConnected })) : null, _jsxs(Popover, { open: showMissingKeySetup &&
4287
- !showInlineMissingKeySetup &&
4288
- missingKeySetupOpen, onOpenChange: setMissingKeySetupOpen, children: [_jsx(AgentComposerFrame, { layoutVariant: composerLayoutVariant, className: cn(composerAreaClassName, isComposerDisabled &&
4289
- !showMissingKeySetup &&
4290
- "opacity-70"), rootClassName: cn(showMissingKeySetup &&
4291
- "agent-composer-root--missing-key"), children: showMissingKeySetup &&
4292
- !showInlineMissingKeySetup ? (_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "agent-composer-missing-key-trigger", "aria-label": t("agentChat.setup.connectToStart"), children: _jsxs("span", { className: "agent-composer-missing-key-content", children: [_jsxs("span", { className: "agent-composer-missing-key-copy", children: [_jsx("span", { className: "agent-composer-missing-key-title", children: missingApiKeySetupLayout ===
4293
- "sidebar"
4294
- ? t("agentChat.setup.connectToChat")
4295
- : t("agentChat.setup.connectToStart") }), missingApiKeySetupLayout !==
4296
- "sidebar" ? (_jsx("span", { className: "agent-composer-missing-key-description", children: t("agentChat.setup.builderCredits") })) : null] }), _jsx("span", { className: "agent-composer-missing-key-cta", children: t("agentChat.setup.connectAi") })] }) }) })) : (_jsxs(_Fragment, { children: [_jsx(ComposerAttachmentPreviewStrip, {}), _jsx(TiptapComposer, { focusRef: tiptapRef, initialText: initialComposerText ?? undefined, initialTextKey: composerDraftScope, onTextChange: isActiveComposer
4297
- ? handleComposerTextChange
4298
- : undefined, disabled: isComposerDisabled ||
4299
- showInlineMissingKeySetup, placeholder: showInlineMissingKeySetup
4300
- ? t("agentChat.setup.connectPlaceholder")
4301
- : engineSetupRequired
4302
- ? t("agentChat.setup.connectPlaceholder")
4303
- : composerDisabled
4304
- ? (composerDisabledPlaceholder ??
4305
- t("agentChat.composer.openDesktop"))
4306
- : isRunning
4307
- ? queuedMessages.length > 0
4308
- ? t("agentChat.queue.followUpWithCount", {
4309
- count: queuedMessages.length,
4310
- })
4311
- : t("agentChat.queue.followUp")
4312
- : resolveAssistantChatComposerPlaceholder(composerPlaceholder), onSubmit: isRunning ||
4313
- visibleComposerContextItems.length > 0
4314
- ? (text, references, attachments, options) => void addToQueue(text, undefined, references.length > 0
4315
- ? references
4316
- : undefined, attachments, undefined, resolveAssistantChatSubmitIntent({
4317
- isRunning,
4318
- requestedIntent: options?.intent,
4319
- }), undefined, true)
4320
- : undefined, willQueue: engineSetupRequired || isRunning, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, availableAgents: availableAgents, selectedAgent: selectedAgent, hostedHarness: hostedHarness, modelListLoading: modelListLoading, onModelChange: shouldShowAssistantChatModelSelector(showModelSelector)
4321
- ? onModelChange
4322
- : undefined, onEffortChange: onEffortChange, onAgentChange: onAgentChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, toolbarSlot: composerToolbarSlot, contextItems: visibleComposerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, voiceEnabled: true, draftScope: composerDraftScope, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: composerExtraActionButton, stopButton: showRunningInUI ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: () => stopActiveRun({
4323
- preserveQueuedMessages: true,
4324
- }), "aria-label": t("agentChat.composer.stopResponse"), "data-agent-composer-slot": "stop-button", className: "shrink-0 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(IconPlayerStopFilled, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: t("agentChat.composer.stopResponse") })] })) : undefined })] })) }), showMissingKeySetup &&
4325
- !showInlineMissingKeySetup ? (_jsx(PopoverContent, { side: missingApiKeySetupLayout === "sidebar"
4326
- ? "top"
4327
- : "bottom", align: "center", sideOffset: 8, collisionPadding: 12, "data-agent-native-composer-popover": "true", className: "z-[260] box-border w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,26rem),26rem)] rounded-lg border-border p-3 shadow-lg", children: _jsx(BuilderSetupContent, { onConnected: handleBuilderConnected, layout: missingApiKeySetupLayout }) })) : null] })] })] }) }) }) }) }) }) }) }) }) }) }));
4277
+ : undefined, children: [_jsx(SelectionAttachedPill, {}), showPlanModeCallout && (_jsx(PlanModeCallout, { canImplementPlan: canImplementPlan, onImplementPlan: handleImplementPlan, onSwitchToAct: handleSwitchToAct })), showMissingKeySetup ? (_jsx(BuilderSetupCard, { fullWidth: true, attached: true, bouncePulse: missingKeyBouncePulse, layout: missingApiKeySetupLayout, onConnected: handleBuilderConnected })) : null, _jsx(AgentComposerFrame, { layoutVariant: composerLayoutVariant, className: cn(composerAreaClassName, showMissingKeySetup &&
4278
+ "agent-composer-area--attached-above", isComposerDisabled &&
4279
+ !showMissingKeySetup &&
4280
+ "opacity-70"), onClick: showMissingKeySetup
4281
+ ? bounceMissingKeySetup
4282
+ : undefined, children: _jsxs(_Fragment, { children: [_jsx(ComposerAttachmentPreviewStrip, {}), _jsx(TiptapComposer, { focusRef: tiptapRef, initialText: initialComposerText ?? undefined, initialTextKey: composerDraftScope, onTextChange: isActiveComposer
4283
+ ? handleComposerTextChange
4284
+ : undefined, disabled: isComposerDisabled || showMissingKeySetup, placeholder: showMissingKeySetup
4285
+ ? t("agentChat.setup.connectPlaceholder")
4286
+ : engineSetupRequired
4287
+ ? t("agentChat.setup.connectPlaceholder")
4288
+ : composerDisabled
4289
+ ? (composerDisabledPlaceholder ??
4290
+ t("agentChat.composer.openDesktop"))
4291
+ : isRunning
4292
+ ? queuedMessages.length > 0
4293
+ ? t("agentChat.queue.followUpWithCount", {
4294
+ count: queuedMessages.length,
4295
+ })
4296
+ : t("agentChat.queue.followUp")
4297
+ : resolveAssistantChatComposerPlaceholder(composerPlaceholder), onSubmit: isRunning ||
4298
+ visibleComposerContextItems.length > 0
4299
+ ? (text, references, attachments, options) => void addToQueue(text, undefined, references.length > 0
4300
+ ? references
4301
+ : undefined, attachments, undefined, resolveAssistantChatSubmitIntent({
4302
+ isRunning,
4303
+ requestedIntent: options?.intent,
4304
+ }), undefined, true)
4305
+ : undefined, willQueue: engineSetupRequired || isRunning, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, availableAgents: availableAgents, selectedAgent: selectedAgent, hostedHarness: hostedHarness, modelListLoading: modelListLoading, onModelChange: shouldShowAssistantChatModelSelector(showModelSelector)
4306
+ ? onModelChange
4307
+ : undefined, onEffortChange: onEffortChange, onAgentChange: onAgentChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, toolbarSlot: composerToolbarSlot, contextItems: visibleComposerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, voiceEnabled: true, draftScope: composerDraftScope, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: composerExtraActionButton, stopButton: showRunningInUI ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: () => stopActiveRun({
4308
+ preserveQueuedMessages: true,
4309
+ }), "aria-label": t("agentChat.composer.stopResponse"), "data-agent-composer-slot": "stop-button", className: "shrink-0 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(IconPlayerStopFilled, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: t("agentChat.composer.stopResponse") })] })) : undefined })] }) })] })] }) }) }) }) }) }) }) }) }) }) }));
4328
4310
  });
4329
4311
  export const AssistantChat = forwardRef(function AssistantChat({ apiUrl = agentNativePath("/_agent-native/agent-chat"), tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer, ...props }, ref) {
4330
4312
  const modelRef = useRef(props.selectedModel);