@jxsuite/studio 5.6.0 → 6.0.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/dist/chunks/{2020-zvax9fmb.js → 2020-cs2sb9t1.js} +3 -3
- package/dist/chunks/adapter-y5gdq19c.js +4 -0
- package/dist/chunks/{cf-account-picker-zfycs4k2.js → cf-account-picker-bb2yq946.js} +3 -3
- package/dist/chunks/cf-settings-20dznc3j.js +4 -0
- package/dist/chunks/components-q037dya7.js +4 -0
- package/dist/chunks/content-source-t0q184js.js +4 -0
- package/dist/chunks/{convert-to-repeater-x19jr6c7.js → convert-to-repeater-vqkt7s3s.js} +3 -3
- package/dist/chunks/editor-j5sq149q.js +4 -0
- package/dist/chunks/entry-commands-fxzgcc4k.js +4 -0
- package/dist/chunks/{extension-sections-y90sa0n4.js → extension-sections-8beq9ac3.js} +3 -3
- package/dist/chunks/file-ops-2n77759h.js +4 -0
- package/dist/chunks/files-fjy7zpxt.js +4 -0
- package/dist/chunks/format-host-wj3mf3n8.js +4 -0
- package/dist/chunks/fs-events-7kcgn4e9.js +4 -0
- package/dist/chunks/git-panel-7z2mpt2d.js +4 -0
- package/dist/chunks/{grid-view-d9mybhn0.js → grid-view-ca10def6.js} +10 -10
- package/dist/chunks/iframe-host-a0zjd6xp.js +4 -0
- package/dist/chunks/{index-vnwy9d44.js → index-9ydfe04y.js} +3 -3
- package/dist/chunks/index-m0x5ztax.js +4 -0
- package/dist/chunks/{javascript-f0nrbdjb.js → javascript-d9pqgc2p.js} +2 -2
- package/dist/chunks/{jsonMode-nfa90dtf.js → jsonMode-800yvbem.js} +4 -4
- package/dist/chunks/jx-validate-zc9zp77s.js +4 -0
- package/dist/chunks/layers-p677nrh5.js +4 -0
- package/dist/chunks/layers-panel-y4w1dq90.js +4 -0
- package/dist/chunks/lit-html-cfn61dn7.js +4 -0
- package/dist/chunks/media-picker-xg7yjpz9.js +4 -0
- package/dist/chunks/menu-fpekzj6w.js +4 -0
- package/dist/chunks/{monaco-binding-antn79jf.js → monaco-binding-62dtmgbf.js} +3 -3
- package/dist/chunks/monaco-buffer-nn4dtx27.js +4 -0
- package/dist/chunks/{monaco-setup-k8s40vwg.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-xtbfgg2r.js +4 -0
- package/dist/chunks/quick-search-d3a6m7j3.js +4 -0
- package/dist/chunks/references-99ws0b0m.js +4 -0
- package/dist/chunks/right-panel-31vbk5xp.js +4 -0
- package/dist/chunks/section-registry-b2yzp9sw.js +4 -0
- package/dist/chunks/{studio-y17505ge.js → studio-01kbbqg3.js} +207 -207
- package/dist/chunks/{studio-gxna6yta.js → studio-01qza1n3.js} +4 -4
- package/dist/chunks/{studio-n2e133d6.js → studio-03wwfvyt.js} +4 -4
- package/dist/chunks/{studio-09n3fyy0.js → studio-0bbxhhce.js} +4 -4
- package/dist/chunks/studio-1a99w39s.js +5 -0
- package/dist/chunks/studio-1q6amtph.js +5 -0
- package/dist/chunks/{studio-eegyf5vg.js → studio-1wvfxkyy.js} +4 -4
- package/dist/chunks/{studio-z47mm077.js → studio-3ck2a5c4.js} +6 -6
- package/dist/chunks/{studio-ymsjnvp9.js → studio-4fde4a0x.js} +4 -4
- package/dist/chunks/{studio-je15p5v8.js → studio-4jj9gme2.js} +5 -5
- package/dist/chunks/{studio-w8w9frjy.js → studio-4m50vh74.js} +12 -12
- package/dist/chunks/{studio-891t1tv9.js → studio-4r0sjtx8.js} +4 -4
- package/dist/chunks/{studio-6exg5z4x.js → studio-4xppwncr.js} +3 -3
- package/dist/chunks/{studio-011em5cf.js → studio-5d7wfmnp.js} +4 -4
- package/dist/chunks/{studio-zkkdbbvd.js → studio-5e9zsamh.js} +6 -6
- package/dist/chunks/{studio-pvj970st.js → studio-5gs9th4b.js} +4 -4
- package/dist/chunks/{studio-dtmdjmqb.js → studio-5vy2wfm5.js} +4 -4
- package/dist/chunks/{studio-654fxcr0.js → studio-7pgy3xaf.js} +4 -4
- package/dist/chunks/{studio-z1vdxq0e.js → studio-7qvs7v0m.js} +4 -4
- package/dist/chunks/studio-84rtc120.js +5 -0
- package/dist/chunks/{studio-ks0ecgjx.js → studio-8xcwewjq.js} +4 -4
- package/dist/chunks/{studio-sadckcpp.js → studio-aekhcdzw.js} +5 -5
- package/dist/chunks/{studio-mf41717j.js → studio-aje966s9.js} +4 -4
- package/dist/chunks/studio-aqmph6k4.js +5 -0
- package/dist/chunks/{studio-28pz4s1k.js → studio-bftp4n4b.js} +4 -4
- package/dist/chunks/{studio-tzs6njnb.js → studio-c6zdm25z.js} +7 -7
- package/dist/chunks/{studio-sgzss898.js → studio-capk0583.js} +7 -7
- package/dist/chunks/{studio-805fwp9k.js → studio-cbr9czkn.js} +4 -4
- package/dist/chunks/{studio-1pxvgx1t.js → studio-chdmwwzw.js} +4 -4
- package/dist/chunks/{studio-dknr8m7b.js → studio-cp1gbff9.js} +7 -7
- package/dist/chunks/{studio-ft8pfpp0.js → studio-cqmtbg2p.js} +4 -4
- package/dist/chunks/studio-e47215hh.js +5 -0
- package/dist/chunks/{studio-9n2j9nqk.js → studio-edbtvcd1.js} +4 -4
- package/dist/chunks/{studio-mm8x32gg.js → studio-evwgp1ym.js} +4 -4
- package/dist/chunks/{studio-02y9cb4h.js → studio-fbx6twps.js} +5 -5
- package/dist/chunks/{studio-0nm2skex.js → studio-fqq890ap.js} +3 -3
- package/dist/chunks/studio-fthwn4kf.js +5 -0
- package/dist/chunks/{studio-s85vah66.js → studio-g8dsj3d2.js} +5 -5
- package/dist/chunks/{studio-0c3fpdqj.js → studio-ge5fqjth.js} +5 -5
- package/dist/chunks/{studio-ebt49j3c.js → studio-gjknvzam.js} +5 -5
- package/dist/chunks/{studio-zkvyd3ev.js → studio-gp02nqnw.js} +4 -4
- package/dist/chunks/{studio-5fc0728f.js → studio-hjbg8d5y.js} +7 -7
- package/dist/chunks/{studio-d007j0z3.js → studio-hnvnfbdv.js} +6 -6
- package/dist/chunks/{studio-b5rxwkj2.js → studio-mk00zd5d.js} +4 -4
- package/dist/chunks/{studio-36s6qjgq.js → studio-n78gjfy8.js} +4 -4
- package/dist/chunks/studio-nbxqcgvf.js +5 -0
- package/dist/chunks/{studio-y164brht.js → studio-pwqfmt4p.js} +4 -4
- package/dist/chunks/studio-qa73881a.js +5 -0
- package/dist/chunks/{studio-6zyt56ek.js → studio-r3kaka4d.js} +4 -4
- package/dist/chunks/{studio-njmg0e73.js → studio-rmn2dz59.js} +5 -5
- package/dist/chunks/{studio-wr0pq67n.js → studio-rr4xkv81.js} +4 -4
- package/dist/chunks/{studio-tj28pxa1.js → studio-semd1s8s.js} +4 -4
- package/dist/chunks/{studio-7w04zw8h.js → studio-st75kn99.js} +5 -5
- package/dist/chunks/{studio-dmymvk2j.js → studio-sxc0zvpt.js} +3 -3
- package/dist/chunks/studio-t1eedsyx.js +4 -0
- package/dist/chunks/{studio-9tkaykt5.js → studio-tmeq6mjb.js} +4 -4
- package/dist/chunks/{studio-153mwkyq.js → studio-tp642n5g.js} +4 -4
- package/dist/chunks/{studio-t0v47dpc.js → studio-tzegmy4w.js} +4 -4
- package/dist/chunks/studio-vavmwshr.js +5 -0
- package/dist/chunks/studio-x5g1xk77.js +5 -0
- package/dist/chunks/studio-x92tgnee.js +64 -0
- package/dist/chunks/{studio-4yztve2t.js → studio-xg1942as.js} +4 -4
- package/dist/chunks/{studio-k9pwvmz8.js → studio-xkfs0dp2.js} +4 -4
- package/dist/chunks/{studio-6k1eswp9.js → studio-y7jehjrp.js} +4 -4
- package/dist/chunks/{studio-81twpmzj.js → studio-yr04mv2j.js} +4 -4
- package/dist/chunks/{studio-gj4j9e7k.js → studio-zd4kayn2.js} +4 -4
- package/dist/chunks/{studio-jsr14n1s.js → studio-zev8ssp1.js} +4 -4
- package/dist/chunks/{studio-r7yeeqc4.js → studio-ztvcxm3w.js} +4 -4
- package/dist/chunks/{tsMode-eq26t9gs.js → tsMode-k9s9fcag.js} +4 -4
- package/dist/chunks/workspace-5vmgsagj.js +4 -0
- package/dist/chunks/{ws-client-3gxqp06s.js → ws-client-0ja08haa.js} +3 -3
- package/dist/iframe-entry.js +3 -3
- package/dist/manifest.json +62 -62
- package/dist/studio.js +95 -95
- package/package.json +12 -12
- package/src/canvas/stage-scrollbar.ts +7 -7
- package/src/files/file-ops.ts +5 -8
- 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/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/studio.ts +2 -5
- package/src/surfaces/block-action-bar.json +6 -1
- 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/ui/schema-form.ts +6 -2
- package/dist/chunks/adapter-6zhjbpmd.js +0 -4
- package/dist/chunks/cf-settings-fpa485q3.js +0 -4
- package/dist/chunks/components-b0vh4sq8.js +0 -4
- package/dist/chunks/content-source-z5t0d0h9.js +0 -4
- package/dist/chunks/editor-7e23n4a8.js +0 -4
- package/dist/chunks/entry-commands-5cm6aba2.js +0 -4
- package/dist/chunks/file-ops-hv7gf757.js +0 -4
- package/dist/chunks/files-kb0epdxf.js +0 -4
- package/dist/chunks/format-host-eg1wy2bp.js +0 -4
- package/dist/chunks/fs-events-ky3vxfmh.js +0 -4
- package/dist/chunks/git-panel-yyg3bzp2.js +0 -4
- package/dist/chunks/iframe-host-9wdf0hes.js +0 -4
- package/dist/chunks/index-2tf0m2ne.js +0 -4
- package/dist/chunks/jx-validate-7w11db9v.js +0 -4
- package/dist/chunks/layers-7zasrpse.js +0 -4
- package/dist/chunks/layers-panel-9nzc4fdq.js +0 -4
- package/dist/chunks/lit-html-5qr7nbng.js +0 -4
- package/dist/chunks/media-picker-maevn9d4.js +0 -4
- package/dist/chunks/menu-wq6q0v3y.js +0 -4
- package/dist/chunks/monaco-buffer-9ry2rn5x.js +0 -4
- package/dist/chunks/notify-g91gjd6w.js +0 -4
- package/dist/chunks/platform-jdwjp2vt.js +0 -4
- package/dist/chunks/publish-panel-8yym447p.js +0 -4
- package/dist/chunks/quick-search-9bzm9kh3.js +0 -4
- package/dist/chunks/references-znssfqvq.js +0 -4
- package/dist/chunks/right-panel-czbzktm8.js +0 -4
- package/dist/chunks/section-registry-0rtq73qs.js +0 -4
- package/dist/chunks/studio-0mm38y62.js +0 -5
- package/dist/chunks/studio-27tc8q0h.js +0 -5
- package/dist/chunks/studio-7djymzt2.js +0 -5
- package/dist/chunks/studio-ant1zbp3.js +0 -5
- package/dist/chunks/studio-b29h7g7s.js +0 -5
- package/dist/chunks/studio-eb5sty2k.js +0 -5
- package/dist/chunks/studio-frv7vjhw.js +0 -5
- package/dist/chunks/studio-mb8jhtr4.js +0 -64
- package/dist/chunks/studio-nvykrgbj.js +0 -4
- package/dist/chunks/studio-nw4q9sdn.js +0 -5
- package/dist/chunks/studio-p6avneaq.js +0 -5
- package/dist/chunks/studio-vz7b43ng.js +0 -5
- package/dist/chunks/workspace-emt8454d.js +0 -4
package/src/panels/pane-grid.ts
CHANGED
|
@@ -5,27 +5,28 @@
|
|
|
5
5
|
* `workspace.panes` has been a real data model since the shell redesign's chrome phase: an ordered
|
|
6
6
|
* list, a focused id, a split, an unsplit. Nothing drew it. `index.html` declared ONE each of
|
|
7
7
|
* `#tab-strip`, `#jump-bar`, `#pane-chrome` and `#canvas-wrap` as flat siblings in `#app`'s grid —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* surfaces that belong to a PANE, laid out as rows and columns of the APPLICATION — so the shell
|
|
9
|
+
* could model two panes and had somewhere to put exactly one. §18.3's stage handover was the
|
|
10
10
|
* workaround: one stage, taken by whichever pane had focus, releasing the loser's artboards on the
|
|
11
11
|
* way past. This module is what that scaffolding was standing in for, and the handover is deleted
|
|
12
12
|
* with it.
|
|
13
13
|
*
|
|
14
14
|
* **The document owns the frame; this module owns the decisions.** `surfaces/pane-grid.json` is the
|
|
15
|
-
* cells, the
|
|
15
|
+
* cells, the four boxes inside each one, the splitter between two of them and every rule that lays
|
|
16
16
|
* them out; `surfaces/pane-grid.ts` is the mount. What is left here is the flow: which panes there
|
|
17
17
|
* are, what a cell's stage is furnished with, in what ORDER a departing cell is taken apart, and
|
|
18
18
|
* what the grid's own tracks are. Everything a cell CONTAINS still arrives through the module that
|
|
19
19
|
* owns it — `canvas/canvas-render.ts` is the render root of the stage and this document puts
|
|
20
|
-
* nothing inside it, `panels/
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* nothing inside it, `panels/pane-context.ts` is handed the chrome box, `panels/tab-strip.ts` is
|
|
21
|
+
* handed the strip, and this module makes the fourth — the right-edge drop zone — a pragmatic
|
|
22
|
+
* target itself. (The jump bar had a box here too, until it moved to the status bar: a pane's
|
|
23
|
+
* address is now the focused pane's breadcrumb trail along the foot of the window.)
|
|
23
24
|
*
|
|
24
25
|
* **Three properties survived the conversion, and each is structural rather than remembered:**
|
|
25
26
|
*
|
|
26
27
|
* 1. _A pane is complete before it is published._ The runtime builds a `$map` row inside its own
|
|
27
|
-
* effect scope and inserts it afterwards, so the
|
|
28
|
-
*
|
|
28
|
+
* effect scope and inserts it afterwards, so the `cellPart` calls below — the surface record,
|
|
29
|
+
* the gestures, the context bar's mount, the pane-focus listener, the drop zone's own
|
|
29
30
|
* registration — all happen while the cell is still detached. There is no frame in which a cell
|
|
30
31
|
* exists with no stage inside it.
|
|
31
32
|
* 2. _A pane's node is never re-parented._ The repeater is keyed on the pane id and the runtime's
|
|
@@ -81,7 +82,6 @@ import { releaseCanvasHosts } from "../canvas/iframe-host";
|
|
|
81
82
|
import { installStageGestures } from "../editor/shortcuts";
|
|
82
83
|
import { scheduleCanvasRender } from "../canvas/canvas-render";
|
|
83
84
|
import { paneRegion, paneStripRegion } from "../ui/regions";
|
|
84
|
-
import { attachJumpBarHost } from "./jump-bar";
|
|
85
85
|
import { attachPaneChromeHost } from "./pane-context";
|
|
86
86
|
import { MAX_PANES, focusPane, workspace } from "../workspace/workspace";
|
|
87
87
|
import { mountPaneGridSurface } from "../surfaces/pane-grid";
|
|
@@ -89,12 +89,11 @@ import type { EffectScope } from "@vue/reactivity";
|
|
|
89
89
|
import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
90
90
|
import type { PaneCellPart, PaneGridRow, PaneGridSurface } from "../surfaces/pane-grid";
|
|
91
91
|
|
|
92
|
-
/** One drawn pane: its root and the
|
|
92
|
+
/** One drawn pane: its root and the four surfaces inside it. */
|
|
93
93
|
export interface PaneCell {
|
|
94
94
|
paneId: string;
|
|
95
95
|
root: HTMLElement;
|
|
96
96
|
strip: HTMLElement;
|
|
97
|
-
jump: HTMLElement;
|
|
98
97
|
chrome: HTMLElement;
|
|
99
98
|
stage: HTMLElement;
|
|
100
99
|
dropZone: HTMLElement;
|
|
@@ -162,7 +161,6 @@ function cellState(paneId: string): CellState {
|
|
|
162
161
|
const cell: CellState = {
|
|
163
162
|
chrome: null as unknown as HTMLElement,
|
|
164
163
|
dropZone: null as unknown as HTMLElement,
|
|
165
|
-
jump: null as unknown as HTMLElement,
|
|
166
164
|
paneId,
|
|
167
165
|
releaseDropZone: null,
|
|
168
166
|
releaseGestures: null,
|
|
@@ -283,15 +281,15 @@ function attachDropZone(paneId: string, element: HTMLElement): () => void {
|
|
|
283
281
|
}
|
|
284
282
|
|
|
285
283
|
/**
|
|
286
|
-
* One of a cell's
|
|
284
|
+
* One of a cell's five boxes has been created. Fill the record in, and wire what belongs to it.
|
|
287
285
|
*
|
|
288
|
-
* The
|
|
289
|
-
* `panels/frontmatter-panel.ts` is handed the stage.
|
|
286
|
+
* The context bar is HANDED its host rather than resolving a region, the same way
|
|
287
|
+
* `panels/frontmatter-panel.ts` is handed the stage. It cannot resolve one the way the tab strip
|
|
290
288
|
* does: the strip's host carries `pane.<id>/tabs` and nothing inside it re-stamps that id, while
|
|
291
|
-
* the
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
289
|
+
* the context bar stamps `pane.<id>/context` on markup it renders INSIDE its box — so a region on
|
|
290
|
+
* the wrapper as well would put the same id on two nested elements, and `resolveRegion` takes the
|
|
291
|
+
* LAST match. Sixty shots crop `pane.primary/context`; a second, larger element carrying it is a
|
|
292
|
+
* silently widened crop.
|
|
295
293
|
*/
|
|
296
294
|
function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): void {
|
|
297
295
|
const cell = cellState(paneId);
|
|
@@ -304,11 +302,6 @@ function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): voi
|
|
|
304
302
|
cell.strip = element;
|
|
305
303
|
return;
|
|
306
304
|
}
|
|
307
|
-
if (part === "jump") {
|
|
308
|
-
cell.jump = element;
|
|
309
|
-
attachJumpBarHost(paneId, element);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
305
|
if (part === "chrome") {
|
|
313
306
|
cell.chrome = element;
|
|
314
307
|
attachPaneChromeHost(paneId, element);
|
|
@@ -333,7 +326,7 @@ function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): voi
|
|
|
333
326
|
*
|
|
334
327
|
* The order is the document's own, and each step needs the one before it:
|
|
335
328
|
*
|
|
336
|
-
* - The
|
|
329
|
+
* - The context bar goes first, disposing a mount whose DOM is inside this cell — the runtime that
|
|
337
330
|
* owns it is about to be unreachable.
|
|
338
331
|
* - `releaseCanvasHosts` goes before `disposePaneSurface` and while `stage` still CONTAINS its
|
|
339
332
|
* frames, which is the whole reason it can find them. A frame released later is noticed only by
|
|
@@ -345,7 +338,6 @@ function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): voi
|
|
|
345
338
|
function disposeCell(cell: CellState): void {
|
|
346
339
|
cell.releaseDropZone?.();
|
|
347
340
|
cell.releaseDropZone = null;
|
|
348
|
-
attachJumpBarHost(cell.paneId, null);
|
|
349
341
|
attachPaneChromeHost(cell.paneId, null);
|
|
350
342
|
cell.releaseGestures?.();
|
|
351
343
|
cell.releaseGestures = null;
|
|
@@ -813,7 +813,7 @@ function buttonGroup(
|
|
|
813
813
|
return {
|
|
814
814
|
icon,
|
|
815
815
|
row: rowKey,
|
|
816
|
-
|
|
816
|
+
checked: v === value ? ("true" as const) : ("false" as const),
|
|
817
817
|
text: icon === "" ? v : "",
|
|
818
818
|
title: v,
|
|
819
819
|
value: v,
|
|
@@ -2112,7 +2112,7 @@ function rememberButtons(view: StylePanelView): void {
|
|
|
2112
2112
|
for (const section of view.sections) {
|
|
2113
2113
|
for (const row of section.rows) {
|
|
2114
2114
|
if (row.widget === "buttons") {
|
|
2115
|
-
_buttonValues.set(row.key, row.buttons.find((b) => b.
|
|
2115
|
+
_buttonValues.set(row.key, row.buttons.find((b) => b.checked === "true")?.value ?? "");
|
|
2116
2116
|
}
|
|
2117
2117
|
}
|
|
2118
2118
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-hooks.ts — what Studio does between the turn engine's steps.
|
|
3
|
+
*
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TurnHooks } from "@jxsuite/ai/harness";
|
|
8
|
+
import { ensureProxyProbe, resetModelCache } from "../ai-models";
|
|
9
|
+
|
|
10
|
+
/** What Studio's hooks act on, injected so the hooks hold no state of their own. */
|
|
11
|
+
export interface StudioTurnHookDeps {
|
|
12
|
+
/** Whether the conversation the turn answers has left the transcript (another chat was opened). */
|
|
13
|
+
superseded: () => boolean;
|
|
14
|
+
/** Called once a call settles into a transcript that replaced the turn's own. */
|
|
15
|
+
onSuperseded: () => void;
|
|
16
|
+
/** Re-anchor the undo batch on the document the tools now edit (specs/ai.md §3.3). */
|
|
17
|
+
reanchor: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Studio's turn hooks.
|
|
22
|
+
*
|
|
23
|
+
* - **After each call**, a turn whose conversation has been replaced stops there: its reply belongs
|
|
24
|
+
* to a request that is no longer on screen, and writing it would land a stray `tool` message in
|
|
25
|
+
* the other conversation. Otherwise the undo batch follows the document the tools moved to, which
|
|
26
|
+
* a tool, project adoption or the author clicking another tab can each change.
|
|
27
|
+
* - **On a stream error**, a lapsed hosted grant (`cf_reconnect_required`) drops the model reading
|
|
28
|
+
* and re-probes (specs/ai.md §2.1): the probe settles once at boot, so without this every gate
|
|
29
|
+
* keeps offering an assistant that cannot answer, and the failed send is the only evidence.
|
|
30
|
+
*
|
|
31
|
+
* @param {StudioTurnHookDeps} deps
|
|
32
|
+
* @returns {TurnHooks}
|
|
33
|
+
*/
|
|
34
|
+
export function studioTurnHooks(deps: StudioTurnHookDeps): TurnHooks {
|
|
35
|
+
return {
|
|
36
|
+
afterTool() {
|
|
37
|
+
if (deps.superseded()) {
|
|
38
|
+
deps.onSuperseded();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
deps.reanchor();
|
|
42
|
+
},
|
|
43
|
+
onStreamError(error) {
|
|
44
|
+
if (error.code === "cf_reconnect_required") {
|
|
45
|
+
resetModelCache();
|
|
46
|
+
ensureProxyProbe();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|