@agent-native/core 0.157.9 → 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.
- package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +5 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +18 -6
- package/corpus/templates/analytics/server/plugins/db.ts +139 -0
- package/corpus/templates/slides/AGENTS.md +14 -10
- package/corpus/templates/slides/actions/export-pptx.ts +1094 -94
- package/corpus/templates/slides/actions/import-file.ts +16 -1
- package/corpus/templates/slides/actions/import-pptx.ts +61 -43
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
- package/corpus/templates/slides/app/global.css +19 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +556 -25
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +1176 -26
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +13 -0
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
- package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
- package/dist/agent/harness/migrations.js +24 -3
- package/dist/agent/production-agent.js +11 -0
- package/dist/agent/run-loop-with-resume.js +21 -4
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +35 -0
- package/dist/cli/code-agent-connector.d.ts +4 -1
- package/dist/cli/code-agent-connector.js +154 -23
- package/dist/cli/code-agent-executor.js +255 -13
- package/dist/cli/code-agent-runs.d.ts +1 -0
- package/dist/cli/code-agent-runs.js +2 -1
- package/dist/cli/portal-workspace.d.ts +49 -0
- package/dist/cli/portal-workspace.js +320 -0
- package/dist/client/AssistantChat.d.ts +1 -1
- package/dist/client/AssistantChat.js +39 -61
- package/dist/client/ConnectBuilderCard.js +33 -5
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/analytics-platform.d.ts +2 -0
- package/dist/client/analytics-platform.js +12 -0
- package/dist/client/analytics.d.ts +3 -0
- package/dist/client/analytics.js +10 -1
- package/dist/client/chat/index.d.ts +1 -0
- package/dist/client/chat/index.js +1 -0
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +2 -2
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +38 -36
- package/dist/client/chat-first/types.d.ts +5 -0
- package/dist/client/chat-first.d.ts +1 -1
- package/dist/client/chat-first.js +4 -4
- package/dist/client/desktop-local-code-change.d.ts +10 -0
- package/dist/client/desktop-local-code-change.js +16 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/observability/ThumbsFeedback.js +21 -7
- package/dist/client/terminal/AgentTerminal.d.ts +9 -1
- package/dist/client/terminal/AgentTerminal.js +38 -3
- package/dist/client/terminal/index.d.ts +1 -1
- package/dist/client/terminal/index.js +1 -1
- package/dist/client/track.js +9 -1
- package/dist/client/use-action.js +3 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/ingestion/pptx.d.ts +78 -0
- package/dist/ingestion/pptx.js +991 -107
- package/dist/integrations/index.d.ts +3 -3
- package/dist/integrations/index.js +2 -2
- package/dist/integrations/plugin.js +95 -6
- package/dist/integrations/remote-commands-store.d.ts +5 -0
- package/dist/integrations/remote-commands-store.js +63 -22
- package/dist/integrations/remote-devices-store.d.ts +2 -1
- package/dist/integrations/remote-devices-store.js +46 -0
- package/dist/integrations/remote-types.d.ts +20 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
- package/dist/jobs/actions/list-recurring-jobs.js +5 -1
- package/dist/jobs/frontmatter.d.ts +16 -0
- package/dist/jobs/frontmatter.js +53 -0
- package/dist/jobs/remote-execution.d.ts +42 -0
- package/dist/jobs/remote-execution.js +287 -0
- package/dist/jobs/scheduler.js +102 -3
- package/dist/jobs/tools.js +50 -3
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-routes.js +4 -2
- package/dist/server/agent-chat-plugin.js +6 -0
- package/dist/server/agent-run-context.d.ts +1 -0
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/core-routes-plugin.js +8 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/shared/analytics-platform.d.ts +6 -0
- package/dist/shared/analytics-platform.js +14 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/styles/agent-native.css +83 -103
- package/dist/terminal/cli-registry.js +5 -0
- package/dist/tracking/registry.js +9 -1
- package/dist/triggers/actions/list-automations.d.ts +3 -0
- package/dist/triggers/actions/list-automations.js +3 -0
- package/dist/triggers/actions/manage-automation.d.ts +9 -0
- package/dist/triggers/actions/manage-automation.js +14 -2
- package/dist/triggers/actions.js +82 -7
- package/docs/content/template-content-developers.mdx +48 -0
- package/docs/content/toolkit-ui.mdx +57 -0
- package/package.json +2 -2
|
@@ -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
|
-
/**
|
|
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,
|
|
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([]);
|
|
@@ -2972,7 +2962,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2972
2962
|
// Nudge the shared hook to re-check after a Builder connect.
|
|
2973
2963
|
const handleBuilderConnected = useCallback(() => {
|
|
2974
2964
|
focusComposerAfterConnectRef.current = true;
|
|
2975
|
-
setMissingKeySetupOpen(false);
|
|
2976
2965
|
window.dispatchEvent(new Event("agent-engine:configured-changed"));
|
|
2977
2966
|
}, []);
|
|
2978
2967
|
useEffect(() => {
|
|
@@ -3615,9 +3604,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
3615
3604
|
const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false, submitMessageId, approvedToolCalls, continuationTurnId) => {
|
|
3616
3605
|
if (isAgentChatSubmitCancelled(submitMessageId))
|
|
3617
3606
|
return;
|
|
3618
|
-
if (engineSetupRequired) {
|
|
3619
|
-
requestMissingKeySetup();
|
|
3620
|
-
}
|
|
3621
3607
|
if (!preserveReconnectAutoRecoveryBudget) {
|
|
3622
3608
|
reconnectAutoRecoveryCountRef.current = 0;
|
|
3623
3609
|
}
|
|
@@ -3830,7 +3816,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
3830
3816
|
markOptimisticRunning,
|
|
3831
3817
|
engineSetupRequired,
|
|
3832
3818
|
appendThreadMessage,
|
|
3833
|
-
requestMissingKeySetup,
|
|
3834
3819
|
selectedEffort,
|
|
3835
3820
|
selectedEngine,
|
|
3836
3821
|
selectedModel,
|
|
@@ -4036,7 +4021,10 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4036
4021
|
const latestAssistantWasPlan = latestMessageRole === "assistant" &&
|
|
4037
4022
|
getRequestModeMetadata(latestMessage) === "plan";
|
|
4038
4023
|
const showMissingKeySetup = engineSetupRequired && !authError;
|
|
4039
|
-
const
|
|
4024
|
+
const [missingKeyBouncePulse, setMissingKeyBouncePulse] = useState(0);
|
|
4025
|
+
const bounceMissingKeySetup = useCallback(() => {
|
|
4026
|
+
setMissingKeyBouncePulse((pulse) => pulse + 1);
|
|
4027
|
+
}, []);
|
|
4040
4028
|
const showPlanModeCallout = execMode === "plan" &&
|
|
4041
4029
|
!planModeDisabled &&
|
|
4042
4030
|
!isComposerDisabled &&
|
|
@@ -4125,7 +4113,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4125
4113
|
(guidedQuestions && guidedQuestions.length > 0) ||
|
|
4126
4114
|
visibleComposerContextItems.length > 0 ||
|
|
4127
4115
|
showPlanModeCallout ||
|
|
4128
|
-
|
|
4116
|
+
showMissingKeySetup);
|
|
4129
4117
|
const approvalResolutionScope = threadId ?? tabId ?? "default";
|
|
4130
4118
|
const [approvalResolutionState, setApprovalResolutionState] = useState(() => ({
|
|
4131
4119
|
scope: approvalResolutionScope,
|
|
@@ -4224,7 +4212,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4224
4212
|
t("agentChat.empty.prompt") }), emptyStateAddon, resolvedSuggestions &&
|
|
4225
4213
|
resolvedSuggestions.length > 0 ? (_jsx("div", { className: "flex w-full max-w-[280px] flex-col gap-1", children: resolvedSuggestions.map((suggestion) => (_jsx("button", { onClick: () => {
|
|
4226
4214
|
if (engineSetupRequired) {
|
|
4227
|
-
requestMissingKeySetup();
|
|
4228
4215
|
return;
|
|
4229
4216
|
}
|
|
4230
4217
|
void addToQueue(suggestion);
|
|
@@ -4287,48 +4274,39 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4287
4274
|
? { submitLabel: guidedQuestionsSubmitLabel }
|
|
4288
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
|
|
4289
4276
|
? "true"
|
|
4290
|
-
: undefined, children: [_jsx(SelectionAttachedPill, {}), showPlanModeCallout && (_jsx(PlanModeCallout, { canImplementPlan: canImplementPlan, onImplementPlan: handleImplementPlan, onSwitchToAct: handleSwitchToAct })),
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
}), undefined, true)
|
|
4324
|
-
: 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)
|
|
4325
|
-
? onModelChange
|
|
4326
|
-
: 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({
|
|
4327
|
-
preserveQueuedMessages: true,
|
|
4328
|
-
}), "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 &&
|
|
4329
|
-
!showInlineMissingKeySetup ? (_jsx(PopoverContent, { side: missingApiKeySetupLayout === "sidebar"
|
|
4330
|
-
? "top"
|
|
4331
|
-
: "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 })] }) })] })] }) }) }) }) }) }) }) }) }) }) }));
|
|
4332
4310
|
});
|
|
4333
4311
|
export const AssistantChat = forwardRef(function AssistantChat({ apiUrl = agentNativePath("/_agent-native/agent-chat"), tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer, ...props }, ref) {
|
|
4334
4312
|
const modelRef = useRef(props.selectedModel);
|
|
@@ -5,6 +5,7 @@ import { withBuilderUtmTrackingParams } from "../shared/builder-link-tracking.js
|
|
|
5
5
|
import { agentNativePath } from "./api-path.js";
|
|
6
6
|
import { BuilderBMark } from "./builder-mark.js";
|
|
7
7
|
import { writeClipboardText } from "./clipboard.js";
|
|
8
|
+
import { requestDesktopLocalCodeChange } from "./desktop-local-code-change.js";
|
|
8
9
|
import { getCallbackOrigin } from "./frame.js";
|
|
9
10
|
import { useBuilderConnectFlow } from "./settings/useBuilderStatus.js";
|
|
10
11
|
import { cn } from "./utils.js";
|
|
@@ -18,6 +19,12 @@ function isLocalDevelopment() {
|
|
|
18
19
|
const hostname = window.location.hostname;
|
|
19
20
|
return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1");
|
|
20
21
|
}
|
|
22
|
+
function hasElectronShellBridge() {
|
|
23
|
+
if (typeof window === "undefined")
|
|
24
|
+
return false;
|
|
25
|
+
const electronApi = window.electronAPI;
|
|
26
|
+
return Boolean(electronApi?.appConfig);
|
|
27
|
+
}
|
|
21
28
|
/**
|
|
22
29
|
* Rich inline card rendered for the `connect-builder` tool call. Shows the
|
|
23
30
|
* Builder handoff when available, or routes local development requests to the
|
|
@@ -48,7 +55,9 @@ export function ConnectBuilderCard({ configured: initialConfigured, builderEnabl
|
|
|
48
55
|
const [sendErr, setSendErr] = useState(null);
|
|
49
56
|
const [copyErr, setCopyErr] = useState(null);
|
|
50
57
|
const [promptCopied, setPromptCopied] = useState(false);
|
|
58
|
+
const [localCodeChangeRequested, setLocalCodeChangeRequested] = useState(false);
|
|
51
59
|
const [localDevelopment] = useState(() => isLocalDevelopment());
|
|
60
|
+
const [electronShell] = useState(() => hasElectronShellBridge());
|
|
52
61
|
const mountedRef = useRef(true);
|
|
53
62
|
const copyResetRef = useRef(null);
|
|
54
63
|
// Tracks whether the user clicked "Connect Builder" *this session*. When
|
|
@@ -154,6 +163,12 @@ export function ConnectBuilderCard({ configured: initialConfigured, builderEnabl
|
|
|
154
163
|
const err = sendErr ?? waitlistErr ?? copyErr ?? flow.error;
|
|
155
164
|
const hasPrompt = prompt.trim().length > 0;
|
|
156
165
|
const canSend = configured && builderEnabled && hasPrompt;
|
|
166
|
+
const showDesktopLocalHandoff = electronShell && hasPrompt;
|
|
167
|
+
const handleDoLocally = useCallback((event) => {
|
|
168
|
+
if (requestDesktopLocalCodeChange(prompt, event.currentTarget)) {
|
|
169
|
+
setLocalCodeChangeRequested(true);
|
|
170
|
+
}
|
|
171
|
+
}, [prompt]);
|
|
157
172
|
// Auto-send the user's pending prompt the moment connecting finishes
|
|
158
173
|
// successfully. Without this, the connect popup closing leaves the user
|
|
159
174
|
// staring at a "Send to Builder" button — feels like they have to
|
|
@@ -181,14 +196,19 @@ export function ConnectBuilderCard({ configured: initialConfigured, builderEnabl
|
|
|
181
196
|
: `AI credits are ready to use. ${CODE_CHANGE_FALLBACK_TEXT}`;
|
|
182
197
|
let title;
|
|
183
198
|
let subtitle;
|
|
184
|
-
if (
|
|
199
|
+
if (localCodeChangeRequested) {
|
|
200
|
+
title = "Preparing a local copy";
|
|
201
|
+
subtitle = "Desktop is cloning this app and applying this request.";
|
|
202
|
+
}
|
|
203
|
+
else if (runResult) {
|
|
185
204
|
title = "Builder is working on it";
|
|
186
205
|
subtitle = (_jsxs(_Fragment, { children: ["Working on branch", " ", _jsx("span", { className: "font-mono text-foreground", children: runResult.branchName }), ". Click through to watch progress in the Visual Editor."] }));
|
|
187
206
|
}
|
|
188
207
|
else if (showExternalAgentHandoff) {
|
|
189
208
|
title = "This requires a code change";
|
|
190
|
-
subtitle =
|
|
191
|
-
"
|
|
209
|
+
subtitle = showDesktopLocalHandoff
|
|
210
|
+
? "Make the change in a local copy of this app, or copy the request to another coding agent."
|
|
211
|
+
: "Open your coding agent in this project, then paste this request.";
|
|
192
212
|
}
|
|
193
213
|
else if (showWaitlist) {
|
|
194
214
|
title = "This requires a code change";
|
|
@@ -206,8 +226,16 @@ export function ConnectBuilderCard({ configured: initialConfigured, builderEnabl
|
|
|
206
226
|
title = "Connect Builder.io";
|
|
207
227
|
subtitle = _jsx(_Fragment, { children: "Builder.io's free tier includes AI credits." });
|
|
208
228
|
}
|
|
209
|
-
return (_jsx("div", { className: cn("my-2 rounded-lg border border-border overflow-hidden"), children: _jsxs("div", { className: "flex items-start gap-3 px-4 py-3.5 bg-gradient-to-br from-teal-500/5 via-transparent to-transparent", children: [_jsx("div", { className: cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", "bg-foreground text-background"), children: runResult ? (_jsx(IconLoader2, { className: "h-5 w-5 animate-spin" })) : showExternalAgentHandoff ? (_jsx(IconCode, { className: "h-5 w-5" })) : (_jsx(BuilderBMark, { className: "h-5 w-5" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "flex items-center gap-2 flex-wrap", children: _jsx("span", { className: "text-sm font-semibold text-foreground", children: title }) }), _jsx("div", { className: "mt-0.5 text-xs text-muted-foreground leading-relaxed", children: subtitle }), showWaitlist && (_jsxs("a", { href: DESKTOP_DOWNLOAD_URL, target: "_blank", rel: "noopener noreferrer", className: "mt-2 inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground no-underline hover:text-foreground", children: ["Download desktop app", _jsx(IconExternalLink, { className: "h-3 w-3" })] })), err && _jsx("div", { className: "mt-2 text-xs text-destructive", children: err }), _jsx("div", { className: "mt-3", children: runResult ? (_jsxs("a", { href: withBuilderUtmTrackingParams(runResult.url, {
|
|
229
|
+
return (_jsx("div", { className: cn("my-2 rounded-lg border border-border overflow-hidden"), children: _jsxs("div", { className: "flex items-start gap-3 px-4 py-3.5 bg-gradient-to-br from-teal-500/5 via-transparent to-transparent", children: [_jsx("div", { className: cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", "bg-foreground text-background"), children: runResult ? (_jsx(IconLoader2, { className: "h-5 w-5 animate-spin" })) : showExternalAgentHandoff ? (_jsx(IconCode, { className: "h-5 w-5" })) : (_jsx(BuilderBMark, { className: "h-5 w-5" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "flex items-center gap-2 flex-wrap", children: _jsx("span", { className: "text-sm font-semibold text-foreground", children: title }) }), _jsx("div", { className: "mt-0.5 text-xs text-muted-foreground leading-relaxed", children: subtitle }), showWaitlist && !showDesktopLocalHandoff && (_jsxs("a", { href: DESKTOP_DOWNLOAD_URL, target: "_blank", rel: "noopener noreferrer", className: "mt-2 inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground no-underline hover:text-foreground", children: ["Download desktop app", _jsx(IconExternalLink, { className: "h-3 w-3" })] })), err && _jsx("div", { className: "mt-2 text-xs text-destructive", children: err }), _jsx("div", { className: "mt-3", children: runResult ? (_jsxs("a", { href: withBuilderUtmTrackingParams(runResult.url, {
|
|
210
230
|
campaign: "product",
|
|
211
231
|
content: "connect_builder_card_branch",
|
|
212
|
-
}), target: "_blank", rel: "noopener noreferrer", className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90"), children: ["Open branch in Builder", _jsx(IconExternalLink, { className: "h-3.5 w-3.5" })] })) : canSend ? (
|
|
232
|
+
}), target: "_blank", rel: "noopener noreferrer", className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90"), children: ["Open branch in Builder", _jsx(IconExternalLink, { className: "h-3.5 w-3.5" })] })) : canSend ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [showDesktopLocalHandoff && (_jsxs("button", { type: "button", "data-desktop-local-code-change": true, onClick: handleDoLocally, disabled: localCodeChangeRequested, className: cn("inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent", localCodeChangeRequested && "cursor-wait opacity-70"), children: [localCodeChangeRequested ? (_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(IconCode, { className: "h-3.5 w-3.5" })), localCodeChangeRequested
|
|
233
|
+
? "Preparing locally…"
|
|
234
|
+
: "Do locally"] })), _jsx("button", { type: "button", onClick: handleSend, disabled: sending, className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90", sending && "opacity-70 cursor-wait"), children: sending ? (_jsxs(_Fragment, { children: [_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" }), "Sending to Builder\u2026"] })) : (_jsx(_Fragment, { children: "Send to Builder" })) })] })) : showExternalAgentHandoff ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [showDesktopLocalHandoff && (_jsxs("button", { type: "button", "data-desktop-local-code-change": true, onClick: handleDoLocally, disabled: localCodeChangeRequested, className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90", localCodeChangeRequested && "cursor-wait opacity-70"), children: [localCodeChangeRequested ? (_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(IconCode, { className: "h-3.5 w-3.5" })), localCodeChangeRequested
|
|
235
|
+
? "Preparing locally…"
|
|
236
|
+
: "Do locally"] })), _jsx("button", { type: "button", onClick: () => void handleCopyPrompt(), className: cn("inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent"), children: promptCopied ? (_jsxs(_Fragment, { children: [_jsx(IconCheck, { className: "h-3.5 w-3.5" }), "Prompt copied"] })) : (_jsxs(_Fragment, { children: [_jsx(IconCopy, { className: "h-3.5 w-3.5" }), "Copy prompt"] })) })] })) : showWaitlist && !waitlistJoined ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [showDesktopLocalHandoff && (_jsxs("button", { type: "button", "data-desktop-local-code-change": true, onClick: handleDoLocally, disabled: localCodeChangeRequested, className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90", localCodeChangeRequested && "cursor-wait opacity-70"), children: [localCodeChangeRequested ? (_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(IconCode, { className: "h-3.5 w-3.5" })), localCodeChangeRequested
|
|
237
|
+
? "Preparing locally…"
|
|
238
|
+
: "Do locally"] })), _jsx("button", { type: "button", onClick: handleJoinWaitlist, disabled: joiningWaitlist, className: cn("inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent", joiningWaitlist && "opacity-70 cursor-wait"), children: joiningWaitlist ? (_jsxs(_Fragment, { children: [_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" }), "Joining\u2026"] })) : (_jsx(_Fragment, { children: "Join the waitlist" })) })] })) : !configured ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [showDesktopLocalHandoff && (_jsxs("button", { type: "button", "data-desktop-local-code-change": true, onClick: handleDoLocally, disabled: localCodeChangeRequested, className: cn("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90", localCodeChangeRequested && "cursor-wait opacity-70"), children: [localCodeChangeRequested ? (_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(IconCode, { className: "h-3.5 w-3.5" })), localCodeChangeRequested
|
|
239
|
+
? "Preparing locally…"
|
|
240
|
+
: "Do locally"] })), _jsx("button", { type: "button", onClick: () => flow.start(), disabled: connecting, className: cn("inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent", connecting && "opacity-70 cursor-wait"), children: connecting ? (_jsxs(_Fragment, { children: [_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" }), "Waiting for Builder\u2026"] })) : ("Connect Builder") })] })) : showDesktopLocalHandoff ? (_jsxs("button", { type: "button", "data-desktop-local-code-change": true, onClick: handleDoLocally, disabled: localCodeChangeRequested, className: cn("inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent", localCodeChangeRequested && "cursor-wait opacity-70"), children: [localCodeChangeRequested ? (_jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(IconCode, { className: "h-3.5 w-3.5" })), localCodeChangeRequested ? "Preparing locally…" : "Do locally"] })) : null })] })] }) }));
|
|
213
241
|
}
|