@jxsuite/studio 5.2.0 → 5.4.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.
- package/data/css-meta.json +28 -27
- package/dist/chunks/{cf-account-picker-h4dab9bq.js → cf-account-picker-3f08rry6.js} +21 -18
- package/dist/chunks/{cf-settings-adg74qsk.js → cf-settings-m79r7p5y.js} +4 -4
- package/dist/chunks/{components-5y78e05d.js → components-xm826zjw.js} +3 -3
- package/dist/chunks/{content-source-sgg1sv6n.js → content-source-jt7rbcbc.js} +22 -22
- package/dist/chunks/{convert-to-repeater-mf1cxgfn.js → convert-to-repeater-vg38bq99.js} +22 -22
- package/dist/chunks/{editor-3qhbg51x.js → editor-ddy019sn.js} +2 -2
- package/dist/chunks/entry-commands-64nb2a9a.js +42 -0
- package/dist/chunks/{extension-sections-c6zwzbre.js → extension-sections-r73an5s6.js} +31 -30
- package/dist/chunks/{file-ops-vrpq1jjk.js → file-ops-3xan8mnj.js} +21 -21
- package/dist/chunks/{files-msaf95y6.js → files-egsb0ry8.js} +30 -29
- package/dist/chunks/{format-host-f64fnesp.js → format-host-mym8qnd0.js} +3 -3
- package/dist/chunks/{fs-events-3y6y0nrm.js → fs-events-z2gg8qjt.js} +4 -4
- package/dist/chunks/{git-panel-bv5e22j0.js → git-panel-a05atk6q.js} +20 -20
- package/dist/chunks/{grid-view-1p6hsyjx.css → grid-view-3g1h2re8.css} +956 -139
- package/dist/chunks/{grid-view-paj67nr5.js → grid-view-qaqer9kr.js} +24 -24
- package/dist/chunks/{icons-yy10wk9r.js → icons-drxq9q10.js} +8 -2
- package/dist/chunks/{iframe-host-dh5np8vk.js → iframe-host-psetcv20.js} +11 -11
- package/dist/chunks/{index-n4j7qxyr.js → index-9yjae3hz.js} +4 -4
- package/dist/chunks/{javascript-4abzjcy9.css → javascript-bc9z461d.css} +688 -54
- package/dist/chunks/{javascript-xvrfrpkb.js → javascript-hr5sg57n.js} +2 -2
- package/dist/chunks/{jsonMode-vvmpmk34.css → jsonMode-qqd9nm1t.css} +1120 -303
- package/dist/chunks/{jsonMode-tyx2kj6p.js → jsonMode-zs88v0wr.js} +4 -4
- package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
- package/dist/chunks/layers-panel-83a16ytx.js +41 -0
- package/dist/chunks/{layers-1vcfdbet.js → layers-s6mc01ry.js} +3 -3
- package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
- package/dist/chunks/{media-picker-p49nhqhs.js → media-picker-nhn6rv4w.js} +11 -11
- package/dist/chunks/{menu-qht48vnc.js → menu-8j4qxjrp.js} +4 -4
- package/dist/chunks/{monaco-binding-sw6en4dw.js → monaco-binding-e8d9wzyk.js} +3 -3
- package/dist/chunks/{monaco-buffer-pykwbm92.js → monaco-buffer-jaq1t9zj.js} +2 -2
- package/dist/chunks/{monaco-setup-8vbyqnhr.css → monaco-setup-ey9cjcm3.css} +954 -137
- package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-gsjdysze.js} +7 -7
- package/dist/chunks/{platform-ts9cg1a4.js → platform-fnchmhy7.js} +5 -3
- package/dist/chunks/publish-panel-ak6hxfma.js +27 -0
- package/dist/chunks/quick-search-c63mkt3b.js +47 -0
- package/dist/chunks/{references-h69xp9zz.js → references-f7bdv3z5.js} +3 -3
- package/dist/chunks/right-panel-t5n8wttg.js +45 -0
- package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
- package/dist/chunks/{studio-sar8g54p.js → studio-01269reb.js} +8 -8
- package/dist/chunks/{studio-rnyf3f8e.js → studio-0a4gncx0.js} +3 -3
- package/dist/chunks/{studio-e1f24eff.js → studio-0d4p4wxj.js} +6778 -2993
- package/dist/chunks/{studio-4wbb9ct0.js → studio-1tejxvba.js} +3006 -1855
- package/dist/chunks/{studio-p6pmwkvh.js → studio-3nprwbd9.js} +147 -28
- package/dist/chunks/{studio-4c041r8y.js → studio-40z39dme.js} +3 -3
- package/dist/chunks/{studio-68fydsrj.js → studio-482eeazk.js} +20 -20
- package/dist/chunks/{studio-y4f9fdge.js → studio-4998spet.js} +10 -9
- package/dist/chunks/{studio-nq5yf5mp.js → studio-557bmn0x.js} +2 -2
- package/dist/chunks/{studio-hdc2jt3z.js → studio-5w26wza5.js} +3 -3
- package/dist/chunks/{studio-6nzaktvy.js → studio-6pwn9j8c.js} +6 -3
- package/dist/chunks/{studio-wnsyffg1.js → studio-6vg0x5fz.js} +3 -2
- package/dist/chunks/{studio-vn5hske5.js → studio-7d3c1m59.js} +12 -5
- package/dist/chunks/{studio-28vs5k2y.js → studio-9vsvz7w7.js} +7 -7
- package/dist/chunks/{studio-t3adrr56.js → studio-9wm0xzpf.js} +19 -19
- package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
- package/dist/chunks/{studio-ympe0n7a.js → studio-ag3z6shb.js} +10 -4
- package/dist/chunks/studio-aj9drheq.js +177 -0
- package/dist/chunks/{studio-vt0tj32j.js → studio-apv9x70c.js} +189 -130
- package/dist/chunks/{studio-c04nzbby.js → studio-b9q3znz8.js} +2 -2
- package/dist/chunks/{studio-2mdbbc76.js → studio-bh47w0cy.js} +9 -10
- package/dist/chunks/{studio-ez11dmdh.js → studio-dgdm6vvt.js} +264 -52
- package/dist/chunks/{studio-5nz00bhh.js → studio-dwdcr2e8.js} +4 -4
- package/dist/chunks/{studio-nqgawfks.js → studio-fbcyz234.js} +146 -139
- package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
- package/dist/chunks/{studio-x72j58vw.js → studio-fqpkpq1t.js} +7 -4
- package/dist/chunks/{studio-15f3kxqe.js → studio-fy320dfc.js} +62 -68
- package/dist/chunks/{studio-zjjde71a.js → studio-gjj4bbfx.js} +2 -2
- package/dist/chunks/{studio-66arbpc2.js → studio-jfhxy8h3.js} +16 -15
- package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
- package/dist/chunks/{studio-c07s5ckd.js → studio-kwewmhhq.js} +4 -4
- package/dist/chunks/{studio-01jeck2y.js → studio-m58xn8at.js} +26 -25
- package/dist/chunks/{studio-mpzwze9x.js → studio-mfkh1q8d.js} +17 -17
- package/dist/chunks/{studio-etvx9s2g.js → studio-mrh0a92f.js} +17 -17
- package/dist/chunks/{studio-qa7fzxan.js → studio-mt79tn56.js} +3 -3
- package/dist/chunks/{studio-hhskq2mr.js → studio-ne593z1j.js} +39 -32
- package/dist/chunks/{studio-h4nvnjvz.js → studio-ptfcxkz5.js} +418 -232
- package/dist/chunks/{studio-4xmjcrg3.js → studio-qxye31h7.js} +7 -7
- package/dist/chunks/{studio-a1pswps3.js → studio-rav5bpmp.js} +81 -59
- package/dist/chunks/{studio-ncn9e189.js → studio-rwmmbrth.js} +6 -6
- package/dist/chunks/{studio-en82c639.js → studio-sceg8ws0.js} +8 -8
- package/dist/chunks/{studio-wjpgxywa.js → studio-sjas5b3a.js} +19 -13
- package/dist/chunks/{studio-4r14yzyy.js → studio-vd67b66d.js} +3 -3
- package/dist/chunks/{studio-14d9tq4d.js → studio-w475cbf9.js} +10 -10
- package/dist/chunks/{studio-ctm7bhx5.js → studio-w7gpcvfj.js} +9 -9
- package/dist/chunks/{studio-9j6sfeba.js → studio-xrbfdxgk.js} +7 -7
- package/dist/chunks/{studio-x10fp4tp.js → studio-y49b6mjv.js} +35 -35
- package/dist/chunks/{studio-5t7ena5z.js → studio-ygvr9e4q.js} +10 -10
- package/dist/chunks/{studio-z2smx5ce.js → studio-yxmmjy2a.js} +9 -6
- package/dist/chunks/{tsMode-jy4m86rv.js → tsMode-ay4c78ne.js} +4 -4
- package/dist/chunks/{tsMode-hnxxb73h.css → tsMode-c72wyz27.css} +954 -137
- package/dist/chunks/{workspace-bnw54cha.js → workspace-b9k2frks.js} +5 -5
- package/dist/chunks/{ws-client-ny4zc51r.js → ws-client-erze4czt.js} +356 -20
- package/dist/codicon.ttf +0 -0
- package/dist/iframe-entry.js +20 -18
- package/dist/studio.css +956 -139
- package/dist/studio.js +1511 -851
- package/dist/workers/editor.worker.js +196 -107
- package/dist/workers/json.worker.js +196 -107
- package/dist/workers/ts.worker.js +196 -107
- package/package.json +18 -17
- package/src/about/about-modal.ts +4 -3
- package/src/browse/library-commands.ts +15 -13
- package/src/browse/library-pane.ts +2 -2
- package/src/canvas/canvas-render.ts +97 -83
- package/src/canvas/canvas-surface.ts +2 -2
- package/src/canvas/canvas-utils.ts +42 -32
- package/src/canvas/diff-toolbar.ts +21 -2
- package/src/canvas/diff-view.ts +30 -6
- package/src/canvas/iframe-entry.ts +2 -2
- package/src/canvas/iframe-host.ts +5 -5
- package/src/canvas/iframe-interaction.ts +7 -6
- package/src/canvas/iframe-overlay.ts +1 -1
- package/src/canvas/iframe-protocol.ts +6 -5
- package/src/canvas/iframe-render.ts +3 -3
- package/src/canvas/popover-state.ts +1 -1
- package/src/canvas/stage-scrollbar.ts +112 -0
- package/src/canvas/surface-registry.ts +9 -2
- package/src/collab/collab-commands.ts +8 -6
- package/src/collab/collab-session.ts +4 -4
- package/src/collab/collab-state.ts +4 -4
- package/src/collab/presence-chips.ts +3 -3
- package/src/commands/app-commands.ts +7 -7
- package/src/commands/budget.ts +17 -16
- package/src/commands/command-args.ts +10 -9
- package/src/commands/context.ts +20 -10
- package/src/commands/defaults.ts +30 -29
- package/src/commands/levels.ts +8 -6
- package/src/commands/live-context.ts +12 -10
- package/src/commands/reference.ts +5 -5
- package/src/commands/registry.ts +2 -2
- package/src/content/draft-state.ts +9 -8
- package/src/content/entry-commands.ts +16 -14
- package/src/content/entry-editor.ts +2 -2
- package/src/content/entry-fields.ts +1 -1
- package/src/editor/context-menu.ts +4 -4
- package/src/editor/convert-to-component.ts +6 -5
- package/src/editor/inline-edit.ts +3 -3
- package/src/editor/shortcuts.ts +88 -20
- package/src/editor/slash-menu.ts +11 -10
- package/src/files/elements.ts +4 -5
- package/src/files/file-ops.ts +1 -1
- package/src/files/files.ts +21 -17
- package/src/files/serialize-document.ts +1 -1
- package/src/format/convert-file.ts +5 -5
- package/src/github/github-auth-status.ts +107 -0
- package/src/github/github-auth.ts +18 -82
- package/src/github/github-publish.ts +7 -6
- package/src/grid/cell-editors.ts +8 -7
- package/src/grid/grid-layout.ts +1 -1
- package/src/grid/grid-open.ts +6 -5
- package/src/grid/grid-panel.ts +13 -12
- package/src/grid/redirects-grid.ts +14 -11
- package/src/grid/redirects.ts +10 -5
- package/src/grid/sources/content-source.ts +10 -9
- package/src/hosting/document.ts +28 -1
- package/src/hosting/stage.ts +15 -4
- package/src/i18n/i18n-commands.ts +9 -7
- package/src/new-project/location-fields.ts +16 -3
- package/src/new-project/new-project-modal.ts +23 -7
- package/src/page-params.ts +6 -6
- package/src/panels/activity-panel.ts +11 -10
- package/src/panels/ai-chat/chat-view.ts +27 -14
- package/src/panels/ai-chat/composer.ts +10 -3
- package/src/panels/ai-panel.ts +87 -29
- package/src/panels/block-action-bar.ts +59 -33
- package/src/panels/bottom-dock.ts +22 -22
- package/src/panels/chat-panel.ts +40 -5
- package/src/panels/data-explorer.ts +9 -9
- package/src/panels/data-grid.ts +1 -1
- package/src/panels/editors.ts +8 -8
- package/src/panels/elements-panel.ts +7 -8
- package/src/panels/empty-state.ts +1 -1
- package/src/panels/events-panel.ts +13 -12
- package/src/panels/formula-workspace.ts +8 -5
- package/src/panels/frontmatter-fields.ts +4 -1
- package/src/panels/frontmatter-panel.ts +18 -18
- package/src/panels/git-panel.ts +19 -13
- package/src/panels/head-panel.ts +9 -10
- package/src/panels/i18n-panel.ts +6 -4
- package/src/panels/imports-panel.ts +1 -1
- package/src/panels/jump-bar.ts +20 -18
- package/src/panels/layers-panel.ts +19 -17
- package/src/panels/left-panel.ts +3 -3
- package/src/panels/navigator-panels.ts +8 -8
- package/src/panels/overlays.ts +1 -1
- package/src/panels/pane-context.ts +56 -28
- package/src/panels/pane-grid.ts +9 -8
- package/src/panels/panel-registry.ts +47 -19
- package/src/panels/problems-panel.ts +11 -12
- package/src/panels/properties-panel.ts +21 -20
- package/src/panels/provenance.ts +3 -3
- package/src/panels/quick-search.ts +15 -11
- package/src/panels/right-panel.ts +4 -3
- package/src/panels/seo-modal.ts +5 -4
- package/src/panels/settings-menu.ts +9 -9
- package/src/panels/settings-pane.ts +3 -3
- package/src/panels/signals-panel.ts +16 -9
- package/src/panels/statement-editor.ts +12 -9
- package/src/panels/style-panel.ts +45 -41
- package/src/panels/stylebook-panel.ts +0 -1
- package/src/panels/tab-strip.ts +26 -23
- package/src/platform-errors.ts +2 -2
- package/src/platform.ts +44 -4
- package/src/platforms/cloud.ts +11 -1
- package/src/platforms/default-platform.ts +141 -4
- package/src/platforms/devserver.ts +23 -3
- package/src/publish/deploy-checklist.ts +11 -10
- package/src/publish/publish-commands.ts +9 -8
- package/src/services/a11y-report.ts +1 -1
- package/src/services/ai-ask.ts +39 -16
- package/src/services/ai-command-tools.ts +28 -14
- package/src/services/ai-import-tools.ts +43 -29
- package/src/services/ai-project-tools.ts +20 -38
- package/src/services/ai-settings.ts +55 -2
- package/src/services/ai-tools.ts +69 -58
- package/src/services/ai-write-report.ts +4 -4
- package/src/services/ai-writes.ts +74 -55
- package/src/services/automation.ts +12 -12
- package/src/services/clock.ts +2 -1
- package/src/services/connection.ts +2 -2
- package/src/services/context-manager.ts +69 -6
- package/src/services/directory-picker.ts +93 -27
- package/src/services/document-assistant.ts +86 -17
- package/src/services/gated-registry.ts +4 -3
- package/src/services/idle.ts +5 -4
- package/src/services/live-surfaces.ts +2 -1
- package/src/services/module-preloads.ts +67 -0
- package/src/services/notify.ts +9 -7
- package/src/services/popover-report.ts +3 -3
- package/src/services/preview-eval.ts +1 -1
- package/src/services/project-adoption.ts +34 -1
- package/src/services/project-path.ts +53 -0
- package/src/services/settings/kernel.ts +13 -2
- package/src/services/tool-executor.ts +135 -30
- package/src/services/tool-outcomes.ts +143 -0
- package/src/settings/contexts-section.ts +8 -8
- package/src/settings/contributed-section.ts +6 -6
- package/src/settings/css-vars-editor.ts +6 -5
- package/src/settings/extension-commands.ts +19 -18
- package/src/settings/extension-sections.ts +7 -7
- package/src/settings/extensions-section.ts +3 -3
- package/src/settings/general-settings.ts +21 -18
- package/src/settings/preferences-accounts.ts +3 -3
- package/src/settings/preferences-dialog.ts +9 -8
- package/src/settings/preferences-sections.ts +1 -1
- package/src/settings/project-sections.ts +2 -2
- package/src/settings/section-registry.ts +7 -6
- package/src/settings/settings-document.ts +41 -43
- package/src/shell/tree.ts +29 -0
- package/src/shell.ts +63 -55
- package/src/site-context.ts +1 -1
- package/src/store.ts +2 -2
- package/src/studio.ts +52 -28
- package/src/style/live-preview.ts +8 -8
- package/src/style/project-styles.ts +8 -9
- package/src/style/token-ref.ts +2 -2
- package/src/surfaces/about.json +6 -2
- package/src/surfaces/add-repo.json +2 -2
- package/src/surfaces/add-repo.ts +2 -2
- package/src/surfaces/ai-chat.json +14 -6
- package/src/surfaces/ai-chat.ts +4 -2
- package/src/surfaces/ai-credentials-form.json +53 -49
- package/src/surfaces/ai-credentials-form.ts +18 -4
- package/src/surfaces/ai-managed-connect.json +4 -4
- package/src/surfaces/block-action-bar.json +4 -4
- package/src/surfaces/block-action-bar.ts +3 -3
- package/src/surfaces/boot-failure.json +228 -0
- package/src/surfaces/boot-failure.ts +156 -0
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/canvas-stage.json +9 -40
- package/src/surfaces/canvas-stage.ts +9 -9
- package/src/surfaces/cf-account-picker.json +11 -8
- package/src/surfaces/commandbar.ts +11 -10
- package/src/surfaces/convert-repeater.ts +3 -2
- package/src/surfaces/dialog.json +7 -1
- package/src/surfaces/dialog.ts +2 -2
- package/src/surfaces/doc-header.json +6 -13
- package/src/surfaces/doc-header.ts +4 -5
- package/src/surfaces/entry-editor.json +1 -1
- package/src/surfaces/entry-editor.ts +2 -1
- package/src/surfaces/expression-editor.json +5 -5
- package/src/surfaces/files-panel.json +4 -3
- package/src/surfaces/files-panel.ts +1 -1
- package/src/surfaces/formula-palette.json +10 -9
- package/src/surfaces/formula-palette.ts +11 -7
- package/src/surfaces/git-panel.json +167 -109
- package/src/surfaces/git-panel.ts +2 -2
- package/src/surfaces/github-auth.json +2 -1
- package/src/surfaces/github-publish.ts +3 -3
- package/src/surfaces/grid-open.json +1 -1
- package/src/surfaces/grid-panel.json +2 -2
- package/src/surfaces/grid-views.json +1 -1
- package/src/surfaces/inspector-dock.json +2 -2
- package/src/surfaces/jump-bar.json +1 -1
- package/src/surfaces/jump-bar.ts +1 -1
- package/src/surfaces/library-pane.ts +2 -1
- package/src/surfaces/logic-panel.json +7 -7
- package/src/surfaces/logic-panel.ts +6 -4
- package/src/surfaces/logic-workspace.json +2 -2
- package/src/surfaces/logic-workspace.ts +2 -2
- package/src/surfaces/media-browser.ts +3 -3
- package/src/surfaces/media-pane.ts +2 -1
- package/src/surfaces/menu.ts +5 -5
- package/src/surfaces/new-project.json +1 -1
- package/src/surfaces/palette.json +8 -7
- package/src/surfaces/pane-context.json +86 -57
- package/src/surfaces/pane-context.ts +2 -1
- package/src/surfaces/pane-grid.json +3 -3
- package/src/surfaces/panel-activity.json +2 -1
- package/src/surfaces/panel-deploy-checklist.json +1 -1
- package/src/surfaces/panel-elements.ts +2 -1
- package/src/surfaces/panel-i18n.ts +2 -1
- package/src/surfaces/panel-outline.json +1 -1
- package/src/surfaces/panel-outline.ts +4 -3
- package/src/surfaces/panel-page.json +2 -2
- package/src/surfaces/panel-page.ts +4 -1
- package/src/surfaces/panel-problems.json +4 -3
- package/src/surfaces/panel-problems.ts +2 -2
- package/src/surfaces/panel-signals.json +23 -8
- package/src/surfaces/panel-signals.ts +5 -1
- package/src/surfaces/panel-stylebook-layers.json +1 -1
- package/src/surfaces/panel-stylebook-layers.ts +2 -2
- package/src/surfaces/preferences.json +21 -8
- package/src/surfaces/preferences.ts +9 -1
- package/src/surfaces/properties-panel.json +4 -4
- package/src/surfaces/properties-panel.ts +2 -2
- package/src/surfaces/publish.json +1 -1
- package/src/surfaces/publish.ts +2 -1
- package/src/surfaces/push-plan.json +4 -3
- package/src/surfaces/rail.json +2 -2
- package/src/surfaces/rail.ts +10 -4
- package/src/surfaces/reference-field.json +1 -1
- package/src/surfaces/schema-form.json +2 -2
- package/src/surfaces/schema-form.ts +1 -1
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/seo.ts +2 -2
- package/src/surfaces/settings-contributed.ts +2 -1
- package/src/surfaces/settings-css-vars.json +4 -4
- package/src/surfaces/settings-deploy.json +4 -4
- package/src/surfaces/settings-deploy.ts +2 -1
- package/src/surfaces/settings-extensions.json +6 -6
- package/src/surfaces/settings-extensions.ts +6 -2
- package/src/surfaces/settings-locales.json +4 -4
- package/src/surfaces/settings-overview.ts +2 -1
- package/src/surfaces/settings-pane.ts +3 -2
- package/src/surfaces/settings-rawjson.ts +1 -1
- package/src/surfaces/slash-menu.ts +4 -4
- package/src/surfaces/statements.json +3 -3
- package/src/surfaces/statusbar.ts +23 -23
- package/src/surfaces/style-panel.json +9 -5
- package/src/surfaces/style-panel.ts +3 -3
- package/src/surfaces/tab-strip.json +2 -2
- package/src/surfaces/target-line.json +1 -1
- package/src/surfaces/target-line.ts +12 -10
- package/src/surfaces/welcome.json +7 -2
- package/src/tabs/doc-op-apply.ts +5 -5
- package/src/tabs/patch-ops.ts +3 -3
- package/src/tabs/project-config.ts +10 -10
- package/src/tabs/selection.ts +1 -1
- package/src/tabs/tab.ts +13 -14
- package/src/tabs/transact.ts +18 -24
- package/src/types.ts +27 -3
- package/src/ui/ai-credentials-form.ts +74 -16
- package/src/ui/ai-managed-connect.ts +68 -19
- package/src/ui/color-selector.ts +1 -1
- package/src/ui/expression-editor.ts +3 -3
- package/src/ui/form-controls.ts +6 -5
- package/src/ui/formula-catalog.ts +4 -4
- package/src/ui/formula-chips.ts +2 -2
- package/src/ui/layers.ts +20 -18
- package/src/ui/panel-resize.ts +8 -28
- package/src/ui/popover-surface.ts +2 -2
- package/src/ui/progress-modal.ts +3 -3
- package/src/ui/regions.ts +14 -9
- package/src/ui/schema-form.ts +10 -9
- package/src/ui/timing.ts +9 -7
- package/src/ui/value-source.ts +4 -4
- package/src/ui/virtual-window.ts +58 -5
- package/src/utils/base64.ts +20 -1
- package/src/utils/canvas-media.ts +1 -1
- package/src/utils/studio-utils.ts +0 -31
- package/src/view.ts +4 -3
- package/src/workspace/pane-derive.ts +14 -13
- package/src/workspace/session.ts +7 -6
- package/src/workspace/workspace.ts +49 -39
- package/styles/canvas.css +1 -27
- package/styles/inspector.css +4 -3
- package/styles/overlays.css +1 -1
- package/styles/tokens.css +17 -1
- package/styles/tokens.json +6 -1
- package/dist/chunks/entry-commands-w9qsxa6q.js +0 -42
- package/dist/chunks/layers-panel-fksbdg4q.js +0 -40
- package/dist/chunks/publish-panel-vz23wwmg.js +0 -27
- package/dist/chunks/quick-search-e9rp74m8.js +0 -46
- package/dist/chunks/right-panel-j97t2yvy.js +0 -45
- package/dist/chunks/studio-727xh0e3.js +0 -76
- package/src/files/json-layout.ts +0 -474
- package/src/services/ai-turn-signal.ts +0 -70
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* exactly three members (spec studio.md §13.5):
|
|
7
7
|
*
|
|
8
8
|
* - `run(id, args)` — **is** `registry.run(id, args)`, behind {@link isScriptable}. There is no
|
|
9
|
-
* second action table. A hand-maintained parallel list of what the app can do is what
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* second action table. A hand-maintained parallel list of what the app can do is what §13 defines
|
|
10
|
+
* as a defect, and this module used to be 39 entries of exactly that, 16 of them holding raw
|
|
11
|
+
* XPath into the shell and three of them matching RENDERED TEXT.
|
|
12
12
|
* - `seed(id, args)` — the {@link SEEDS} registry, under the **Remote Rule**: a seed may only write
|
|
13
13
|
* state whose real writer is a network or IPC boundary. It stands in for a remote, never for a
|
|
14
14
|
* user. {@link REFUSED_SEEDS} names what that excludes and why.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `state()` (the full `CommandContext`), `commands()`, `seeds()`, and `pointAt`/`revealPath`,
|
|
17
17
|
* which compose the app's own transforms and answer in top-document coordinates.
|
|
18
18
|
*
|
|
19
|
-
* **What is deliberately absent**, each a normative refusal in
|
|
19
|
+
* **What is deliberately absent**, each a normative refusal in studio.md §13.5:
|
|
20
20
|
*
|
|
21
21
|
* 1. No method that takes a selector. If a script cannot say it in command ids and `JxPath`s, it
|
|
22
22
|
* cannot say it here.
|
|
@@ -97,7 +97,7 @@ export function isScriptable(command: AnyCommand): boolean {
|
|
|
97
97
|
*
|
|
98
98
|
* - `command` — a thing a user does, so it becomes a registry record. The value names who lands it.
|
|
99
99
|
* - `seed` — a remote's state, addressed through {@link AutomationApi.seed}.
|
|
100
|
-
* - `refused` — §13.
|
|
100
|
+
* - `refused` — §13.5 says the app will never provide it; the manifest step is deleted.
|
|
101
101
|
*/
|
|
102
102
|
export type GapDisposition = "command" | "seed" | "refused";
|
|
103
103
|
|
|
@@ -118,8 +118,8 @@ export interface ManifestId {
|
|
|
118
118
|
* — only to explain, when the registry refuses an id, which of the three fates that id has.
|
|
119
119
|
*
|
|
120
120
|
* This is a countdown, in the idiom of the checker's own `TOGGLE_DEBT`: it may only shrink. It went
|
|
121
|
-
* 39 → 8 when the manifest converted
|
|
122
|
-
* (
|
|
121
|
+
* 39 → 8 when the manifest converted, and 8 → 7 when `element.insertData` became `insert.data`
|
|
122
|
+
* (`canvas/canvas-render.ts`). What left: every `seed` entry, now read off {@link seedIds} and
|
|
123
123
|
* answered from the live registry rather than from a hand-kept list; every `toggle*` entry, which
|
|
124
124
|
* {@link TOGGLE_ID} refuses before this map is ever consulted; and every `command` entry whose
|
|
125
125
|
* record has since landed.
|
|
@@ -127,7 +127,7 @@ export interface ManifestId {
|
|
|
127
127
|
* What is left is two kinds. Two `command` entries are the manifest's real registry gaps — each
|
|
128
128
|
* reached today by an `input` step clicking a hand-stamped region, and each of those steps carries
|
|
129
129
|
* an `unstable` hatch naming the phase that lands the record, so this map and that hatch count are
|
|
130
|
-
* the same debt. Five `refused` entries are NOT a countdown: they are §13.
|
|
130
|
+
* the same debt. Five `refused` entries are NOT a countdown: they are §13.5's normative refusals,
|
|
131
131
|
* and they stay so that a caller reaching for one gets the reason instead of "unknown command".
|
|
132
132
|
*/
|
|
133
133
|
export const AUTOMATION_COMMANDS: Readonly<Record<string, ManifestId>> = {
|
|
@@ -137,9 +137,9 @@ export const AUTOMATION_COMMANDS: Readonly<Record<string, ManifestId>> = {
|
|
|
137
137
|
},
|
|
138
138
|
"layers.contextMenu": {
|
|
139
139
|
disposition: "refused",
|
|
140
|
-
note: "matched RENDERED TEXT, which
|
|
140
|
+
note: "matched RENDERED TEXT, which R1 of scripts/screenshots/README.md forbids; a row is addressed by its data-jx-path",
|
|
141
141
|
},
|
|
142
|
-
"media.browse": { disposition: "command", note: "
|
|
142
|
+
"media.browse": { disposition: "command", note: "media.browse, site-architecture.md §9.4" },
|
|
143
143
|
"project.showWelcome": {
|
|
144
144
|
disposition: "refused",
|
|
145
145
|
note: "cold start is a startup profile (?profile=fresh), not an action",
|
|
@@ -176,7 +176,7 @@ export const REFUSED_SEEDS: Readonly<Record<string, string>> = {
|
|
|
176
176
|
setActivity: 'a user picks a Navigator panel — run("navigator.showPanel", { panel })',
|
|
177
177
|
setRightTab: 'a user picks an Inspector tab — run("inspector.setTab", { tab })',
|
|
178
178
|
setStatus:
|
|
179
|
-
"the status bar reports what the app just did; staging it is the one lie §13.
|
|
179
|
+
"the status bar reports what the app just did; staging it is the one lie the Remote Rule (studio.md §13.5) names outright",
|
|
180
180
|
setZoom: 'a user zooms — run("canvas.setZoom", { zoom })',
|
|
181
181
|
};
|
|
182
182
|
|
|
@@ -315,7 +315,7 @@ export interface ScriptableCommand {
|
|
|
315
315
|
args?: object;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
/** What `pointAt` can address. Region ids join `path` when
|
|
318
|
+
/** What `pointAt` can address. Region ids join `path` when a region registry lands. */
|
|
319
319
|
export interface AutomationTarget {
|
|
320
320
|
path: JxPath;
|
|
321
321
|
}
|
package/src/services/clock.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 1. **They were untestable.** A formatter that reads the wall clock can only be asserted against an
|
|
9
9
|
* offset from the real present, so "yesterday" and "over a year ago" were never covered.
|
|
10
10
|
* 2. **They drift.** Two captures of the same state minutes apart legitimately read `01:59 PM` and
|
|
11
|
-
* `02:04 PM` (
|
|
11
|
+
* `02:04 PM` (the shot contract's Determinism section measured exactly that on the git-panel
|
|
12
|
+
* shot).
|
|
12
13
|
*
|
|
13
14
|
* This is deliberately NOT a global `Date.now` override. Monaco, every `setTimeout`, the collab
|
|
14
15
|
* clock and the transaction log all read the real clock and must keep reading it — pinning time for
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* The transport now rejects those calls, and this is the other half: a person needs to be told
|
|
12
12
|
* which of "it is broken" and "it is coming back" they are in. One keyed problem so a flapping
|
|
13
13
|
* connection cannot stack up a column of identical rows, dismissed on recovery so a fixed thing
|
|
14
|
-
* does not stay on the Problems list (§16.1's
|
|
15
|
-
*
|
|
14
|
+
* does not stay on the Problems list (§16.1's second and third rules: a record is retired when what
|
|
15
|
+
* it names is fixed, and a repeat is not a new row).
|
|
16
16
|
*
|
|
17
17
|
* A platform without `subscribeConnection` — the dev server, electrobun — installs nothing. There
|
|
18
18
|
* is no connection to lose there, and a notice about one would be a lie.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Context-manager.js — Token budget management for the AI assistant
|
|
3
3
|
*
|
|
4
4
|
* Estimates token usage and trims conversation history before each send to keep the
|
|
5
|
-
* context window within safe limits (
|
|
5
|
+
* context window within safe limits (specs/ai.md §2 and §2.1).
|
|
6
6
|
*
|
|
7
7
|
* Strategy (MVP): keep the system prompt + the most recent messages, dropping the oldest
|
|
8
8
|
* messages when the estimated total exceeds the configured token budget. Dropped messages
|
|
@@ -30,7 +30,8 @@ const CHARS_PER_TOKEN = 4;
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Approximate context windows (in tokens) per model id, longest-prefix matched. Used to derive a
|
|
33
|
-
* model-aware budget instead of a flat cap (
|
|
33
|
+
* model-aware budget instead of a flat cap (specs/ai.md §2.1). Conservative fallback for unknown
|
|
34
|
+
* models.
|
|
34
35
|
*/
|
|
35
36
|
const MODEL_CONTEXT_WINDOWS: [string, number][] = [
|
|
36
37
|
["gpt-4o", 128_000],
|
|
@@ -123,6 +124,64 @@ function estimateTokens(input: string | MessageArrayEntry[]): number {
|
|
|
123
124
|
return total;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/** The wire-shaped entry {@link estimateTokens} reads, for one transcript message. */
|
|
128
|
+
function wireEntry(msg: ReturnType<typeof createChatState>["messages"][number]): MessageArrayEntry {
|
|
129
|
+
const entry: MessageArrayEntry = { content: msg.content, role: msg.role };
|
|
130
|
+
if (msg.toolCalls) {
|
|
131
|
+
entry.tool_calls = msg.toolCalls.map((tc) => ({
|
|
132
|
+
function: { arguments: tc.arguments, name: tc.name },
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
return entry;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The context's size in tokens, as close to the provider's own count as the transcript allows.
|
|
140
|
+
*
|
|
141
|
+
* When the provider reported a count (`chatState.usage`) for a transcript this one still extends
|
|
142
|
+
* (the message at the count's boundary is the one it was taken at), that count covers everything up
|
|
143
|
+
* to it exactly, tool schemas included, so only the messages added since are estimated, plus
|
|
144
|
+
* whatever the system prompt has grown by. Without one, or once the transcript has been trimmed,
|
|
145
|
+
* rewound or repaired beneath it, the whole history is estimated at four characters per token,
|
|
146
|
+
* which is what every budget used before the count was forwarded.
|
|
147
|
+
*/
|
|
148
|
+
function contextTokens(
|
|
149
|
+
chatState: ReturnType<typeof createChatState>,
|
|
150
|
+
systemTokens: number,
|
|
151
|
+
): number {
|
|
152
|
+
const { usage, messages } = chatState;
|
|
153
|
+
const boundary = usage ? messages[usage.messageCount - 1] : undefined;
|
|
154
|
+
if (usage && boundary && boundary.id === usage.lastMessageId) {
|
|
155
|
+
/* Only what will be SENT: `toMessagesArray` drops an assistant turn carrying neither text nor
|
|
156
|
+
tool calls — the placeholder `sendMessage` pushes is always one — so it is not counted. */
|
|
157
|
+
const since = messages
|
|
158
|
+
.slice(usage.messageCount)
|
|
159
|
+
.filter((msg) => msg.role !== "assistant" || msg.content || (msg.toolCalls?.length ?? 0) > 0)
|
|
160
|
+
.map((msg) => wireEntry(msg));
|
|
161
|
+
/* The system prompt is rebuilt on every send from live state (the document outline, the file
|
|
162
|
+
inventory), so the one the count covered is not the one about to go out. What it has grown
|
|
163
|
+
or shrunk by is added, estimated the same way on both sides. */
|
|
164
|
+
const promptDrift = usage.systemTokens === undefined ? 0 : systemTokens - usage.systemTokens;
|
|
165
|
+
const reused = Math.max(0, usage.contextTokens + promptDrift) + estimateTokens(since);
|
|
166
|
+
// A count this module cannot trust is no count at all.
|
|
167
|
+
if (Number.isFinite(reused)) {
|
|
168
|
+
return reused;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return systemTokens + estimateTokens(chatState.toMessagesArray() as MessageArrayEntry[]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The estimate of a system prompt, in the same units {@link trimContext} budgets in — what the loop
|
|
176
|
+
* records beside a provider's count so the next budget can measure the prompt's drift.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} systemPrompt
|
|
179
|
+
* @returns {number}
|
|
180
|
+
*/
|
|
181
|
+
export function estimatePromptTokens(systemPrompt: string): number {
|
|
182
|
+
return estimateTokens(systemPrompt);
|
|
183
|
+
}
|
|
184
|
+
|
|
126
185
|
// ─── Trim ───────────────────────────────────────────────────────────────────
|
|
127
186
|
|
|
128
187
|
/**
|
|
@@ -146,8 +205,7 @@ export function trimContext(
|
|
|
146
205
|
|
|
147
206
|
const systemTokens = estimateTokens(systemPrompt);
|
|
148
207
|
const allMessages = chatState.messages;
|
|
149
|
-
const
|
|
150
|
-
const total = systemTokens + messageTokens;
|
|
208
|
+
const total = contextTokens(chatState, systemTokens);
|
|
151
209
|
|
|
152
210
|
if (total <= maxTokens) {
|
|
153
211
|
chatState.setTokenCount(total);
|
|
@@ -194,6 +252,8 @@ export function trimContext(
|
|
|
194
252
|
|
|
195
253
|
const droppedCount = keepFrom;
|
|
196
254
|
allMessages.splice(0, keepFrom);
|
|
255
|
+
// The provider counted a transcript that no longer exists; the estimate is all there is now.
|
|
256
|
+
chatState.clearUsage();
|
|
197
257
|
|
|
198
258
|
// Insert a summary note so the model knows context was trimmed.
|
|
199
259
|
allMessages.unshift({
|
|
@@ -212,8 +272,11 @@ export function trimContext(
|
|
|
212
272
|
|
|
213
273
|
// ─── Orphaned tool calls ────────────────────────────────────────────────────
|
|
214
274
|
|
|
215
|
-
/**
|
|
216
|
-
|
|
275
|
+
/**
|
|
276
|
+
* What a synthesized result says, so a reader and the model see the same reason. Exported so a
|
|
277
|
+
* restore can tell a seal from a reply (services/tool-outcomes.ts).
|
|
278
|
+
*/
|
|
279
|
+
export const UNANSWERED_TOOL_RESULT = JSON.stringify({
|
|
217
280
|
success: false,
|
|
218
281
|
error:
|
|
219
282
|
"This tool call was never completed — the session was reloaded or the history was trimmed.",
|
|
@@ -12,9 +12,17 @@
|
|
|
12
12
|
* drop a hidden {@link LOCATION_ID_FILE} holding a freshly generated id and ask the backend which
|
|
13
13
|
* directory holds that exact id. A fixed filename with the identity in its _contents_ is what makes
|
|
14
14
|
* the match unambiguous: two folders may share a basename, and a stale file from a crashed session
|
|
15
|
-
* may still be lying around, but only one file anywhere holds this id. The
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* may still be lying around, but only one file anywhere holds this id. The file lives in the chosen
|
|
16
|
+
* **Location** (the parent the project will be created in), never in the new project folder. The
|
|
17
|
+
* backend deletes it as soon as it matches, and this module removes it on every path the page
|
|
18
|
+
* survives, including a write that failed after the empty file was created. Only a tab closed
|
|
19
|
+
* mid-lookup can leave one behind, and a stale id can never match a later pick.
|
|
20
|
+
*
|
|
21
|
+
* **`null` means the user dismissed the chooser, and nothing else does.** Every other failure
|
|
22
|
+
* rejects with a sentence fit to show under the Location field: the chooser could not open, the
|
|
23
|
+
* folder cannot be written, the backend could not be reached, or it found no folder holding the id.
|
|
24
|
+
* This used to resolve `null` for all of them, which made a broken lookup indistinguishable from a
|
|
25
|
+
* cancel, and Browse… a button that silently did nothing.
|
|
18
26
|
*
|
|
19
27
|
* Only the plain dev-server browser session needs this. Every packaged build has a real native
|
|
20
28
|
* dialog that returns a path directly and keeps it — electrobun's `Utils.openFileDialog`, and the
|
|
@@ -24,6 +32,7 @@
|
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
34
|
import { LOCATION_ID_FILE } from "@jxsuite/protocol/routes";
|
|
35
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
27
36
|
|
|
28
37
|
/** Re-exported so a caller wiring up `locate` need not also reach into the protocol package. */
|
|
29
38
|
export { LOCATION_ID_FILE } from "@jxsuite/protocol/routes";
|
|
@@ -62,54 +71,111 @@ function newLocationId(): string {
|
|
|
62
71
|
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A thrown value's `name`, read structurally. A DOMException from another realm (an iframe's
|
|
76
|
+
* `showDirectoryPicker`, a test double) is not `instanceof Error` here, but it still says what it
|
|
77
|
+
* is.
|
|
78
|
+
*/
|
|
79
|
+
function nameOf(error: unknown): string {
|
|
80
|
+
return typeof error === "object" &&
|
|
81
|
+
error !== null &&
|
|
82
|
+
"name" in error &&
|
|
83
|
+
typeof error.name === "string"
|
|
84
|
+
? error.name
|
|
85
|
+
: "";
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
/**
|
|
66
89
|
* Open the browser's native folder chooser and resolve the chosen folder's absolute path.
|
|
67
90
|
*
|
|
68
|
-
* Resolves `null` when the user
|
|
69
|
-
*
|
|
70
|
-
*
|
|
91
|
+
* Resolves `null` only when the user dismisses the chooser (an `AbortError`, which is also what a
|
|
92
|
+
* declined readwrite grant reaches the page as: a user choice either way). Everything else rejects
|
|
93
|
+
* with an Error whose message is a plain sentence the caller can show as is:
|
|
94
|
+
*
|
|
95
|
+
* - No `showDirectoryPicker` at all (the dev-server adapter never offers Browse… then, so only an
|
|
96
|
+
* embedder wiring this helper by hand reaches it);
|
|
97
|
+
* - The chooser refused to open (a `SecurityError` included: this runs synchronously from the click,
|
|
98
|
+
* so a lost gesture means the environment is wrong, and silence would be a dead button);
|
|
99
|
+
* - The folder cannot be written, so it cannot be tagged, and a project could not be created there
|
|
100
|
+
* either;
|
|
101
|
+
* - `locate` threw (backend unreachable, non-ok), or answered `null` (no folder holds the id).
|
|
71
102
|
*
|
|
72
103
|
* MUST be called synchronously from a user gesture; `showDirectoryPicker()` is invoked before this
|
|
73
104
|
* function awaits anything so a click handler that calls it directly keeps the gesture.
|
|
74
105
|
*
|
|
75
|
-
* @param locate Ask the backend which directory holds `id`, given the handle's `name`.
|
|
106
|
+
* @param locate Ask the backend which directory holds `id`, given the handle's `name`. Resolve the
|
|
107
|
+
* path, resolve `null` for "no directory holds this id", and throw on a transport or backend
|
|
108
|
+
* failure. The not-found sentence assumes a backend that searches the home folder, as the dev
|
|
109
|
+
* server does.
|
|
76
110
|
*/
|
|
77
111
|
export async function pickDirectoryPath(
|
|
78
112
|
locate: (query: { name: string; id: string }) => Promise<string | null>,
|
|
79
113
|
): Promise<string | null> {
|
|
80
114
|
const show = (globalThis as { showDirectoryPicker?: ShowDirectoryPicker }).showDirectoryPicker;
|
|
81
115
|
if (typeof show !== "function") {
|
|
82
|
-
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
"This browser has no folder chooser. Type the folder's path into Location instead.",
|
|
118
|
+
);
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
let handle: DirectoryHandle;
|
|
86
122
|
try {
|
|
87
123
|
// `id` makes Chrome reopen at the last folder chosen for this purpose across sessions.
|
|
88
124
|
handle = await show({ id: "jx-new-project-location", mode: "readwrite", startIn: "documents" });
|
|
89
|
-
} catch {
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (nameOf(error) === "AbortError") {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
throw new Error(
|
|
130
|
+
`The folder chooser could not open (${errorMessage(error)}). Type the folder's path into Location instead.`,
|
|
131
|
+
{ cause: error },
|
|
132
|
+
);
|
|
92
133
|
}
|
|
93
134
|
|
|
135
|
+
const { name } = handle;
|
|
94
136
|
const id = newLocationId();
|
|
137
|
+
/* The `getFileHandle({ create: true })` call puts an empty file on disk before anything is
|
|
138
|
+
written, so a write that fails after it still leaves a tag to clean up. Only remove what this
|
|
139
|
+
call created. */
|
|
140
|
+
let tagged = false;
|
|
95
141
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
try {
|
|
143
|
+
const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
|
|
144
|
+
tagged = true;
|
|
145
|
+
const writable = await file.createWritable();
|
|
146
|
+
await writable.write(id);
|
|
147
|
+
await writable.close();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
/* The readwrite grant was just issued, so this is the filesystem refusing: a read-only
|
|
150
|
+
mount, OS permissions, a full disk. Creating the project there would fail the same way. */
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Jx Studio can't write to "${name}" (${errorMessage(error)}), so it can't create a project there. Choose a folder you can write to.`,
|
|
153
|
+
{ cause: error },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let path: string | null;
|
|
158
|
+
try {
|
|
159
|
+
path = await locate({ id, name });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Jx Studio could not look up where "${name}" is: ${errorMessage(error)}. Type the folder's path into Location instead.`,
|
|
163
|
+
{ cause: error },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (!path) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Jx Studio could not find where "${name}" is. From a browser it can only find folders inside your home folder. Type its full path into Location instead.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return path;
|
|
108
172
|
} finally {
|
|
109
173
|
// The backend removes the file the moment it matches; this covers every path where it did not
|
|
110
|
-
// (no match, a network failure, a backend that never ran the lookup).
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
174
|
+
// (no match, a network failure, a backend that never ran the lookup, a half-finished write).
|
|
175
|
+
if (tagged) {
|
|
176
|
+
await handle.removeEntry(LOCATION_ID_FILE).catch(() => {
|
|
177
|
+
/* Already gone — the backend beat us to it. */
|
|
178
|
+
});
|
|
179
|
+
}
|
|
114
180
|
}
|
|
115
181
|
}
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Wires the @jxsuite/ai infrastructure (chat-state, proxy streaming client, tool registry) to
|
|
6
6
|
* the active Jx document via `transactDoc()`-backed tools, and drives the error-correction
|
|
7
|
-
* agent loop. See
|
|
7
|
+
* agent loop. See specs/ai.md §3.
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createChatState, createProxyStreamingClient, createToolRegistry } from "@jxsuite/ai";
|
|
13
|
+
import { createSessionFacts } from "@jxsuite/ai/tools";
|
|
13
14
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
14
15
|
import { getPlatform } from "../platform";
|
|
15
16
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
16
|
-
import { toRaw } from "../reactivity";
|
|
17
|
+
import { shallowRef, toRaw } from "../reactivity";
|
|
17
18
|
import { projectState } from "../store";
|
|
18
19
|
import { adoptProjectConfig } from "../tabs/project-config";
|
|
19
20
|
import type { Tab } from "../tabs/tab";
|
|
@@ -27,10 +28,11 @@ import {
|
|
|
27
28
|
createCommandToolRegistry,
|
|
28
29
|
} from "./ai-command-tools";
|
|
29
30
|
import { registerProjectTools } from "./ai-project-tools";
|
|
30
|
-
import { registerImportTools
|
|
31
|
+
import { registerImportTools } from "./ai-import-tools";
|
|
31
32
|
import { createGatedToolRegistry } from "./gated-registry";
|
|
32
33
|
import type { ToolAvailability } from "./gated-registry";
|
|
33
34
|
import { adoptProject } from "./project-adoption";
|
|
35
|
+
import { resetAiWrites } from "./ai-writes";
|
|
34
36
|
import { abortImportRun, resetImportRuns } from "./import-run";
|
|
35
37
|
import { runAgentLoop } from "./tool-executor";
|
|
36
38
|
import { AI_TOOL_TIERS, buildSystemPrompt, toolActive } from "./ai-system-prompt";
|
|
@@ -43,10 +45,12 @@ import { validateDoc } from "./jx-validate";
|
|
|
43
45
|
import { reloadFileInTab } from "../files/files";
|
|
44
46
|
import { getExtensionCatalog, refreshExtensionUi } from "../format/format-host";
|
|
45
47
|
import * as sessionStore from "./ai-session-store";
|
|
48
|
+
import { backfillToolResults, stripToolResults } from "./tool-outcomes";
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
|
-
* Project root scoping the session store
|
|
49
|
-
*
|
|
51
|
+
* Project root scoping the session store, so conversations don't bleed across projects (the history
|
|
52
|
+
* section of docs/studio/ai/chat.md). Falls back to a shared unscoped store when no project is
|
|
53
|
+
* open.
|
|
50
54
|
*
|
|
51
55
|
* @returns {string}
|
|
52
56
|
*/
|
|
@@ -73,6 +77,9 @@ export function createDocumentAssistant() {
|
|
|
73
77
|
|
|
74
78
|
/** The persisted session backing the live chat; null = fresh unsaved chat. */
|
|
75
79
|
let sessionId: string | null = null;
|
|
80
|
+
/* What the conversation's tools remember across its turns (an import already ran): every call's
|
|
81
|
+
`ctx.session`. New Chat starts over; a new session id keeps what the conversation knew. */
|
|
82
|
+
let sessionFacts = createSessionFacts();
|
|
76
83
|
|
|
77
84
|
const getProjectStyle = () =>
|
|
78
85
|
(workspace.projectConfig as ProjectConfig | null)?.style as Record<string, string> | undefined;
|
|
@@ -88,7 +95,10 @@ export function createDocumentAssistant() {
|
|
|
88
95
|
|
|
89
96
|
const innerRegistry = createToolRegistry();
|
|
90
97
|
// Ungated: a question is not a function of what happens to be open.
|
|
91
|
-
|
|
98
|
+
/* A question suspends the turn on the author, for as long as they like, and the conversation is
|
|
99
|
+
otherwise saved only when a turn starts and ends. Saving here is what lets a reload find the
|
|
100
|
+
question to restore as open rather than losing the whole unfinished turn. */
|
|
101
|
+
registerAskTool(innerRegistry, { onPending: () => persistChat() });
|
|
92
102
|
registerAiTools(innerRegistry, {
|
|
93
103
|
getTab: () => activeTab.value,
|
|
94
104
|
saveFile: async (relPath: string, content: string) => {
|
|
@@ -222,6 +232,19 @@ export function createDocumentAssistant() {
|
|
|
222
232
|
|
|
223
233
|
let controller: AbortController | null = null;
|
|
224
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Whether a turn is in flight: true from the moment a send is accepted until its loop has ended.
|
|
237
|
+
*
|
|
238
|
+
* Not the chat's `status`, which belongs to the token stream: it reads idle while a round's tools
|
|
239
|
+
* run, while a question waits on the author and during an import. Guarding on it let a second
|
|
240
|
+
* send start a second turn beside the first, and let the composer offer Send in the middle of
|
|
241
|
+
* one. One window runs one turn, and this is the fact that says whether it is running.
|
|
242
|
+
*/
|
|
243
|
+
const turnActive = shallowRef(false);
|
|
244
|
+
|
|
245
|
+
/** Settles when the turn in flight has ended, finally included; already settled when none is. */
|
|
246
|
+
let turnEnded: Promise<void> = Promise.resolve();
|
|
247
|
+
|
|
225
248
|
function buildPrompt() {
|
|
226
249
|
const tab = activeTab.value;
|
|
227
250
|
const inventory = projectState
|
|
@@ -264,7 +287,7 @@ export function createDocumentAssistant() {
|
|
|
264
287
|
}
|
|
265
288
|
|
|
266
289
|
async function sendMessage(text: string) {
|
|
267
|
-
if (!text.trim() || chatState.status === "streaming") {
|
|
290
|
+
if (!text.trim() || turnActive.value || chatState.status === "streaming") {
|
|
268
291
|
return;
|
|
269
292
|
}
|
|
270
293
|
|
|
@@ -290,14 +313,25 @@ export function createDocumentAssistant() {
|
|
|
290
313
|
// Persist after trimming so the saved history reflects what's actually sent.
|
|
291
314
|
persistChat();
|
|
292
315
|
|
|
316
|
+
/* Stop is armed before the send's first wait. The turn's signal used to be created after
|
|
317
|
+
`await plat.aiChatUrl()`, which on desktop is an IPC round trip while the chat already reads
|
|
318
|
+
as streaming: a Stop pressed in that window found no controller, so the turn went on to
|
|
319
|
+
stream and run its tools, into a transcript the Stop had already cleared. The URL is now
|
|
320
|
+
resolved inside the first request, on the armed signal. */
|
|
321
|
+
controller = new AbortController();
|
|
322
|
+
const { signal } = controller;
|
|
323
|
+
turnActive.value = true;
|
|
324
|
+
let settleTurnEnded!: () => void;
|
|
325
|
+
turnEnded = new Promise<void>((settle) => {
|
|
326
|
+
settleTurnEnded = settle;
|
|
327
|
+
});
|
|
293
328
|
try {
|
|
294
329
|
const plat = getPlatform();
|
|
295
|
-
const chatUrl = await Promise.resolve(plat.aiChatUrl());
|
|
296
330
|
// Re-read the persisted model each send: the session is constructed once at module load
|
|
297
331
|
// (before the user sets a key/model), so the picker's choice must be picked up here.
|
|
298
332
|
chatState.setModel(preferredModel());
|
|
299
333
|
const streamingClient = createProxyStreamingClient({
|
|
300
|
-
chatUrl,
|
|
334
|
+
chatUrl: () => plat.aiChatUrl(),
|
|
301
335
|
model: chatState.model,
|
|
302
336
|
// Sent as X-Api-Key; the proxy falls back to the server's OPENAI_API_KEY when empty.
|
|
303
337
|
apiKey: getOpenAiKey() || undefined,
|
|
@@ -305,29 +339,52 @@ export function createDocumentAssistant() {
|
|
|
305
339
|
baseUrl: getBaseUrl() || undefined,
|
|
306
340
|
});
|
|
307
341
|
|
|
308
|
-
|
|
342
|
+
if (sessionFacts.sessionId !== sessionId) {
|
|
343
|
+
sessionFacts = createSessionFacts(sessionId, sessionFacts.toJSON());
|
|
344
|
+
}
|
|
309
345
|
await runAgentLoop({
|
|
310
346
|
chatState,
|
|
311
347
|
streamingClient,
|
|
312
348
|
toolRegistry,
|
|
313
349
|
systemPrompt: buildPrompt(),
|
|
314
|
-
signal
|
|
350
|
+
signal,
|
|
351
|
+
session: sessionFacts,
|
|
315
352
|
getTab: () => activeTab.value,
|
|
316
353
|
});
|
|
317
354
|
} catch (error) {
|
|
318
355
|
/*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
356
|
+
* A failure outside the stream's own error frames: the platform not registered, or its chat
|
|
357
|
+
* URL lookup rejecting inside the first request (a lookup that fails after a Stop ends the
|
|
358
|
+
* stream as cancelled instead). Set the error so the panel can display it.
|
|
321
359
|
*/
|
|
322
360
|
chatState.setError(error instanceof Error ? error.message : String(error));
|
|
323
361
|
} finally {
|
|
324
362
|
controller = null;
|
|
363
|
+
turnActive.value = false;
|
|
325
364
|
// Persist again once the stream settled so the completed reply (or the state
|
|
326
365
|
// After an error/abort cleanup) survives a reload without another send.
|
|
327
366
|
persistChat();
|
|
367
|
+
settleTurnEnded();
|
|
328
368
|
}
|
|
329
369
|
}
|
|
330
370
|
|
|
371
|
+
/** Whether a turn is in flight, tools and questions included. Reactive: read it in an effect. */
|
|
372
|
+
function isTurnActive(): boolean {
|
|
373
|
+
return turnActive.value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Resolves once the turn in flight, if any, has ended.
|
|
378
|
+
*
|
|
379
|
+
* Stop, New Chat and Open Session end a turn but cannot finish it: they abort its signal, and the
|
|
380
|
+
* loop unwinds afterwards, at least a microtask later and as late as the call it was running
|
|
381
|
+
* allows. Until then the window still holds that turn, so a send made in the meantime is refused.
|
|
382
|
+
* A caller that means to start the next turn waits here first.
|
|
383
|
+
*/
|
|
384
|
+
function whenTurnEnds(): Promise<void> {
|
|
385
|
+
return turnEnded;
|
|
386
|
+
}
|
|
387
|
+
|
|
331
388
|
function stop() {
|
|
332
389
|
controller?.abort();
|
|
333
390
|
/* The abort settles an outstanding question through the turn signal, but only when the turn
|
|
@@ -342,7 +399,11 @@ export function createDocumentAssistant() {
|
|
|
342
399
|
stop();
|
|
343
400
|
resetAsk();
|
|
344
401
|
resetImportRuns();
|
|
345
|
-
|
|
402
|
+
/* The record of what each turn changed is drawn under that turn's message; with the messages
|
|
403
|
+
gone there is nothing to draw it under, and a Restore offered from it would undo whatever
|
|
404
|
+
document is active now (ai.md §3.2). A stopped turn still unwinding files under no message. */
|
|
405
|
+
resetAiWrites();
|
|
406
|
+
sessionFacts = createSessionFacts();
|
|
346
407
|
chatState.clearChat();
|
|
347
408
|
sessionId = null;
|
|
348
409
|
sessionStore.setActiveSession(projectRoot(), null);
|
|
@@ -395,12 +456,18 @@ export function createDocumentAssistant() {
|
|
|
395
456
|
const msgs = chatState.messages.filter(
|
|
396
457
|
(m) => m.role !== "assistant" || m.content || (m.toolCalls?.length ?? 0) > 0,
|
|
397
458
|
);
|
|
398
|
-
|
|
459
|
+
/* Without each call's `result`: the tool message that answers it is the outcome's one copy,
|
|
460
|
+
and a restore reads it back from there (services/tool-outcomes.ts). */
|
|
461
|
+
sessionStore.saveSession(projectRoot(), sessionId, stripToolResults(msgs));
|
|
399
462
|
}
|
|
400
463
|
|
|
401
|
-
/**
|
|
464
|
+
/**
|
|
465
|
+
* Push persisted messages into chat state, synthesizing ids where missing, with each tool call's
|
|
466
|
+
* outcome backfilled from the tool message that answered it — so a restored chip shows how its
|
|
467
|
+
* call ended, and an answered question shows its answer.
|
|
468
|
+
*/
|
|
402
469
|
function pushRestoredMessages(msgs: sessionStore.PersistedMessage[]) {
|
|
403
|
-
for (const m of msgs) {
|
|
470
|
+
for (const m of backfillToolResults(msgs)) {
|
|
404
471
|
chatState.messages.push({
|
|
405
472
|
...m,
|
|
406
473
|
id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
@@ -428,7 +495,9 @@ export function createDocumentAssistant() {
|
|
|
428
495
|
|
|
429
496
|
return {
|
|
430
497
|
chatState,
|
|
498
|
+
isTurnActive,
|
|
431
499
|
sendMessage,
|
|
500
|
+
whenTurnEnds,
|
|
432
501
|
stop,
|
|
433
502
|
newChat,
|
|
434
503
|
listSessions,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @license MIT
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type { ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
|
|
16
|
+
import type { ToolContext, ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
|
|
17
17
|
|
|
18
18
|
export interface ToolAvailability {
|
|
19
19
|
/** Whether the tool is currently advertised/executable. */
|
|
@@ -58,7 +58,7 @@ export function createGatedToolRegistry(
|
|
|
58
58
|
validate(toolName: string, args: object) {
|
|
59
59
|
return inner.validate(toolName, args);
|
|
60
60
|
},
|
|
61
|
-
async execute(toolName: string, args: object) {
|
|
61
|
+
async execute(toolName: string, args: object, ctx?: ToolContext) {
|
|
62
62
|
if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
|
|
63
63
|
const gate = availability.get(toolName);
|
|
64
64
|
return {
|
|
@@ -66,7 +66,8 @@ export function createGatedToolRegistry(
|
|
|
66
66
|
error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// The call's context reaches the tool unchanged: its signal, id, ledger and session.
|
|
70
|
+
return inner.execute(toolName, args, ctx);
|
|
70
71
|
},
|
|
71
72
|
};
|
|
72
73
|
}
|