@jxsuite/studio 5.1.0 → 5.2.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 (103) hide show
  1. package/dist/chunks/{cf-account-picker-qwkm9zrf.js → cf-account-picker-h4dab9bq.js} +10 -10
  2. package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
  3. package/dist/chunks/{components-vj72gqtq.js → components-5y78e05d.js} +4 -4
  4. package/dist/chunks/content-source-sgg1sv6n.js +46 -0
  5. package/dist/chunks/{convert-to-repeater-n64h8znc.js → convert-to-repeater-mf1cxgfn.js} +22 -22
  6. package/dist/chunks/entry-commands-w9qsxa6q.js +42 -0
  7. package/dist/chunks/{extension-sections-3hpcambp.js → extension-sections-c6zwzbre.js} +26 -26
  8. package/dist/chunks/{file-ops-8yhd83xr.js → file-ops-vrpq1jjk.js} +22 -22
  9. package/dist/chunks/{files-hnxqxtsg.js → files-msaf95y6.js} +31 -30
  10. package/dist/chunks/{format-host-779sfp2n.js → format-host-f64fnesp.js} +2 -2
  11. package/dist/chunks/{fs-events-b88z7g4h.js → fs-events-3y6y0nrm.js} +4 -4
  12. package/dist/chunks/{git-panel-xwj8fpab.js → git-panel-bv5e22j0.js} +23 -22
  13. package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
  14. package/dist/chunks/grid-view-paj67nr5.js +15322 -0
  15. package/dist/chunks/{iframe-host-syr0f9sb.js → iframe-host-dh5np8vk.js} +13 -13
  16. package/dist/chunks/{layers-ff81wthn.js → layers-1vcfdbet.js} +5 -5
  17. package/dist/chunks/layers-panel-fksbdg4q.js +40 -0
  18. package/dist/chunks/{media-picker-cqhhxhhe.js → media-picker-p49nhqhs.js} +13 -13
  19. package/dist/chunks/menu-qht48vnc.js +15 -0
  20. package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
  21. package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
  22. package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
  23. package/dist/chunks/publish-panel-vz23wwmg.js +27 -0
  24. package/dist/chunks/quick-search-e9rp74m8.js +46 -0
  25. package/dist/chunks/{references-qvvxej7h.js → references-h69xp9zz.js} +2 -2
  26. package/dist/chunks/right-panel-j97t2yvy.js +45 -0
  27. package/dist/chunks/{studio-b0k5tqgp.js → studio-01jeck2y.js} +31 -27
  28. package/dist/chunks/{studio-wqz6dggm.js → studio-14d9tq4d.js} +9 -9
  29. package/dist/chunks/{studio-try13h8c.js → studio-15f3kxqe.js} +18 -18
  30. package/dist/chunks/studio-1tbjfqv4.js +146 -0
  31. package/dist/chunks/{studio-vw7mzp6z.js → studio-28vs5k2y.js} +2 -2
  32. package/dist/chunks/{studio-93zafjcm.js → studio-2mdbbc76.js} +10 -10
  33. package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
  34. package/dist/chunks/{studio-g3jevyjt.js → studio-4c041r8y.js} +2 -2
  35. package/dist/chunks/{studio-3773086z.js → studio-4r14yzyy.js} +4 -4
  36. package/dist/chunks/{studio-3rz5md06.js → studio-4xmjcrg3.js} +6 -6
  37. package/dist/chunks/{studio-syg2tadj.js → studio-5nz00bhh.js} +3 -3
  38. package/dist/chunks/{studio-1knfqgy7.js → studio-5t7ena5z.js} +8 -8
  39. package/dist/chunks/{studio-a79k90n9.js → studio-66arbpc2.js} +8 -8
  40. package/dist/chunks/{studio-b4s9eykd.js → studio-68fydsrj.js} +15 -15
  41. package/dist/chunks/{studio-mhhyamxf.js → studio-9j6sfeba.js} +6 -6
  42. package/dist/chunks/{studio-7xnmgv66.js → studio-a1pswps3.js} +37 -37
  43. package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
  44. package/dist/chunks/{studio-nzzpvzx9.js → studio-c07s5ckd.js} +4 -4
  45. package/dist/chunks/{studio-js8nv53n.js → studio-ctm7bhx5.js} +14 -14
  46. package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
  47. package/dist/chunks/{studio-jxp12rnj.js → studio-en82c639.js} +6 -6
  48. package/dist/chunks/{studio-1z1kcjv4.js → studio-etvx9s2g.js} +11 -11
  49. package/dist/chunks/{studio-k75n7vhp.js → studio-ez11dmdh.js} +74 -36
  50. package/dist/chunks/{studio-qhhm6hbt.js → studio-hdc2jt3z.js} +4 -4
  51. package/dist/chunks/{studio-r6nvacqs.js → studio-hhskq2mr.js} +19 -19
  52. package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
  53. package/dist/chunks/{studio-qk90fgqm.js → studio-mpzwze9x.js} +7 -7
  54. package/dist/chunks/{studio-e1pgga3k.js → studio-ncn9e189.js} +7 -7
  55. package/dist/chunks/{studio-kkqkcp3d.js → studio-p6pmwkvh.js} +10 -10
  56. package/dist/chunks/{studio-aj37eer3.js → studio-qa7fzxan.js} +4 -4
  57. package/dist/chunks/{studio-sbjyc8bt.js → studio-sar8g54p.js} +8 -8
  58. package/dist/chunks/studio-t171hxhe.js +23 -0
  59. package/dist/chunks/{studio-pxbgypmg.js → studio-t3adrr56.js} +17 -14
  60. package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
  61. package/dist/chunks/{studio-5wt9nvhd.js → studio-vt0tj32j.js} +29 -42
  62. package/dist/chunks/{studio-qk4bfsq2.js → studio-wjpgxywa.js} +4 -4
  63. package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
  64. package/dist/chunks/{studio-gb13095a.js → studio-x10fp4tp.js} +59 -20
  65. package/dist/chunks/{studio-8scdew0e.js → studio-x72j58vw.js} +4 -4
  66. package/dist/chunks/{studio-tqgryxwg.js → studio-y4f9fdge.js} +7 -7
  67. package/dist/chunks/{studio-vfq8qpjt.js → studio-ympe0n7a.js} +3 -3
  68. package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
  69. package/dist/chunks/{studio-zdn2g6dt.js → studio-zjjde71a.js} +2 -2
  70. package/dist/chunks/{workspace-t55jdjbx.js → workspace-bnw54cha.js} +18 -10
  71. package/dist/iframe-entry.js +7 -3
  72. package/dist/studio.css +1 -2
  73. package/dist/studio.js +866 -15698
  74. package/package.json +6 -6
  75. package/src/files/files.ts +38 -15
  76. package/src/grid/grid-lazy.ts +59 -0
  77. package/src/grid/grid-open.ts +20 -3
  78. package/src/grid/grid-panel.ts +43 -9
  79. package/src/media/media-open.ts +14 -2
  80. package/src/panels/pane-context.ts +5 -4
  81. package/src/panels/pane-grid.ts +100 -10
  82. package/src/panels/tab-drop.ts +220 -0
  83. package/src/panels/tab-strip.ts +235 -30
  84. package/src/services/ai-models.ts +13 -0
  85. package/src/studio.ts +10 -6
  86. package/src/surfaces/commandbar.json +26 -16
  87. package/src/surfaces/pane-grid.json +35 -3
  88. package/src/surfaces/pane-grid.ts +26 -4
  89. package/src/surfaces/tab-strip.json +16 -39
  90. package/src/surfaces/tab-strip.ts +76 -12
  91. package/src/ui/ai-credentials-form.ts +12 -1
  92. package/src/workspace/pane-derive.ts +7 -5
  93. package/src/workspace/session.ts +3 -3
  94. package/src/workspace/workspace.ts +160 -43
  95. package/styles/shell-frame.css +8 -0
  96. package/styles/shell-frame.json +4 -0
  97. package/dist/chunks/content-source-5mex4x1h.js +0 -46
  98. package/dist/chunks/entry-commands-234hjnqw.js +0 -42
  99. package/dist/chunks/layers-panel-pnnghw12.js +0 -40
  100. package/dist/chunks/menu-9p2w46gj.js +0 -15
  101. package/dist/chunks/publish-panel-5zt840sy.js +0 -27
  102. package/dist/chunks/quick-search-3748564r.js +0 -45
  103. package/dist/chunks/right-panel-eccz92qt.js +0 -45
