@jxsuite/studio 3.0.0 → 4.1.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 (197) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
  3. package/dist/chunks/Overlay.dev-etqjwyf9.js +23 -0
  4. package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
  5. package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
  6. package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
  7. package/dist/chunks/content-source-48xg86wj.js +49 -0
  8. package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-43tfn7cs.js} +33 -25
  9. package/dist/chunks/entry-commands-e63f1xc0.js +46 -0
  10. package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-txv08xxg.js} +34 -26
  11. package/dist/chunks/file-ops-wbh4hhsx.js +48 -0
  12. package/dist/chunks/files-3kfkwhk4.js +93 -0
  13. package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
  14. package/dist/chunks/{format-host-8fr3545e.js → format-host-w16q81tf.js} +3 -3
  15. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  16. package/dist/chunks/{git-panel-g01d93vc.js → git-panel-532whr1y.js} +22 -16
  17. package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
  18. package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8er6gbxy.js} +6 -4
  19. package/dist/chunks/layers-panel-07hm7g5j.js +67 -0
  20. package/dist/chunks/layers-q75tj2mf.js +45 -0
  21. package/dist/chunks/lit-html-j055mkh8.js +22 -0
  22. package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
  23. package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-vx9bhp1d.js} +4 -4
  24. package/dist/chunks/notify-yjm79n4s.js +30 -0
  25. package/dist/chunks/parse-24d0gay3.js +19 -0
  26. package/dist/chunks/platform-ba1j1gbh.js +16 -0
  27. package/dist/chunks/publish-panel-v9st96ja.js +29 -0
  28. package/dist/chunks/quick-search-zccjp6gv.js +66 -0
  29. package/dist/chunks/references-3qx8fkyp.js +29 -0
  30. package/dist/chunks/right-panel-vm3b6kwy.js +46 -0
  31. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +18 -0
  32. package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
  33. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
  34. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +15 -0
  35. package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
  36. package/dist/chunks/{studio-wdt9y92a.js → studio-0bq6eh7v.js} +2 -2
  37. package/dist/chunks/studio-1ngbejft.js +181 -0
  38. package/dist/chunks/studio-1wagb550.js +118 -0
  39. package/dist/chunks/{studio-j595e748.js → studio-2b946z26.js} +172 -95
  40. package/dist/chunks/{studio-pkp61mh8.js → studio-3bqpr0zh.js} +25 -17
  41. package/dist/chunks/{studio-k4jvjcfx.js → studio-4b5z1x43.js} +46 -12
  42. package/dist/chunks/{studio-ry1vz5sv.js → studio-4n2b5gzb.js} +15 -13
  43. package/dist/chunks/{studio-p5dja96s.js → studio-4r1pv6zf.js} +2 -2
  44. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  45. package/dist/chunks/{studio-tzx3xmvx.js → studio-5fx3g9jg.js} +33 -52
  46. package/dist/chunks/{studio-rh1cnamv.js → studio-5myknm0a.js} +565 -135
  47. package/dist/chunks/studio-6yw4y3my.js +91 -0
  48. package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
  49. package/dist/chunks/studio-92ab2jqe.js +282 -0
  50. package/dist/chunks/{studio-6z5eyjwn.js → studio-950k0dx6.js} +72 -135
  51. package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
  52. package/dist/chunks/studio-9n4mrmnb.js +184 -0
  53. package/dist/chunks/{studio-hcxzyk3x.js → studio-ahn3pdgx.js} +6 -6
  54. package/dist/chunks/studio-b55y6ewx.js +498 -0
  55. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  56. package/dist/chunks/studio-cft9r4r7.js +119 -0
  57. package/dist/chunks/{studio-dc8s8heb.js → studio-cmb03bg5.js} +6904 -8321
  58. package/dist/chunks/studio-dm19q43r.js +195 -0
  59. package/dist/chunks/{studio-2x9zbxmx.js → studio-ef49pnzx.js} +11 -9
  60. package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
  61. package/dist/chunks/{studio-5efrtn1m.js → studio-ew0djmhg.js} +23 -19
  62. package/dist/chunks/{studio-tr20m8g8.js → studio-f3wqjtk5.js} +39 -3
  63. package/dist/chunks/{studio-xntfdy1x.js → studio-fbf92ckf.js} +36 -23
  64. package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
  65. package/dist/chunks/{studio-7p35254k.js → studio-gfgb8pe6.js} +36 -193
  66. package/dist/chunks/studio-jc994dnr.js +1066 -0
  67. package/dist/chunks/{studio-hj2se583.js → studio-jkftxyvz.js} +69 -3423
  68. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  69. package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
  70. package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
  71. package/dist/chunks/studio-msd847zn.js +1315 -0
  72. package/dist/chunks/{studio-g5gdqnyd.js → studio-mtfnaqs9.js} +19 -19
  73. package/dist/chunks/{studio-7ejemhej.js → studio-n6j9t10q.js} +6 -6
  74. package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
  75. package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
  76. package/dist/chunks/studio-q43a5y9h.js +394 -0
  77. package/dist/chunks/studio-q7v10cg2.js +106 -0
  78. package/dist/chunks/{studio-6kv3apa9.js → studio-q9jd1a60.js} +27 -11
  79. package/dist/chunks/studio-qcy05dp8.js +1743 -0
  80. package/dist/chunks/{studio-a0yp7syn.js → studio-qjy2z9bv.js} +29 -132
  81. package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
  82. package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
  83. package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
  84. package/dist/chunks/studio-t7qb9db8.js +665 -0
  85. package/dist/chunks/studio-tmc5emrw.js +8 -0
  86. package/dist/chunks/{studio-r5xbtctf.js → studio-trxrabqf.js} +292 -2297
  87. package/dist/chunks/studio-wcy8trfx.js +263 -0
  88. package/dist/chunks/{studio-3dkcd4c5.js → studio-x4qkdat5.js} +62 -57
  89. package/dist/chunks/studio-x813zp48.js +195 -0
  90. package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
  91. package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
  92. package/dist/chunks/{studio-8ezdegcx.js → studio-zqazsct2.js} +40 -36
  93. package/dist/chunks/workspace-fjx52ryv.js +89 -0
  94. package/dist/iframe-entry.js +141 -56
  95. package/dist/manifest.json +7 -0
  96. package/dist/studio.js +2605 -1950
  97. package/favicon.ico +0 -0
  98. package/index.html +1 -0
  99. package/package.json +20 -18
  100. package/src/about/about-modal.ts +4 -3
  101. package/src/browse/library-pane.ts +57 -3
  102. package/src/canvas/canvas-render.ts +23 -1
  103. package/src/canvas/iframe-patch.ts +47 -21
  104. package/src/canvas/iframe-render.ts +22 -6
  105. package/src/commands/app-commands.ts +4 -0
  106. package/src/commands/command-args.ts +29 -0
  107. package/src/commands/context.ts +15 -1
  108. package/src/commands/defaults.ts +29 -1
  109. package/src/commands/levels.ts +16 -0
  110. package/src/commands/live-context.ts +3 -0
  111. package/src/content/collection-match.ts +267 -0
  112. package/src/content/entry-commands.ts +38 -29
  113. package/src/content/entry-fields.ts +6 -0
  114. package/src/editor/shortcuts.ts +5 -0
  115. package/src/files/file-ops.ts +10 -42
  116. package/src/files/files.ts +487 -59
  117. package/src/files/fs-events.ts +9 -0
  118. package/src/files/gitignore.ts +432 -0
  119. package/src/files/media-paths.ts +13 -42
  120. package/src/files/media-usage.ts +41 -131
  121. package/src/files/serialize-document.ts +60 -0
  122. package/src/format/convert-file.ts +511 -0
  123. package/src/format/format-choices.ts +199 -0
  124. package/src/grid/grid-view.ts +16 -2
  125. package/src/hosting/document.ts +2 -0
  126. package/src/hosting/layout.ts +8 -0
  127. package/src/media/media-open.ts +58 -0
  128. package/src/media/media-pane.ts +310 -0
  129. package/src/new-project/import-tab.ts +357 -163
  130. package/src/new-project/location-fields.ts +8 -5
  131. package/src/new-project/new-project-modal.ts +27 -34
  132. package/src/packages/ensure-deps.ts +14 -7
  133. package/src/packages/jxsuite-update.ts +11 -11
  134. package/src/page-params.ts +10 -46
  135. package/src/panels/activity-bar.ts +45 -16
  136. package/src/panels/ai-chat/attached-context.ts +6 -1
  137. package/src/panels/ai-chat/chat-view.ts +255 -13
  138. package/src/panels/ai-chat/composer.ts +31 -97
  139. package/src/panels/ai-panel.ts +116 -2
  140. package/src/panels/navigator-panels.ts +7 -6
  141. package/src/panels/settings-menu.ts +799 -0
  142. package/src/panels/toolbar.ts +135 -15
  143. package/src/platforms/cloud.ts +261 -70
  144. package/src/platforms/devserver.ts +67 -33
  145. package/src/preview/preview-overlay.ts +226 -0
  146. package/src/publish/publish-panel.ts +62 -2
  147. package/src/services/ai-ask.ts +247 -0
  148. package/src/services/ai-import-tools.ts +532 -0
  149. package/src/services/ai-models.ts +108 -1
  150. package/src/services/ai-project-tools.ts +20 -26
  151. package/src/services/ai-system-prompt.ts +88 -2
  152. package/src/services/ai-turn-signal.ts +70 -0
  153. package/src/services/connection.ts +74 -0
  154. package/src/services/context-manager.ts +101 -1
  155. package/src/services/document-assistant.ts +45 -4
  156. package/src/services/import-client.ts +33 -6
  157. package/src/services/import-run.ts +158 -0
  158. package/src/services/import-seed.ts +120 -0
  159. package/src/services/jx-validate.ts +58 -6
  160. package/src/services/project-adoption.ts +71 -0
  161. package/src/services/references.ts +0 -0
  162. package/src/services/settings/definitions.ts +17 -0
  163. package/src/services/tool-executor.ts +45 -2
  164. package/src/settings/contexts-section.ts +45 -3
  165. package/src/settings/dependencies-editor.ts +43 -23
  166. package/src/settings/general-settings.ts +30 -12
  167. package/src/settings/preferences-accounts.ts +193 -12
  168. package/src/settings/preferences-dialog.ts +93 -59
  169. package/src/settings/preferences-sections.ts +57 -0
  170. package/src/settings/settings-document.ts +65 -9
  171. package/src/studio.ts +33 -6
  172. package/src/tabs/transact.ts +42 -19
  173. package/src/types.ts +129 -7
  174. package/src/ui/ai-managed-connect.ts +62 -13
  175. package/src/ui/ai-model-picker.ts +189 -0
  176. package/src/ui/cf-account-picker.ts +200 -0
  177. package/src/ui/choice-field.ts +72 -0
  178. package/src/ui/layers.ts +83 -5
  179. package/src/utils/studio-utils.ts +22 -52
  180. package/styles/forced-colors.css +7 -0
  181. package/styles/overlays.css +256 -0
  182. package/styles/panels.css +13 -1
  183. package/styles/shell.css +147 -0
  184. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  185. package/dist/chunks/content-source-7fy9qrm0.js +0 -40
  186. package/dist/chunks/files-xcybanxe.js +0 -70
  187. package/dist/chunks/layers-panel-27essgkc.js +0 -60
  188. package/dist/chunks/publish-panel-09sy2bab.js +0 -22
  189. package/dist/chunks/quick-search-sefb61v8.js +0 -55
  190. package/dist/chunks/right-panel-9r32jd5j.js +0 -39
  191. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  192. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  193. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  194. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  195. package/dist/chunks/studio-bw5dz6km.js +0 -608
  196. package/dist/chunks/studio-c31yb4t6.js +0 -55
  197. 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 {
@@ -35,13 +58,16 @@ export interface ImportTabCtx {
35
58
  form: { name: string; directory: string };
36
59
  /**
37
60
  * Validate the shared Parameters fields and return where to write, or null when something is
38
- * missing (the modal has already rendered the reason inline). Import is always a `"path"`
39
- * destination — cloud ships no `importSite`, so the Import tab never renders there.
61
+ * missing (the modal has already rendered the reason inline). Either destination shape: a hosted
62
+ * backend imports into a repository it creates, so `"repo"` reaches here too.
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,85 @@ 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
+
204
+ /**
205
+ * Whether this backend can answer the fidelity question at all.
206
+ *
207
+ * `verify` builds the emitted project with the compiler and serves it to screenshot every page
208
+ * against the original — it executes the project's own JavaScript, so `@jxsuite/import` keeps that
209
+ * phase out of the portable pipeline entirely and a hosted backend deliberately never runs it.
210
+ *
211
+ * There is no PAL member for "this backend compiles", so the nearest DECLARED fact stands in:
212
+ * `createDestination`. A `"repo"` platform commits the emitted project into a git tree it never
213
+ * checks out, which is the same set of hosts and one the tab can read without naming a platform by
214
+ * id. Offering the switch there would collect an answer nothing acts on.
215
+ */
216
+ function canVerify(): boolean {
217
+ return getPlatform().createDestination !== "repo";
218
+ }
219
+
79
220
  function slugOf(name: string): string {
80
221
  return name
81
222
  .toLowerCase()
@@ -83,19 +224,6 @@ function slugOf(name: string): string {
83
224
  .replaceAll(/^-|-$/g, "");
84
225
  }
85
226
 
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
227
  /**
100
228
  * Validate the source step (the URL) before advancing to the Parameters step. Sets the inline error
101
229
  * and returns false when invalid.
@@ -116,89 +244,79 @@ export function validateImportSource(ctx: ImportTabCtx): boolean {
116
244
  return true;
117
245
  }
118
246
 
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
- }
247
+ /**
248
+ * The brief this form has gathered, or null when something is still missing (the modal has already
249
+ * rendered the reason inline).
250
+ *
251
+ * Exported so a test can read what the form would hand over without driving the assistant.
252
+ *
253
+ * @param {ImportTabCtx} ctx
254
+ * @returns {ImportBrief | null}
255
+ */
256
+ export function importBriefFor(ctx: ImportTabCtx): ImportBrief | null {
125
257
  let parsed: URL;
126
258
  try {
127
259
  parsed = new URL(_url.trim());
128
260
  } catch {
129
261
  _errorMsg = "Enter a valid URL (e.g. https://example.com)";
130
262
  ctx.rerender();
131
- return;
263
+ return null;
132
264
  }
133
265
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
134
266
  _errorMsg = "The URL must start with http:// or https://";
135
267
  ctx.rerender();
136
- return;
268
+ return null;
137
269
  }
138
270
  const destination = ctx.resolveDestination();
139
- if (!destination || destination.kind !== "path") {
140
- return;
271
+ if (!destination) {
272
+ return null;
141
273
  }
274
+ return {
275
+ aiComponents: _aiNaming,
276
+ breakpoints: breakpointPolicyFromForm(),
277
+ depth: _depth,
278
+ directory: destinationPath(destination, ctx.form.directory),
279
+ maxPages: _maxPages,
280
+ model: _model,
281
+ name: ctx.form.name.trim(),
282
+ prompt: _prompt.trim(),
283
+ minFidelity: _minFidelity,
284
+ url: parsed.href,
285
+ /* Never asked for where it cannot run: the switch is not rendered there, and a value left over
286
+ from a session on another platform must not travel as a request the backend would ignore. */
287
+ verify: canVerify() && _verify,
288
+ };
289
+ }
142
290
 
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();
291
+ /**
292
+ * Hand the brief to the assistant and close the wizard. Wired to the modal footer's primary button.
293
+ *
294
+ * **The wizard does not run the import any more.** It used to, and the run's whole account of
295
+ * itself — every phase line, every warning — was destroyed the moment it succeeded and the modal
296
+ * handed off. It is a tool call now (`services/ai-import-tools.ts`), so the progress lives in the
297
+ * transcript, the model can read what the crawl actually found, and it can stop and ask about
298
+ * anything the pipeline had to guess at.
299
+ *
300
+ * @param {ImportTabCtx} ctx
301
+ */
302
+ export function handoffImport(ctx: ImportTabCtx) {
303
+ if (!getPlatform().importSite) {
304
+ return;
193
305
  }
306
+ const brief = importBriefFor(ctx);
307
+ if (!brief) {
308
+ return;
309
+ }
310
+ /* Stored HERE, by the form that gathered it, rather than inside the assistant's hand-off: two
311
+ readers need it — the turn that gets composed now, and `import_site` minutes later for the
312
+ destination — and the producer is the one place both can be sure it was written. */
313
+ setPendingImportBrief(brief);
314
+ ctx.onHandoff(brief);
194
315
  }
195
316
 
196
- /** The footer's primary-button label for the Import tab's current state. */
317
+ /** The footer's primary-button label. One state: the wizard hands off and closes. */
197
318
  export function importButtonLabel(): string {
198
- if (_status === "running") {
199
- return "Importing…";
200
- }
201
- return _status === "error" ? "Retry Import" : "Import Site";
319
+ return "Import Site";
202
320
  }
203
321
 
204
322
  function onUrlInput(ctx: ImportTabCtx) {
@@ -222,59 +340,96 @@ function onUrlInput(ctx: ImportTabCtx) {
222
340
  };
223
341
  }
224
342
 
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
- }
343
+ /**
344
+ * The Breakpoints control: how many of the site's own breakpoints the project ends up with.
345
+ *
346
+ * Three controls rather than one because they answer different questions and only some of them
347
+ * apply at a time — a count is meaningless once you have named the widths, and both are meaningless
348
+ * when you are keeping everything. The rounding picker stays for the two that use it: it decides
349
+ * which DECLARED width backs a kept one, and the styles flip where the site says they do.
350
+ *
351
+ * @param {ImportTabCtx} ctx
352
+ * @returns {TemplateResult}
353
+ */
354
+ function breakpointsField(ctx: ImportTabCtx): TemplateResult {
355
+ const onMode = (e: Event) => {
356
+ _breakpointMode = (e.target as HTMLInputElement).value as BreakpointMode;
357
+ ctx.rerender();
358
+ };
256
359
  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
- }
360
+ <label class="new-project-field">
361
+ <span class="new-project-label">Breakpoints</span>
362
+ <div class="new-project-import-breakpoints">
363
+ <sp-picker
364
+ size="m"
365
+ class="new-project-breakpoint-mode"
366
+ label="Breakpoints"
367
+ .value=${live(_breakpointMode)}
368
+ @change=${onMode}
369
+ >
370
+ <sp-menu-item value="limit">Limit to</sp-menu-item>
371
+ <sp-menu-item value="explicit">Custom widths</sp-menu-item>
372
+ <sp-menu-item value="all">Keep all</sp-menu-item>
373
+ </sp-picker>
374
+ ${
375
+ _breakpointMode === "limit"
376
+ ? html`<sp-number-field
377
+ class="new-project-breakpoint-count"
378
+ min="1"
379
+ max=${MAX_BREAKPOINTS}
380
+ .value=${live(_breakpointCount)}
381
+ @change=${(e: Event) => {
382
+ _breakpointCount = Math.trunc(Number((e.target as HTMLInputElement).value)) || 3;
383
+ }}
384
+ ></sp-number-field>`
385
+ : nothing
386
+ }
387
+ ${
388
+ _breakpointMode === "explicit"
389
+ ? html`<sp-textfield
390
+ class="new-project-breakpoint-widths"
391
+ placeholder="640, 1024, 1440"
392
+ .value=${live(_breakpointWidths)}
393
+ @input=${(e: Event) => {
394
+ _breakpointWidths = (e.target as HTMLInputElement).value;
395
+ }}
396
+ ></sp-textfield>`
397
+ : nothing
398
+ }
399
+ ${
400
+ _breakpointMode === "all"
401
+ ? nothing
402
+ : html`<sp-picker
403
+ size="m"
404
+ class="new-project-breakpoint-rounding"
405
+ label="Rounding"
406
+ .value=${live(_breakpointRounding)}
407
+ @change=${(e: Event) => {
408
+ _breakpointRounding = (e.target as HTMLInputElement).value as BreakpointRounding;
409
+ }}
410
+ >
411
+ <sp-menu-item value="nearest">nearest</sp-menu-item>
412
+ <sp-menu-item value="down">round down</sp-menu-item>
413
+ <sp-menu-item value="up">round up</sp-menu-item>
414
+ </sp-picker>`
415
+ }
416
+ </div>
417
+ <span class="new-project-hint">
418
+ ${
419
+ _breakpointMode === "all"
420
+ ? "Every breakpoint the site declares becomes a canvas size. Real sites often declare nine."
421
+ : _breakpointMode === "explicit"
422
+ ? "The project gets these widths, each backed by the declared width nearest it."
423
+ : "Evenly spaced across the widths the site declares — the narrowest, the widest, and the ones between."
424
+ }
425
+ </span>
426
+ </label>
274
427
  `;
275
428
  }
276
429
 
277
430
  export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
431
+ // `ctx` is rebuilt per render, so the picker's scheduler has to be re-pointed at the current one.
432
+ _rerender = ctx.rerender;
278
433
  if (!ctx.aiGateOpen()) {
279
434
  return html`
280
435
  <div class="new-project-tab-intro">
@@ -297,7 +452,7 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
297
452
  <span class="new-project-label">Site URL *</span>
298
453
  <sp-textfield
299
454
  placeholder="https://example.com"
300
- .value=${_url}
455
+ .value=${live(_url)}
301
456
  @input=${onUrlInput(ctx)}
302
457
  style="width: 100%"
303
458
  ></sp-textfield>
@@ -307,8 +462,8 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
307
462
  <span class="new-project-label">Crawl Depth</span>
308
463
  <sp-number-field
309
464
  min="0"
310
- max="2"
311
- .value=${_depth}
465
+ max=${MAX_DEPTH}
466
+ .value=${live(_depth)}
312
467
  @change=${(e: Event) => {
313
468
  _depth = Math.trunc(Number((e.target as HTMLInputElement).value)) || 0;
314
469
  }}
@@ -319,37 +474,76 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
319
474
  <sp-number-field
320
475
  min="1"
321
476
  max="100"
322
- .value=${_maxPages}
477
+ .value=${live(_maxPages)}
323
478
  @change=${(e: Event) => {
324
479
  _maxPages = Math.trunc(Number((e.target as HTMLInputElement).value)) || 1;
325
480
  }}
326
481
  ></sp-number-field>
327
482
  </label>
328
483
  </div>
484
+ ${breakpointsField(ctx)}
329
485
  <sp-switch
330
- ?checked=${_aiNaming}
486
+ .checked=${live(_aiNaming)}
331
487
  @change=${(e: Event) => {
332
488
  _aiNaming = (e.target as HTMLInputElement).checked;
333
489
  }}
334
490
  >
335
491
  AI component naming
336
492
  </sp-switch>
337
- ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
338
493
  ${
339
- _status === "error" && _log.length > 0
494
+ canVerify()
495
+ ? html`<sp-switch
496
+ .checked=${live(_verify)}
497
+ @change=${(e: Event) => {
498
+ _verify = (e.target as HTMLInputElement).checked;
499
+ // The bar below it only means anything while the check runs, so it appears with it.
500
+ ctx.rerender();
501
+ }}
502
+ >
503
+ Check fidelity against the original (slower)
504
+ </sp-switch>`
505
+ : nothing
506
+ }
507
+ ${
508
+ canVerify() && _verify
340
509
  ? 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>
510
+ <label class="new-project-field">
511
+ <span class="new-project-label">Minimum fidelity</span>
512
+ <sp-number-field
513
+ class="new-project-min-fidelity"
514
+ min="0"
515
+ max="100"
516
+ .value=${live(_minFidelity)}
517
+ @change=${(e: Event) => {
518
+ const raw = Math.trunc(Number((e.target as HTMLInputElement).value));
519
+ _minFidelity = Number.isNaN(raw) ? 0 : Math.min(100, Math.max(0, raw));
520
+ }}
521
+ ></sp-number-field>
522
+ <span class="new-project-hint">
523
+ Below this average, the assistant reports that the clone did not match the original.
524
+ A floor rather than a target: a faithful import of a complicated site still lands
525
+ well under 100. Set 0 to report the score without judging it.
526
+ </span>
527
+ </label>
351
528
  `
352
529
  : ""
353
530
  }
531
+ <label class="new-project-field">
532
+ <span class="new-project-label">Model</span>
533
+ ${modelPicker().render()}
534
+ </label>
535
+ <label class="new-project-field">
536
+ <span class="new-project-label">What should the assistant do with it?</span>
537
+ <sp-textfield
538
+ multiline
539
+ class="new-project-import-prompt"
540
+ placeholder="Keep the layout but modernise the typography, and turn the news list into a content collection…"
541
+ .value=${live(_prompt)}
542
+ @input=${(e: Event) => {
543
+ _prompt = (e.target as HTMLInputElement).value;
544
+ }}
545
+ ></sp-textfield>
546
+ </label>
547
+ ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
354
548
  `;
355
549
  }
@@ -140,13 +140,16 @@ function joinPath(parent: string, name: string): string {
140
140
  }
141
141
 
142
142
  /**
143
- * The absolute directory a `"path"` destination writes to. The import pipeline takes a plain
144
- * absolute `directory` rather than a destination object (it is never repo-mode — cloud ships no
145
- * `importSite`), so the modal resolves it here.
143
+ * The destination as the import pipeline names it: `importSite` takes a plain `directory` string
144
+ * rather than a destination object, so the modal flattens one here.
145
+ *
146
+ * Both shapes, because both platforms import now. A `"path"` destination flattens to the absolute
147
+ * folder it writes; a `"repo"` one to `owner/repo`, which is what a backend that commits the
148
+ * emitted project into a git tree can act on — there is no directory for it to name.
146
149
  */
147
150
  export function destinationPath(destination: CreateProjectDestination, slug: string): string {
148
- if (destination.kind !== "path") {
149
- throw new Error("Only filesystem destinations have a path");
151
+ if (destination.kind === "repo") {
152
+ return `${destination.owner}/${destination.repo || slug.trim()}`;
150
153
  }
151
154
  return joinPath(destination.parent, slug.trim());
152
155
  }