@jxsuite/studio 0.20.0 → 0.21.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 (37) hide show
  1. package/dist/studio.js +1229 -477
  2. package/dist/studio.js.map +41 -39
  3. package/package.json +6 -5
  4. package/src/browse/browse-modal.js +16 -13
  5. package/src/browse/browse.js +19 -16
  6. package/src/canvas/canvas-live-render.js +19 -0
  7. package/src/canvas/canvas-utils.js +13 -6
  8. package/src/editor/context-menu.js +27 -13
  9. package/src/editor/convert-targets.js +60 -0
  10. package/src/editor/convert-to-component.js +9 -10
  11. package/src/editor/convert-to-repeater.js +226 -0
  12. package/src/editor/inline-edit.js +3 -0
  13. package/src/editor/shortcuts.js +10 -2
  14. package/src/editor/slash-menu.js +36 -17
  15. package/src/files/files.js +244 -16
  16. package/src/github/github-publish.js +26 -15
  17. package/src/panels/activity-bar.js +5 -5
  18. package/src/panels/ai-panel.js +10 -3
  19. package/src/panels/block-action-bar.js +73 -24
  20. package/src/panels/git-panel.js +7 -2
  21. package/src/panels/imports-panel.js +6 -1
  22. package/src/panels/left-panel.js +20 -1
  23. package/src/panels/properties-panel.js +16 -11
  24. package/src/panels/quick-search.js +4 -4
  25. package/src/panels/right-panel.js +16 -14
  26. package/src/panels/signals-panel.js +120 -0
  27. package/src/panels/statusbar.js +6 -2
  28. package/src/panels/style-panel.js +6 -2
  29. package/src/panels/tab-strip.js +5 -2
  30. package/src/settings/content-types-editor.js +1 -1
  31. package/src/settings/settings-modal.js +12 -9
  32. package/src/store.js +15 -9
  33. package/src/studio.js +17 -9
  34. package/src/ui/layers.js +31 -1
  35. package/src/utils/edit-display.js +1 -6
  36. package/src/utils/studio-utils.js +11 -11
  37. package/src/workspace/workspace.js +19 -0
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { html, render as litRender, nothing } from "lit-html";
11
+ import { ref } from "lit-html/directives/ref.js";
11
12
  import { getLayerSlot } from "../ui/layers.js";
12
13
 
13
14
  // ─── Commands ─────────────────────────────────────────────────────────────────