@@ -21,9 +21,6 @@
21
21
  "flex": "1",
22
22
  "flexShrink": "999"
23
23
  },
24
- "& jx-button, & jx-action-button, & button": {
25
- "WebkitAppRegion": "no-drag"
26
- },
27
24
  "& [part=\"csd-lead\"], & [part=\"csd-trail\"], & [part=\"menu-slot\"], & [part=\"layouts-slot\"], & [part=\"presence-slot\"]": {
28
25
  "display": "contents"
29
26
  },
@@ -264,7 +261,8 @@
264
261
  "size": "sm"
265
262
  },
266
263
  "attributes": {
267
- "data-control": "close"
264
+ "data-control": "close",
265
+ "class": "electrobun-webkit-app-region-no-drag"
268
266
  },
269
267
  "onclick": {
270
268
  "$prototype": "Function",
@@ -288,7 +286,8 @@
288
286
  "size": "sm"
289
287
  },
290
288
  "attributes": {
291
- "data-control": "minimize"
289
+ "data-control": "minimize",
290
+ "class": "electrobun-webkit-app-region-no-drag"
292
291
  },
293
292
  "onclick": {
294
293
  "$prototype": "Function",
@@ -312,7 +311,8 @@
312
311
  "size": "sm"
313
312
  },
314
313
  "attributes": {
315
- "data-control": "maximize"
314
+ "data-control": "maximize",
315
+ "class": "electrobun-webkit-app-region-no-drag"
316
316
  },
317
317
  "onclick": {
318
318
  "$prototype": "Function",
@@ -354,7 +354,8 @@
354
354
  }
355
355
  },
356
356
  "attributes": {
357
- "data-menu": "studio"
357
+ "data-menu": "studio",
358
+ "class": "electrobun-webkit-app-region-no-drag"
358
359
  },
