@jxsuite/studio 3.0.0 → 4.0.0
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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
- package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
- package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-xntfdy1x.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-7p35254k.js → studio-ex8zyp98.js} +30 -26
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
- package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-g5gdqnyd.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-p5dja96s.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-k4jvjcfx.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-5efrtn1m.js → studio-wx5609xd.js} +21 -17
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +108 -24
- package/dist/studio.js +2362 -1695
- package/package.json +16 -15
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +485 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +332 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +20 -0
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +103 -5
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ai-import-tools.ts — `import_site`, the assistant's other way to bootstrap a project.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `create_project` (`ai-project-tools.ts`) with a different backend: it drives the
|
|
5
|
+
* headless-Chrome pipeline behind `POST /__studio/import-site`, streams its progress into a record
|
|
6
|
+
* the transcript draws, and then adopts the result exactly as a scaffold is adopted. Its own module
|
|
7
|
+
* rather than an eighth tool in `ai-project-tools.ts`, which is already 650 lines under a per-file
|
|
8
|
+
* coverage bar.
|
|
9
|
+
*
|
|
10
|
+
* Two things it does NOT do are the point:
|
|
11
|
+
*
|
|
12
|
+
* - **It does not invent a destination.** `create_project` requires the model to supply `location`
|
|
13
|
+
* and refuses without one — correct when a bare chat bootstraps a project, a regression on the
|
|
14
|
+
* wizard path where the Location field already has a folder picker, validation and a live preview.
|
|
15
|
+
* The destination comes from the pending brief; a model-supplied one must match it.
|
|
16
|
+
* - **It does not decide what the crawl found.** The pipeline is one-way: it reports and finishes.
|
|
17
|
+
* Everything worth a human's opinion — which skipped pages matter, whether the components it
|
|
18
|
+
* extracted are really components — is in the summary for the model to raise with `ask_user`
|
|
19
|
+
* AFTERWARDS, against real numbers rather than a guess made before the browser launched.
|
|
20
|
+
*
|
|
21
|
+
* @license MIT
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createToolDefinition, toolError, toolSuccess } from "@jxsuite/ai/tools";
|
|
25
|
+
import { getPlatform } from "../platform";
|
|
26
|
+
import { workspace } from "../workspace/workspace";
|
|
27
|
+
import { getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
28
|
+
import { preferredModel } from "./ai-models";
|
|
29
|
+
import { clearPendingImportBrief, pendingImportBrief } from "./import-seed";
|
|
30
|
+
import {
|
|
31
|
+
abortImportRun,
|
|
32
|
+
beginImportRun,
|
|
33
|
+
finishImportRun,
|
|
34
|
+
importRun,
|
|
35
|
+
recordImportProgress,
|
|
36
|
+
} from "./import-run";
|
|
37
|
+
import { adoptCreatedProject } from "./project-adoption";
|
|
38
|
+
import type { AdoptOutcome } from "./project-adoption";
|
|
39
|
+
import { currentToolCallId, turnSignal } from "./ai-turn-signal";
|
|
40
|
+
|
|
41
|
+
import type { ToolRegistry } from "@jxsuite/ai/tools";
|
|
42
|
+
import type { Tab } from "../tabs/tab";
|
|
43
|
+
import type { ImportBreakpointPolicy, ImportReadyEvent, ImportSiteSummary } from "../types";
|
|
44
|
+
|
|
45
|
+
/** The same bounds `packages/server/src/import-api.ts` clamps to. */
|
|
46
|
+
const MAX_DEPTH = 5;
|
|
47
|
+
const MAX_PAGES = 100;
|
|
48
|
+
|
|
49
|
+
/** Log lines quoted back on a hard failure — enough to see WHICH phase died. */
|
|
50
|
+
const FAILURE_TAIL = 5;
|
|
51
|
+
|
|
52
|
+
/** Low-fidelity pages named in the summary. A list of twenty is a wall, not a finding. */
|
|
53
|
+
const WEAKEST_PAGES = 3;
|
|
54
|
+
|
|
55
|
+
/** Below this, a page is worth naming. Above it, "close enough" is the honest reading. */
|
|
56
|
+
const WEAK_FIDELITY = 90;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The default bar an import is measured against, matching `jx-import --min-fidelity`.
|
|
60
|
+
*
|
|
61
|
+
* A floor for "this is not a clone of anything" rather than a quality target: a faithful import of
|
|
62
|
+
* a complicated site lands well under 100 for reasons no importer can fix.
|
|
63
|
+
*/
|
|
64
|
+
const DEFAULT_MIN_FIDELITY = 25;
|
|
65
|
+
const MAX_FIDELITY = 100;
|
|
66
|
+
|
|
67
|
+
export interface ImportToolsCtx {
|
|
68
|
+
/** The active tab, read AFTER adoption to re-anchor the agent loop's undo batch. */
|
|
69
|
+
getTab: () => Tab | null;
|
|
70
|
+
/** Open the imported project in this window. Defaults to the late-bound adopter. */
|
|
71
|
+
adoptProject?: (root: string) => Promise<void>;
|
|
72
|
+
/** Fired once adoption is VERIFIED — the session store re-keys the live chat here. */
|
|
73
|
+
onProjectAdopted?: (root: string) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clamp(value: number, min: number, max: number): number {
|
|
77
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The breakpoint policy for this run: what the model asked for, else what the wizard collected.
|
|
82
|
+
*
|
|
83
|
+
* The model may narrow this — it is the one option a person is likely to have an opinion about
|
|
84
|
+
* after seeing the crawl — but it never invents an override the user did not ask for, because the
|
|
85
|
+
* wizard's control is already the answer to "how many breakpoints should this project have".
|
|
86
|
+
*
|
|
87
|
+
* @param {object} args - The model's `maxBreakpoints` / `breakpointWidths` / `breakpointRounding`
|
|
88
|
+
* @param {ImportBreakpointPolicy | undefined} fromBrief - What the wizard collected, if anything
|
|
89
|
+
* @returns {ImportBreakpointPolicy | undefined}
|
|
90
|
+
*/
|
|
91
|
+
function breakpointPolicyFor(
|
|
92
|
+
args: { maxBreakpoints?: unknown; widths?: unknown; rounding?: unknown },
|
|
93
|
+
fromBrief: ImportBreakpointPolicy | undefined,
|
|
94
|
+
): ImportBreakpointPolicy | undefined {
|
|
95
|
+
const rounding =
|
|
96
|
+
args.rounding === "down" || args.rounding === "up" || args.rounding === "nearest"
|
|
97
|
+
? args.rounding
|
|
98
|
+
: (fromBrief?.rounding ?? "nearest");
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(args.widths) && args.widths.length > 0) {
|
|
101
|
+
return { mode: "explicit", rounding, widths: args.widths.map(Number) };
|
|
102
|
+
}
|
|
103
|
+
if (typeof args.maxBreakpoints === "number" && Number.isFinite(args.maxBreakpoints)) {
|
|
104
|
+
// Zero is the only spelling a count has for "keep all", and it is worth accepting: it is what
|
|
105
|
+
// A model reaches for when a person says "keep them all".
|
|
106
|
+
return args.maxBreakpoints <= 0
|
|
107
|
+
? { mode: "all" }
|
|
108
|
+
: { count: Math.trunc(args.maxBreakpoints), mode: "limit", rounding };
|
|
109
|
+
}
|
|
110
|
+
return fromBrief;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Absolute on POSIX or Windows — the test `create_project` already applies to its `location`. */
|
|
114
|
+
function isAbsolutePath(path: string): boolean {
|
|
115
|
+
return /^(?:[a-zA-Z]:[/\\]|\/)/.test(path);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One run has already succeeded in this session.
|
|
120
|
+
*
|
|
121
|
+
* Not the same question as `workspace.projectRoot`, and that is the whole reason it exists: on
|
|
122
|
+
* desktop, adoption may open the project in ANOTHER window, leaving this one's root empty — so the
|
|
123
|
+
* `no-project` tier would happily advertise a second import over the top of the first.
|
|
124
|
+
* `create_project` has the identical hazard today.
|
|
125
|
+
*/
|
|
126
|
+
let imported = false;
|
|
127
|
+
|
|
128
|
+
/** Forget that an import ran. New Chat, and tests. */
|
|
129
|
+
export function resetImportGuard(): void {
|
|
130
|
+
imported = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A compact, factual account of the run for the model to reason about — and to ask about.
|
|
135
|
+
*
|
|
136
|
+
* Counts only where they are real. `streamImport` returns `{ root, config }` and nothing else, so
|
|
137
|
+
* the page and file totals are not available here; inventing them from log prose is exactly how a
|
|
138
|
+
* confidently wrong number gets into a summary. It says what it knows and points at the tool that
|
|
139
|
+
* knows the rest.
|
|
140
|
+
*/
|
|
141
|
+
function describeRun(id: string, root: string, summary?: ImportSiteSummary): string {
|
|
142
|
+
const record = importRun(id);
|
|
143
|
+
const warnings = [...(record?.warnings ?? []), ...(summary?.warnings ?? [])];
|
|
144
|
+
const lines = [`Imported ${record?.url ?? "the site"} into ${root} and opened it.`];
|
|
145
|
+
|
|
146
|
+
const pages = summary?.pages ?? [];
|
|
147
|
+
if (pages.length > 0) {
|
|
148
|
+
lines.push(
|
|
149
|
+
`${pages.length} page${pages.length === 1 ? "" : "s"}` +
|
|
150
|
+
`${summary?.fileCount ? `, ${summary.fileCount} files` : ""}: ` +
|
|
151
|
+
`${pages.map((p) => p.route).join(", ")}.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Per page, because the average cannot name one. "84% average" is a fact nobody can act on;
|
|
156
|
+
"the pricing page renders at 61%" is a decision — and it is the one finding here worth
|
|
157
|
+
interrupting a person for. */
|
|
158
|
+
if (summary?.verify) {
|
|
159
|
+
const weakest = (summary.verify.pages ?? [])
|
|
160
|
+
.filter((p) => p.fidelity < WEAK_FIDELITY)
|
|
161
|
+
.toSorted((a, b) => a.fidelity - b.fidelity)
|
|
162
|
+
.slice(0, WEAKEST_PAGES);
|
|
163
|
+
const detail =
|
|
164
|
+
weakest.length > 0
|
|
165
|
+
? `; weakest: ${weakest.map((p) => `${p.route} at ${p.fidelity}%`).join(", ")}.`
|
|
166
|
+
: " on every page.";
|
|
167
|
+
lines.push(
|
|
168
|
+
`Fidelity against the original averaged ${summary.verify.averageFidelity}%${detail}`,
|
|
169
|
+
);
|
|
170
|
+
/*
|
|
171
|
+
* The bar the user set, and whether this run cleared it. Stated FIRST among the details,
|
|
172
|
+
* because it is the difference between a number to note and a result to act on: an import at
|
|
173
|
+
* 8% used to report exactly like one at 95%, and the only way to tell them apart was to open
|
|
174
|
+
* the project and look.
|
|
175
|
+
*/
|
|
176
|
+
if (summary.verify.passed === false && (summary.verify.buildErrors ?? []).length === 0) {
|
|
177
|
+
lines.push(
|
|
178
|
+
`That is below the ${summary.verify.minFidelity ?? 0}% minimum this import was asked ` +
|
|
179
|
+
"for, so the clone does not match the original closely enough. Say so plainly, and " +
|
|
180
|
+
"offer to look at what went wrong rather than moving on.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
/*
|
|
184
|
+
* The score says a page looks wrong; this says why. A page that 404s on fifteen asset
|
|
185
|
+
* references scores badly for one fixable reason, and reading a percentage on its own sends
|
|
186
|
+
* the reader looking for the wrong thing entirely (jxsuite/jx issue 232).
|
|
187
|
+
*/
|
|
188
|
+
const missing = (summary.verify.pages ?? []).reduce(
|
|
189
|
+
(sum, page) => sum + (page.failedRequests ?? 0),
|
|
190
|
+
0,
|
|
191
|
+
);
|
|
192
|
+
if (missing > 0) {
|
|
193
|
+
lines.push(
|
|
194
|
+
`${missing} request${missing === 1 ? "" : "s"} failed or 404'd in the rendered pages — ` +
|
|
195
|
+
"check the emitted asset paths before treating a low score as a layout problem.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
for (const error of summary.verify.buildErrors ?? []) {
|
|
199
|
+
lines.push(`The emitted project did not build cleanly: ${error}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (warnings.length > 0) {
|
|
204
|
+
lines.push(
|
|
205
|
+
`${warnings.length} warning${warnings.length === 1 ? "" : "s"} during the run: ` +
|
|
206
|
+
`${warnings.join("; ")}.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
lines.push(
|
|
210
|
+
"The file and document tools are available now. Read the emitted pages, layouts and " +
|
|
211
|
+
"components before proposing changes — then ask the user about anything the import had to " +
|
|
212
|
+
"guess at (pages it skipped, components it may have split wrongly, a layout it did not find, " +
|
|
213
|
+
"a page that did not render faithfully).",
|
|
214
|
+
);
|
|
215
|
+
return lines.join(" ");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Register `import_site`.
|
|
220
|
+
*
|
|
221
|
+
* @param {Pick<ToolRegistry, "register">} registry
|
|
222
|
+
* @param {ImportToolsCtx} ctx
|
|
223
|
+
*/
|
|
224
|
+
export function registerImportTools(
|
|
225
|
+
registry: Pick<ToolRegistry, "register">,
|
|
226
|
+
{ getTab, adoptProject, onProjectAdopted }: ImportToolsCtx,
|
|
227
|
+
): void {
|
|
228
|
+
registry.register(
|
|
229
|
+
createToolDefinition({
|
|
230
|
+
name: "import_site",
|
|
231
|
+
description:
|
|
232
|
+
"Clone a live website into a new Jx project and open it in the studio: pages, styles, " +
|
|
233
|
+
"assets, a shared layout and recurring components. Only available while no project is " +
|
|
234
|
+
"open. Progress streams into the conversation as it runs. When the user reached you " +
|
|
235
|
+
"through the New Project Import form, the URL, crawl options and destination are already " +
|
|
236
|
+
"settled — call this with the url alone and the rest is filled in. After it succeeds the " +
|
|
237
|
+
"file and document tools become available in the next round.",
|
|
238
|
+
parameters: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
url: { type: "string", description: "The site to clone. Must be http(s)." },
|
|
242
|
+
directory: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description:
|
|
245
|
+
"Absolute destination folder. Omit it when an import was started from the New " +
|
|
246
|
+
"Project form — that destination is the one the user chose, and it wins.",
|
|
247
|
+
},
|
|
248
|
+
depth: {
|
|
249
|
+
type: "number",
|
|
250
|
+
description:
|
|
251
|
+
"Crawl depth. 0 imports the single page and runs an entirely different, much faster " +
|
|
252
|
+
"pipeline — no crawl, no shared-layout detection. Default 1.",
|
|
253
|
+
},
|
|
254
|
+
maxPages: { type: "number", description: "Most pages to capture (1–100). Default 20." },
|
|
255
|
+
verify: {
|
|
256
|
+
type: "boolean",
|
|
257
|
+
description:
|
|
258
|
+
"Build the imported project and screenshot-diff every page against the original, " +
|
|
259
|
+
"reporting a per-page fidelity score. Roughly doubles the run, and is the only " +
|
|
260
|
+
"finding that says how WELL the clone came out rather than what was skipped.",
|
|
261
|
+
},
|
|
262
|
+
minFidelity: {
|
|
263
|
+
type: "number",
|
|
264
|
+
description:
|
|
265
|
+
"With verify on, the average fidelity (0-100) the clone must reach to count as " +
|
|
266
|
+
"matching the original. A floor rather than a target: a faithful import of a " +
|
|
267
|
+
"complicated site lands well under 100, so raise it only when the user asks for a " +
|
|
268
|
+
"stricter bar. 0 reports the score without judging it. Default 25.",
|
|
269
|
+
},
|
|
270
|
+
aiComponents: {
|
|
271
|
+
type: "boolean",
|
|
272
|
+
description:
|
|
273
|
+
"Refine component and prop names with the model. Costs one model call per extracted " +
|
|
274
|
+
"component, so it is worth asking about on a wide crawl. Default true.",
|
|
275
|
+
},
|
|
276
|
+
maxBreakpoints: {
|
|
277
|
+
type: "number",
|
|
278
|
+
description:
|
|
279
|
+
"How many of the site's declared breakpoints to keep, evenly spaced across its " +
|
|
280
|
+
"range (1-12). A real site declares as many as it has accumulated frameworks, and " +
|
|
281
|
+
"each one is a canvas size and a column in every style editor. Default 3; pass 0 to " +
|
|
282
|
+
"keep every one. Ignored when breakpointWidths is given.",
|
|
283
|
+
},
|
|
284
|
+
breakpointWidths: {
|
|
285
|
+
type: "array",
|
|
286
|
+
items: { type: "number" },
|
|
287
|
+
description:
|
|
288
|
+
"Keep these widths instead of a count, e.g. [640, 1024, 1440]. Each is backed by " +
|
|
289
|
+
"the declared width nearest it, because the styles flip where the site says they " +
|
|
290
|
+
"do. Ask the user before choosing widths they did not name.",
|
|
291
|
+
},
|
|
292
|
+
breakpointRounding: {
|
|
293
|
+
type: "string",
|
|
294
|
+
enum: ["nearest", "down", "up"],
|
|
295
|
+
description: "How a kept width matches a declared one. Default nearest.",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
required: ["url"],
|
|
299
|
+
},
|
|
300
|
+
async execute(args) {
|
|
301
|
+
const {
|
|
302
|
+
url,
|
|
303
|
+
directory,
|
|
304
|
+
depth,
|
|
305
|
+
maxPages,
|
|
306
|
+
aiComponents,
|
|
307
|
+
verify,
|
|
308
|
+
minFidelity,
|
|
309
|
+
maxBreakpoints,
|
|
310
|
+
breakpointWidths,
|
|
311
|
+
breakpointRounding,
|
|
312
|
+
} = args as {
|
|
313
|
+
url?: unknown;
|
|
314
|
+
directory?: unknown;
|
|
315
|
+
depth?: unknown;
|
|
316
|
+
maxPages?: unknown;
|
|
317
|
+
aiComponents?: unknown;
|
|
318
|
+
verify?: unknown;
|
|
319
|
+
minFidelity?: unknown;
|
|
320
|
+
maxBreakpoints?: unknown;
|
|
321
|
+
breakpointWidths?: unknown;
|
|
322
|
+
breakpointRounding?: unknown;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (workspace.projectRoot) {
|
|
326
|
+
return toolError(
|
|
327
|
+
"A project is already open in this window — import_site is only for bootstrapping.",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (imported) {
|
|
331
|
+
return toolError(
|
|
332
|
+
"A site has already been imported in this conversation. Start a new chat to import " +
|
|
333
|
+
"another, or use the file tools on the project that was created.",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const platform = getPlatform();
|
|
337
|
+
if (!platform.importSite) {
|
|
338
|
+
return toolError("This Studio platform has no site-import backend.");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let target: URL;
|
|
342
|
+
try {
|
|
343
|
+
target = new URL(String(url ?? "").trim());
|
|
344
|
+
} catch {
|
|
345
|
+
return toolError('Pass "url" — an absolute http(s) address, e.g. https://example.com.');
|
|
346
|
+
}
|
|
347
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
348
|
+
return toolError("The url must start with http:// or https://.");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const brief = pendingImportBrief();
|
|
352
|
+
const requested = typeof directory === "string" ? directory.trim() : "";
|
|
353
|
+
if (requested && brief && requested !== brief.directory) {
|
|
354
|
+
/* The wizard's Location field is the user's answer to "where does this go". A model that
|
|
355
|
+
disagrees with it is guessing at a decision that was already made in front of them. */
|
|
356
|
+
return toolError(
|
|
357
|
+
`The destination is already set to ${brief.directory} — call import_site without ` +
|
|
358
|
+
'"directory", or ask the user before changing it.',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const destination = requested || brief?.directory || "";
|
|
362
|
+
if (!destination) {
|
|
363
|
+
return toolError(
|
|
364
|
+
'Pass "directory" — the absolute folder to create the project in. Ask the user where ' +
|
|
365
|
+
"the project should live rather than guessing.",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (!isAbsolutePath(destination)) {
|
|
369
|
+
return toolError(`"directory" must be an absolute path: ${destination}`);
|
|
370
|
+
}
|
|
371
|
+
if (destination.split(/[/\\]/).includes("..")) {
|
|
372
|
+
return toolError(`"directory" must not contain "..": ${destination}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const id = currentToolCallId();
|
|
376
|
+
const signal = beginImportRun(id, { directory: destination, url: target.href });
|
|
377
|
+
/* The turn's own signal too. `assistant.stop` aborts the request through `abortImportRun`,
|
|
378
|
+
but that is not the only way a turn ends: the loop's signal can be aborted directly, and
|
|
379
|
+
a run left going after its turn died would keep a headless browser open for minutes with
|
|
380
|
+
nothing left to receive the result. */
|
|
381
|
+
turnSignal()?.addEventListener("abort", () => {
|
|
382
|
+
abortImportRun();
|
|
383
|
+
finishImportRun(id, { status: "stopped" });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const apiKey = getOpenAiKey();
|
|
387
|
+
const baseUrl = getBaseUrl();
|
|
388
|
+
const model = brief?.model || preferredModel();
|
|
389
|
+
|
|
390
|
+
const breakpoints = breakpointPolicyFor(
|
|
391
|
+
{ maxBreakpoints, rounding: breakpointRounding, widths: breakpointWidths },
|
|
392
|
+
brief?.breakpoints,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* Adoption happens the moment the destination is a project, not when the run ends.
|
|
397
|
+
*
|
|
398
|
+
* A crawl takes minutes, and it used to spend all of them with the author on the welcome
|
|
399
|
+
* screen — the tool opened the project once `importSite` resolved, so the one view of what
|
|
400
|
+
* the import was doing was a log in the sidebar. Opening at `ready` puts them IN the project
|
|
401
|
+
* while it fills: the Files tree gets each page, component and asset as the pipeline writes
|
|
402
|
+
* it, through the watcher that is already running.
|
|
403
|
+
*
|
|
404
|
+
* It runs at most once (a backend may send the line once, and only one can be first), and a
|
|
405
|
+
* failure here is not the run's failure: the import continues either way, and the terminal
|
|
406
|
+
* path below reports honestly on whichever state the window ended up in.
|
|
407
|
+
*/
|
|
408
|
+
let adoption: Promise<AdoptOutcome> | null = null;
|
|
409
|
+
const onReady = ({ root }: ImportReadyEvent) => {
|
|
410
|
+
adoption ??= adoptCreatedProject(root, {
|
|
411
|
+
getTab,
|
|
412
|
+
...(adoptProject ? { adopt: adoptProject } : {}),
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
let result: { root: string; result?: ImportSiteSummary };
|
|
417
|
+
try {
|
|
418
|
+
result = await platform.importSite(
|
|
419
|
+
{
|
|
420
|
+
aiComponents:
|
|
421
|
+
typeof aiComponents === "boolean" ? aiComponents : (brief?.aiComponents ?? true),
|
|
422
|
+
depth: clamp(Number(depth ?? brief?.depth ?? 1) || 0, 0, MAX_DEPTH),
|
|
423
|
+
directory: destination,
|
|
424
|
+
maxPages: clamp(Number(maxPages ?? brief?.maxPages ?? 20) || 1, 1, MAX_PAGES),
|
|
425
|
+
name: brief?.name || target.hostname.replace(/^www\./, ""),
|
|
426
|
+
url: target.href,
|
|
427
|
+
...(breakpoints === undefined ? {} : { breakpoints }),
|
|
428
|
+
...(typeof verify === "boolean"
|
|
429
|
+
? { verify }
|
|
430
|
+
: brief?.verify === undefined
|
|
431
|
+
? {}
|
|
432
|
+
: { verify: brief.verify }),
|
|
433
|
+
/* The wizard's number is the user's own answer to "how close is close enough"; the
|
|
434
|
+
model may only override it when it was told to. */
|
|
435
|
+
verifyMinFidelity: clamp(
|
|
436
|
+
Number(minFidelity ?? brief?.minFidelity ?? DEFAULT_MIN_FIDELITY) || 0,
|
|
437
|
+
0,
|
|
438
|
+
MAX_FIDELITY,
|
|
439
|
+
),
|
|
440
|
+
...(apiKey ? { apiKey } : {}),
|
|
441
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
442
|
+
...(model ? { model } : {}),
|
|
443
|
+
},
|
|
444
|
+
(evt) => recordImportProgress(id, evt),
|
|
445
|
+
signal,
|
|
446
|
+
onReady,
|
|
447
|
+
);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
450
|
+
if (signal.aborted) {
|
|
451
|
+
finishImportRun(id, { error: message, status: "stopped" });
|
|
452
|
+
return toolError("The import was stopped.");
|
|
453
|
+
}
|
|
454
|
+
finishImportRun(id, { error: message, status: "failed" });
|
|
455
|
+
/* The tail, not just the message: the commonest recovery is retrying at depth 0, which is
|
|
456
|
+
an entirely different code path, and the phase that died is what says whether that
|
|
457
|
+
would help. */
|
|
458
|
+
const tail = (importRun(id)?.log ?? [])
|
|
459
|
+
.slice(-FAILURE_TAIL)
|
|
460
|
+
.map((e) => `[${e.phase}] ${e.message}`)
|
|
461
|
+
.join("\n");
|
|
462
|
+
return toolError(
|
|
463
|
+
`The import failed: ${message}${tail ? `\n\nLast steps:\n${tail}` : ""}`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
finishImportRun(id, { status: "done" });
|
|
468
|
+
imported = true;
|
|
469
|
+
clearPendingImportBrief();
|
|
470
|
+
|
|
471
|
+
/* The early adoption if there was one, otherwise adopt now: a backend that sends no `ready`
|
|
472
|
+
line is not broken, it is older, and the project must still open. */
|
|
473
|
+
const { adopted, error: adoptionError } = await (adoption ??
|
|
474
|
+
adoptCreatedProject(result.root, {
|
|
475
|
+
getTab,
|
|
476
|
+
...(adoptProject ? { adopt: adoptProject } : {}),
|
|
477
|
+
}));
|
|
478
|
+
if (adopted) {
|
|
479
|
+
onProjectAdopted?.(result.root);
|
|
480
|
+
return toolSuccess(
|
|
481
|
+
{ root: result.root, ...(result.result ? { summary: result.result } : {}) },
|
|
482
|
+
describeRun(id, result.root, result.result),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return toolSuccess(
|
|
486
|
+
{ root: result.root },
|
|
487
|
+
`Imported the site into ${result.root}, but it was not opened in this window` +
|
|
488
|
+
`${adoptionError ? ` (${adoptionError})` : " (it may have opened in another window)"}. ` +
|
|
489
|
+
"Ask the user to open it from the welcome screen or recent projects.",
|
|
490
|
+
);
|
|
491
|
+
},
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
@@ -20,8 +20,9 @@ import type { ToolRegistry, ToolResult } from "@jxsuite/ai/tools";
|
|
|
20
20
|
import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
21
21
|
import { getPlatform } from "../platform";
|
|
22
22
|
import { workspace } from "../workspace/workspace";
|
|
23
|
+
import { projectState } from "../store";
|
|
23
24
|
import type { Tab } from "../tabs/tab";
|
|
24
|
-
import {
|
|
25
|
+
import { adoptCreatedProject } from "./project-adoption";
|
|
25
26
|
import { translateValidationError } from "./ai-tools";
|
|
26
27
|
import { validateDoc, validateProjectConfig } from "./jx-validate";
|
|
27
28
|
import { recordWrite } from "./ai-writes";
|
|
@@ -355,8 +356,16 @@ export function registerProjectTools(
|
|
|
355
356
|
/* The per-project entry document closes the composition over every enabled extension
|
|
356
357
|
(extensions.md §5.2), so this is what catches a typo'd section key or a misshapen
|
|
357
358
|
extension section. Without it the model shipped config its own tool call called clean
|
|
358
|
-
and Monaco flagged the moment a human opened the file.
|
|
359
|
-
|
|
359
|
+
and Monaco flagged the moment a human opened the file.
|
|
360
|
+
|
|
361
|
+
Errors the file ALREADY had are subtracted, the same way `services/ai-tools.ts`
|
|
362
|
+
subtracts them from a document edit: a project whose config was written by something
|
|
363
|
+
else — an importer, a hand edit, an extension that was removed — would otherwise refuse
|
|
364
|
+
every write the model attempted, blaming it for a violation it did not introduce and
|
|
365
|
+
cannot reach from the edit it was asked to make. */
|
|
366
|
+
const before = new Set(await validateProject(projectState?.projectConfig ?? {}));
|
|
367
|
+
const found = await validateProject(parsedConfig);
|
|
368
|
+
const configErrors = found.filter((error) => !before.has(error));
|
|
360
369
|
if (configErrors.length > 0) {
|
|
361
370
|
const formatted = configErrors
|
|
362
371
|
.map((e) => `- ${translateValidationError(e)}`)
|
|
@@ -585,30 +594,15 @@ export function registerProjectTools(
|
|
|
585
594
|
}
|
|
586
595
|
|
|
587
596
|
/*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
|
|
592
|
-
const wasBatching = isBatching();
|
|
593
|
-
if (wasBatching) {
|
|
594
|
-
endBatch();
|
|
595
|
-
}
|
|
596
|
-
let adoptionError: string | null = null;
|
|
597
|
-
try {
|
|
598
|
-
await adoptProject(result.root);
|
|
599
|
-
} catch (error) {
|
|
600
|
-
adoptionError = error instanceof Error ? error.message : String(error);
|
|
601
|
-
}
|
|
602
|
-
if (wasBatching) {
|
|
603
|
-
beginBatch(getTab());
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/*
|
|
607
|
-
* The adopter (openRecentProject) swallows failures into a status message, so a resolved
|
|
608
|
-
* promise is not proof of adoption — verify against the workspace before re-keying the
|
|
609
|
-
* chat session.
|
|
597
|
+
* Git init, then the full project-open flow, then a check that it landed — all three in
|
|
598
|
+
* `services/project-adoption.ts`, because `import_site` is the same act with a different
|
|
599
|
+
* backend. Git init in particular was missing here: `specs/desktop.md` §4.5 promises every
|
|
600
|
+
* create path initialises a repository, and only the New Project modal was keeping it.
|
|
610
601
|
*/
|
|
611
|
-
const adopted =
|
|
602
|
+
const { adopted, error: adoptionError } = await adoptCreatedProject(result.root, {
|
|
603
|
+
adopt: adoptProject,
|
|
604
|
+
getTab,
|
|
605
|
+
});
|
|
612
606
|
if (adopted) {
|
|
613
607
|
onProjectAdopted?.(result.root);
|
|
614
608
|
return {
|