@jxsuite/studio 4.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 (86) hide show
  1. package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
  2. package/dist/chunks/{Overlay.dev-723bv0df.js → Overlay.dev-etqjwyf9.js} +4 -4
  3. package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
  4. package/dist/chunks/{content-source-t88vb2dr.js → content-source-48xg86wj.js} +17 -17
  5. package/dist/chunks/{convert-to-repeater-dhw1nrz9.js → convert-to-repeater-43tfn7cs.js} +24 -24
  6. package/dist/chunks/{entry-commands-6z6bhxg0.js → entry-commands-e63f1xc0.js} +20 -20
  7. package/dist/chunks/{extension-sections-3ydgjbdy.js → extension-sections-txv08xxg.js} +23 -23
  8. package/dist/chunks/{file-ops-rskmhxyq.js → file-ops-wbh4hhsx.js} +16 -16
  9. package/dist/chunks/{files-0cratsr0.js → files-3kfkwhk4.js} +25 -24
  10. package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
  11. package/dist/chunks/{format-host-tgcmfs4p.js → format-host-w16q81tf.js} +2 -2
  12. package/dist/chunks/fs-events-hrrg328g.js +1 -1
  13. package/dist/chunks/{git-panel-0yywyk76.js → git-panel-532whr1y.js} +16 -15
  14. package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
  15. package/dist/chunks/{jx-validate-8q2g05x1.js → jx-validate-8er6gbxy.js} +3 -3
  16. package/dist/chunks/{layers-panel-z3tqe5cd.js → layers-panel-07hm7g5j.js} +19 -19
  17. package/dist/chunks/{monaco-setup-rws7k4br.js → monaco-setup-vx9bhp1d.js} +2 -2
  18. package/dist/chunks/{publish-panel-54jvhskq.js → publish-panel-v9st96ja.js} +11 -10
  19. package/dist/chunks/{quick-search-cgfmjwq5.js → quick-search-zccjp6gv.js} +26 -25
  20. package/dist/chunks/{right-panel-cqp8bc4v.js → right-panel-vm3b6kwy.js} +17 -17
  21. package/dist/chunks/{sp-overlay.dev-yztmtrs1.js → sp-overlay.dev-qgvhnxf4.js} +5 -5
  22. package/dist/chunks/{sp-tooltip.dev-xcvpnqr0.js → sp-tooltip.dev-4sastdnw.js} +3 -3
  23. package/dist/chunks/{studio-txwkm6p9.js → studio-0bq6eh7v.js} +2 -2
  24. package/dist/chunks/studio-1ngbejft.js +181 -0
  25. package/dist/chunks/{studio-gv8vvshc.js → studio-1wagb550.js} +3 -3
  26. package/dist/chunks/{studio-kjjsjak4.js → studio-2b946z26.js} +58 -57
  27. package/dist/chunks/{studio-4pqqxbrt.js → studio-3bqpr0zh.js} +16 -16
  28. package/dist/chunks/{studio-v38hmvx1.js → studio-4b5z1x43.js} +42 -8
  29. package/dist/chunks/{studio-9c4k4vhv.js → studio-4n2b5gzb.js} +4 -4
  30. package/dist/chunks/{studio-p8j86wag.js → studio-4r1pv6zf.js} +2 -2
  31. package/dist/chunks/{studio-mmv72a27.js → studio-5fx3g9jg.js} +29 -48
  32. package/dist/chunks/{studio-5rywrkxz.js → studio-5myknm0a.js} +42 -42
  33. package/dist/chunks/{studio-sf7zjgv1.js → studio-6yw4y3my.js} +3 -3
  34. package/dist/chunks/{studio-pdfjntbk.js → studio-950k0dx6.js} +35 -35
  35. package/dist/chunks/{studio-eenfkt4e.js → studio-ahn3pdgx.js} +5 -5
  36. package/dist/chunks/{studio-eesa1myv.js → studio-b55y6ewx.js} +9 -9
  37. package/dist/chunks/{studio-cb12yz9w.js → studio-cmb03bg5.js} +99 -1264
  38. package/dist/chunks/studio-dm19q43r.js +195 -0
  39. package/dist/chunks/{studio-ahjfdy6e.js → studio-ef49pnzx.js} +6 -6
  40. package/dist/chunks/{studio-wx5609xd.js → studio-ew0djmhg.js} +10 -10
  41. package/dist/chunks/{studio-9r6j5ev1.js → studio-f3wqjtk5.js} +4 -3
  42. package/dist/chunks/{studio-8swrjbny.js → studio-fbf92ckf.js} +14 -14
  43. package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
  44. package/dist/chunks/{studio-ex8zyp98.js → studio-gfgb8pe6.js} +23 -184
  45. package/dist/chunks/{studio-r1r7afxx.js → studio-jc994dnr.js} +24 -24
  46. package/dist/chunks/{studio-yxrkyv8a.js → studio-jkftxyvz.js} +52 -3416
  47. package/dist/chunks/{studio-hbtp0eh1.js → studio-msd847zn.js} +13 -13
  48. package/dist/chunks/{studio-kyk2v92a.js → studio-mtfnaqs9.js} +14 -14
  49. package/dist/chunks/{studio-vadzgkrz.js → studio-n6j9t10q.js} +2 -2
  50. package/dist/chunks/{studio-68n67094.js → studio-q9jd1a60.js} +4 -4
  51. package/dist/chunks/studio-qcy05dp8.js +1743 -0
  52. package/dist/chunks/{studio-htsf0w6m.js → studio-qjy2z9bv.js} +12 -30
  53. package/dist/chunks/{studio-2zkz8evn.js → studio-trxrabqf.js} +12 -12
  54. package/dist/chunks/{studio-5q69gfag.js → studio-x4qkdat5.js} +12 -11
  55. package/dist/chunks/studio-x813zp48.js +195 -0
  56. package/dist/chunks/{studio-1hbqnt19.js → studio-zqazsct2.js} +29 -29
  57. package/dist/chunks/{workspace-xpres40v.js → workspace-fjx52ryv.js} +4 -4
  58. package/dist/iframe-entry.js +34 -33
  59. package/dist/manifest.json +7 -0
  60. package/dist/studio.js +534 -546
  61. package/favicon.ico +0 -0
  62. package/index.html +1 -0
  63. package/package.json +15 -14
  64. package/src/about/about-modal.ts +4 -3
  65. package/src/files/files.ts +6 -4
  66. package/src/files/media-paths.ts +13 -42
  67. package/src/files/media-usage.ts +41 -131
  68. package/src/hosting/document.ts +2 -0
  69. package/src/hosting/layout.ts +8 -0
  70. package/src/new-project/import-tab.ts +37 -15
  71. package/src/new-project/location-fields.ts +8 -5
  72. package/src/panels/ai-chat/composer.ts +10 -0
  73. package/src/platforms/cloud.ts +241 -70
  74. package/src/publish/publish-panel.ts +62 -2
  75. package/src/services/ai-import-tools.ts +50 -12
  76. package/src/services/ai-models.ts +108 -1
  77. package/src/services/context-manager.ts +12 -1
  78. package/src/services/tool-executor.ts +11 -0
  79. package/src/settings/preferences-accounts.ts +193 -12
  80. package/src/settings/preferences-dialog.ts +75 -18
  81. package/src/types.ts +27 -3
  82. package/src/ui/ai-managed-connect.ts +62 -13
  83. package/src/ui/ai-model-picker.ts +29 -2
  84. package/src/ui/cf-account-picker.ts +200 -0
  85. package/styles/shell.css +36 -0
  86. package/dist/chunks/studio-bw5dz6km.js +0 -608