@@ -34,7 +35,13 @@ const SLASH_COMMANDS = [
34
35
 
35
36
  // ─── State ────────────────────────────────────────────────────────────────────
36
37
 
37
- /** @type {{ onSelect: (cmd: SlashCommand) => void; showFilter?: boolean } | null} */
38
+ /**
39
+ * @type {{
40
+ * onSelect: (cmd: SlashCommand) => void;
41
+ * showFilter?: boolean;
42
+ * commands?: SlashCommand[];
43
+ * } | null}
44
+ */
38
45
  let callbacks = null;
39
46
  let activeIdx = 0;
40
47
  /** @type {SlashCommand[]} */
@@ -44,6 +51,10 @@ let open = false;
44
51
  let _anchorEl = null;
45
52
  /** @type {DOMRect | null} */
46
53
  let _anchorRect = null;
54
+ /** @type {HTMLInputElement | null} */
55
+ let _filterEl = null;
56
+ /** @type {HTMLElement | null} */
57
+ let _popoverEl = null;
47
58
 
48
59
  /** @returns {HTMLElement} */
49
60
  function getHost() {
@@ -62,18 +73,23 @@ export function isSlashMenuOpen() {
62
73
  *
63
74
  * @param {HTMLElement} anchorEl — the element being edited (for positioning)
64
75
  * @param {string} filter — current typed filter text (after the "/")
65
- * @param {{ onSelect: (cmd: SlashCommand) => void; showFilter?: boolean }} cbs
76
+ * @param {{
77
+ * onSelect: (cmd: SlashCommand) => void;
78
+ * showFilter?: boolean;
79
+ * commands?: SlashCommand[];
80
+ * }} cbs
66
81
  */
67
82
  export function showSlashMenu(anchorEl, filter, cbs) {
68
83
  callbacks = cbs;
69
84
  _anchorEl = anchorEl;
70
85
  _anchorRect = anchorEl.getBoundingClientRect();
71
86
 
87
+ const source = cbs.commands || SLASH_COMMANDS;
72
88
  filteredItems = filter
73
- ? SLASH_COMMANDS.filter(
89
+ ? source.filter(
74
90
  (c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
75
91
  )
76
- : SLASH_COMMANDS;
92
+ : source;
77
93
 
78
94
  if (!filteredItems.length && !cbs.showFilter) {
79
95
  dismissSlashMenu();
@@ -94,10 +110,7 @@ export function showSlashMenu(anchorEl, filter, cbs) {
94
110
 
95
111
  if (cbs.showFilter) {
96
112
  requestAnimationFrame(() => {
97
- const input = /** @type {HTMLInputElement | null} */ (
98
- getHost().querySelector(".slash-filter")
99
- );
100
- if (input) input.focus();
113
+ if (_filterEl) _filterEl.focus();
101
114
  });
102
115
  }
103
116
  }
@@ -108,6 +121,8 @@ export function dismissSlashMenu() {
108
121
  callbacks = null;
109
122
  _anchorEl = null;
110
123
  _anchorRect = null;
124
+ _filterEl = null;
125
+ _popoverEl = null;
111
126
  filteredItems = [];
112
127
  document.removeEventListener("keydown", onKeydown, true);
113
128
  document.removeEventListener("mousedown", onOutsideClick, true);
@@ -127,6 +142,9 @@ function render(anchorEl, showFilter) {
127
142
  html`
128
143
  <sp-popover
129
144
  open
145
+ ${ref((el) => {
146
+ _popoverEl = /** @type {HTMLElement | null} */ (el || null);
147
+ })}
130
148
  style="position:fixed;left:${rect.left}px;top:${rect.bottom +
131
149
  4}px;z-index:9999;max-height:320px;overflow-y:auto"
132
150
  >
@@ -137,6 +155,9 @@ function render(anchorEl, showFilter) {
137
155
  placeholder="Filter…"
138
156
  autocomplete="off"
139
157
  style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
158
+ ${ref((el) => {
159
+ _filterEl = /** @type {HTMLInputElement | null} */ (el || null);
160
+ })}
140
161
  @input=${onFilterInput}
141
162
  />`
142
163
  : nothing}
@@ -169,9 +190,7 @@ function render(anchorEl, showFilter) {
169
190
 
170
191
  /** @param {MouseEvent} e */
171
192
  function onOutsideClick(e) {
172
- const host = getHost();
173
- const popover = host.querySelector("sp-popover");
174
- if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
193
+ if (_popoverEl && !_popoverEl.contains(/** @type {Node} */ (e.target))) {
175
194
  dismissSlashMenu();
176
195
  }
177
196
  }
@@ -188,21 +207,21 @@ function onFilterInput(e) {
188
207
  const input = /** @type {HTMLInputElement} */ (e.target);
189
208
  const filter = input.value.toLowerCase();
190
209
 
210
+ const source = callbacks?.commands || SLASH_COMMANDS;
191
211
  filteredItems = filter
192
- ? SLASH_COMMANDS.filter(
212
+ ? source.filter(
193
213
  (c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
194
214
  )
195
- : SLASH_COMMANDS;
215
+ : source;
196
216
 
197
217
  activeIdx = 0;
198
218
  if (_anchorEl) render(_anchorEl, true);
199
219
 
200
220
  // Re-focus input after re-render
201
221
  requestAnimationFrame(() => {
202
- const el = /** @type {HTMLInputElement | null} */ (getHost().querySelector(".slash-filter"));
203
- if (el && el !== document.activeElement) {
204
- el.focus();
205
- el.selectionStart = el.selectionEnd = el.value.length;
222
+ if (_filterEl && _filterEl !== document.activeElement) {
223
+ _filterEl.focus();
224
+ _filterEl.selectionStart = _filterEl.selectionEnd = _filterEl.value.length;
206
225
  }
207
226
  });
208
227
  }
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { html, nothing } from "lit-html";
9
9
  import { classMap } from "lit-html/directives/class-map.js";
10
+ import { ref } from "lit-html/directives/ref.js";
10
11
  import { unified } from "unified";
11
12
  import remarkStringify from "remark-stringify";
12
- import { renderPopover } from "../ui/layers.js";
13
+ import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers.js";
13
14
  import remarkDirective from "remark-directive";
14
15
  import { stringify as stringifyYaml } from "yaml";
15
16
  import { jxToMd } from "../markdown/md-convert.js";
@@ -17,10 +18,17 @@ import { createState, projectState, setProjectState, requireProjectState } from
17
18
  import { getPlatform } from "../platform.js";
18
19
  import { statusMessage } from "../panels/statusbar.js";
19
20
  import { loadComponentRegistry } from "./components.js";
21
+ import {
22
+ draggable,
23
+ dropTargetForElements,
24
+ monitorForElements,
25
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
26
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
20
27
  import {
21
28
  workspace,
22
29
  openTab,
23
30
  activateTab,
31
+ renameTab,
24
32
  replaceAllTabs,
25
33
  activeTab,
26
34
  } from "../workspace/workspace.js";
@@ -407,6 +415,170 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
407
415
  if (first) first.setAttribute("tabindex", "0");
408
416
  }
409
417
 
418
+ /** @type {(() => void)[]} */
419
+ let _fileTreeDndCleanups = [];
420
+
421
+ /**
422
+ * Register drag-and-drop on file tree items. Called after each file tree render.
423
+ *
424
+ * @param {{ renderLeftPanel: () => void }} ctx
425
+ */
426
+ export function registerFileTreeDnD({ renderLeftPanel }) {
427
+ // Clean up previous registrations
428
+ for (const fn of _fileTreeDndCleanups) fn();
429
+ _fileTreeDndCleanups = [];
430
+
431
+ requestAnimationFrame(() => {
432
+ const tree = /** @type {HTMLElement | null} */ (document.querySelector(".file-tree"));
433
+ if (!tree) return;
434
+
435
+ const items = /** @type {NodeListOf<HTMLElement>} */ (tree.querySelectorAll(".file-tree-item"));
436
+
437
+ items.forEach((row) => {
438
+ const path = row.dataset.path;
439
+ const type = row.dataset.type;
440
+ if (!path) return;
441
+
442
+ const cleanups = [
443
+ draggable({
444
+ element: row,
445
+ getInitialData() {
446
+ return { type: "file-tree", path, entryType: type };
447
+ },
448
+ onDragStart() {
449
+ row.classList.add("dragging");
450
+ },
451
+ onDrop() {
452
+ row.classList.remove("dragging");
453
+ },
454
+ }),
455
+ ];
456
+
457
+ if (type === "directory") {
458
+ cleanups.push(
459
+ dropTargetForElements({
460
+ element: row,
461
+ canDrop({ source }) {
462
+ if (source.data.type !== "file-tree") return false;
463
+ const srcPath = /** @type {string} */ (source.data.path);
464
+ if (srcPath === path) return false;
465
+ if (srcPath.startsWith(path + "/")) return false;
466
+ const srcParent = parentDir(srcPath);
467
+ if (srcParent === path) return false;
468
+ return true;
469
+ },
470
+ onDragEnter() {
471
+ row.classList.add("drag-over");
472
+ },
473
+ onDrag() {
474
+ if (!row.classList.contains("drag-over")) row.classList.add("drag-over");
475
+ },
476
+ onDragLeave() {
477
+ row.classList.remove("drag-over");
478
+ },
479
+ onDrop() {
480
+ row.classList.remove("drag-over");
481
+ },
482
+ getData() {
483
+ return { type: "file-tree-target", targetDir: path };
484
+ },
485
+ }),
486
+ );
487
+ }
488
+
489
+ _fileTreeDndCleanups.push(combine(...cleanups));
490
+ });
491
+
492
+ // Root-level drop target (move to project root)
493
+ const rootCleanup = dropTargetForElements({
494
+ element: tree,
495
+ canDrop({ source }) {
496
+ if (source.data.type !== "file-tree") return false;
497
+ const srcPath = /** @type {string} */ (source.data.path);
498
+ return parentDir(srcPath) !== ".";
499
+ },
500
+ onDragEnter() {
501
+ tree.classList.add("drag-over-root");
502
+ },
503
+ onDragLeave() {
504
+ tree.classList.remove("drag-over-root");
505
+ },
506
+ onDrop() {
507
+ tree.classList.remove("drag-over-root");
508
+ },
509
+ getData() {
510
+ return { type: "file-tree-target", targetDir: "." };
511
+ },
512
+ });
513
+ _fileTreeDndCleanups.push(rootCleanup);
514
+
515
+ // Monitor for drop events
516
+ const monitorCleanup = monitorForElements({
517
+ onDrop({ source, location }) {
518
+ const target = location.current.dropTargets[0];
519
+ if (!target) return;
520
+ if (source.data.type !== "file-tree") return;
521
+ if (target.data.type !== "file-tree-target") return;
522
+
523
+ const srcPath = /** @type {string} */ (source.data.path);
524
+ const targetDirPath = /** @type {string} */ (target.data.targetDir);
525
+ const fileName = srcPath.split("/").pop();
526
+ const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
527
+
528
+ if (newPath === srcPath) return;
529
+
530
+ moveFileEntry(srcPath, /** @type {string} */ (newPath), renderLeftPanel);
531
+ },
532
+ });
533
+ _fileTreeDndCleanups.push(monitorCleanup);
534
+ });
535
+ }
536
+
537
+ /**
538
+ * Move a file/directory and update all affected state.
539
+ *
540
+ * @param {string} oldPath
541
+ * @param {string} newPath
542
+ * @param {() => void} renderLeftPanel
543
+ */
544
+ async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
545
+ const platform = getPlatform();
546
+ try {
547
+ await platform.renameFile(oldPath, newPath);
548
+
549
+ // Update open tabs referencing the moved path
550
+ for (const [id] of workspace.tabs.entries()) {
551
+ if (id === oldPath) {
552
+ renameTab(oldPath, newPath, newPath);
553
+ } else if (id.startsWith(oldPath + "/")) {
554
+ const newTabPath = newPath + id.slice(oldPath.length);
555
+ renameTab(id, newTabPath, newTabPath);
556
+ }
557
+ }
558
+
559
+ // Refresh affected directories
560
+ const oldParent = parentDir(oldPath);
561
+ const newParent = parentDir(newPath);
562
+ await loadDirectory(oldParent);
563
+ if (newParent !== oldParent) await loadDirectory(newParent);
564
+
565
+ // Auto-expand target directory
566
+ if (newParent !== ".") requireProjectState().expanded.add(newParent);
567
+
568
+ renderLeftPanel();
569
+ statusMessage(`Moved to ${newPath}`);
570
+ } catch (/** @type {unknown} */ e) {
571
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
572
+ }
573
+ }
574
+
575
+ /** @param {string} path @returns {string} */
576
+ function parentDir(path) {
577
+ const normalized = path.replaceAll("\\", "/");
578
+ const lastSlash = normalized.lastIndexOf("/");
579
+ return lastSlash === -1 ? "." : normalized.substring(0, lastSlash);
580
+ }
581
+
410
582
  // ─── Context menu ─────────────────────────────────────────────────────────────
411
583
 
412
584
  /** @type {ReturnType<typeof renderPopover> | null} */
@@ -452,7 +624,22 @@ function showFileContextMenu(
452
624
  y = e.clientY;
453
625
 
454
626
  _fileCtxHandle = renderPopover(
455
- html`<sp-popover open style="position:fixed;z-index:10000;left:${x}px;top:${y}px">
627
+ html`<sp-popover
628
+ open
629
+ style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
630
+ ${ref((el) => {
631
+ if (!el) return;
632
+ requestAnimationFrame(() => {
633
+ const popover = /** @type {HTMLElement} */ (el);
634
+ const menuRect = popover.getBoundingClientRect();
635
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
636
+ if (y + menuRect.height > window.innerHeight)
637
+ y = window.innerHeight - menuRect.height - 4;
638
+ popover.style.left = `${x}px`;
639
+ popover.style.top = `${y}px`;
640
+ });
641
+ })}
642
+ >
456
643
  <sp-menu>
457
644
  ${items.map((item) =>
458
645
  item.label === "\u2014"
@@ -475,18 +662,6 @@ function showFileContextMenu(
475
662
  },
476
663
  },
477
664
  );
478
-
479
- requestAnimationFrame(() => {
480
- const popover = /** @type {HTMLElement | null} */ (
481
- _fileCtxHandle?.host.querySelector("sp-popover")
482
- );
483
- if (!popover) return;
484
- const menuRect = popover.getBoundingClientRect();
485
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
486
- if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
487
- popover.style.left = `${x}px`;
488
- popover.style.top = `${y}px`;
489
- });
490
665
  }
491
666
 
492
667
  // ─── File CRUD ────────────────────────────────────────────────────────────────
@@ -509,11 +684,57 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
509
684
  }
510
685
  }
511
686
 
687
+ /** @param {string} currentName @returns {Promise<string | null>} */
688
+ function showRenameFileDialog(currentName) {
689
+ let value = currentName;
690
+ return showDialog((done) => {
691
+ function confirm() {
692
+ const trimmed = value.trim();
693
+ if (!trimmed) return;
694
+ done(trimmed);
695
+ }
696
+ const tpl = html`
697
+ <sp-dialog-wrapper
698
+ open
699
+ underlay
700
+ headline="Rename"
701
+ confirm-label="Rename"
702
+ cancel-label="Cancel"
703
+ size="s"
704
+ @confirm=${confirm}
705
+ @cancel=${() => done(null)}
706
+ @close=${() => done(null)}
707
+ >
708
+ <sp-textfield
709
+ style="width:100%"
710
+ value=${currentName}
711
+ @input=${(/** @type {Event} */ e) => {
712
+ value = /** @type {HTMLInputElement} */ (e.target).value || "";
713
+ }}
714
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
715
+ if (e.key === "Enter") confirm();
716
+ }}
717
+ ></sp-textfield>
718
+ </sp-dialog-wrapper>
719
+ `;
720
+ requestAnimationFrame(() => {
721
+ const layer = document.getElementById("layer-dialog");
722
+ const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
723
+ if (tf) {
724
+ tf.focus();
725
+ const input = tf.shadowRoot?.querySelector("input");
726
+ if (input) input.select();
727
+ }
728
+ });
729
+ return tpl;
730
+ });
731
+ }
732
+
512
733
  async function renameFile(
513
734
  /** @type {{ name: string; path: string; type: string }} */ entry,
514
735
  /** @type {() => void} */ renderLeftPanel,
515
736
  ) {
516
- const newName = prompt("New name:", entry.name);
737
+ const newName = await showRenameFileDialog(entry.name);
517
738
  if (!newName || newName === entry.name) return;
518
739
  const entryPath = entry.path.replaceAll("\\", "/");
519
740
  const parentDir = entryPath.includes("/")
@@ -527,6 +748,9 @@ async function renameFile(
527
748
  if (requireProjectState().selectedPath === entry.path) {
528
749
  requireProjectState().selectedPath = newPath;
529
750
  }
751
+ if (workspace.tabs.has(entry.path)) {
752
+ renameTab(entry.path, newPath, newPath);
753
+ }
530
754
  renderLeftPanel();
531
755
  statusMessage(`Renamed to ${newName}`);
532
756
  } catch (/** @type {unknown} */ e) {
@@ -538,7 +762,11 @@ async function deleteFile(
538
762
  /** @type {{ name: string; path: string; type: string }} */ entry,
539
763
  /** @type {() => void} */ renderLeftPanel,
540
764
  ) {
541
- if (!confirm(`Delete "${entry.name}"?`)) return;
765
+ const confirmed = await showConfirmDialog("Delete File", `Delete "${entry.name}"?`, {
766
+ confirmLabel: "Delete",
767
+ destructive: true,
768
+ });
769
+ if (!confirmed) return;
542
770
  try {
543
771
  const platform = getPlatform();
544
772
  await platform.deleteFile(entry.path);
@@ -1,6 +1,7 @@
1
1
  /** Publish a local project to GitHub — creates a new repo and pushes. */
2
2
 
3
3
  import { html } from "lit-html";
4
+ import { ref } from "lit-html/directives/ref.js";
4
5
  import { showDialog } from "../ui/layers.js";
5
6
  import { authenticateGithub } from "./github-auth.js";
6
7
  import { getPlatform } from "../platform.js";
@@ -19,27 +20,24 @@ export async function publishToGithub({ projectName }) {
19
20
  if (!token) return false;
20
21
 
21
22
  const repoOpts = await showDialog((done) => {
23
+ /** @type {HTMLInputElement | null} */
24
+ let _nameInput = null;
25
+ /** @type {HTMLInputElement | null} */
26
+ let _descInput = null;
27
+ /** @type {HTMLInputElement | null} */
28
+ let _privateToggle = null;
29
+
22
30
  return html`
23
31
  <sp-dialog-wrapper
24
32
  open
25
33
  headline="Publish to GitHub"
26
34
  confirm-label="Create Repository"
27
35
  cancel-label="Cancel"
28
- @confirm=${(/** @type {Event} */ e) => {
29
- const dialog = /** @type {HTMLElement} */ (e.target);
30
- const nameInput = /** @type {HTMLInputElement | null} */ (
31
- dialog.querySelector('[name="repo-name"]')
32
- );
33
- const descInput = /** @type {HTMLInputElement | null} */ (
34
- dialog.querySelector('[name="repo-desc"]')
35
- );
36
- const privateToggle = /** @type {HTMLInputElement | null} */ (
37
- dialog.querySelector('[name="repo-private"]')
38
- );
36
+ @confirm=${() => {
39
37
  done({
40
- name: nameInput?.value || projectName,
41
- description: descInput?.value || "",
42
- isPrivate: privateToggle?.checked ?? true,
38
+ name: _nameInput?.value || projectName,
39
+ description: _descInput?.value || "",
40
+ isPrivate: _privateToggle?.checked ?? true,
43
41
  });
44
42
  }}
45
43
  @cancel=${() => done(null)}
@@ -52,6 +50,9 @@ export async function publishToGithub({ projectName }) {
52
50
  name="repo-name"
53
51
  value="${projectName}"
54
52
  placeholder="my-project"
53
+ ${ref((el) => {
54
+ _nameInput = /** @type {HTMLInputElement | null} */ (el || null);
55
+ })}
55
56
  ></sp-textfield>
56
57
 
57
58
  <sp-field-label for="repo-desc">Description (optional)</sp-field-label>
@@ -59,10 +60,20 @@ export async function publishToGithub({ projectName }) {
59
60
  id="repo-desc"
60
61
  name="repo-desc"
61
62
  placeholder="A brief description"
63
+ ${ref((el) => {
64
+ _descInput = /** @type {HTMLInputElement | null} */ (el || null);
65
+ })}
62
66
  ></sp-textfield>
63
67
 
64
68
  <sp-field-label>Visibility</sp-field-label>
65
- <sp-switch name="repo-private" checked>Private repository</sp-switch>
69
+ <sp-switch
70
+ name="repo-private"
71
+ checked
72
+ ${ref((el) => {
73
+ _privateToggle = /** @type {HTMLInputElement | null} */ (el || null);
74
+ })}
75
+ >Private repository</sp-switch
76
+ >
66
77
  </div>
67
78
  </sp-dialog-wrapper>
68
79
  `;
@@ -16,10 +16,11 @@ export function mount() {
16
16
  _scope.run(() => {
17
17
  effect(() => {
18
18
  const tab = activeTab.value;
19
- if (!tab) return;
20
- const gs = tab.session.ui.gitStatus;
21
- if (!gs && !tab.session.ui.gitLoading) {
22
- refreshGitStatus();
19
+ if (tab) {
20
+ const gs = tab.session.ui.gitStatus;
21
+ if (!gs && !tab.session.ui.gitLoading) {
22
+ refreshGitStatus();
23
+ }
23
24
  }
24
25
  renderActivityBar();
25
26
  });
@@ -92,7 +93,6 @@ export function tabIcon(tag, size) {
92
93
 
93
94
  export function renderActivityBar() {
94
95
  const tab = activeTab.value;
95
- if (!tab) return;
96
96
  const leftTab = view.leftTab;
97
97
  const gitFileCount = tab?.session.ui.gitStatus?.files?.length || 0;
98
98
  const tabs = [
@@ -5,9 +5,9 @@
5
5
  */
6
6
 
7
7
  import { html, nothing } from "lit-html";
8
+ import { ref } from "lit-html/directives/ref.js";
8
9
  import quikchat from "quikchat/md";
9
10
  import { getPlatform } from "../platform.js";
10
- import { rightPanel } from "../store.js";
11
11
  import { reloadFileInTab } from "../files/files.js";
12
12
 
13
13
  // ─── State (module-level, persists across tab switches) ─────────────────────
@@ -34,6 +34,8 @@ let mounted = false;
34
34
  let chatInstance = null;
35
35
  /** @type {Element | null} */
36
36
  let chatContainerEl = null;
37
+ /** @type {HTMLElement | null} */
38
+ let _quikChatEl = null;
37
39
  let currentStreamMsgId = /** @type {number | null} */ (null);
38
40
  let streamStarted = false;
39
41
  /** @type {Set<string>} */
@@ -61,7 +63,7 @@ export function registerRightPanelRender(/** @type {Function} */ fn) {
61
63
  // ─── QuikChat Mount ────────────────────────────────────────────────────────
62
64
 
63
65
  export function mountQuikChat() {
64
- const container = rightPanel.querySelector("#ai-quikchat");
66
+ const container = _quikChatEl;
65
67
  if (!container) return;
66
68
  if (chatInstance && chatContainerEl === container) return;
67
69
 
@@ -393,7 +395,12 @@ export function renderAiPanelTemplate() {
393
395
  New Chat
394
396
  </sp-action-button>
395
397
  </div>
396
- <div id="ai-quikchat"></div>
398
+ <div
399
+ id="ai-quikchat"
400
+ ${ref((el) => {
401
+ _quikChatEl = /** @type {HTMLElement | null} */ (el || null);
402
+ })}
403
+ ></div>
397
404
  </div>
398
405
  `;
399
406
  }