359
360
  "onclick": {
360
361
  "$prototype": "Function",
@@ -438,7 +439,8 @@
438
439
  "aria-selected": "${$map.item.active ? 'true' : 'false'}",
439
440
  "data-layout": "${$map.item.id}",
440
441
  "data-active": "${$map.item.active ? '' : null}",
441
- "title": "${$map.item.title}"
442
+ "title": "${$map.item.title}",
443
+ "class": "electrobun-webkit-app-region-no-drag"
442
444
  },
443
445
  "textContent": "${$map.item.name}",
444
446
  "onclick": {
@@ -484,7 +486,8 @@
484
486
  "size": "sm"
485
487
  },
486
488
  "attributes": {
487
- "part": "layout-add"
489
+ "part": "layout-add",
490
+ "class": "electrobun-webkit-app-region-no-drag"
488
491
  },
489
492
  "onclick": {
490
493
  "$prototype": "Function",
@@ -514,7 +517,8 @@
514
517
  "attributes": {
515
518
  "part": "center",
516
519
  "role": "group",
517
- "aria-label": "Command Center"
520
+ "aria-label": "Command Center",
521
+ "class": "electrobun-webkit-app-region-no-drag"
518
522
  },
519
523
  "onclick": {
520
524
  "$prototype": "Function",
@@ -581,7 +585,8 @@
581
585
  "part": "segment",
582
586
  "type": "button",
583
587
  "data-segment": "${$map.item.key}",
584
- "title": "${$map.item.title}"
588
+ "title": "${$map.item.title}",
589
+ "class": "electrobun-webkit-app-region-no-drag"
585
590
  },
586
591
  "textContent": "${$map.item.label}",
587
592
  "onclick": {
@@ -666,7 +671,8 @@
666
671
  "quiet": true
667
672
  },
668
673
  "attributes": {
669
- "data-command-id": "${$map.item.id}"
674
+ "data-command-id": "${$map.item.id}",
675
+ "class": "electrobun-webkit-app-region-no-drag"
670
676
  },
671
677
  "onclick": {
672
678
  "$prototype": "Function",
@@ -865,7 +871,8 @@
865
871
  "size": "sm"
866
872
  },
867
873
  "attributes": {
868
- "data-command-id": "${$map.item.id}"
874
+ "data-command-id": "${$map.item.id}",
875
+ "class": "electrobun-webkit-app-region-no-drag"
869
876
  },
870
877
  "onchange": {
871
878
  "$prototype": "Function",
@@ -914,7 +921,8 @@
914
921
  "size": "sm"
915
922
  },
916
923
  "attributes": {
917
- "data-control": "minimize"
924
+ "data-control": "minimize",
925
+ "class": "electrobun-webkit-app-region-no-drag"
918
926
  },
919
927
  "onclick": {
920
928
  "$prototype": "Function",
@@ -938,7 +946,8 @@
938
946
  "size": "sm"
939
947
  },
940
948
  "attributes": {
941
- "data-control": "maximize"
949
+ "data-control": "maximize",
950
+ "class": "electrobun-webkit-app-region-no-drag"
942
951
  },
943
952
  "onclick": {
944
953
  "$prototype": "Function",
@@ -962,7 +971,8 @@
962
971
  "size": "sm"
963
972
  },
964
973
  "attributes": {
965
- "data-control": "close"
974
+ "data-control": "close",
975
+ "class": "electrobun-webkit-app-region-no-drag"
966
976
  },
967
977
  "onclick": {
968
978
  "$prototype": "Function",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioPaneGrid",
4
- "$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the four inside a cell are hosts other modules fill (`panels/tab-strip.ts` the strip, `panels/jump-bar.ts` the jump bar, `panels/pane-context.ts` the chrome layer, `canvas/canvas-render.ts` the stage), and the stage is an ISLAND in the §9.4 sense: the canvas host mounts a cross-origin `<iframe>` into it, so nothing this document draws may ever go inside it. What the document owns is the STRUCTURE and the STYLE — the cell's own two-row grid, the three row-2 children that share its lower cell, and the splitter — which is `styles/shell.css`'s `.pane*` block and `styles/canvas.css`'s `.pane-stage` rule, carried here and keyed on `part`. Two things are load-bearing and easy to undo. (1) **A row that moves reloads an iframe**, so the keyed `$map` is not an optimisation: `workspace.panes[0]` is always the primary and `MAX_PANES` is 2, the key is the pane id, and the runtime's forward-cursor pass therefore never moves a kept row — gaining or losing the side pane leaves the primary's node, and the frame inside it, untouched. (2) **The splitter must sit BETWEEN two cells and inside neither**, so each row is a `display: contents` wrapper holding its own leading splitter and its cell: the wrapper generates no box, so the splitter and the two cells are the grid items of `#pane-grid` in DOM order, and a pointer landing on the splitter is in no cell at all. Every selector below is a `>` chain from the root, because a pane's stage hosts other surface documents and a bare `[part=\"row\"]` descendant rule would reach into them.",
4
+ "$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the FIVE inside a cell are hosts other modules fill (`panels/tab-strip.ts` the strip, `panels/jump-bar.ts` the jump bar, `panels/pane-context.ts` the chrome layer, `canvas/canvas-render.ts` the stage, `panels/pane-grid.ts` itself the right-edge drop zone), and the stage is an ISLAND in the §9.4 sense: the canvas host mounts a cross-origin `<iframe>` into it, so nothing this document draws may ever go inside it. What the document owns is the STRUCTURE and the STYLE — the cell's own two-row grid, the three row-2 children that share its lower cell, and the splitter — which is `styles/shell.css`'s `.pane*` block and `styles/canvas.css`'s `.pane-stage` rule, carried here and keyed on `part`. Two things are load-bearing and easy to undo. (1) **A row that moves reloads an iframe**, so the keyed `$map` is not an optimisation: `workspace.panes[0]` is always the primary and `MAX_PANES` is 2, the key is the pane id, and the runtime's forward-cursor pass therefore never moves a kept row — gaining or losing the side pane leaves the primary's node, and the frame inside it, untouched. (2) **The splitter must sit BETWEEN two cells and inside neither**, so each row is a `display: contents` wrapper holding its own leading splitter and its cell: the wrapper generates no box, so the splitter and the two cells are the grid items of `#pane-grid` in DOM order, and a pointer landing on the splitter is in no cell at all. Every selector below is a `>` chain from the root, because a pane's stage hosts other surface documents and a bare `[part=\"row\"]` descendant rule would reach into them.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "grid",
@@ -27,7 +27,7 @@
27
27
  "zIndex": "10"
28
28
  },
29
29
  "& > [part=\"row\"] > [part=\"pane\"]": {
30
- "$description": "ONE PANE, and one COLUMN, declared. Three children name `grid-row: 2` in order to share the lower cell, and auto-placement answers that by inventing an implicit column each: the jump bar, the chrome layer and the stage lined up side by side, the stage taking a third of the pane and pinning the document to the right edge. `grid-template-columns` is what makes \"share the cell\" mean it, and every row-2 child restates `grid-column: 1` so a future fourth one cannot re-open the same hole. No unit test can see this — happy-dom does no layout, so every rect is zero — which is why the screenshot lane is what caught it.",
30
+ "$description": "ONE PANE, and one COLUMN, declared. Four children name `grid-row: 2` in order to share the lower cell, and auto-placement answers that by inventing an implicit column each: the jump bar, the chrome layer, the stage and the drop zone lined up side by side, the stage taking a third of the pane and pinning the document to the right edge. `grid-template-columns` is what makes \"share the cell\" mean it, and every row-2 child restates `grid-column: 1` so a future fifth one cannot re-open the same hole. No unit test can see this — happy-dom does no layout, so every rect is zero — which is why the screenshot lane is what caught it.",
31
31
  "display": "grid",
32
32
  "gridTemplateColumns": "minmax(0, 1fr)",
33
33
  "gridTemplateRows": "auto minmax(0, 1fr)",
@@ -82,11 +82,33 @@
82
82
  "display": "flex",
83
83
  "gap": "var(--jx-space-6)",
84
84
  "alignItems": "flex-start"
85
+ },
86
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"]": {
87
+ "$description": "The right-edge target that creates a second pane (§18.1). Sits ABOVE the stage (`z-index: 4` against the stage's own stacking, jump's 3, chrome's 2) so a drag reaches it before it reaches the cross-origin iframe underneath, which would otherwise swallow every drag event with no affordance to say so. `pointer-events: none` when unarmed — a mouse doing anything other than dragging a tab or a file must pass straight through to the stage — and `auto` only while `data-armed` is set, which `panels/pane-grid.ts`'s own arming monitor writes for the DURATION of a drag it recognises, never for a click.",
88
+ "gridRow": "2",
89
+ "gridColumn": "1",
90
+ "position": "relative",
91
+ "zIndex": "4",
92
+ "minWidth": "0",
93
+ "pointerEvents": "none"
94
+ },
95
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-armed]": {
96
+ "pointerEvents": "auto"
97
+ },
98
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-over]::after": {
99
+ "$description": "The right BAND, not the whole cell: the zone only ever ACCEPTS a drop in that band (`panels/pane-grid.ts`'s `edgeData`), so lighting the whole cell would promise a drop this element refuses everywhere else in it.",
100
+ "content": "\"\"",
101
+ "position": "absolute",
102
+ "insetBlock": "0",
103
+ "insetInlineEnd": "0",
104
+ "insetInlineStart": "50%",
105
+ "background": "color-mix(in srgb, var(--accent) 14%, transparent)",
106
+ "borderInlineStart": "2px solid var(--accent)"
85
107
  }
