@jxsuite/studio 2.4.3 → 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 (208) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
  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-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
  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-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
  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-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
  33. package/dist/chunks/studio-2zkz8evn.js +3163 -0
  34. package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -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-hyn3vhwr.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-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
  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-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-rqemfhz6.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-ke244pam.js → studio-ex8zyp98.js} +51 -25
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
  57. package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/studio-kyh9avvv.js +28 -0
  60. package/dist/chunks/{studio-jnakhg87.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-sccge2wf.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
  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-dgj7gq44.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-hb7c46jy.js → studio-wx5609xd.js} +21 -15
  81. package/dist/chunks/studio-xrapxwwc.js +1539 -0
  82. package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
  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 +450 -48
  86. package/dist/studio.js +2455 -1761
  87. package/package.json +16 -15
  88. package/src/browse/library-layouts.ts +3 -3
  89. package/src/browse/library-pane.ts +59 -5
  90. package/src/browse/library-preview.ts +19 -3
  91. package/src/canvas/asset-refs.ts +119 -0
  92. package/src/canvas/asset-resolve.ts +256 -0
  93. package/src/canvas/canvas-live-render.ts +23 -15
  94. package/src/canvas/canvas-render.ts +23 -1
  95. package/src/canvas/iframe-entry.ts +1 -0
  96. package/src/canvas/iframe-host.ts +4 -0
  97. package/src/canvas/iframe-patch.ts +47 -21
  98. package/src/canvas/iframe-protocol.ts +9 -0
  99. package/src/canvas/iframe-render.ts +50 -16
  100. package/src/commands/app-commands.ts +4 -0
  101. package/src/commands/command-args.ts +29 -0
  102. package/src/commands/context.ts +15 -1
  103. package/src/commands/defaults.ts +29 -1
  104. package/src/commands/levels.ts +16 -0
  105. package/src/commands/live-context.ts +3 -0
  106. package/src/content/collection-match.ts +267 -0
  107. package/src/content/entry-commands.ts +38 -29
  108. package/src/content/entry-fields.ts +6 -0
  109. package/src/editor/file-drop-action.ts +2 -1
  110. package/src/editor/shortcuts.ts +5 -0
  111. package/src/files/file-ops.ts +10 -42
  112. package/src/files/files.ts +487 -61
  113. package/src/files/fs-events.ts +9 -0
  114. package/src/files/gitignore.ts +432 -0
  115. package/src/files/media-paths.ts +37 -14
  116. package/src/files/media-upload.ts +47 -6
  117. package/src/files/serialize-document.ts +60 -0
  118. package/src/format/convert-file.ts +511 -0
  119. package/src/format/format-choices.ts +199 -0
  120. package/src/grid/grid-idle.ts +45 -0
  121. package/src/grid/grid-view.ts +35 -0
  122. package/src/grid/schema-columns.ts +2 -1
  123. package/src/grid/sources/content-source.ts +24 -3
  124. package/src/media/media-open.ts +58 -0
  125. package/src/media/media-pane.ts +310 -0
  126. package/src/new-project/import-tab.ts +333 -160
  127. package/src/new-project/new-project-modal.ts +27 -34
  128. package/src/packages/ensure-deps.ts +14 -7
  129. package/src/packages/jxsuite-update.ts +11 -11
  130. package/src/page-params.ts +10 -46
  131. package/src/panels/activity-bar.ts +45 -16
  132. package/src/panels/ai-chat/attached-context.ts +6 -1
  133. package/src/panels/ai-chat/chat-view.ts +255 -13
  134. package/src/panels/ai-chat/composer.ts +21 -80
  135. package/src/panels/ai-panel.ts +116 -2
  136. package/src/panels/frontmatter-fields.ts +6 -2
  137. package/src/panels/navigator-panels.ts +7 -6
  138. package/src/panels/properties-panel.ts +8 -2
  139. package/src/panels/seo-modal.ts +5 -6
  140. package/src/panels/settings-menu.ts +799 -0
  141. package/src/panels/signals-panel.ts +2 -1
  142. package/src/panels/toolbar.ts +135 -15
  143. package/src/platforms/cloud.ts +34 -3
  144. package/src/platforms/devserver.ts +72 -34
  145. package/src/preview/preview-overlay.ts +226 -0
  146. package/src/services/ai-ask.ts +247 -0
  147. package/src/services/ai-import-tools.ts +494 -0
  148. package/src/services/ai-models.ts +101 -11
  149. package/src/services/ai-project-tools.ts +20 -26
  150. package/src/services/ai-settings.ts +69 -71
  151. package/src/services/ai-system-prompt.ts +88 -2
  152. package/src/services/ai-turn-signal.ts +70 -0
  153. package/src/services/cf-settings.ts +19 -37
  154. package/src/services/connection.ts +74 -0
  155. package/src/services/context-manager.ts +89 -0
  156. package/src/services/document-assistant.ts +49 -7
  157. package/src/services/idle.ts +6 -1
  158. package/src/services/import-client.ts +33 -6
  159. package/src/services/import-run.ts +158 -0
  160. package/src/services/import-seed.ts +120 -0
  161. package/src/services/jx-validate.ts +58 -6
  162. package/src/services/project-adoption.ts +71 -0
  163. package/src/services/references.ts +0 -0
  164. package/src/services/settings/definitions.ts +181 -0
  165. package/src/services/settings/kernel.ts +335 -0
  166. package/src/services/settings/write-queue.ts +116 -0
  167. package/src/services/tool-executor.ts +34 -2
  168. package/src/settings/contexts-section.ts +45 -3
  169. package/src/settings/dependencies-editor.ts +43 -23
  170. package/src/settings/general-settings.ts +30 -12
  171. package/src/settings/preferences-accounts.ts +6 -19
  172. package/src/settings/preferences-dialog.ts +18 -41
  173. package/src/settings/preferences-keymap.ts +8 -18
  174. package/src/settings/preferences-sections.ts +57 -0
  175. package/src/settings/schema-field-ui.ts +2 -2
  176. package/src/settings/settings-document.ts +65 -9
  177. package/src/shell.ts +6 -14
  178. package/src/studio.ts +65 -7
  179. package/src/tabs/transact.ts +42 -19
  180. package/src/types.ts +164 -8
  181. package/src/ui/ai-credentials-form.ts +52 -27
  182. package/src/ui/ai-model-picker.ts +162 -0
  183. package/src/ui/choice-field.ts +72 -0
  184. package/src/ui/layers.ts +83 -5
  185. package/src/ui/media-picker.ts +6 -11
  186. package/src/utils/studio-utils.ts +43 -31
  187. package/styles/forced-colors.css +7 -0
  188. package/styles/overlays.css +256 -0
  189. package/styles/panels.css +13 -1
  190. package/styles/shell.css +111 -0
  191. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  192. package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
  193. package/dist/chunks/content-source-k9724vcv.js +0 -39
  194. package/dist/chunks/files-yew27sn9.js +0 -69
  195. package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
  196. package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
  197. package/dist/chunks/quick-search-9avyc4pc.js +0 -54
  198. package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
  199. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  200. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  201. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  202. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  203. package/dist/chunks/studio-a7pzveq1.js +0 -110
  204. package/dist/chunks/studio-c31yb4t6.js +0 -55
  205. package/dist/chunks/studio-ra7ar50f.js +0 -6682
  206. package/src/canvas/content-assets.ts +0 -263
  207. package/src/canvas/site-style-css.ts +0 -121
  208. package/src/services/settings-store.ts +0 -99