package/favicon.ico ADDED
Binary file
package/index.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Jx Studio</title>
7
+ <link rel="icon" href="./favicon.ico" />
7
8
  <link rel="stylesheet" href="./dist/studio.css" />
8
9
  <link rel="stylesheet" href="./styles/tokens.css" />
9
10
  <link rel="stylesheet" href="./styles/shell.css" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,6 +21,7 @@
21
21
  "dist/workers",
22
22
  "index.html",
23
23
  "canvas.html",
24
+ "favicon.ico",
24
25
  "styles",
25
26
  "fonts"
26
27
  ],
@@ -55,17 +56,17 @@
55
56
  "upgrade": "bun update --latest && bun install"
56
57
  },
57
58
  "dependencies": {
58
- "@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
59
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
60
- "@jxsuite/ai": "^0.36.5",
61
- "@jxsuite/collab": "^0.8.4",
62
- "@jxsuite/create": "^1.3.8",
63
- "@jxsuite/formulas": "^0.0.15",
64
- "@jxsuite/markup": "^0.4.8",
65
- "@jxsuite/protocol": "^2.1.0",
66
- "@jxsuite/runtime": "^3.0.0",
67
- "@jxsuite/schema": "^2.0.0",
68
- "@jxsuite/site": "^1.0.0",
59
+ "@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
60
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.0",
61
+ "@jxsuite/ai": "^0.37.0",
62
+ "@jxsuite/collab": "^0.8.5",
63
+ "@jxsuite/create": "^1.3.9",
64
+ "@jxsuite/formulas": "^0.0.16",
65
+ "@jxsuite/markup": "^0.4.9",
66
+ "@jxsuite/protocol": "^2.2.0",
67
+ "@jxsuite/runtime": "^3.1.0",
68
+ "@jxsuite/schema": "^2.1.0",
69
+ "@jxsuite/site": "^1.1.0",
69
70
  "@spectrum-web-components/accordion": "^1.12.2",
70
71
  "@spectrum-web-components/action-bar": "1.12.2",
71
72
  "@spectrum-web-components/action-button": "^1.12.2",
@@ -102,7 +103,7 @@
102
103
  "@spectrum-web-components/toast": "^1.12.2",
103
104
  "@spectrum-web-components/tooltip": "^1.12.2",
104
105
  "@spectrum-web-components/underlay": "^1.12.2",
105
- "@vue/reactivity": "3.5.41",
106
+ "@vue/reactivity": "3.5.42",
106
107
  "ajv": "^8.20.0",
107
108
  "ajv-formats": "^3.0.1",
108
109
  "lit": "^3.3.3",
@@ -112,7 +113,7 @@
112
113
  "yaml": "^2.9.0"
113
114
  },
