@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.
Files changed (173) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
  4. package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
  5. package/dist/chunks/content-source-t88vb2dr.js +49 -0
  6. package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
  9. package/dist/chunks/file-ops-rskmhxyq.js +48 -0
  10. package/dist/chunks/files-0cratsr0.js +92 -0
  11. package/dist/chunks/{format-host-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
  14. package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
  15. package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
  16. package/dist/chunks/layers-q75tj2mf.js +45 -0
  17. package/dist/chunks/lit-html-j055mkh8.js +22 -0
  18. package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
  19. package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
  20. package/dist/chunks/notify-yjm79n4s.js +30 -0
  21. package/dist/chunks/parse-24d0gay3.js +19 -0
  22. package/dist/chunks/platform-ba1j1gbh.js +16 -0
  23. package/dist/chunks/publish-panel-54jvhskq.js +28 -0
  24. package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
  25. package/dist/chunks/references-3qx8fkyp.js +29 -0
  26. package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
  27. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
  28. package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
  29. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
  30. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
  31. package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
  32. package/dist/chunks/{studio-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
  33. package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
  34. package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -133
  38. package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
  39. package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
  40. package/dist/chunks/{studio-xntfdy1x.js → studio-8swrjbny.js} +31 -18
  41. package/dist/chunks/studio-92ab2jqe.js +282 -0
  42. package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
  43. package/dist/chunks/{studio-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
  44. package/dist/chunks/studio-9n4mrmnb.js +184 -0
  45. package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
  46. package/dist/chunks/{studio-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-hcxzyk3x.js → studio-eenfkt4e.js} +6 -6
  51. package/dist/chunks/studio-eesa1myv.js +498 -0
  52. package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
  53. package/dist/chunks/{studio-7p35254k.js → studio-ex8zyp98.js} +30 -26
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
  57. package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
  60. package/dist/chunks/{studio-g5gdqnyd.js → studio-kyk2v92a.js} +14 -14
  61. package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
  62. package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
  63. package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
  64. package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
  65. package/dist/chunks/{studio-p5dja96s.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
  67. package/dist/chunks/studio-q43a5y9h.js +394 -0
  68. package/dist/chunks/studio-q7v10cg2.js +106 -0
  69. package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
  70. package/dist/chunks/studio-r1r7afxx.js +1066 -0
  71. package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
  72. package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
  73. package/dist/chunks/studio-sf7zjgv1.js +91 -0
  74. package/dist/chunks/studio-t7qb9db8.js +665 -0
  75. package/dist/chunks/studio-tmc5emrw.js +8 -0
  76. package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
  77. package/dist/chunks/{studio-k4jvjcfx.js → studio-v38hmvx1.js} +7 -7
  78. package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
  79. package/dist/chunks/studio-wcy8trfx.js +263 -0
  80. package/dist/chunks/{studio-5efrtn1m.js → studio-wx5609xd.js} +21 -17
  81. package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
  82. package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
  83. package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
  84. package/dist/chunks/workspace-xpres40v.js +89 -0
  85. package/dist/iframe-entry.js +108 -24
  86. package/dist/studio.js +2362 -1695
  87. package/package.json +16 -15
  88. package/src/browse/library-pane.ts +57 -3
  89. package/src/canvas/canvas-render.ts +23 -1
  90. package/src/canvas/iframe-patch.ts +47 -21
  91. package/src/canvas/iframe-render.ts +22 -6
  92. package/src/commands/app-commands.ts +4 -0
  93. package/src/commands/command-args.ts +29 -0
  94. package/src/commands/context.ts +15 -1
  95. package/src/commands/defaults.ts +29 -1
  96. package/src/commands/levels.ts +16 -0
  97. package/src/commands/live-context.ts +3 -0
  98. package/src/content/collection-match.ts +267 -0
  99. package/src/content/entry-commands.ts +38 -29
  100. package/src/content/entry-fields.ts +6 -0
  101. package/src/editor/shortcuts.ts +5 -0
  102. package/src/files/file-ops.ts +10 -42
  103. package/src/files/files.ts +485 -59
  104. package/src/files/fs-events.ts +9 -0
  105. package/src/files/gitignore.ts +432 -0
  106. package/src/files/serialize-document.ts +60 -0
  107. package/src/format/convert-file.ts +511 -0
  108. package/src/format/format-choices.ts +199 -0
  109. package/src/grid/grid-view.ts +16 -2
  110. package/src/media/media-open.ts +58 -0
  111. package/src/media/media-pane.ts +310 -0
  112. package/src/new-project/import-tab.ts +332 -160
  113. package/src/new-project/new-project-modal.ts +27 -34
  114. package/src/packages/ensure-deps.ts +14 -7
  115. package/src/packages/jxsuite-update.ts +11 -11
  116. package/src/page-params.ts +10 -46
  117. package/src/panels/activity-bar.ts +45 -16
  118. package/src/panels/ai-chat/attached-context.ts +6 -1
  119. package/src/panels/ai-chat/chat-view.ts +255 -13
  120. package/src/panels/ai-chat/composer.ts +21 -97
  121. package/src/panels/ai-panel.ts +116 -2
  122. package/src/panels/navigator-panels.ts +7 -6
  123. package/src/panels/settings-menu.ts +799 -0
  124. package/src/panels/toolbar.ts +135 -15
  125. package/src/platforms/cloud.ts +20 -0
  126. package/src/platforms/devserver.ts +67 -33
  127. package/src/preview/preview-overlay.ts +226 -0
  128. package/src/services/ai-ask.ts +247 -0
  129. package/src/services/ai-import-tools.ts +494 -0
  130. package/src/services/ai-project-tools.ts +20 -26
  131. package/src/services/ai-system-prompt.ts +88 -2
  132. package/src/services/ai-turn-signal.ts +70 -0
  133. package/src/services/connection.ts +74 -0
  134. package/src/services/context-manager.ts +89 -0
  135. package/src/services/document-assistant.ts +45 -4
  136. package/src/services/import-client.ts +33 -6
  137. package/src/services/import-run.ts +158 -0
  138. package/src/services/import-seed.ts +120 -0
  139. package/src/services/jx-validate.ts +58 -6
  140. package/src/services/project-adoption.ts +71 -0
  141. package/src/services/references.ts +0 -0
  142. package/src/services/settings/definitions.ts +17 -0
  143. package/src/services/tool-executor.ts +34 -2
  144. package/src/settings/contexts-section.ts +45 -3
  145. package/src/settings/dependencies-editor.ts +43 -23
  146. package/src/settings/general-settings.ts +30 -12
  147. package/src/settings/preferences-dialog.ts +18 -41
  148. package/src/settings/preferences-sections.ts +57 -0
  149. package/src/settings/settings-document.ts +65 -9
  150. package/src/studio.ts +33 -6
  151. package/src/tabs/transact.ts +42 -19
  152. package/src/types.ts +103 -5
  153. package/src/ui/ai-model-picker.ts +162 -0
  154. package/src/ui/choice-field.ts +72 -0
  155. package/src/ui/layers.ts +83 -5
  156. package/src/utils/studio-utils.ts +22 -52
  157. package/styles/forced-colors.css +7 -0
  158. package/styles/overlays.css +256 -0
  159. package/styles/panels.css +13 -1
  160. package/styles/shell.css +111 -0
  161. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  162. package/dist/chunks/content-source-7fy9qrm0.js +0 -40
  163. package/dist/chunks/files-xcybanxe.js +0 -70
  164. package/dist/chunks/layers-panel-27essgkc.js +0 -60
  165. package/dist/chunks/publish-panel-09sy2bab.js +0 -22
  166. package/dist/chunks/quick-search-sefb61v8.js +0 -55
  167. package/dist/chunks/right-panel-9r32jd5j.js +0 -39
  168. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  169. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  170. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  171. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  172. package/dist/chunks/studio-c31yb4t6.js +0 -55
  173. package/src/canvas/site-style-css.ts +0 -117
@@ -1,22 +1,45 @@
1
1
  /**
2
2
  * The New Project modal's Import source step: an AI-guided clone of an existing site. Gated behind
3
3
  * the AI credentials gate (key form + the keyless Cloudflare option) until AI is usable — a local
4
- * key, or a backend holding its own; otherwise a URL + crawl-options form. The name/directory
5
- * parameters live on the modal's shared Parameters step; the running pipeline streams its progress
6
- * into a log here and resolves the modal with the imported project.
4
+ * key, or a backend holding its own; otherwise a URL, crawl options, a model and a brief. The
5
+ * name/directory parameters live on the modal's shared Parameters step.
6
+ *
7
+ * **This form no longer runs the import.** It gathers a brief and hands it to the assistant, which
8
+ * runs the pipeline as a tool call. The wizard used to own the run, and the consequence was that a
9
+ * successful import destroyed its own account of itself: every phase line and every warning lived
10
+ * in a log that vanished with the modal at the moment it handed off. In the transcript the run
11
+ * survives, the model can read what the crawl actually found, and it can stop and ask about
12
+ * anything the pipeline had to guess at.
7
13
  */
8
14
 
9
- import { html } from "lit-html";
15
+ import { html, nothing } from "lit-html";
16
+ import { live } from "lit/directives/live.js";
10
17
  import { getPlatform } from "../platform";
11
- import { getBaseUrl, getOpenAiKey } from "../services/ai-settings";
12
18
  import { preferredModel } from "../services/ai-models";
13
- import { IMPORT_WARNING_PHASE } from "../services/import-client";
14
- import { notify } from "../services/notify";
15
- import { errorMessage } from "@jxsuite/schema/parse";
19
+ import { createModelPicker } from "../ui/ai-model-picker";
16
20
  import { destinationPath } from "./location-fields";
17
21
  import type { TemplateResult } from "lit-html";
18
- import type { ProjectConfig } from "@jxsuite/schema/types";
19
- import type { CreateProjectDestination, ImportProgressEvent } from "../types";
22
+ import type { CreateProjectDestination, ImportBreakpointPolicy } from "../types";
23
+
24
+ import { setPendingImportBrief } from "../services/import-seed";
25
+ import type { ImportBrief } from "../services/import-seed";
26
+
27
+ /** The three answers the Breakpoints control offers. */
28
+ type BreakpointMode = "all" | "limit" | "explicit";
29
+
30
+ /** How a kept width matches one the site declared. */
31
+ type BreakpointRounding = "nearest" | "down" | "up";
32
+
33
+ /** The most breakpoints the control will offer to keep — `@jxsuite/import`'s own ceiling. */
34
+ const MAX_BREAKPOINTS = 12;
35
+
36
+ /**
37
+ * The deepest crawl the pipeline accepts.
38
+ *
39
+ * The field said 2 while `import_site` and the endpoint both clamped to 5, so the wizard could not
40
+ * ask for a depth the backend was willing to run.
41
+ */
42
+ const MAX_DEPTH = 5;
20
43
 
21
44
  /** Structural view of src/ui/ai-credentials-form's controller (what this tab renders when gated). */
22
45
  export interface CredsFormLike {
@@ -40,8 +63,11 @@ export interface ImportTabCtx {
40
63
  */
41
64
  resolveDestination: () => CreateProjectDestination | null;
42
65
  rerender: () => void;
43
- /** Called with the imported project; the modal resolves and closes. */
44
- onDone: (result: { root: string; config: ProjectConfig }) => void;
66
+ /**
67
+ * Called with the brief the form gathered. The modal closes WITHOUT a project — the assistant
68
+ * runs the import as a tool call and creates it.
69
+ */
70
+ onHandoff: (brief: ImportBrief) => void;
45
71
  /** The modal's shared credentials-form instance, rendered while the gate is closed. */
46
72
  credsForm: CredsFormLike;
47
73
  /**
@@ -56,11 +82,55 @@ export interface ImportTabCtx {
56
82
  let _url = "";
57
83
  let _depth = 1;
58
84
  let _maxPages = 20;
85
+ /**
86
+ * How many of the site's breakpoints the project keeps, and how a kept width matches a declared
87
+ * one.
88
+ *
89
+ * Three by default, and that default is the point of the control. A real site declares as many
90
+ * breakpoints as it has accumulated frameworks — nine is ordinary — and the importer used to keep
91
+ * every one, so an imported project opened with nine canvas sizes and nine columns in every style
92
+ * editor. Nobody authors against nine, and nobody chose these nine.
93
+ */
94
+ let _breakpointMode: BreakpointMode = "limit";
95
+ let _breakpointCount = 3;
96
+ let _breakpointWidths = "";
97
+ let _breakpointRounding: BreakpointRounding = "nearest";
59
98
  let _aiNaming = true;
60
- let _status: "idle" | "running" | "error" = "idle";
61
- let _log: ImportProgressEvent[] = [];
99
+ /**
100
+ * The model this import will run its AI passes on. Empty means "whatever the assistant would use",
101
+ * which is what the tab did silently before there was a picker.
102
+ *
103
+ * A DRAFT, not the `jx.ai.model` preference: choosing a model for one import must not retarget the
104
+ * assistant for every later conversation. `startImport` falls back to {@link preferredModel} so an
105
+ * untouched picker behaves exactly as the tab did before.
106
+ */
107
+ let _model = "";
108
+ /** What the user wants done with the site once it is imported. Handed to the assistant. */
109
+ let _prompt = "";
110
+ /**
111
+ * Build the result and screenshot-diff it against the original.
112
+ *
113
+ * Off by default because it roughly doubles the run: a full compile plus a second browser pass.
114
+ * Worth offering because it produces the only finding that says how WELL the clone came out — every
115
+ * other one is a count of things that were skipped, and a page at 61% fidelity is a question the
116
+ * assistant can put to a person.
117
+ */
118
+ let _verify = false;
119
+ /**
120
+ * The average fidelity the clone has to reach before the run counts as having matched the original.
121
+ *
122
+ * A floor, not a quality target: a faithful import of a complicated site lands well under 100 for
123
+ * reasons no importer can fix (a rotating hero, a font rendering a hair differently). What it
124
+ * catches is the other case — the clone that came out at 8% and reported success anyway. Same
125
+ * default as `jx-import --min-fidelity`, so the two surfaces answer the same question the same
126
+ * way.
127
+ *
128
+ * Failing it is a FINDING here rather than a failure: the project is written and opened either way,
129
+ * and the assistant reports that the bar was missed. The CLI turns the same number into an exit
130
+ * code because a script in a pipeline has nobody to tell.
131
+ */
132
+ let _minFidelity = 25;
62
133
  let _errorMsg = "";
63
- let _abort: AbortController | null = null;
64
134
  let _dirManual = false;
65
135
 
66
136
  /** Reset the tab's state when the modal opens. */
@@ -68,14 +138,69 @@ export function resetImportTab() {
68
138
  _url = "";
69
139
  _depth = 1;
70
140
  _maxPages = 20;
141
+ _breakpointMode = "limit";
142
+ _breakpointCount = 3;
143
+ _breakpointWidths = "";
144
+ _breakpointRounding = "nearest";
71
145
  _aiNaming = true;
72
- _status = "idle";
73
- _log = [];
146
+ _model = "";
147
+ _prompt = "";
148
+ _verify = false;
149
+ _minFidelity = 25;
74
150
  _errorMsg = "";
75
- _abort = null;
76
151
  _dirManual = false;
77
152
  }
78
153
 
154
+ /**
155
+ * The tab's model picker, and the scheduler it repaints through.
156
+ *
157
+ * Created once and kept: `createModelPicker` holds the in-flight fetch and the failed-connection
158
+ * record, and a picker rebuilt per render would re-fetch the catalogue on every keystroke. The
159
+ * scheduler is a SLOT rather than a captured closure because `ImportTabCtx` is rebuilt per render
160
+ * (see `importCtxFor`), so the picker must reach whichever one is current.
161
+ */
162
+ let _rerender: (() => void) | null = null;
163
+ let _picker: ReturnType<typeof createModelPicker> | null = null;
164
+
165
+ function modelPicker() {
166
+ _picker ??= createModelPicker({
167
+ className: "new-project-import-model",
168
+ getModel: () => _model || preferredModel(),
169
+ // A draft, deliberately not `setModel` — see `_model`.
170
+ onChange: (id) => {
171
+ _model = id;
172
+ },
173
+ requestRender: () => _rerender?.(),
174
+ });
175
+ return _picker;
176
+ }
177
+
178
+ /**
179
+ * The policy the three controls describe.
180
+ *
181
+ * Exported so a test can read what the form would send without driving the assistant, for the same
182
+ * reason {@link importBriefFor} is.
183
+ *
184
+ * @returns {ImportBreakpointPolicy}
185
+ */
186
+ export function breakpointPolicyFromForm(): ImportBreakpointPolicy {
187
+ if (_breakpointMode === "all") {
188
+ return { mode: "all" };
189
+ }
190
+ if (_breakpointMode === "explicit") {
191
+ const widths = _breakpointWidths
192
+ .split(",")
193
+ .map((part) => Math.trunc(Number(part.trim())))
194
+ .filter((width) => Number.isFinite(width) && width > 0);
195
+ /* A half-typed width list is not an instruction to keep nothing. Until it parses into at least
196
+ one width, the form still means "three, evenly spaced" — the value the field started from. */
197
+ if (widths.length > 0) {
198
+ return { mode: "explicit", rounding: _breakpointRounding, widths };
199
+ }
200
+ }
201
+ return { count: _breakpointCount, mode: "limit", rounding: _breakpointRounding };
202
+ }
203
+
79
204
  function slugOf(name: string): string {
80
205
  return name
81
206
  .toLowerCase()
@@ -83,19 +208,6 @@ function slugOf(name: string): string {
83
208
  .replaceAll(/^-|-$/g, "");
84
209
  }
85
210
 
86
- /** True while an import stream is active (the modal disables tab switching and close). */
87
- export function isImportRunning(): boolean {
88
- return _status === "running";
89
- }
90
-
91
- /** Abort the running import (the footer's Cancel Import button). */
92
- export function cancelImport(ctx: ImportTabCtx) {
93
- _abort?.abort();
94
- _abort = null;
95
- _status = "idle";
96
- ctx.rerender();
97
- }
98
-
99
211
  /**
100
212
  * Validate the source step (the URL) before advancing to the Parameters step. Sets the inline error
101
213
  * and returns false when invalid.
@@ -116,89 +228,77 @@ export function validateImportSource(ctx: ImportTabCtx): boolean {
116
228
  return true;
117
229
  }
118
230
 
119
- /** Validate and kick off the import. Wired to the modal footer's primary button. */
120
- export async function startImport(ctx: ImportTabCtx) {
121
- const platform = getPlatform();
122
- if (_status === "running" || !platform.importSite) {
123
- return;
124
- }
231
+ /**
232
+ * The brief this form has gathered, or null when something is still missing (the modal has already
233
+ * rendered the reason inline).
234
+ *
235
+ * Exported so a test can read what the form would hand over without driving the assistant.
236
+ *
237
+ * @param {ImportTabCtx} ctx
238
+ * @returns {ImportBrief | null}
239
+ */
240
+ export function importBriefFor(ctx: ImportTabCtx): ImportBrief | null {
125
241
  let parsed: URL;
126
242
  try {
127
243
  parsed = new URL(_url.trim());
128
244
  } catch {
129
245
  _errorMsg = "Enter a valid URL (e.g. https://example.com)";
130
246
  ctx.rerender();
131
- return;
247
+ return null;
132
248
  }
133
249
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
134
250
  _errorMsg = "The URL must start with http:// or https://";
135
251
  ctx.rerender();
136
- return;
252
+ return null;
137
253
  }
138
254
  const destination = ctx.resolveDestination();
139
255
  if (!destination || destination.kind !== "path") {
140
- return;
256
+ return null;
141
257
  }
258
+ return {
259
+ aiComponents: _aiNaming,
260
+ breakpoints: breakpointPolicyFromForm(),
261
+ depth: _depth,
262
+ directory: destinationPath(destination, ctx.form.directory),
263
+ maxPages: _maxPages,
264
+ model: _model,
265
+ name: ctx.form.name.trim(),
266
+ prompt: _prompt.trim(),
267
+ minFidelity: _minFidelity,
268
+ url: parsed.href,
269
+ verify: _verify,
270
+ };
271
+ }
142
272
 
143
- _status = "running";
144
- _errorMsg = "";
145
- _log = [];
146
- _abort = new AbortController();
147
- ctx.rerender();
148
-
149
- try {
150
- const key = getOpenAiKey();
151
- const baseUrl = getBaseUrl();
152
- const model = preferredModel();
153
- const result = await platform.importSite(
154
- {
155
- url: parsed.href,
156
- name: ctx.form.name.trim(),
157
- directory: destinationPath(destination, ctx.form.directory),
158
- depth: _depth,
159
- maxPages: _maxPages,
160
- aiComponents: _aiNaming,
161
- ...(key ? { apiKey: key } : {}),
162
- ...(baseUrl ? { baseUrl } : {}),
163
- ...(model ? { model } : {}),
164
- },
165
- (evt) => {
166
- _log = [..._log, evt];
167
- // The log is destroyed the moment a successful import hands off, so the one line the client
168
- // Emits about lines it could not read is re-posted where it will outlive that.
169
- if (evt.phase === IMPORT_WARNING_PHASE) {
170
- notify.warn(evt.message);
171
- }
172
- ctx.rerender();
173
- requestAnimationFrame(() => {
174
- const el = document.querySelector(".new-project-import-log");
175
- if (el) {
176
- el.scrollTop = el.scrollHeight;
177
- }
178
- });
179
- },
180
- _abort.signal,
181
- );
182
- _status = "idle";
183
- _abort = null;
184
- ctx.onDone(result);
185
- } catch (error) {
186
- if (_status !== "running") {
187
- return; // Cancelled by the user — cancelImport already reset the tab.
188
- }
189
- _abort = null;
190
- _status = "error";
191
- _errorMsg = errorMessage(error);
192
- ctx.rerender();
273
+ /**
274
+ * Hand the brief to the assistant and close the wizard. Wired to the modal footer's primary button.
275
+ *
276
+ * **The wizard does not run the import any more.** It used to, and the run's whole account of
277
+ * itself — every phase line, every warning — was destroyed the moment it succeeded and the modal
278
+ * handed off. It is a tool call now (`services/ai-import-tools.ts`), so the progress lives in the
279
+ * transcript, the model can read what the crawl actually found, and it can stop and ask about
280
+ * anything the pipeline had to guess at.
281
+ *
282
+ * @param {ImportTabCtx} ctx
283
+ */
284
+ export function handoffImport(ctx: ImportTabCtx) {
285
+ if (!getPlatform().importSite) {
286
+ return;
193
287
  }
288
+ const brief = importBriefFor(ctx);
289
+ if (!brief) {
290
+ return;
291
+ }
292
+ /* Stored HERE, by the form that gathered it, rather than inside the assistant's hand-off: two
293
+ readers need it — the turn that gets composed now, and `import_site` minutes later for the
294
+ destination — and the producer is the one place both can be sure it was written. */
295
+ setPendingImportBrief(brief);
296
+ ctx.onHandoff(brief);
194
297
  }
195
298
 
196
- /** The footer's primary-button label for the Import tab's current state. */
299
+ /** The footer's primary-button label. One state: the wizard hands off and closes. */
197
300
  export function importButtonLabel(): string {
198
- if (_status === "running") {
199
- return "Importing…";
200
- }
201
- return _status === "error" ? "Retry Import" : "Import Site";
301
+ return "Import Site";
202
302
  }
203
303
 
204
304
  function onUrlInput(ctx: ImportTabCtx) {
@@ -222,59 +322,96 @@ function onUrlInput(ctx: ImportTabCtx) {
222
322
  };
223
323
  }
224
324
 
225
- function renderProgress(): TemplateResult {
226
- const latest = _log.at(-1);
227
- return html`
228
- <div class="new-project-import-progress">
229
- <sp-progress-circle indeterminate size="s"></sp-progress-circle>
230
- <span class="new-project-import-phase">${latest ? latest.phase : "starting"}</span>
231
- <span>${latest ? latest.message : "Starting import…"}</span>
232
- </div>
233
- <div class="new-project-import-log">
234
- ${_log.map(
235
- (evt) => html`
236
- <div class="new-project-import-log-line">
237
- <span class="new-project-import-phase">${evt.phase}</span>
238
- ${evt.message}
239
- </div>
240
- `,
241
- )}
242
- </div>
243
- `;
244
- }
245
-
246
- /** The streaming progress view shown (in place of the Parameters step) while a run is active. */
247
- export function renderImportProgress(): TemplateResult {
248
- return renderProgress();
249
- }
250
-
251
- /** Inline error + retained log, rendered on the Parameters step after a failed run. */
252
- export function renderImportStatus(): TemplateResult {
253
- if (_status !== "error") {
254
- return html``;
255
- }
325
+ /**
326
+ * The Breakpoints control: how many of the site's own breakpoints the project ends up with.
327
+ *
328
+ * Three controls rather than one because they answer different questions and only some of them
329
+ * apply at a time — a count is meaningless once you have named the widths, and both are meaningless
330
+ * when you are keeping everything. The rounding picker stays for the two that use it: it decides
331
+ * which DECLARED width backs a kept one, and the styles flip where the site says they do.
332
+ *
333
+ * @param {ImportTabCtx} ctx
334
+ * @returns {TemplateResult}
335
+ */
336
+ function breakpointsField(ctx: ImportTabCtx): TemplateResult {
337
+ const onMode = (e: Event) => {
338
+ _breakpointMode = (e.target as HTMLInputElement).value as BreakpointMode;
339
+ ctx.rerender();
340
+ };
256
341
  return html`
257
- ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
258
- ${
259
- _log.length > 0
260
- ? html`
261
- <div class="new-project-import-log">
262
- ${_log.map(
263
- (evt) => html`
264
- <div class="new-project-import-log-line">
265
- <span class="new-project-import-phase">${evt.phase}</span>
266
- ${evt.message}
267
- </div>
268
- `,
269
- )}
270
- </div>
271
- `
272
- : ""
273
- }
342
+ <label class="new-project-field">
343
+ <span class="new-project-label">Breakpoints</span>
344
+ <div class="new-project-import-breakpoints">
345
+ <sp-picker
346
+ size="m"
347
+ class="new-project-breakpoint-mode"
348
+ label="Breakpoints"
349
+ .value=${live(_breakpointMode)}
350
+ @change=${onMode}
351
+ >
352
+ <sp-menu-item value="limit">Limit to</sp-menu-item>
353
+ <sp-menu-item value="explicit">Custom widths</sp-menu-item>
354
+ <sp-menu-item value="all">Keep all</sp-menu-item>
355
+ </sp-picker>
356
+ ${
357
+ _breakpointMode === "limit"
358
+ ? html`<sp-number-field
359
+ class="new-project-breakpoint-count"
360
+ min="1"
361
+ max=${MAX_BREAKPOINTS}
362
+ .value=${live(_breakpointCount)}
363
+ @change=${(e: Event) => {
364
+ _breakpointCount = Math.trunc(Number((e.target as HTMLInputElement).value)) || 3;
365
+ }}
366
+ ></sp-number-field>`
367
+ : nothing
368
+ }
369
+ ${
370
+ _breakpointMode === "explicit"
371
+ ? html`<sp-textfield
372
+ class="new-project-breakpoint-widths"
373
+ placeholder="640, 1024, 1440"
374
+ .value=${live(_breakpointWidths)}
375
+ @input=${(e: Event) => {
376
+ _breakpointWidths = (e.target as HTMLInputElement).value;
377
+ }}
378
+ ></sp-textfield>`
379
+ : nothing
380
+ }
381
+ ${
382
+ _breakpointMode === "all"
383
+ ? nothing
384
+ : html`<sp-picker
385
+ size="m"
386
+ class="new-project-breakpoint-rounding"
387
+ label="Rounding"
388
+ .value=${live(_breakpointRounding)}
389
+ @change=${(e: Event) => {
390
+ _breakpointRounding = (e.target as HTMLInputElement).value as BreakpointRounding;
391
+ }}
392
+ >
393
+ <sp-menu-item value="nearest">nearest</sp-menu-item>
394
+ <sp-menu-item value="down">round down</sp-menu-item>
395
+ <sp-menu-item value="up">round up</sp-menu-item>
396
+ </sp-picker>`
397
+ }
398
+ </div>
399
+ <span class="new-project-hint">
400
+ ${
401
+ _breakpointMode === "all"
402
+ ? "Every breakpoint the site declares becomes a canvas size. Real sites often declare nine."
403
+ : _breakpointMode === "explicit"
404
+ ? "The project gets these widths, each backed by the declared width nearest it."
405
+ : "Evenly spaced across the widths the site declares — the narrowest, the widest, and the ones between."
406
+ }
407
+ </span>
408
+ </label>
274
409
  `;
275
410
  }
276
411
 
277
412
  export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
413
+ // `ctx` is rebuilt per render, so the picker's scheduler has to be re-pointed at the current one.
414
+ _rerender = ctx.rerender;
278
415
  if (!ctx.aiGateOpen()) {
279
416
  return html`
280
417
  <div class="new-project-tab-intro">
@@ -297,7 +434,7 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
297
434
  <span class="new-project-label">Site URL *</span>
298
435
  <sp-textfield
299
436
  placeholder="https://example.com"
300
- .value=${_url}
437
+ .value=${live(_url)}
301
438
  @input=${onUrlInput(ctx)}
302
439
  style="width: 100%"
303
440
  ></sp-textfield>
@@ -307,8 +444,8 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
307
444
  <span class="new-project-label">Crawl Depth</span>
308
445
  <sp-number-field
309
446
  min="0"
310
- max="2"
311
- .value=${_depth}
447
+ max=${MAX_DEPTH}
448
+ .value=${live(_depth)}
312
449
  @change=${(e: Event) => {
313
450
  _depth = Math.trunc(Number((e.target as HTMLInputElement).value)) || 0;
314
451
  }}
@@ -319,37 +456,72 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
319
456
  <sp-number-field
320
457
  min="1"
321
458
  max="100"
322
- .value=${_maxPages}
459
+ .value=${live(_maxPages)}
323
460
  @change=${(e: Event) => {
324
461
  _maxPages = Math.trunc(Number((e.target as HTMLInputElement).value)) || 1;
325
462
  }}
326
463
  ></sp-number-field>
327
464
  </label>
328
465
  </div>
466
+ ${breakpointsField(ctx)}
329
467
  <sp-switch
330
- ?checked=${_aiNaming}
468
+ .checked=${live(_aiNaming)}
331
469
  @change=${(e: Event) => {
332
470
  _aiNaming = (e.target as HTMLInputElement).checked;
333
471
  }}
334
472
  >
335
473
  AI component naming
336
474
  </sp-switch>
337
- ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
475
+ <sp-switch
476
+ .checked=${live(_verify)}
477
+ @change=${(e: Event) => {
478
+ _verify = (e.target as HTMLInputElement).checked;
479
+ // The bar below it only means anything while the check runs, so it appears with it.
480
+ ctx.rerender();
481
+ }}
482
+ >
483
+ Check fidelity against the original (slower)
484
+ </sp-switch>
338
485
  ${
339
- _status === "error" && _log.length > 0
486
+ _verify
340
487
  ? html`
341
- <div class="new-project-import-log">
342
- ${_log.map(
343
- (evt) => html`
344
- <div class="new-project-import-log-line">
345
- <span class="new-project-import-phase">${evt.phase}</span>
346
- ${evt.message}
347
- </div>
348
- `,
349
- )}
350
- </div>
488
+ <label class="new-project-field">
489
+ <span class="new-project-label">Minimum fidelity</span>
490
+ <sp-number-field
491
+ class="new-project-min-fidelity"
492
+ min="0"
493
+ max="100"
494
+ .value=${live(_minFidelity)}
495
+ @change=${(e: Event) => {
496
+ const raw = Math.trunc(Number((e.target as HTMLInputElement).value));
497
+ _minFidelity = Number.isNaN(raw) ? 0 : Math.min(100, Math.max(0, raw));
498
+ }}
499
+ ></sp-number-field>
500
+ <span class="new-project-hint">
501
+ Below this average, the assistant reports that the clone did not match the original.
502
+ A floor rather than a target: a faithful import of a complicated site still lands
503
+ well under 100. Set 0 to report the score without judging it.
504
+ </span>
505
+ </label>
351
506
  `
352
507
  : ""
353
508
  }
509
+ <label class="new-project-field">
510
+ <span class="new-project-label">Model</span>
511
+ ${modelPicker().render()}
512
+ </label>
513
+ <label class="new-project-field">
514
+ <span class="new-project-label">What should the assistant do with it?</span>
515
+ <sp-textfield
516
+ multiline
517
+ class="new-project-import-prompt"
518
+ placeholder="Keep the layout but modernise the typography, and turn the news list into a content collection…"
519
+ .value=${live(_prompt)}
520
+ @input=${(e: Event) => {
521
+ _prompt = (e.target as HTMLInputElement).value;
522
+ }}
523
+ ></sp-textfield>
524
+ </label>
525
+ ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
354
526
  `;
355
527
  }