@@ -1,21 +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, getModel, getOpenAiKey } from "../services/ai-settings";
12
- import { IMPORT_WARNING_PHASE } from "../services/import-client";
13
- import { notify } from "../services/notify";
14
- import { errorMessage } from "@jxsuite/schema/parse";
18
+ import { preferredModel } from "../services/ai-models";
19
+ import { createModelPicker } from "../ui/ai-model-picker";
15
20
  import { destinationPath } from "./location-fields";
16
21
  import type { TemplateResult } from "lit-html";
17
- import type { ProjectConfig } from "@jxsuite/schema/types";
18
- 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;
19
43
 
20
44
  /** Structural view of src/ui/ai-credentials-form's controller (what this tab renders when gated). */
21
45
  export interface CredsFormLike {
@@ -39,8 +63,11 @@ export interface ImportTabCtx {
39
63
  */
40
64
  resolveDestination: () => CreateProjectDestination | null;
41
65
  rerender: () => void;
42
- /** Called with the imported project; the modal resolves and closes. */
43
- 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;
44
71
  /** The modal's shared credentials-form instance, rendered while the gate is closed. */
45
72
  credsForm: CredsFormLike;
46
73
  /**
@@ -55,11 +82,55 @@ export interface ImportTabCtx {
55
82
  let _url = "";
56
83
  let _depth = 1;
57
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";
58
98
  let _aiNaming = true;
59
- let _status: "idle" | "running" | "error" = "idle";
60
- 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;
61
133
  let _errorMsg = "";
62
- let _abort: AbortController | null = null;
63
134
  let _dirManual = false;
64
135
 
65
136
  /** Reset the tab's state when the modal opens. */
@@ -67,14 +138,69 @@ export function resetImportTab() {
67
138
  _url = "";
68
139
  _depth = 1;
69
140
  _maxPages = 20;
141
+ _breakpointMode = "limit";
142
+ _breakpointCount = 3;
143
+ _breakpointWidths = "";
144
+ _breakpointRounding = "nearest";
70
145
  _aiNaming = true;
71
- _status = "idle";
72
- _log = [];
146
+ _model = "";
147
+ _prompt = "";
148
+ _verify = false;
149
+ _minFidelity = 25;
73
150
  _errorMsg = "";
74
- _abort = null;
75
151
  _dirManual = false;
76
152
  }
77
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
+
78
204
  function slugOf(name: string): string {
79
205
  return name
80
206
  .toLowerCase()
@@ -82,19 +208,6 @@ function slugOf(name: string): string {
82
208
  .replaceAll(/^-|-$/g, "");
83
209
  }
84
210
 
85
- /** True while an import stream is active (the modal disables tab switching and close). */
86
- export function isImportRunning(): boolean {
87
- return _status === "running";
88
- }
89
-
90
- /** Abort the running import (the footer's Cancel Import button). */
91
- export function cancelImport(ctx: ImportTabCtx) {
92
- _abort?.abort();
93
- _abort = null;
94
- _status = "idle";
95
- ctx.rerender();
96
- }
97
-
98
211
  /**
99
212
  * Validate the source step (the URL) before advancing to the Parameters step. Sets the inline error
100
213
  * and returns false when invalid.
@@ -115,89 +228,77 @@ export function validateImportSource(ctx: ImportTabCtx): boolean {
115
228
  return true;
116
229
  }
117
230
 
118
- /** Validate and kick off the import. Wired to the modal footer's primary button. */
119
- export async function startImport(ctx: ImportTabCtx) {
120
- const platform = getPlatform();
121
- if (_status === "running" || !platform.importSite) {
122
- return;
123
- }
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 {
124
241
  let parsed: URL;
125
242
  try {
126
243
  parsed = new URL(_url.trim());
127
244
  } catch {
128
245
  _errorMsg = "Enter a valid URL (e.g. https://example.com)";
129
246
  ctx.rerender();
130
- return;
247
+ return null;
131
248
  }
132
249
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
133
250
  _errorMsg = "The URL must start with http:// or https://";
134
251
  ctx.rerender();
135
- return;
252
+ return null;
136
253
  }
137
254
  const destination = ctx.resolveDestination();
138
255
  if (!destination || destination.kind !== "path") {
139
- return;
256
+ return null;
140
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
+ }
141
272
 
142
- _status = "running";
143
- _errorMsg = "";
144
- _log = [];
145
- _abort = new AbortController();
146
- ctx.rerender();
147
-
148
- try {
149
- const key = getOpenAiKey();
150
- const baseUrl = getBaseUrl();
151
- const model = getModel();
152
- const result = await platform.importSite(
153
- {
154
- url: parsed.href,
155
- name: ctx.form.name.trim(),
156
- directory: destinationPath(destination, ctx.form.directory),
157
- depth: _depth,
158
- maxPages: _maxPages,
159
- aiComponents: _aiNaming,
160
- ...(key ? { apiKey: key } : {}),
161
- ...(baseUrl ? { baseUrl } : {}),
162
- ...(model ? { model } : {}),
163
- },
164
- (evt) => {
165
- _log = [..._log, evt];
166
- // The log is destroyed the moment a successful import hands off, so the one line the client
167
- // Emits about lines it could not read is re-posted where it will outlive that.
168
- if (evt.phase === IMPORT_WARNING_PHASE) {
169
- notify.warn(evt.message);
170
- }
171
- ctx.rerender();
172
- requestAnimationFrame(() => {
173
- const el = document.querySelector(".new-project-import-log");
174
- if (el) {
175
- el.scrollTop = el.scrollHeight;
176
- }
177
- });
178
- },
179
- _abort.signal,
180
- );
181
- _status = "idle";
182
- _abort = null;
183
- ctx.onDone(result);
184
- } catch (error) {
185
- if (_status !== "running") {
186
- return; // Cancelled by the user — cancelImport already reset the tab.
187
- }
188
- _abort = null;
189
- _status = "error";
190
- _errorMsg = errorMessage(error);
191
- 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;
192
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);
193
297
  }
194
298
 
195
- /** 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. */
196
300
  export function importButtonLabel(): string {
197
- if (_status === "running") {
198
- return "Importing…";
199
- }
200
- return _status === "error" ? "Retry Import" : "Import Site";
301
+ return "Import Site";
201
302
  }
202
303
 
203
304
  function onUrlInput(ctx: ImportTabCtx) {
@@ -221,59 +322,96 @@ function onUrlInput(ctx: ImportTabCtx) {
221
322
  };
222
323
  }
223
324
 
224
- function renderProgress(): TemplateResult {
225
- const latest = _log.at(-1);
226
- return html`
227
- <div class="new-project-import-progress">
228
- <sp-progress-circle indeterminate size="s"></sp-progress-circle>
229
- <span class="new-project-import-phase">${latest ? latest.phase : "starting"}</span>
230
- <span>${latest ? latest.message : "Starting import…"}</span>
231
- </div>
232
- <div class="new-project-import-log">
233
- ${_log.map(
234
- (evt) => html`
235
- <div class="new-project-import-log-line">
236
- <span class="new-project-import-phase">${evt.phase}</span>
237
- ${evt.message}
238
- </div>
239
- `,
240
- )}
241
- </div>
242
- `;
243
- }
244
-
245
- /** The streaming progress view shown (in place of the Parameters step) while a run is active. */
246
- export function renderImportProgress(): TemplateResult {
247
- return renderProgress();
248
- }
249
-
250
- /** Inline error + retained log, rendered on the Parameters step after a failed run. */
251
- export function renderImportStatus(): TemplateResult {
252
- if (_status !== "error") {
253
- return html``;
254
- }
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
+ };
255
341
  return html`
256
- ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
257
- ${
258
- _log.length > 0
259
- ? html`
260
- <div class="new-project-import-log">
261
- ${_log.map(
262
- (evt) => html`
263
- <div class="new-project-import-log-line">
264
- <span class="new-project-import-phase">${evt.phase}</span>
265
- ${evt.message}
266
- </div>
267
- `,
268
- )}
269
- </div>
270
- `
271
- : ""
272
- }
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>
273
409
  `;
274
410
  }
275
411
 
276
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;
277
415
  if (!ctx.aiGateOpen()) {
278
416
  return html`
279
417
  <div class="new-project-tab-intro">
@@ -296,7 +434,7 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
296
434
  <span class="new-project-label">Site URL *</span>
297
435
  <sp-textfield
298
436
  placeholder="https://example.com"
299
- .value=${_url}
437
+ .value=${live(_url)}
300
438
  @input=${onUrlInput(ctx)}
301
439
  style="width: 100%"
302
440
  ></sp-textfield>
@@ -306,8 +444,8 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
306
444
  <span class="new-project-label">Crawl Depth</span>
307
445
  <sp-number-field
308
446
  min="0"
309
- max="2"
310
- .value=${_depth}
447
+ max=${MAX_DEPTH}
448
+ .value=${live(_depth)}
311
449
  @change=${(e: Event) => {
312
450
  _depth = Math.trunc(Number((e.target as HTMLInputElement).value)) || 0;
313
451
  }}
@@ -318,37 +456,72 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
318
456
  <sp-number-field
319
457
  min="1"
320
458
  max="100"
321
- .value=${_maxPages}
459
+ .value=${live(_maxPages)}
322
460
  @change=${(e: Event) => {
323
461
  _maxPages = Math.trunc(Number((e.target as HTMLInputElement).value)) || 1;
324
462
  }}
325
463
  ></sp-number-field>
326
464
  </label>
327
465
  </div>
466
+ ${breakpointsField(ctx)}
328
467
  <sp-switch
329
- ?checked=${_aiNaming}
468
+ .checked=${live(_aiNaming)}
330
469
  @change=${(e: Event) => {
331
470
  _aiNaming = (e.target as HTMLInputElement).checked;
332
471
  }}
333
472
  >
334
473
  AI component naming
335
474
  </sp-switch>
336
- ${_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>
337
485
  ${
338
- _status === "error" && _log.length > 0
486
+ _verify
339
487
  ? html`
340
- <div class="new-project-import-log">
341
- ${_log.map(
342
- (evt) => html`
343
- <div class="new-project-import-log-line">
344
- <span class="new-project-import-phase">${evt.phase}</span>
345
- ${evt.message}
346
- </div>
347
- `,
348
- )}
349
- </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>
350
506
  `
351
507
  : ""
352
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>` : ""}
353
526
  `;
354
527
  }