114
115
  "devDependencies": {
115
- "@happy-dom/global-registrator": "^20.11.8",
116
+ "@happy-dom/global-registrator": "^20.12.0",
116
117
  "@types/mdast": "^4.0.4",
117
118
  "@webref/css": "^8.7.3",
118
119
  "@webref/elements": "^2.8.0",
@@ -152,8 +152,9 @@ function renderModal() {
152
152
  *
153
153
  * The rail foot carries **Preferences** and nothing else (plan §3.2 ②). About is not a view you
154
154
  * switch to and it is opened roughly once in an app's lifetime, so it costs a rail slot it cannot
155
- * repay; as a record it stays reachable by name from the palette, which is where a thing you look
156
- * for by name belongs.
155
+ * repay; instead it sits at the bottom of the ⬢ Studio menu (`commandbar/overflow`, `group:
156
+ * "9_help"` sorts after every other group there) and stays reachable by name from the palette
157
+ * besides.
157
158
  */
158
159
  export function aboutCommands(): Command[] {
159
160
  return [
@@ -162,7 +163,7 @@ export function aboutCommands(): Command[] {
162
163
  title: `About ${APP_NAME}`,
163
164
  category: "Help",
164
165
  level: "application",
165
- menus: ["palette"],
166
+ menus: ["commandbar/overflow", "palette"],
166
167
  group: "9_help",
167
168
  run: () => {
168
169
  openAboutModal();
@@ -1910,10 +1910,12 @@ function renameStatus(newName: string, report: RenameResult): string {
1910
1910
  *
1911
1911
  * The dialog above a rename states, in a modal the user has to accept, that N references "will be
1912
1912
  * updated automatically. Nothing else changes." The engine keeps that promise for every document it
1913
- * can write — but some it cannot: a content source like a `.csv` collection has a parser and
1914
- * deliberately no serializer, and a document that fails to parse cannot be rewritten either. In
1915
- * both cases `applyRename` names the file rather than dropping it, and this is the half that makes
1916
- * the naming reach a person. Silence here is what turns a stated promise into a false one.
1913
+ * can write, and it can write more than it round-trips: a `.csv` collection has a parser and
1914
+ * deliberately no serializer, but declares the narrower `rewrite` capability, so a reference in one
1915
+ * is repaired cell by cell. What is left over is a document that fails to parse, and a tag rename
1916
+ * inside a format with no serializer. In those `applyRename` names the file rather than dropping
1917
+ * it, and this is the half that makes the naming reach a person. Silence here is what turns a
1918
+ * stated promise into a false one.
1917
1919
  *
1918
1920
  * A drag-move makes no promise at all — it has no dialog — which is exactly why it needs this more,
1919
1921
  * not less. A format CONVERSION makes the strongest promise of the three, and shares this for that
@@ -15,14 +15,19 @@
15
15
  * mount (specs/site-architecture.md §9.3), so the same file can also be named
16
16
  * `/content/blog/images/hero.png` when the collection's `source` is not already `content/blog`.
17
17
  *
18
- * **Why this module exists at all.** `findReferences` resolves each authored ref the way the
19
- * compiler does — rooted values against the project root, relative ones against the referencing
20
- * document — and compares the RESULT to the path it was asked about. Ask it about `public/hero.jpg`
21
- * and every `/hero.jpg` in the project resolves to `hero.jpg`, matches nothing, and the answer is a
22
- * confident zero. That is the exact shape of bug that makes a delete look safe: seven pages break
23
- * and the dialog says nothing else refers to it. {@link authoredRefTargets} is the fix — it
24
- * enumerates what an authored reference to this file RESOLVES to, so the query is keyed on the ref
25
- * as written rather than on the file as stored.
18
+ * **Why this module exists at all.** A surface that has a FILE and needs a URL cannot get one by
19
+ * prefixing a slash. The library grid did exactly that, so every `public/` image asked for
20
+ * `/public/hero.jpg` — a URL the site does not publish — and every content-collection image skipped
21
+ * its mount entirely. {@link mediaSiteUrl} and {@link previewFileSrc} are the answer, and they take
22
+ * a FILE PATH: an authored reference is a different thing and resolves through
23
+ * `canvas/asset-refs`.
24
+ *
25
+ * This module used to carry a third function, `authoredRefTargets`, enumerating every path an
26
+ * authored reference to a file could resolve to, so a usage query could ask about all of them and
27
+ * union the answers. That existed because `findReferences` resolved a rooted reference against the
28
+ * project root alone; the engine now resolves every lane itself (`site-architecture.md` §9.3), on
29
+ * the write side as well as the read side, which a client-side union never could. Asking once is
30
+ * both simpler and more correct, so the helper is gone.
26
31
  *
27
32
  * Everything here is pure string math over project-relative, forward-slashed paths. The only input
28
33
  * from outside is the open project's content sections, read the same way `canvas/asset-refs.ts`
@@ -101,40 +106,6 @@ export function mediaSiteUrl(path: string): string {
101
106
  return mount ? `/${mountedPath(normalized, mount)}` : `/${normalized}`;
102
107
  }
103
108
 
104
- /**
105
- * Every path an authored reference to this media file resolves to — the keys a usage query must ask
106
- * about, most literal first.
107
- *
108
- * 1. The file itself. A sibling's `./images/hero.png`, a rooted `/content/blog/images/hero.png` and a
109
- * bare `assets/logo.svg` all land here.
110
- * 2. Its served path, for `public/` media. `/hero.jpg` resolves to `hero.jpg`, which is not a file on
111
- * disk and does not need to be — the sweep compares resolved strings, and this is the string
112
- * every reference to a public asset produces.
113
- * 3. Its asset-mount path, when a collection's `source` differs from its mount prefix. Identical to
114
- * (1) for the ordinary `content/<type>/` layout, and deduplicated when it is.
115
- *
116
- * The list is a UNION and is deliberately generous: over-counting a reference makes a delete dialog
117
- * more alarming than it needs to be, while missing one makes it lie.
118
- */
119
- export function authoredRefTargets(path: string): string[] {
120
- const normalized = normalizeProjectPath(path);
121
- if (normalized === "") {
122
- return [];
123
- }
124
- const targets = [normalized];
125
- if (normalized.startsWith(`${PUBLIC_DIR}/`)) {
126
- targets.push(normalized.slice(PUBLIC_DIR.length + 1));
127
- }
128
- const mount = mountOf(normalized);
129
- if (mount) {
130
- const mounted = mountedPath(normalized, mount);
131
- if (!targets.includes(mounted)) {
132
- targets.push(mounted);
133
- }
134
- }
135
- return targets;
136
- }
137
-
138
109
  /**
139
110
  * The src a PARENT-REALM preview should load for a PROJECT FILE PATH — a library thumbnail, a
140
111
  * media-browser tile.
@@ -1,164 +1,74 @@
1
1
  /**
2
- * Media usage — "which pages use this image?", asked in the shape an author actually wrote.
2
+ * Media usage — "which pages use this image?", asked once.
3
3
  *
4
- * `services/references.ts` already answers "where is this used?" for documents, and its answer is
5
- * what every destructive confirmation says out loud. Media does not fit through it unmodified, for
6
- * one reason: **the reference index resolves refs, and a media file's authored ref usually resolves
7
- * somewhere else.** `public/hero.jpg` is written `/hero.jpg`, which the compiler — and therefore
8
- * the sweep — resolves to `hero.jpg`. Ask about the file and every reference to it is invisible;
9
- * the query succeeds, returns zero, and the delete dialog says nothing else refers to it while
10
- * seven pages break. A confident zero is the one answer a destructive dialog must never invent.
4
+ * `services/references.ts` answers "where is this used?" for every kind of file, and its answer is
5
+ * what every destructive confirmation says out loud. Media needs three things on top of it, and no
6
+ * longer needs the fourth this module was built for.
11
7
  *
12
- * So the query is keyed on the AUTHORED ref. {@link authoredRefTargets} enumerates every path a
13
- * reference to this file resolves to — the file, its served path, its asset-mount path — and this
14
- * module asks all of them and unions the answers. Everything else is deliberately inherited rather
15
- * than re-implemented: the same `loadUsages` cache (so a dialog and a panel asking at once are one
16
- * round trip), the same `invalidateUsages` on any filesystem event, the same `usageWarning`
17
- * sentence in the confirmation, and the same `usageHeadline` wording on a panel.
8
+ * **What it still does.** A media path is normalized before it becomes a query (a leading `./` is
9
+ * not part of a file's identity), an empty path is a FAILURE rather than "nothing uses it", and
10
+ * {@link mediaUsageHeadline} renders the one line a media surface shows beside a file — where a
11
+ * failed query says **unknown**, which is a different fact from "unused" and has to read like one.
18
12
  *
19
- * **A partial answer is `failed`, not a total.** If one lane of the union cannot be counted, the
20
- * union cannot either — a number assembled from the lanes that worked reads exactly like a complete
21
- * one, and would understate what a delete breaks. Unknown is the honest answer, and
22
- * {@link mediaUsageHeadline} says the word.
13
+ * **What it no longer does, and why that matters.** A media file's authored reference usually
14
+ * resolves somewhere else: `public/hero.jpg` is written `/hero.jpg`, and a content asset is written
15
+ * `./images/hero.png` and republished at its collection's mount. `findReferences` used to resolve a
16
+ * rooted reference against the project root ALONE, so asking it about the file matched none of
17
+ * those and returned a confident zero — the one answer a destructive dialog must never invent. This
18
+ * module's answer was to enumerate every authored spelling of the file and union the results.
19
+ *
20
+ * That was a workaround in the wrong place. A client-side union can make a COUNT come out right and
21
+ * can do nothing at all about the REWRITE, which happens inside the engine — so the engine learned
22
+ * to resolve every lane itself (issue 239/241, `site-architecture.md` §9.3), and the union became a
23
+ * second implementation of a rule that now has one. It is gone: one path in, one query, one answer,
24
+ * and the lanes are the engine's business.
23
25
  *
24
26
  * @docs studio/projects/media
25
27
  */
26
28
 
27
- import { authoredRefTargets } from "./media-paths";
29
+ import { normalizeProjectPath } from "./media-paths";
28
30
  import { loadUsages, peekUsages, retryUsages, usageHeadline } from "../services/references";
29
- import type { UsageQuery, UsageState } from "../services/references";
30
- import type { ReferenceFile, ReferenceHit, ReferencesResult } from "../types";
31
+ import type { UsageState } from "../services/references";
31
32
 
32
- /**
33
- * The queries whose union answers "what uses this media file?".
34
- *
35
- * One per authored form. An empty list means there is no question to ask (an empty path), which
36
- * {@link loadMediaUsages} reports as a failure rather than as "nothing uses it".
37
- */
38
- export function mediaUsageQueries(path: string): UsageQuery[] {
39
- return authoredRefTargets(path).map((target) => ({ path: target }));
40
- }
41
-
42
- /** Merge one file's hits into the accumulator, summing counts per `(refType, ref)`. */
43
- function mergeFile(into: Map<string, ReferenceFile>, file: ReferenceFile): void {
44
- const existing = into.get(file.path);
45
- if (!existing) {
46
- into.set(file.path, { count: file.count, path: file.path, refs: [...file.refs] });
47
- return;
48
- }
49
- for (const hit of file.refs) {
50
- const same = existing.refs.find(
51
- (candidate: ReferenceHit) => candidate.refType === hit.refType && candidate.ref === hit.ref,
52
- );
53
- if (same) {
54
- same.count += hit.count;
55
- } else {
56
- existing.refs.push({ ...hit });
57
- }
58
- }
59
- existing.count += file.count;
60
- }
33
+ /** The state a caller gets for a path that names no file. */
34
+ const NO_FILE: UsageState = { message: "no media file to look for", status: "failed" };
61
35
 
62
36
  /**
63
- * One result out of several, for the same file asked about under different names.
37
+ * The query for a media file, or null when the path names nothing.
64
38
  *
65
- * The lanes cannot double-count: each asks about a DIFFERENT resolved path, and one authored string
66
- * resolves to exactly one of them. Errors are unioned by path so a document that failed to parse in
67
- * every lane is reported once, and `path` reports the media file the caller asked about rather than
68
- * whichever lane happened to be first.
39
+ * Normalizing here rather than at each call site is the whole of what this wrapper adds over
40
+ * `loadUsages`: the file tree, a drop handler and a library tile do not agree on whether a path
41
+ * carries a leading `./`, and two spellings of one file must not become two cache entries.
69
42
  */
70
- function unionResults(path: string, results: readonly ReferencesResult[]): ReferencesResult {
71
- const files = new Map<string, ReferenceFile>();
72
- const errors = new Map<string, { path: string; error: string }>();
73
- for (const result of results) {
74
- for (const file of result.files) {
75
- mergeFile(files, file);
76
- }
77
- for (const error of result.errors) {
78
- if (!errors.has(error.path)) {
79
- errors.set(error.path, error);
80
- }
81
- }
82
- }
83
- const merged = [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));
84
- return {
85
- errors: [...errors.values()],
86
- files: merged,
87
- filesReferencing: merged.length,
88
- path,
89
- refsTotal: merged.reduce((sum, file) => sum + file.count, 0),
90
- tagName: null,
91
- };
92
- }
93
-
94
- /**
95
- * Reduce the lanes to one state.
96
- *
97
- * Precedence is strictly least-informative-wins, because every step down is a step away from a
98
- * number the caller could act on: unsupported (the host cannot answer at all) beats failed (it
99
- * tried and could not) beats pending beats a real union.
100
- */
101
- function combine(path: string, states: readonly UsageState[]): UsageState {
102
- if (states.some((state) => state.status === "unsupported")) {
103
- return { status: "unsupported" };
104
- }
105
- const failure = states.find((state) => state.status === "failed");
106
- if (failure) {
107
- return failure;
108
- }
109
- if (states.some((state) => state.status === "pending")) {
110
- return { status: "pending" };
111
- }
112
- return {
113
- result: unionResults(
114
- path,
115
- states.map((state) => (state as { result: ReferencesResult }).result),
116
- ),
117
- status: "ready",
118
- };
43
+ function mediaQuery(path: string): { path: string } | null {
44
+ const normalized = normalizeProjectPath(path);
45
+ return normalized === "" ? null : { path: normalized };
119
46
  }
120
47
 
121
48
  /**
122
49
  * The answer right now, WITHOUT starting a request — the synchronous read a lit template needs.
123
50
  *
124
- * `null` means at least one lane has never been asked, and the caller decides whether this paint is
125
- * the one that asks (see {@link loadMediaUsages}).
51
+ * `null` means the question has never been asked, and the caller decides whether this paint is the
52
+ * one that asks (see {@link loadMediaUsages}).
126
53
  */
127
54
  export function peekMediaUsages(path: string): UsageState | null {
128
- const queries = mediaUsageQueries(path);
129
- if (queries.length === 0) {
130
- return null;
131
- }
132
- const states: UsageState[] = [];
133
- for (const query of queries) {
134
- const state = peekUsages(query);
135
- if (state === null) {
136
- return null;
137
- }
138
- states.push(state);
139
- }
140
- return combine(path, states);
55
+ const query = mediaQuery(path);
56
+ return query === null ? null : peekUsages(query);
141
57
  }
142
58
 
143
59
  /**
144
- * Ask every lane and union the answers. Never rejects — a failure is a STATE, because the caller
60
+ * Ask, or join the ask already in flight. Never rejects — a failure is a STATE, because the caller
145
61
  * has to render something and "unknown" is the honest something.
146
62
  */
147
63
  export async function loadMediaUsages(path: string): Promise<UsageState> {
148
- const queries = mediaUsageQueries(path);
149
- if (queries.length === 0) {
150
- return { message: "no media file to look for", status: "failed" };
151
- }
152
- return combine(path, await Promise.all(queries.map((query) => loadUsages(query))));
64
+ const query = mediaQuery(path);
65
+ return query === null ? NO_FILE : loadUsages(query);
153
66
  }
154
67
 
155
- /** Forget every lane's answer and ask again — the Retry behind a failed media count. */
68
+ /** Forget the answer and ask again — the Retry behind a failed media count. */
156
69
  export async function retryMediaUsages(path: string): Promise<UsageState> {
157
- const queries = mediaUsageQueries(path);
158
- if (queries.length === 0) {
159
- return { message: "no media file to look for", status: "failed" };
160
- }
161
- return combine(path, await Promise.all(queries.map((query) => retryUsages(query))));
70
+ const query = mediaQuery(path);
71
+ return query === null ? NO_FILE : retryUsages(query);
162
72
  }
163
73
 
164
74
  /**
@@ -15,6 +15,7 @@ import {
15
15
  assetUrl,
16
16
  STUDIO_BUNDLE_CSS,
17
17
  STUDIO_ENTRY,
18
+ STUDIO_FAVICON,
18
19
  STUDIO_IFRAME_ENTRY,
19
20
  STUDIO_STYLESHEETS,
20
21
  } from "./layout";
@@ -71,6 +72,7 @@ export function studioShellHtml(options: DocumentOptions = {}): string {
71
72
  <meta charset="utf-8" />
72
73
  <meta name="viewport" content="width=device-width, initial-scale=1" />
73
74
  <title>${options.title ?? "Jx Studio"}</title>
75
+ <link rel="icon" href="${url(STUDIO_FAVICON)}" />
74
76
  ${links}
75
77
  </head>
76
78
  <body>
@@ -66,6 +66,13 @@ export const STUDIO_ASSETS: readonly StudioAsset[] = [
66
66
  required: true,
67
67
  why: "the canvas iframe document. Without it every canvas frame 404s at boot.",
68
68
  },
69
+ {
70
+ dir: false,
71
+ kind: "asset",
72
+ path: "favicon.ico",
73
+ required: true,
74
+ why: "the shell document's <link rel=\"icon\">. A Chromium --app window (the NixOS desktop install) has no native title-bar icon of its own and falls back to the page's favicon, so without this the window chrome shows a generic icon even though the taskbar/dock entry is already branded via the .desktop file.",
75
+ },
69
76
  {
70
77
  dir: false,
71
78
  kind: "entry",
@@ -143,6 +150,7 @@ export const PUBLISHED_EXTRAS: readonly string[] = ["src", "data"];
143
150
 
144
151
  export const STUDIO_SHELL = "index.html";
145
152
  export const STUDIO_CANVAS = "canvas.html";
153
+ export const STUDIO_FAVICON = "favicon.ico";
146
154
  export const STUDIO_ENTRY = "dist/studio.js";
147
155
  export const STUDIO_IFRAME_ENTRY = "dist/iframe-entry.js";
148
156
  export const STUDIO_BUNDLE_CSS = "dist/studio.css";
@@ -58,8 +58,8 @@ export interface ImportTabCtx {
58
58
  form: { name: string; directory: string };
59
59
  /**
60
60
  * Validate the shared Parameters fields and return where to write, or null when something is
61
- * missing (the modal has already rendered the reason inline). Import is always a `"path"`
62
- * 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.
63
63
  */
64
64
  resolveDestination: () => CreateProjectDestination | null;
65
65
  rerender: () => void;
@@ -201,6 +201,22 @@ export function breakpointPolicyFromForm(): ImportBreakpointPolicy {
201
201
  return { count: _breakpointCount, mode: "limit", rounding: _breakpointRounding };
202
202
  }
203
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
+
204
220
  function slugOf(name: string): string {
205
221
  return name
206
222
  .toLowerCase()
@@ -252,7 +268,7 @@ export function importBriefFor(ctx: ImportTabCtx): ImportBrief | null {
252
268
  return null;
253
269
  }
254
270
  const destination = ctx.resolveDestination();
255
- if (!destination || destination.kind !== "path") {
271
+ if (!destination) {
256
272
  return null;
257
273
  }
258
274
  return {
@@ -266,7 +282,9 @@ export function importBriefFor(ctx: ImportTabCtx): ImportBrief | null {
266
282
  prompt: _prompt.trim(),
267
283
  minFidelity: _minFidelity,
268
284
  url: parsed.href,
269
- verify: _verify,
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,
270
288
  };
271
289
  }
272
290
 
@@ -472,18 +490,22 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
472
490
  >
473
491
  AI component naming
474
492
  </sp-switch>
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>
485
493
  ${
486
- _verify
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
487
509
  ? html`
488
510
  <label class="new-project-field">
489
511
  <span class="new-project-label">Minimum fidelity</span>
@@ -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
  }
@@ -280,6 +280,16 @@ export function createComposer(opts: ComposerOptions): Composer {
280
280
  `
281
281
  }
282
282
  </div>
283
+ ${
284
+ /* Said once, quietly, under the picker that caused it. A chat-only model still answers,
285
+ so this is not a gate — but the agent loop it silently disables is the whole reason
286
+ the panel exists, and nothing else on screen would have mentioned it. */
287
+ modelPicker.selectedLacksTools()
288
+ ? html`<div class="ai-composer-note">
289
+ This model can't use editing tools — the assistant will answer but not edit.
290
+ </div>`
291
+ : nothing
292
+ }
283
293
  </div>
284
294
  `;
285
295
  }