86
108
  },
87
109
  "children": [
88
110
  {
89
- "$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its four hosts and its splitter) inside the row's own effect scope before it is inserted, so there is no frame in which a cell exists with no stage inside it.",
111
+ "$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its five hosts and its splitter) inside the row's own effect scope before it is inserted, so there is no frame in which a cell exists with no stage inside it.",
90
112
  "$prototype": "Array",
91
113
  "items": { "$ref": "#/state/rows" },
92
114
  "key": { "$ref": "$map/item/id" },
@@ -178,6 +200,16 @@
178
200
  "part": "pane-stage",
179
201
  "data-jx-region": "${$map.item.region}"
180
202
  }
203
+ },
204
+ {
205
+ "$description": "The right-edge drop target (§18.1). No `data-jx-region`: `pane.<id>` uniqueness names the stage, and a second element answering to it would be a silently widened crop the moment a shot cropped this pane.",
206
+ "tagName": "div",
207
+ "attributes": {
208
+ "part": "drop-zone",
209
+ "role": "none",
210
+ "data-armed": "${state.armed ? '' : null}",
211
+ "data-over": "${state.edgePane === $map.item.id ? '' : null}"
212
+ }
181
213
  }
182
214
  ]
183
215
  }
@@ -63,8 +63,8 @@ export const PANE_SELECTOR = `[part="${PANE_PART}"]`;
63
63
  */
