@jxsuite/studio 5.5.0 → 5.7.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/LICENSE +1 -1
- package/dist/chunks/{2020-g8mwpbkq.js → 2020-cs2sb9t1.js} +3 -3
- package/dist/chunks/adapter-y5gdq19c.js +4 -0
- package/dist/chunks/cf-account-picker-882a7ecm.js +4 -0
- package/dist/chunks/cf-settings-20dznc3j.js +4 -0
- package/dist/chunks/components-q037dya7.js +4 -0
- package/dist/chunks/content-source-37x3vsap.js +4 -0
- package/dist/chunks/convert-to-repeater-grfep2w8.js +4 -0
- package/dist/chunks/editor-j5sq149q.js +4 -0
- package/dist/chunks/entry-commands-c5h3nqck.js +4 -0
- package/dist/chunks/extension-sections-dp8endhx.js +4 -0
- package/dist/chunks/file-ops-937cvqk6.js +4 -0
- package/dist/chunks/files-tznwe1p1.js +4 -0
- package/dist/chunks/format-host-e2pjvd25.js +4 -0
- package/dist/chunks/fs-events-fv64ak3s.js +4 -0
- package/dist/chunks/git-panel-gb713ttc.js +4 -0
- package/dist/chunks/grid-view-5amhg604.js +51 -0
- package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
- package/dist/chunks/iframe-host-t819mcg5.js +4 -0
- package/dist/chunks/{index-arg188jq.js → index-9ydfe04y.js} +3 -3
- package/dist/chunks/index-m0x5ztax.js +4 -0
- package/dist/chunks/{javascript-zcdmye2k.js → javascript-d9pqgc2p.js} +2 -2
- package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-800yvbem.js} +5 -5
- package/dist/chunks/jx-validate-zc9zp77s.js +4 -0
- package/dist/chunks/layers-4bsr82wz.js +4 -0
- package/dist/chunks/layers-panel-b6shnaax.js +4 -0
- package/dist/chunks/lit-html-cfn61dn7.js +4 -0
- package/dist/chunks/media-picker-ej0kdh1j.js +4 -0
- package/dist/chunks/menu-rtxf9chb.js +4 -0
- package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-62dtmgbf.js} +3 -3
- package/dist/chunks/monaco-buffer-nn4dtx27.js +4 -0
- package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-tj13ndtm.js} +3 -3
- package/dist/chunks/notify-ahs57a2z.js +4 -0
- package/dist/chunks/platform-v9xyfkbx.js +4 -0
- package/dist/chunks/publish-panel-bq36xcmj.js +4 -0
- package/dist/chunks/quick-search-zm00h9dh.js +4 -0
- package/dist/chunks/references-99ws0b0m.js +4 -0
- package/dist/chunks/right-panel-pq2f71q3.js +4 -0
- package/dist/chunks/section-registry-b2yzp9sw.js +4 -0
- package/dist/chunks/studio-01kbbqg3.js +928 -0
- package/dist/chunks/{studio-hf4qdjc8.js → studio-01qza1n3.js} +4 -4
- package/dist/chunks/{studio-pkkw32j0.js → studio-03wwfvyt.js} +4 -4
- package/dist/chunks/{studio-7gqyhjns.js → studio-0bbxhhce.js} +4 -4
- package/dist/chunks/studio-0k9mvpgj.js +14 -0
- package/dist/chunks/studio-0scebvb9.js +6 -0
- package/dist/chunks/studio-1a99w39s.js +5 -0
- package/dist/chunks/studio-1wvfxkyy.js +5 -0
- package/dist/chunks/studio-1x8x26k3.js +5 -0
- package/dist/chunks/studio-222rqyyc.js +5 -0
- package/dist/chunks/studio-4xppwncr.js +4 -0
- package/dist/chunks/{studio-xcng8nx7.js → studio-54g077x3.js} +5 -5
- package/dist/chunks/studio-56z7evx4.js +10 -0
- package/dist/chunks/studio-5e9zsamh.js +9 -0
- package/dist/chunks/studio-65fhspdd.js +5 -0
- package/dist/chunks/studio-693nhyky.js +5 -0
- package/dist/chunks/{studio-7jazdy6y.js → studio-7pgy3xaf.js} +4 -4
- package/dist/chunks/studio-888knnkc.js +5 -0
- package/dist/chunks/studio-9mqfamps.js +5 -0
- package/dist/chunks/studio-a7xqhc3n.js +5 -0
- package/dist/chunks/studio-aje966s9.js +5 -0
- package/dist/chunks/studio-aqmph6k4.js +5 -0
- package/dist/chunks/studio-c6w3tq7t.js +5 -0
- package/dist/chunks/{studio-6j2jn55a.js → studio-c6zdm25z.js} +8 -8
- package/dist/chunks/{studio-f8jyp45j.js → studio-c9d3pxf7.js} +4 -4
- package/dist/chunks/studio-ceb34dxj.js +5 -0
- package/dist/chunks/studio-cvbkkfqb.js +5 -0
- package/dist/chunks/studio-cxt150pj.js +7 -0
- package/dist/chunks/studio-dkn3x3hz.js +5 -0
- package/dist/chunks/studio-e6re3rk1.js +5 -0
- package/dist/chunks/{studio-5n1km79v.js → studio-edbtvcd1.js} +4 -4
- package/dist/chunks/{studio-xc8ebsts.js → studio-eh0rac07.js} +4 -4
- package/dist/chunks/{studio-625ezz59.js → studio-evwgp1ym.js} +4 -4
- package/dist/chunks/{studio-he2fhrgp.js → studio-f195h2hq.js} +4 -4
- package/dist/chunks/studio-fqq890ap.js +4 -0
- package/dist/chunks/studio-frb0qbf7.js +6 -0
- package/dist/chunks/studio-gfn1kpxj.js +6 -0
- package/dist/chunks/studio-gh97gngt.js +5 -0
- package/dist/chunks/{studio-awqghqxv.js → studio-gp02nqnw.js} +4 -4
- package/dist/chunks/{studio-x9sagq2d.js → studio-hjbg8d5y.js} +7 -7
- package/dist/chunks/studio-j6gnhvaa.js +5 -0
- package/dist/chunks/studio-mwxgyn9h.js +7 -0
- package/dist/chunks/{studio-x5esgzct.js → studio-n78gjfy8.js} +4 -4
- package/dist/chunks/studio-nbxqcgvf.js +5 -0
- package/dist/chunks/{studio-p1eggxcf.js → studio-nt06yybj.js} +4 -4
- package/dist/chunks/{studio-hc9q4j24.js → studio-p7c1agz0.js} +5 -5
- package/dist/chunks/{studio-8r4y2x0z.js → studio-pwqfmt4p.js} +4 -4
- package/dist/chunks/{studio-5wfrgee9.js → studio-r3kaka4d.js} +4 -4
- package/dist/chunks/studio-semd1s8s.js +5 -0
- package/dist/chunks/studio-skzanb0k.js +5 -0
- package/dist/chunks/studio-spf2skgc.js +5 -0
- package/dist/chunks/{studio-b4epb26c.js → studio-st75kn99.js} +7 -7
- package/dist/chunks/{studio-eq27rrr0.js → studio-sxc0zvpt.js} +3 -3
- package/dist/chunks/studio-t1eedsyx.js +4 -0
- package/dist/chunks/{studio-73czgk6a.js → studio-tp642n5g.js} +4 -4
- package/dist/chunks/studio-tw7rre6s.js +5 -0
- package/dist/chunks/{studio-7by9a93k.js → studio-tzegmy4w.js} +4 -4
- package/dist/chunks/studio-vavmwshr.js +5 -0
- package/dist/chunks/studio-wma2c6g8.js +5 -0
- package/dist/chunks/studio-x92tgnee.js +64 -0
- package/dist/chunks/studio-xk0z6msk.js +5 -0
- package/dist/chunks/{studio-c4pnt1c6.js → studio-xry7tk3z.js} +5 -5
- package/dist/chunks/studio-xta2vxp6.js +9 -0
- package/dist/chunks/{studio-21enxdr8.js → studio-xy3tv3az.js} +4 -4
- package/dist/chunks/studio-y7jehjrp.js +5 -0
- package/dist/chunks/{studio-k1eaqyzm.js → studio-yr04mv2j.js} +4 -4
- package/dist/chunks/{studio-mhhc9btt.js → studio-zd4kayn2.js} +4 -4
- package/dist/chunks/studio-zpnk016a.js +16 -0
- package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-k9s9fcag.js} +5 -5
- package/dist/chunks/workspace-b69mgm0e.js +4 -0
- package/dist/chunks/ws-client-0ja08haa.js +4 -0
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +62 -59
- package/dist/studio.css +1 -1
- package/dist/studio.js +121 -123
- package/package.json +5 -5
- package/src/account/action-flow.ts +294 -0
- package/src/account/action-required.ts +68 -0
- package/src/account-status.ts +29 -1
- package/src/browse/library-model.ts +3 -1
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +3 -1
- package/src/canvas/asset-resolve.ts +79 -1
- package/src/canvas/iframe-entry.ts +38 -1
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/canvas/stage-scrollbar.ts +7 -7
- package/src/files/asset-versions.ts +294 -0
- package/src/files/file-ops.ts +5 -8
- package/src/files/files.ts +61 -9
- package/src/files/fs-events.ts +279 -28
- package/src/files/media-meta.ts +2 -1
- package/src/files/media-paths.ts +5 -2
- package/src/files/media-upload.ts +5 -1
- package/src/new-project/add-repo-modal.ts +12 -1
- package/src/new-project/new-project-modal.ts +12 -1
- package/src/panels/git-panel.ts +25 -2
- package/src/panels/jump-bar.ts +132 -158
- package/src/panels/pane-context.ts +8 -8
- package/src/panels/pane-grid.ts +18 -26
- package/src/panels/style-panel.ts +2 -2
- package/src/platform-errors.ts +61 -1
- package/src/platforms/cloud.ts +389 -50
- package/src/platforms/devserver.ts +17 -1
- package/src/publish/pages-service.ts +17 -7
- package/src/services/ai-models.ts +30 -6
- package/src/services/ai-system-prompt.ts +81 -51
- package/src/services/context-manager.ts +41 -70
- package/src/services/document-assistant.ts +12 -3
- package/src/services/harness/chat-reducer.ts +115 -0
- package/src/services/harness/turn-hooks.ts +50 -0
- package/src/services/tool-executor.ts +69 -288
- package/src/services/tool-outcomes.ts +12 -10
- package/src/settings/preferences-accounts.ts +42 -3
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/studio.ts +20 -6
- package/src/surfaces/block-action-bar.json +6 -1
- package/src/surfaces/dialog.ts +5 -0
- package/src/surfaces/jump-bar.json +9 -12
- package/src/surfaces/jump-bar.ts +15 -16
- package/src/surfaces/pane-context.json +2 -2
- package/src/surfaces/pane-grid.json +8 -25
- package/src/surfaces/pane-grid.ts +7 -8
- package/src/surfaces/statusbar.json +71 -3
- package/src/surfaces/statusbar.ts +66 -139
- package/src/surfaces/style-panel.json +3 -3
- package/src/surfaces/style-panel.ts +11 -1
- package/src/surfaces/welcome.json +86 -1
- package/src/surfaces/welcome.ts +61 -2
- package/src/types.ts +84 -2
- package/src/ui/ai-managed-connect.ts +41 -4
- package/src/ui/media-picker.ts +2 -1
- package/dist/chunks/adapter-bsbs78x9.js +0 -4
- package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
- package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
- package/dist/chunks/components-s51h3b7c.js +0 -4
- package/dist/chunks/content-source-ty3be0qg.js +0 -4
- package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
- package/dist/chunks/editor-t002b41m.js +0 -4
- package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
- package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
- package/dist/chunks/file-ops-78zeytkm.js +0 -4
- package/dist/chunks/files-gatzrz8v.js +0 -4
- package/dist/chunks/format-host-r59b1651.js +0 -4
- package/dist/chunks/fs-events-f8h91ndt.js +0 -4
- package/dist/chunks/git-panel-y12zf7nj.js +0 -4
- package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
- package/dist/chunks/grid-view-tafenfm2.css +0 -1
- package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
- package/dist/chunks/index-5aghyrfh.js +0 -4
- package/dist/chunks/jx-validate-653krkrf.js +0 -4
- package/dist/chunks/layers-32p8gtae.js +0 -4
- package/dist/chunks/layers-panel-rs58m105.js +0 -4
- package/dist/chunks/lit-html-54bmkwz3.js +0 -4
- package/dist/chunks/media-picker-5bb04wct.js +0 -4
- package/dist/chunks/menu-cbha7w2e.js +0 -4
- package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
- package/dist/chunks/notify-dc1nz5ar.js +0 -4
- package/dist/chunks/platform-2m49s1zf.js +0 -4
- package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
- package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
- package/dist/chunks/references-vx7mbj9f.js +0 -4
- package/dist/chunks/right-panel-7jzpbxja.js +0 -4
- package/dist/chunks/section-registry-gjryse41.js +0 -4
- package/dist/chunks/studio-3znfhcxg.js +0 -5
- package/dist/chunks/studio-58qam27v.js +0 -4
- package/dist/chunks/studio-6j59fwaw.js +0 -5
- package/dist/chunks/studio-7z69g1gx.js +0 -5
- package/dist/chunks/studio-93j3v6zx.js +0 -5
- package/dist/chunks/studio-9gf6v8zy.js +0 -5
- package/dist/chunks/studio-9rhmettf.js +0 -4
- package/dist/chunks/studio-ac5aq677.js +0 -10
- package/dist/chunks/studio-as65vbns.js +0 -5
- package/dist/chunks/studio-bkv0sga4.js +0 -6
- package/dist/chunks/studio-bttmqef4.js +0 -5
- package/dist/chunks/studio-c3y9d41k.js +0 -5
- package/dist/chunks/studio-ccegp7gz.js +0 -9
- package/dist/chunks/studio-chxabn98.js +0 -6
- package/dist/chunks/studio-cktcdsxz.js +0 -16
- package/dist/chunks/studio-cnspfke5.js +0 -5
- package/dist/chunks/studio-de3p1ktm.js +0 -5
- package/dist/chunks/studio-fsbcn530.js +0 -928
- package/dist/chunks/studio-gftga7a3.js +0 -64
- package/dist/chunks/studio-hf1rpcq0.js +0 -4
- package/dist/chunks/studio-hz02hq70.js +0 -5
- package/dist/chunks/studio-jnb285we.js +0 -5
- package/dist/chunks/studio-jsv3s3s1.js +0 -5
- package/dist/chunks/studio-ks7cksh0.js +0 -5
- package/dist/chunks/studio-nptx0dbs.js +0 -5
- package/dist/chunks/studio-pg0z7k98.js +0 -5
- package/dist/chunks/studio-pve0b2f2.js +0 -5
- package/dist/chunks/studio-q7zmjv3b.js +0 -6
- package/dist/chunks/studio-rpmvw7kj.js +0 -5
- package/dist/chunks/studio-rwhss52s.js +0 -5
- package/dist/chunks/studio-sbg3g3h0.js +0 -5
- package/dist/chunks/studio-sq1bn60g.js +0 -14
- package/dist/chunks/studio-vwz7rma2.js +0 -5
- package/dist/chunks/studio-wa55e8gf.js +0 -5
- package/dist/chunks/studio-x0mzdha2.js +0 -5
- package/dist/chunks/studio-xrgpakvc.js +0 -9
- package/dist/chunks/studio-xt3djk1w.js +0 -7
- package/dist/chunks/studio-yh3nggx1.js +0 -7
- package/dist/chunks/studio-zmecp6hq.js +0 -5
- package/dist/chunks/workspace-g6dvaete.js +0 -4
- package/dist/chunks/ws-client-shf41j5p.js +0 -4
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Pages-service — Cloudflare Pages publish domain logic over the PAL's
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* tell whether the publish workflow already exists.
|
|
3
|
+
* Pages-service — Cloudflare Pages publish domain logic over the PAL's `cfApi` passthrough. Works
|
|
4
|
+
* identically on every platform that provides it: the dev server / desktop (user API token via
|
|
5
|
+
* cf-settings → /__studio/cf/proxy) and the cloud platform (hosted OAuth). Publish state is written
|
|
6
|
+
* to project.json `build.deploy` — it travels with the repo, so any Studio can tell whether the
|
|
7
|
+
* publish workflow already exists.
|
|
9
8
|
*
|
|
10
9
|
* @license MIT
|
|
11
10
|
*/
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
import { updateWranglerConfig } from "@jxsuite/create/scaffold";
|
|
14
13
|
import type { DeployConfig, ProjectConfig } from "@jxsuite/schema/types";
|
|
15
14
|
import { getPlatform } from "../platform";
|
|
15
|
+
import { actIfRequired } from "../account/action-flow";
|
|
16
16
|
import { updateSiteConfig } from "../site-context";
|
|
17
17
|
|
|
18
18
|
export interface CfAccount {
|
|
@@ -52,12 +52,22 @@ export function platformSupportsPublish(): boolean {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* One Cloudflare call. A refusal that offers an action (desktop.md §10.4) is offered here, once for
|
|
57
|
+
* every publish step, and still thrown: the step that asked failed, and its caller reports that in
|
|
58
|
+
* its own words while the offer says how to fix it.
|
|
59
|
+
*/
|
|
55
60
|
async function cfApi<T>(path: string, init?: { method?: string; body?: unknown }): Promise<T> {
|
|
56
61
|
const api = getPlatform().cfApi;
|
|
57
62
|
if (!api) {
|
|
58
63
|
throw new Error("This platform cannot reach the Cloudflare API");
|
|
59
64
|
}
|
|
60
|
-
|
|
65
|
+
try {
|
|
66
|
+
return (await api(path, init)) as T;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
void actIfRequired(error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export async function listAccounts(): Promise<CfAccount[]> {
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Ai-models.js — available-model listing from the studio AI proxy.
|
|
4
4
|
*
|
|
5
|
-
* Fetches the proxy's /models endpoint (the chat endpoint's sibling), forwarding the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* after credentials change).
|
|
5
|
+
* Fetches the proxy's /models endpoint (the chat endpoint's sibling), forwarding the stored API key
|
|
6
|
+
* as X-Api-Key and the endpoint override as X-Api-Base-URL so the proxy lists models from the
|
|
7
|
+
* user's chosen provider. Shared by the credentials form and the chat composer's model picker;
|
|
8
|
+
* results are cached module-wide until invalidated (e.g. after credentials change).
|
|
10
9
|
*
|
|
11
10
|
* @license MIT
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
import type { AiModelsResponse } from "@jxsuite/protocol";
|
|
13
|
+
import type { AiModelsResponse, OfferedAction } from "@jxsuite/protocol";
|
|
15
14
|
import { getPlatform } from "../platform";
|
|
15
|
+
import { offeredActions } from "../platform-errors";
|
|
16
16
|
import { getBaseUrl, getOpenAiKey, hasOpenAiKey, storedModel } from "./ai-settings";
|
|
17
17
|
import { SETTINGS } from "./settings/definitions";
|
|
18
18
|
import { onSettingsChanged } from "./settings/kernel";
|
|
@@ -76,6 +76,14 @@ let proxyManaged = false;
|
|
|
76
76
|
let proxyDefaultModel = "";
|
|
77
77
|
let proxyCode: AiModelsResponse["code"];
|
|
78
78
|
let proxyModelsError = "";
|
|
79
|
+
/** What an `action_required` probe offered; null for every other answer. */
|
|
80
|
+
let proxyAction: ProxyActionOffer | null = null;
|
|
81
|
+
|
|
82
|
+
/** What a managed backend says the assistant needs first, in the backend's own words. */
|
|
83
|
+
export interface ProxyActionOffer {
|
|
84
|
+
detail?: string;
|
|
85
|
+
actions: OfferedAction[];
|
|
86
|
+
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
89
|
* One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
|
|
@@ -118,6 +126,7 @@ export function resetModelCache() {
|
|
|
118
126
|
proxyDefaultModel = "";
|
|
119
127
|
proxyCode = undefined;
|
|
120
128
|
proxyModelsError = "";
|
|
129
|
+
proxyAction = null;
|
|
121
130
|
/* The probe's result IS the flags above. Clearing them while keeping the settled promise
|
|
122
131
|
would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
|
|
123
132
|
would no-op forever and the managed option would vanish until a full reload. */
|
|
@@ -278,6 +287,14 @@ export function proxyModelsErrorMessage(): string {
|
|
|
278
287
|
return proxyModelsError;
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The offer the last probe carried — set only when the backend answered `action_required` (ai.md
|
|
292
|
+
* §2.1), and null for every other answer, so no gate can offer an action the backend did not name.
|
|
293
|
+
*/
|
|
294
|
+
export function proxyActionOffer(): ProxyActionOffer | null {
|
|
295
|
+
return proxyAction;
|
|
296
|
+
}
|
|
297
|
+
|
|
281
298
|
/** The proxy's preferred model id ("" when it does not declare one). */
|
|
282
299
|
export function getProxyDefaultModel(): string {
|
|
283
300
|
return proxyDefaultModel;
|
|
@@ -369,6 +386,13 @@ export async function fetchAvailableModels(
|
|
|
369
386
|
proxyDefaultModel = data.defaultModel ?? "";
|
|
370
387
|
proxyCode = data.code;
|
|
371
388
|
proxyModelsError = data.upstreamError !== undefined ? (data.upstreamMessage ?? "") : "";
|
|
389
|
+
proxyAction =
|
|
390
|
+
data.code === "action_required"
|
|
391
|
+
? {
|
|
392
|
+
actions: offeredActions(data.actions),
|
|
393
|
+
...(typeof data.detail === "string" && data.detail ? { detail: data.detail } : {}),
|
|
394
|
+
}
|
|
395
|
+
: null;
|
|
372
396
|
/* Capabilities are kept, not dropped. The backend has reported `toolSupport` all along and the
|
|
373
397
|
ingest mapped `{id, name}` only, so a Workers AI model that cannot call tools looked exactly
|
|
374
398
|
like one that can — the agent loop ran, called nothing, and answered as if that were normal.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ai-system-prompt.js — Dynamic system prompt builder for the Jx AI assistant
|
|
3
3
|
*
|
|
4
|
-
* Constructs the system prompt based on the current project context, open document,
|
|
5
|
-
*
|
|
4
|
+
* Constructs the system prompt based on the current project context, open document, and available
|
|
5
|
+
* components. The quality of AI output depends critically on this file.
|
|
6
6
|
*
|
|
7
7
|
* @license MIT
|
|
8
8
|
*/
|
|
@@ -656,6 +656,75 @@ When asked to build a site with multiple pages:
|
|
|
656
656
|
|
|
657
657
|
// ─── System prompt builder ───────────────────────────────────────────────────
|
|
658
658
|
|
|
659
|
+
const ROLE = `You are an expert Jx builder assistant embedded in Jx Studio. You help users build websites, components, pages, and layouts using the Jx JSON schema. The live jxsuite.com marketing site is built entirely with Jx — you can produce production-quality Jx code.`;
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* What to do when a tool call fails. Static: it names tools and validation messages, never the
|
|
663
|
+
* state, so it belongs to the cacheable prefix rather than after the dynamic sections.
|
|
664
|
+
*/
|
|
665
|
+
const ERROR_RECOVERY = `## Error Recovery
|
|
666
|
+
|
|
667
|
+
If a tool call fails (returns { success: false }):
|
|
668
|
+
1. Read the error message carefully — it includes a "→ Fix:" hint telling you exactly how to correct the error.
|
|
669
|
+
2. Each error points to a specific path in the document and a specific rule violation.
|
|
670
|
+
3. Apply the suggested fix using set_property, delete_node, or add_child as appropriate.
|
|
671
|
+
4. Do NOT re-issue the exact same tool call with the same arguments — you must CHANGE something.
|
|
672
|
+
5. If you see the SAME error after 2 attempts, try a completely different approach (e.g., remove and re-add the node instead of patching it).
|
|
673
|
+
|
|
674
|
+
### Common validation errors and their fixes:
|
|
675
|
+
|
|
676
|
+
| Error pattern | What happened | How to fix |
|
|
677
|
+
|---|---|---|
|
|
678
|
+
| "must NOT have additional property" in style | You used a non-camelCase CSS property (e.g. "background-color") or put an HTML attribute directly on the element. | Use camelCase: "backgroundColor". Put aria-*, data-*, role, and other non-IDL attributes inside the "attributes" object: { "attributes": { "aria-label": "..." } } |
|
|
679
|
+
| "must match pattern" on tagName | A custom element tag name doesn't contain a hyphen. | Add a hyphen: "newsletter-form" not "newsletter". Standard HTML elements use their exact name ("div", "p", "input"). |
|
|
680
|
+
| "must be string" | A value is an unquoted number, boolean, or bare word. | Wrap the value in quotes: "10px" not 10px. All CSS values and text must be strings. |
|
|
681
|
+
| "must be number" / "must be integer" | A numeric field (like index, tabIndex) is wrapped in quotes. | Remove the quotes: use 0 not "0". |
|
|
682
|
+
| "must have required property" | A required field is missing from the node. | Add the missing property. Every element must have "tagName". |
|
|
683
|
+
| "must be object" | A field that expects an object (like style or attributes) received a string or other type. | Use {} not a string. |
|
|
684
|
+
| "No node exists at path" | The path you provided doesn't point to an existing node. | Call read_document first to see the current structure and valid paths, then use the correct path. |
|
|
685
|
+
|
|
686
|
+
### If you keep getting errors:
|
|
687
|
+
- Call read_document again — the document may have changed since you last read it.
|
|
688
|
+
- Remove the problematic node entirely with delete_node, then re-create it correctly with add_child.
|
|
689
|
+
- If the error message points to a different path than you expected, the node might have moved due to previous edits.`;
|
|
690
|
+
|
|
691
|
+
/** What every section of the prompt is joined with. */
|
|
692
|
+
const SECTION_SEPARATOR = "\n\n---\n\n";
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The part of every system prompt that does not depend on the state, byte for byte: the role, the
|
|
696
|
+
* Jx reference sections, and the error-recovery guidance, each followed by the section separator.
|
|
697
|
+
*
|
|
698
|
+
* It exists for providers' prompt caches. A cache matches on a PREFIX of the request, and the
|
|
699
|
+
* system prompt is the request's first message, so the reference material (most of the prompt's
|
|
700
|
+
* tokens) only hits when nothing that changes between rounds comes before it. The prompt used to
|
|
701
|
+
* open with the role, the workflow for the current state and its tool list, so opening a document
|
|
702
|
+
* or registering one more command tool moved the first changed byte to the second paragraph and
|
|
703
|
+
* every request after it paid for the whole reference again. Everything dynamic — the mode, the
|
|
704
|
+
* tool list, the document, project and file summaries — now follows this prefix, and the closing
|
|
705
|
+
* instructions stay last. {@link buildSystemPrompt} always starts with it, and its tests hold that
|
|
706
|
+
* for every combination of the state flags.
|
|
707
|
+
*/
|
|
708
|
+
export const SYSTEM_PROMPT_STATIC_PREFIX =
|
|
709
|
+
[
|
|
710
|
+
// 1. Role
|
|
711
|
+
ROLE,
|
|
712
|
+
// 2. Jx schema reference — kept in ALL modes (pre-project the model plans starter content)
|
|
713
|
+
JX_SCHEMA_REFERENCE,
|
|
714
|
+
// 2a. State shape decision tree
|
|
715
|
+
STATE_SHAPE_DECISION_TREE,
|
|
716
|
+
// 2b. Real-world patterns
|
|
717
|
+
REAL_WORLD_PATTERNS,
|
|
718
|
+
// 2c. Design principles — spacing, type, color, layout, restraint
|
|
719
|
+
DESIGN_PRINCIPLES,
|
|
720
|
+
// 2d. Control flow & reactivity — signals, list rendering ($map), conditionals ($switch)
|
|
721
|
+
CONTROL_FLOW_PATTERNS,
|
|
722
|
+
// 2e. Multi-page site building — layouts, file-based routing, navigation
|
|
723
|
+
MULTI_PAGE_PATTERNS,
|
|
724
|
+
// 3. Error recovery guidance
|
|
725
|
+
ERROR_RECOVERY,
|
|
726
|
+
].join(SECTION_SEPARATOR) + SECTION_SEPARATOR;
|
|
727
|
+
|
|
659
728
|
/**
|
|
660
729
|
* Build a dynamic system prompt for the AI assistant.
|
|
661
730
|
*
|
|
@@ -681,7 +750,7 @@ export function buildSystemPrompt({
|
|
|
681
750
|
}: BuildSystemPromptOptions = {}) {
|
|
682
751
|
const hasDocument = Boolean(document);
|
|
683
752
|
|
|
684
|
-
//
|
|
753
|
+
// The state-appropriate workflow and the tool list for the current state.
|
|
685
754
|
// The list the model is TOLD about and the list the gate will honour are the same filter, so a
|
|
686
755
|
// Refusal is never a surprise to it. The command-projected tools arrive already filtered by
|
|
687
756
|
// Their records' own gates, for the same reason — one function answers both the prompt and the
|
|
@@ -695,8 +764,6 @@ export function buildSystemPrompt({
|
|
|
695
764
|
.map((blurb) => `- ${blurb}`)
|
|
696
765
|
.join("\n");
|
|
697
766
|
|
|
698
|
-
const role = `You are an expert Jx builder assistant embedded in Jx Studio. You help users build websites, components, pages, and layouts using the Jx JSON schema. The live jxsuite.com marketing site is built entirely with Jx — you can produce production-quality Jx code.`;
|
|
699
|
-
|
|
700
767
|
let workflow: string;
|
|
701
768
|
if (!hasProject && !hasDocument) {
|
|
702
769
|
workflow = `No project is open yet. Your first job is to bootstrap one:
|
|
@@ -734,23 +801,10 @@ ${ASKING_THE_USER}
|
|
|
734
801
|
|
|
735
802
|
Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
736
803
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
sections
|
|
741
|
-
// 2. Jx schema reference — kept in ALL modes (pre-project the model plans starter content)
|
|
742
|
-
JX_SCHEMA_REFERENCE,
|
|
743
|
-
// 3. State shape decision tree
|
|
744
|
-
STATE_SHAPE_DECISION_TREE,
|
|
745
|
-
// 4. Real-world patterns
|
|
746
|
-
REAL_WORLD_PATTERNS,
|
|
747
|
-
// 4a. Design principles — spacing, type, color, layout, restraint
|
|
748
|
-
DESIGN_PRINCIPLES,
|
|
749
|
-
// 4b. Control flow & reactivity — signals, list rendering ($map), conditionals ($switch)
|
|
750
|
-
CONTROL_FLOW_PATTERNS,
|
|
751
|
-
// 4c. Multi-page site building — layouts, file-based routing, navigation
|
|
752
|
-
MULTI_PAGE_PATTERNS,
|
|
753
|
-
);
|
|
804
|
+
/* 4. The mode. Everything from here on is a function of what is open right now, so it all follows
|
|
805
|
+
SYSTEM_PROMPT_STATIC_PREFIX: a provider's prompt cache matches on a prefix, and the first byte
|
|
806
|
+
that differs between two requests ends the match. */
|
|
807
|
+
const sections = [`## Current Mode\n\n${workflow}`];
|
|
754
808
|
|
|
755
809
|
// 5. Current document context
|
|
756
810
|
if (document) {
|
|
@@ -758,7 +812,7 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
|
758
812
|
sections.push(`## Current Document\n\n${summary}`);
|
|
759
813
|
}
|
|
760
814
|
|
|
761
|
-
//
|
|
815
|
+
// 5a. Project context
|
|
762
816
|
if (hasProject && (projectConfig || components || projectRoot)) {
|
|
763
817
|
const projectSummary = buildProjectSummary({
|
|
764
818
|
components,
|
|
@@ -771,7 +825,7 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
|
771
825
|
}
|
|
772
826
|
}
|
|
773
827
|
|
|
774
|
-
//
|
|
828
|
+
// 5b. File inventory — a compact map of the project for cross-file work
|
|
775
829
|
if (hasProject && fileInventory && fileInventory.length > 0) {
|
|
776
830
|
const capped = fileInventory.slice(0, FILE_INVENTORY_CAP);
|
|
777
831
|
const more =
|
|
@@ -781,34 +835,10 @@ Be concise. Don't explain what Jx is unless asked. Just build.`;
|
|
|
781
835
|
sections.push(`## Project Files\n\n${capped.join("\n")}${more}`);
|
|
782
836
|
}
|
|
783
837
|
|
|
784
|
-
//
|
|
785
|
-
sections.push(
|
|
786
|
-
|
|
787
|
-
If a tool call fails (returns { success: false }):
|
|
788
|
-
1. Read the error message carefully — it includes a "→ Fix:" hint telling you exactly how to correct the error.
|
|
789
|
-
2. Each error points to a specific path in the document and a specific rule violation.
|
|
790
|
-
3. Apply the suggested fix using set_property, delete_node, or add_child as appropriate.
|
|
791
|
-
4. Do NOT re-issue the exact same tool call with the same arguments — you must CHANGE something.
|
|
792
|
-
5. If you see the SAME error after 2 attempts, try a completely different approach (e.g., remove and re-add the node instead of patching it).
|
|
793
|
-
|
|
794
|
-
### Common validation errors and their fixes:
|
|
795
|
-
|
|
796
|
-
| Error pattern | What happened | How to fix |
|
|
797
|
-
|---|---|---|
|
|
798
|
-
| "must NOT have additional property" in style | You used a non-camelCase CSS property (e.g. "background-color") or put an HTML attribute directly on the element. | Use camelCase: "backgroundColor". Put aria-*, data-*, role, and other non-IDL attributes inside the "attributes" object: { "attributes": { "aria-label": "..." } } |
|
|
799
|
-
| "must match pattern" on tagName | A custom element tag name doesn't contain a hyphen. | Add a hyphen: "newsletter-form" not "newsletter". Standard HTML elements use their exact name ("div", "p", "input"). |
|
|
800
|
-
| "must be string" | A value is an unquoted number, boolean, or bare word. | Wrap the value in quotes: "10px" not 10px. All CSS values and text must be strings. |
|
|
801
|
-
| "must be number" / "must be integer" | A numeric field (like index, tabIndex) is wrapped in quotes. | Remove the quotes: use 0 not "0". |
|
|
802
|
-
| "must have required property" | A required field is missing from the node. | Add the missing property. Every element must have "tagName". |
|
|
803
|
-
| "must be object" | A field that expects an object (like style or attributes) received a string or other type. | Use {} not a string. |
|
|
804
|
-
| "No node exists at path" | The path you provided doesn't point to an existing node. | Call read_document first to see the current structure and valid paths, then use the correct path. |
|
|
805
|
-
|
|
806
|
-
### If you keep getting errors:
|
|
807
|
-
- Call read_document again — the document may have changed since you last read it.
|
|
808
|
-
- Remove the problematic node entirely with delete_node, then re-create it correctly with add_child.
|
|
809
|
-
- If the error message points to a different path than you expected, the node might have moved due to previous edits.`);
|
|
838
|
+
// 6. Closing — last, where a long prompt's final instructions carry the most weight.
|
|
839
|
+
sections.push(closing);
|
|
810
840
|
|
|
811
|
-
return sections.join(
|
|
841
|
+
return SYSTEM_PROMPT_STATIC_PREFIX + sections.join(SECTION_SEPARATOR);
|
|
812
842
|
}
|
|
813
843
|
|
|
814
844
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Context-manager.js — Token budget management for the AI assistant
|
|
3
3
|
*
|
|
4
|
-
* Estimates token usage and trims conversation history before each send to keep the
|
|
5
|
-
*
|
|
4
|
+
* Estimates token usage and trims conversation history before each send to keep the context window
|
|
5
|
+
* within safe limits (specs/ai.md §2 and §2.1).
|
|
6
6
|
*
|
|
7
|
-
* Strategy (MVP): keep the system prompt + the most recent messages, dropping the oldest
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Strategy (MVP): keep the system prompt + the most recent messages, dropping the oldest messages
|
|
8
|
+
* when the estimated total exceeds the configured token budget. Dropped messages are replaced with
|
|
9
|
+
* a single summary note so the model knows history was truncated.
|
|
10
10
|
*
|
|
11
11
|
* @license MIT
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { createChatState } from "@jxsuite/ai/chat-state";
|
|
15
|
+
import {
|
|
16
|
+
SEAL_RELOADED,
|
|
17
|
+
repairToolPairs,
|
|
18
|
+
toChatMessages,
|
|
19
|
+
toLiveMessages,
|
|
20
|
+
} from "@jxsuite/ai/messages";
|
|
15
21
|
import { modelContextWindow } from "./ai-models";
|
|
16
22
|
|
|
17
23
|
/** Shape of a single entry returned by `chatState.toMessagesArray()`. */
|
|
@@ -273,91 +279,56 @@ export function trimContext(
|
|
|
273
279
|
// ─── Orphaned tool calls ────────────────────────────────────────────────────
|
|
274
280
|
|
|
275
281
|
/**
|
|
276
|
-
* What a
|
|
277
|
-
* restore can
|
|
282
|
+
* What a seal says for a call that was not cut off, so a reader and the model see the same reason.
|
|
283
|
+
* Exported so a restore can name the outcome of an ordinary call nothing answered
|
|
284
|
+
* (services/tool-outcomes.ts); `isSealContent` tells any seal from a reply.
|
|
278
285
|
*/
|
|
279
|
-
export const UNANSWERED_TOOL_RESULT = JSON.stringify({
|
|
280
|
-
success: false,
|
|
281
|
-
error:
|
|
282
|
-
"This tool call was never completed — the session was reloaded or the history was trimmed.",
|
|
283
|
-
});
|
|
286
|
+
export const UNANSWERED_TOOL_RESULT = JSON.stringify({ success: false, error: SEAL_RELOADED });
|
|
284
287
|
|
|
285
288
|
/**
|
|
286
289
|
* Repair tool-call pairing in place, and report how many messages it touched.
|
|
287
290
|
*
|
|
288
291
|
* `toMessagesArray()` serializes the array verbatim: an assistant message with `tool_calls` becomes
|
|
289
292
|
* a `tool_calls` request, and a `tool` message becomes a reply carrying `tool_call_id`. Providers
|
|
290
|
-
* require the
|
|
293
|
+
* require each reply directly after the request that made the call, and **four ordinary things
|
|
294
|
+
* break that**:
|
|
291
295
|
*
|
|
292
296
|
* 1. `saveSession` persists `msgs.slice(-MAX_PERSIST_MESSAGES)` and `restoreChat` pushes the result
|
|
293
297
|
* back, so a session longer than the cap can be restored starting mid-pair.
|
|
294
298
|
* 2. {@link trimContext} splices from the front for the token budget, with no more regard for pairs.
|
|
295
299
|
* 3. A turn that stopped while a tool was still running — which `ask_user` makes routine, because its
|
|
296
300
|
* tool is _designed_ to be outstanding — leaves a request with no reply.
|
|
301
|
+
* 4. A transcript written before one window ran one turn can hold a reply after a later message.
|
|
297
302
|
*
|
|
298
303
|
* Each of those produced a 400 on the NEXT send, from a conversation the user could see was fine.
|
|
299
|
-
* So the repair belongs on the send path, after trimming, rather than at any one of the
|
|
300
|
-
*
|
|
301
|
-
*
|
|
304
|
+
* So the repair belongs on the send path, after trimming, rather than at any one of the sites. It
|
|
305
|
+
* is `repairToolPairs` (`@jxsuite/ai/messages`, specs/ai.md §3.4), the same repair the turn engine
|
|
306
|
+
* runs at turn start, applied to the chat store: pairing is per request rather than by call id
|
|
307
|
+
* across the transcript, because a provider may reuse `call_0` every round; a reply away from its
|
|
308
|
+
* request is moved back beside it; a request with no reply is sealed with a synthesized failure
|
|
309
|
+
* (keeping the assistant's text, which dropping the message would lose) that says whether the call
|
|
310
|
+
* was cut off mid-arguments; and a reply with no request is dropped.
|
|
311
|
+
*
|
|
312
|
+
* The store is rebuilt with one `splice`, and only when something changed. Every message the repair
|
|
313
|
+
* kept is the store's own object, which the streaming turn and the panel hold by identity, and the
|
|
314
|
+
* array itself is never reassigned.
|
|
302
315
|
*
|
|
303
316
|
* @param {ReturnType<typeof createChatState>} chatState
|
|
304
|
-
* @returns {{
|
|
317
|
+
* @returns {{ dropped: number; moved: number; sealed: number }}
|
|
305
318
|
*/
|
|
306
319
|
export function pruneOrphanToolMessages(chatState: ReturnType<typeof createChatState>): {
|
|
307
|
-
sealed: number;
|
|
308
320
|
dropped: number;
|
|
321
|
+
moved: number;
|
|
322
|
+
sealed: number;
|
|
309
323
|
} {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (msg.role === "assistant" && msg.toolCalls) {
|
|
316
|
-
for (const call of msg.toolCalls) {
|
|
317
|
-
requested.add(call.id);
|
|
318
|
-
}
|
|
319
|
-
} else if (msg.role === "tool" && msg.toolCallId) {
|
|
320
|
-
answered.add(msg.toolCallId);
|
|
321
|
-
}
|
|
324
|
+
const live = chatState.messages;
|
|
325
|
+
const neutral = toChatMessages(live);
|
|
326
|
+
const { messages, sealed, dropped, moved } = repairToolPairs(neutral);
|
|
327
|
+
if (sealed.length + dropped.length + moved.length === 0) {
|
|
328
|
+
return { dropped: 0, moved: 0, sealed: 0 };
|
|
322
329
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
328
|
-
const msg = messages[i]!;
|
|
329
|
-
if (msg.role === "tool" && (!msg.toolCallId || !requested.has(msg.toolCallId))) {
|
|
330
|
-
messages.splice(i, 1);
|
|
331
|
-
dropped += 1;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/* Requests with no reply. Walk backwards so each insertion is past the indices still to visit,
|
|
336
|
-
and seal immediately after the requesting message — `toMessagesArray` emits in array order, so
|
|
337
|
-
that is the only position the pair is adjacent in. */
|
|
338
|
-
let sealed = 0;
|
|
339
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
340
|
-
const msg = messages[i]!;
|
|
341
|
-
if (msg.role !== "assistant" || !msg.toolCalls) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
const missing = msg.toolCalls.filter((call) => !answered.has(call.id));
|
|
345
|
-
if (missing.length === 0) {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
messages.splice(
|
|
349
|
-
i + 1,
|
|
350
|
-
0,
|
|
351
|
-
...missing.map((call, n) => ({
|
|
352
|
-
content: UNANSWERED_TOOL_RESULT,
|
|
353
|
-
id: `sealed_${call.id}_${n}`,
|
|
354
|
-
role: "tool" as const,
|
|
355
|
-
timestamp: msg.timestamp,
|
|
356
|
-
toolCallId: call.id,
|
|
357
|
-
})),
|
|
358
|
-
);
|
|
359
|
-
sealed += missing.length;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return { dropped, sealed };
|
|
330
|
+
const liveOf = new Map(neutral.map((message, index) => [message, live[index]!]));
|
|
331
|
+
const next = messages.map((message) => liveOf.get(message) ?? toLiveMessages([message])[0]!);
|
|
332
|
+
live.splice(0, live.length, ...next);
|
|
333
|
+
return { dropped: dropped.length, moved: moved.length, sealed: sealed.length };
|
|
363
334
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createChatState, createProxyStreamingClient, createToolRegistry } from "@jxsuite/ai";
|
|
13
|
+
import { createTurnLock } from "@jxsuite/ai/harness";
|
|
13
14
|
import { createSessionFacts } from "@jxsuite/ai/tools";
|
|
14
15
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
15
16
|
import { getPlatform } from "../platform";
|
|
@@ -233,14 +234,17 @@ export function createDocumentAssistant() {
|
|
|
233
234
|
let controller: AbortController | null = null;
|
|
234
235
|
|
|
235
236
|
/**
|
|
236
|
-
*
|
|
237
|
+
* One window runs one turn. The lock is what decides it: a send is accepted only when it can take
|
|
238
|
+
* the lock, and holds it until its loop has ended.
|
|
237
239
|
*
|
|
238
240
|
* Not the chat's `status`, which belongs to the token stream: it reads idle while a round's tools
|
|
239
241
|
* run, while a question waits on the author and during an import. Guarding on it let a second
|
|
240
242
|
* send start a second turn beside the first, and let the composer offer Send in the middle of
|
|
241
|
-
* one.
|
|
243
|
+
* one. `turnActive` is the lock's reactive reading, for the panel's effects.
|
|
242
244
|
*/
|
|
245
|
+
const turnLock = createTurnLock();
|
|
243
246
|
const turnActive = shallowRef(false);
|
|
247
|
+
let turnCount = 0;
|
|
244
248
|
|
|
245
249
|
/** Settles when the turn in flight has ended, finally included; already settled when none is. */
|
|
246
250
|
let turnEnded: Promise<void> = Promise.resolve();
|
|
@@ -287,7 +291,7 @@ export function createDocumentAssistant() {
|
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
async function sendMessage(text: string) {
|
|
290
|
-
if (!text.trim() ||
|
|
294
|
+
if (!text.trim() || turnLock.active !== null || chatState.status === "streaming") {
|
|
291
295
|
return;
|
|
292
296
|
}
|
|
293
297
|
|
|
@@ -320,6 +324,10 @@ export function createDocumentAssistant() {
|
|
|
320
324
|
resolved inside the first request, on the armed signal. */
|
|
321
325
|
controller = new AbortController();
|
|
322
326
|
const { signal } = controller;
|
|
327
|
+
/* Taken here rather than at the guard so nothing above can leave it held. Everything since the
|
|
328
|
+
guard is synchronous, so no other send can have taken it in between: it is free. */
|
|
329
|
+
turnCount += 1;
|
|
330
|
+
const releaseTurn = turnLock.acquire(`send:${turnCount}`)!;
|
|
323
331
|
turnActive.value = true;
|
|
324
332
|
let settleTurnEnded!: () => void;
|
|
325
333
|
turnEnded = new Promise<void>((settle) => {
|
|
@@ -363,6 +371,7 @@ export function createDocumentAssistant() {
|
|
|
363
371
|
chatState.setError(error instanceof Error ? error.message : String(error));
|
|
364
372
|
} finally {
|
|
365
373
|
controller = null;
|
|
374
|
+
releaseTurn();
|
|
366
375
|
turnActive.value = false;
|
|
367
376
|
// Persist again once the stream settled so the completed reply (or the state
|
|
368
377
|
// After an error/abort cleanup) survives a reload without another send.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-reducer.ts — a turn's events, applied to Studio's chat store.
|
|
3
|
+
*
|
|
4
|
+
* The chat store is Studio's transcript of record: the panel renders it and the session store saves
|
|
5
|
+
* it. The turn engine (`@jxsuite/ai/harness`) reports what a turn does as events, and this is the
|
|
6
|
+
* one place they become chat-store calls, in the order the agent loop always made them (specs/ai.md
|
|
7
|
+
* §3.8): the recorded agent traces hold the two to the same sequence, call for call.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { createChatState } from "@jxsuite/ai/chat-state";
|
|
13
|
+
import type { HarnessEvent, TurnOutcome } from "@jxsuite/ai/harness";
|
|
14
|
+
import type { ImportProgressEvent } from "../../types";
|
|
15
|
+
import { recordImportProgress } from "../import-run";
|
|
16
|
+
|
|
17
|
+
type ChatStore = ReturnType<typeof createChatState>;
|
|
18
|
+
|
|
19
|
+
/** How a turn's end reads in the transcript: the cap message, an error row, or nothing more. */
|
|
20
|
+
function applyOutcome(chat: ChatStore, outcome: TurnOutcome): void {
|
|
21
|
+
switch (outcome.kind) {
|
|
22
|
+
case "cap_partial": {
|
|
23
|
+
// A partial success is said as the assistant, not as a failure (specs/ai.md §3.2).
|
|
24
|
+
chat.beginAssistantTurn();
|
|
25
|
+
chat.appendDelta(outcome.cap?.text ?? "");
|
|
26
|
+
chat.finishStream("length");
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "cap_failed":
|
|
30
|
+
case "empty":
|
|
31
|
+
case "error": {
|
|
32
|
+
/* `setError` alone, never after `finishStream`: it removes the round's partial message, and
|
|
33
|
+
only while that message is still the streaming one. */
|
|
34
|
+
chat.setError(outcome.error?.message ?? "");
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
default: {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Apply one turn event to the chat store.
|
|
45
|
+
*
|
|
46
|
+
* Round one answers into the placeholder `sendMessage` already pushed, so only a later round opens
|
|
47
|
+
* a message. A round that failed, or a turn that drew nothing, is never finished: its error row
|
|
48
|
+
* removes the partial. `turn_start`, `tool_start` and `write` change nothing here; the ledger
|
|
49
|
+
* records writes itself. Nor does `transcript_repaired`: the send path ran the same repair on the
|
|
50
|
+
* chat store before the turn began (`pruneOrphanToolMessages`), so the store is already the history
|
|
51
|
+
* the engine repaired.
|
|
52
|
+
*
|
|
53
|
+
* @param {ChatStore} chat
|
|
54
|
+
* @param {HarnessEvent} event
|
|
55
|
+
*/
|
|
56
|
+
export function applyHarnessEvent(chat: ChatStore, event: HarnessEvent): void {
|
|
57
|
+
switch (event.type) {
|
|
58
|
+
case "round_start": {
|
|
59
|
+
if (event.round > 1) {
|
|
60
|
+
chat.beginAssistantTurn();
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "text": {
|
|
65
|
+
chat.appendDelta(event.text);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "reasoning": {
|
|
69
|
+
/* Kept on the turn, not shown as answer text: the next round has to replay it
|
|
70
|
+
(specs/ai.md §2.2). */
|
|
71
|
+
chat.appendReasoning(event.text);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "tool_call_start": {
|
|
75
|
+
chat.appendToolCallStart(event.callId, event.name);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "tool_call_delta": {
|
|
79
|
+
chat.appendToolCallDelta(event.callId, event.args);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "tool_call_end": {
|
|
83
|
+
chat.appendToolCallEnd(event.callId);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "usage": {
|
|
87
|
+
/* The provider's own count replaces the four-characters-per-token estimate; the prompt it
|
|
88
|
+
covered is recorded with it, because the next send rebuilds the prompt. */
|
|
89
|
+
chat.recordUsage(event.frame, { systemTokens: event.systemTokens });
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "round_end": {
|
|
93
|
+
if (!event.dropped) {
|
|
94
|
+
chat.finishStream(event.stopReason);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "tool_progress": {
|
|
99
|
+
recordImportProgress(event.callId, event.progress as unknown as ImportProgressEvent);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "tool_result": {
|
|
103
|
+
chat.appendToolResult(event.callId, event.result);
|
|
104
|
+
chat.pushToolResultMessage(event.callId, JSON.stringify(event.result));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "turn_end": {
|
|
108
|
+
applyOutcome(chat, event.outcome);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
default: {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|