64
64
  export const STAGE_SELECTOR = `[part="${STAGE_PART}"]`;
65
65
 
66
- /** The five boxes a cell is made of, in the order the document draws them. */
67
- export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage";
66
+ /** The six boxes a cell is made of, in the order the document draws them. */
67
+ export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage" | "dropZone";
68
68
 
69
69
  const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
70
70
  ["pane", "pane"],
@@ -72,6 +72,7 @@ const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
72
72
  ["jump", "jump"],
73
73
  ["chrome", "chrome"],
74
74
  ["pane-stage", "stage"],
75
+ ["drop-zone", "dropZone"],
75
76
  ]);
76
77
 
77
78
  /**
@@ -134,6 +135,14 @@ export interface PaneGridActions {
134
135
  settle: () => void;
135
136
  }
136
137
 
138
+ /** Whether the right-edge zones are armed, and which pane's is under the pointer. */
139
+ export interface PaneGridDragState {
140
+ /** Whether ANY tab-drop-eligible drag is live — the zones' shared `pointer-events` gate. */
141
+ armed: boolean;
142
+ /** The pane whose zone is under the pointer, or `""` for none. */
143
+ edgePane: string;
144
+ }
145
+
137
146
  export interface PaneGridSurface {
138
147
  /** Resolves once the first projection is in the document. */
139
148
  ready: Promise<void>;
@@ -146,12 +155,18 @@ export interface PaneGridSurface {
146
155
  * gains or loses a pane, which is the only moment anything else could have moved it.
147
156
  */
148
157
  update: (rows: PaneGridRow[], split: number) => void;
158
+ /**
159
+ * Write the right-edge indicators, independently of {@link update} — the same reason
160
+ * `TabStripSurface.drag` is separate from its own `update`: a re-project the drag itself did not
161
+ * cause must not clear an indicator the arming monitor just lit.
162
+ */
163
+ drag: (state: PaneGridDragState) => void;
149
164
  /** Take the document down and give `#pane-grid` back empty. Idempotent. */
150
165
  dispose: () => void;
151
166
  }
152
167
 
153
168
  /** The scope `pane-grid.json` reads. */
154
- interface PaneGridScope extends Record<string, unknown> {
169
+ interface PaneGridScope extends Record<string, unknown>, PaneGridDragState {
155
170
  rows: PaneGridRow[];
156
171
  /** `jx-split`'s `value`. */
157
172
  paneSplit: number;
@@ -202,6 +217,8 @@ export function mountPaneGridSurface(
202
217
  ): PaneGridSurface {
203
218
  host.replaceChildren();
204
219
  const scope = reactive<PaneGridScope>({
220
+ armed: false,
221
+ edgePane: "",
205
222
  move: actions.move,
206
223
  paneSplit: split.value,
207
224
  rows,
@@ -217,7 +234,7 @@ export function mountPaneGridSurface(
217
234
  const ready = mountSurface("pane-grid", scope, host, {
218
235
  /* No `instanceof` narrowing and no "is there a pane" guard, and both absences are structural.
219
236
  `pane-grid.json` draws no text and no `textContent`, so every node the runtime reports here
220
- is an element; and the five cell parts exist only inside the repeater's rows, so a `$map`
237
+ is an element; and the six cell parts exist only inside the repeater's rows, so a `$map`
221
238
  scope is always there to name the pane. A guard on either would be a branch the document
222
239
  cannot reach — which is a line no test could ever cover, which is a claim nothing checks. */
223
240
  onNodeCreated: (element, _path, def, state) => {
@@ -241,6 +258,11 @@ export function mountPaneGridSurface(
241
258
  mounted = null;
242
259
  host.replaceChildren();
243
260
  },
261
+ drag(state) {
262
+ // Independent of `update`'s field list — see {@link PaneGridSurface.drag}.
263
+ scope.armed = state.armed;
264
+ scope.edgePane = state.edgePane;
265
+ },
244
266
  ready,
245
267
  update(next, value) {
246
268
  scope.rows = next;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioTabStrip",
4
- "$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now.",
4
+ "$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now. **Drag is `@atlaskit/pragmatic-drag-and-drop`, not native HTML5 DnD**: a chip carries no `draggable` attribute and no `ondrag*` handler, because `panels/tab-strip.ts` registers every chip and every strip as a pragmatic source/target through `onNodeCreated` and resolves a completed drag in `panels/tab-drop.ts`. What is left here is three SCOPE comparisons, exactly as `files-panel.json` marks a row under drag: `data-dragging` on the chip being carried, `data-drop` on the chip a drop would land beside, `data-drop-tail` on the strip when the drop is its empty tail.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "strip-row",
@@ -44,6 +44,13 @@
44
44
  "display": "none"
45
45
  }
46
46
  },
47
+ "& [part=\"tabs\"][data-drop-tail]::after": {
48
+ "$description": "The strip's own tail target: a drop onto the empty space after the last chip, or onto a pane whose strip carries no chips at all — an empty pane made a target during a drag by `panels/tab-strip.ts`'s `project`. A fixed-width bar rather than a background tint, so it reads the same as a chip's own `data-drop` edge instead of a second visual language for the same gesture.",
49
+ "content": "\"\"",
50
+ "flex": "0 0 2px",
51
+ "alignSelf": "stretch",
52
+ "background": "var(--accent)"
53
+ },
47
54
  "& [part=\"tab\"]": {
48
55
  "$description": "`.tab-strip-tab`. The kit's tab caps itself at 220px and reserves a control's height; a chrome row is tighter than that and its labels are the shortest unique suffix, so both are re-declared. `flex-shrink: 0` is what makes the strip scroll rather than squeeze fifteen documents into one width.",
49
56
  "gap": "var(--jx-space-1)",
@@ -64,6 +71,10 @@
64
71
  },
65
72
  "&[data-dragging]": {
66
73
  "opacity": "0.4"
74
+ },
75
+ "&[data-drop]": {
76
+ "$description": "The chip a drop would land BESIDE. An inset shadow rather than a border, so the indicator draws no layout shift mid-drag — the same reason `files-panel.json`'s row indicators are backgrounds rather than borders.",
77
+ "boxShadow": "inset 2px 0 0 var(--accent)"
67
78
  }
68
79
  },
69
80
  "& [part=\"tab\"] [part=\"label\"]": {
@@ -206,7 +217,8 @@
206
217
  "attributes": {
207
218
  "part": "tabs",
208
219
  "label": "${state.stripLabel}",
209
- "selected": "${state.active}"
220
+ "selected": "${state.active}",
221
+ "data-drop-tail": "${state.dropTail ? '' : null}"
210
222
  },
211
223
  "onchange": {
212
224
  "$prototype": "Function",
@@ -245,11 +257,11 @@
245
257
  "label": "${$map.item.label}",
246
258
  "title": "${$map.item.title}",
247
259
  "closable": "",
248
- "draggable": "true",
249
260
  "dirty": "${$map.item.dirty ? '' : null}",
250
261
  "data-pinned": "${$map.item.pinned ? '' : null}",
251
262
  "data-preview": "${$map.item.preview ? '' : null}",
252
- "data-dragging": "${$map.item.dragging ? '' : null}"
263
+ "data-dragging": "${state.dragKey === $map.item.key ? '' : null}",
264
+ "data-drop": "${state.dropIndex === $map.item.index ? '' : null}"
253
265
  },
254
266
  "ondblclick": {
255
267
  "$prototype": "Function",
@@ -297,41 +309,6 @@
297
309
  }
298
310
  ]
299
311
  },
300
- "ondragstart": {
301
- "$prototype": "Function",
302
- "body": [
303
- {
304
- "operator": "call",
305
- "target": { "$ref": "#/state/dragStart" },
306
- "value": [{ "$ref": "$map/item/key" }, { "$ref": "event#/dataTransfer" }]
307
- }
308
- ]
309
- },
310
- "ondragend": {
311
- "$prototype": "Function",
312
- "body": [
313
- {
314
- "operator": "call",
315
- "target": { "$ref": "#/state/dragEnd" },
316
- "value": []
317
- }
318
- ]
319
- },
320
- "ondragover": {
321
- "$prototype": "Function",
322
- "body": [{ "preventDefault": true }]
323
- },
324
- "ondrop": {
325
- "$prototype": "Function",
326
- "body": [
327
- { "preventDefault": true },
328
- {
329
- "operator": "call",
330
- "target": { "$ref": "#/state/drop" },
331
- "value": [{ "$ref": "$map/item/index" }, { "$ref": "event#/dataTransfer" }]
332
- }
333
- ]
334
- },
335
312
  "children": [
336
313
  {
337
314
  "$description": "The drill-in marker: this document was opened FROM another one. `jx-tab`'s icon slot draws it before the label, which is where it has always been.",
@@ -31,7 +31,8 @@
31
31
  import { reactive } from "../reactivity";
32
32
  import { mountSurface, registerSurface } from "../ui/surface";
33
33
  import tabStripDoc from "./tab-strip.json";
34
- import type { JxDocument } from "@jxsuite/schema/types";
34
+ import type { JxDocument, JxElement } from "@jxsuite/schema/types";
35
+ import type { JxScope } from "@jxsuite/runtime/types";
35
36
  import type { SurfaceHandle } from "../ui/surface";
36
37
 
37
38
  registerSurface("tab-strip", tabStripDoc as unknown as JxDocument);
@@ -50,7 +51,6 @@ export interface TabChipView extends Record<string, unknown> {
50
51
  dirty: boolean;
51
52
  pinned: boolean;
52
53
  preview: boolean;
53
- dragging: boolean;
54
54
  /** Whether to draw the drill-in marker. */
55
55
  origin: boolean;
56
56
  /** "Pin" or "Unpin" — the pin button's whole accessible name. */
@@ -106,18 +106,39 @@ export interface TabStripActions {
106
106
  context: (id: string, x: number, y: number) => void;
107
107
  /** The chevron, or a derived pane's ✕. The anchor is the button that was pressed. */
108
108
  openTrailing: (anchor: HTMLElement | null) => void;
109
- /** A drag left this chip. The `DataTransfer` is the platform's and may be absent. */
110
- dragStart: (id: string, transfer: DataTransfer | null) => void;
111
- dragEnd: () => void;
112
- /** A drop landed on the chip at `index`. */
113
- drop: (index: number, transfer: DataTransfer | null) => void;
114
109
  /** A wheel over the strip. A host function in handler position, so it takes the event. */
115
110
  wheel: (scope: unknown, event: WheelEvent) => void;
111
+ /** A chip element exists. `panels/tab-strip.ts` makes it a pragmatic drag source and drop target. */
112
+ chipHost: (element: HTMLElement, id: string) => void;
113
+ /**
114
+ * The `jx-tabs` element exists. `panels/tab-strip.ts` makes it a drop target of its own — the
115
+ * strip's empty tail, and the whole of an empty pane's strip while a drag is live.
116
+ */
117
+ stripHost: (element: HTMLElement) => void;
118
+ }
119
+
120
+ /**
121
+ * What one strip shows about the drag in progress — written independently of {@link TabStripValues}
122
+ * so the indicators survive an ordinary re-project mid-drag.
123
+ */
124
+ export interface TabStripDragState {
125
+ /** The tab id being carried, or `""` when nothing from this strip is. */
126
+ dragKey: string;
127
+ /** The chip a drop would land beside, by slot index, or `-1` for none. */
128
+ dropIndex: number;
129
+ /** Whether a drop would land in the strip's empty tail. */
130
+ dropTail: boolean;
116
131
  }
117
132
 
118
133
  export interface TabStripSurface {
119
134
  /** Bring the standing document up to date. An assignment; the mount is never rebuilt. */
120
135
  update: (values: TabStripValues) => void;
136
+ /**
137
+ * Write the drag indicators, independently of {@link update}. The one writer of `dragKey`,
138
+ * `dropIndex` and `dropTail` — `update` never touches them, which is what keeps an indicator lit
139
+ * across a repaint the drag itself did not cause.
140
+ */
141
+ drag: (state: TabStripDragState) => void;
121
142
  /** The `jx-tabs` element, once it exists — the one thing a measurement needs. */
122
143
  strip: () => HTMLElement | null;
123
144
  /** Take the document down and give the host back empty. Idempotent. */
@@ -125,7 +146,21 @@ export interface TabStripSurface {
125
146
  }
126
147
 
127
148
  /** The scope `tab-strip.json` reads. */
128
- interface TabStripScope extends Record<string, unknown>, TabStripValues, TabStripActions {}
149
+ interface TabStripScope
150
+ extends Record<string, unknown>, TabStripValues, TabStripActions, TabStripDragState {}
151
+
152
+ /** The `part` a node's definition carries, or `""` for a text node or an unmarked element. */
153
+ function partOf(def: JxElement | string): string {
154
+ const part = typeof def === "string" ? undefined : def.attributes?.["part"];
155
+ return typeof part === "string" ? part : "";
156
+ }
157
+
158
+ /** The tab id a mapped `jx-tab` node was rendered for, read from the row scope, or `""`. */
159
+ function mappedChipKey(state: JxScope | undefined): string {
160
+ const map = state?.["$map"] as { item?: { key?: unknown } } | undefined;
161
+ const key = map?.item?.key;
162
+ return typeof key === "string" ? key : "";
163
+ }
129
164
 
130
165
  /**
131
166
  * Mount one strip into `host`.
@@ -143,17 +178,38 @@ export function mountTabStripSurface(
143
178
  values: TabStripValues,
144
179
  actions: TabStripActions,
145
180
  ): TabStripSurface {
146
- const scope = reactive<TabStripScope>({ ...values, ...actions }) as TabStripScope;
181
+ const scope = reactive<TabStripScope>({
182
+ ...values,
183
+ ...actions,
184
+ dragKey: "",
185
+ dropIndex: -1,
186
+ dropTail: false,
187
+ }) as TabStripScope;
147
188
 
148
189
  host.replaceChildren();
149
190
  let mounted: SurfaceHandle | null = null;
150
191
  let disposed = false;
151
192
  let strip: HTMLElement | null = null;
152
193
  void mountSurface("tab-strip", scope, host, {
153
- onNodeCreated: (element, _path, def) => {
154
- const part = typeof def === "string" ? undefined : def.attributes?.["part"];
155
- if (part === "tabs" && element instanceof HTMLElement) {
194
+ onNodeCreated: (element, _path, def, state) => {
195
+ /* `disposed` first, for the reason `files-panel.ts`'s `mountFilesPanelSurface` guards it: the
196
+ mount is asynchronous, and a strip taken down inside the same turn — the pane whose host
197
+ this is has gone away — still walks this callback for every node its document creates.
198
+ Adopting one then would register a drag source nothing is left to release. */
199
+ if (disposed || !(element instanceof HTMLElement)) {
200
+ return;
201
+ }
202
+ const part = partOf(def);
203
+ if (part === "tabs") {
156
204
  strip = element;
205
+ actions.stripHost(element);
206
+ return;
207
+ }
208
+ if (part === "tab") {
209
+ const id = mappedChipKey(state);
210
+ if (id) {
211
+ actions.chipHost(element, id);
212
+ }
157
213
  }
158
214
  },
159
215
  }).then((surface) => {
@@ -172,6 +228,14 @@ export function mountTabStripSurface(
172
228
  strip = null;
173
229
  host.replaceChildren();
174
230
  },
231
+ drag(state) {
232
+ /* Independent of {@link update}'s field list, which is the whole point: these three are
233
+ written by the drag monitor on every pointer move, and `update`'s ordinary re-project must
234
+ not clear them out from under a drag it did not cause. */
235
+ scope.dragKey = state.dragKey;
236
+ scope.dropIndex = state.dropIndex;
237
+ scope.dropTail = state.dropTail;
238
+ },
175
239
  strip: () => strip,
176
240
  update(next) {
177
241
  /* Field by field, so an unchanged one is inert. A whole-object assignment would re-run every
@@ -23,7 +23,7 @@
23
23
  * @license MIT
24
24
  */
25
25
 
26
- import { fetchAvailableModels } from "../services/ai-models";
26
+ import { fetchAvailableModels, proxyModelsErrorMessage } from "../services/ai-models";
27
27
  import {
28
28
  getBaseUrl,
29
29
  getOpenAiKey,
@@ -165,6 +165,17 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
165
165
  },
166
166
  force: true,
167
167
  });
168
+ /*
169
+ * A 200 from the proxy does not mean the UPSTREAM listing worked — handleModels answers with
170
+ * defaults and `configured: true` even when the provider's /models route failed, so the list
171
+ * fetch never throws for that case. Cloudflare Workers AI's OpenAI-compatible surface has no
172
+ * /models route at all, so this is the only way that failure ever reaches the form; without
173
+ * it, the field silently filled with OpenAI's default ids instead of the user's own models.
174
+ */
175
+ const upstreamMessage = proxyModelsErrorMessage();
176
+ if (upstreamMessage) {
177
+ modelsError = `${upstreamMessage} — type the model ID directly instead.`;
178
+ }
168
179
  } catch (error: unknown) {
169
180
  modelsError = (error as Error).message || "Failed to fetch models";
170
181
  } finally {
@@ -43,9 +43,9 @@ import {
43
43
  activePane,
44
44
  detachTab,
45
45
  insertIntoPane,
46
+ paneBeside,
46
47
  paneById,
47
48
  promoteTab,
48
- sidePane,
49
49
  workspace,
50
50
  } from "./workspace";
51
51
  import {
@@ -1143,9 +1143,11 @@ function pinnablePane(): Pane | null {
1143
1143
  * **Unlike `pane.derive`, the COMMAND's subject is the grid rather than the focused pane**, and
1144
1144
  * that is deliberate: the author reaches "Keep This Document" from the palette while the keyboard
1145
1145
  * is in the page they are editing, and a grid holds at most one derived pane — `pane.derive` puts
1146
- * the projection in `sidePane()`, and `workspace.ts`'s `MAX_PANES` is 2. {@link pinnablePane} finds
1147
- * it by scanning, which reads no focus and is therefore the same answer from anywhere — so the
1148
- * preset menu can ask it about its own pane and get an answer about its own pane.
1146
+ * the projection in `paneBeside(source.id)`, and `workspace.ts`'s `MAX_PANES` is 2.
1147
+ * {@link
1148
+ * pinnablePane} finds it by scanning, which reads no focus and is therefore the same answer
1149
+ * from anywhere — so the preset menu can ask it about its own pane and get an answer about its own
1150
+ * pane.
1149
1151
  *
1150
1152
  * @param {string} paneId
1151
1153
  * @returns {string | null}
@@ -1227,7 +1229,7 @@ export function derivationCommands(deps: DerivationDeps): AnyCommand[] {
1227
1229
  `command "pane.derive" argument "preset": "${preset}" requires ${refusal}`,
1228
1230
  );
1229
1231
  }
1230
- const target = sidePane();
1232
+ const target = paneBeside(source.id);
1231
1233
  /* The DERIVATION IS PUBLISHED FIRST, and the order is not cosmetic.
1232
1234
  §18.1 rule 3 removes a pane with no subject, and `detachTab` applies it as each tab
1233
1235
  leaves — so handing the side pane's tabs back before it had a derivation collapsed the
@@ -217,9 +217,9 @@ export interface RestoreDeps {
217
217
  /**
218
218
  * Make sure the grid HAS a second pane, without moving anything into it.
219
219
  *
220
- * `receivingPane()`, not `splitRight()`: a split moves the focused pane's active tab, and at this
221
- * point nothing is open — so `splitRight()` returns `null`, the pane is never created, and the
222
- * second pane's documents all land in the first.
220
+ * `receivingPane(PRIMARY_PANE)`, not `splitRight()`: a split moves the focused pane's active tab,
221
+ * and at this point nothing is open — so `splitRight()` returns `null`, the pane is never
222
+ * created, and the second pane's documents all land in the first.
223
223
  */
224
224
  ensureSecondPane: () => void;
225
225
  }