@jxsuite/studio 2.4.3 → 4.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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
|
@@ -198,3 +198,92 @@ export function trimContext(
|
|
|
198
198
|
|
|
199
199
|
return { estimatedTokens: newTotal, droppedCount };
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
// ─── Orphaned tool calls ────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** What a synthesized result says, so a reader and the model see the same reason. */
|
|
205
|
+
const UNANSWERED_TOOL_RESULT = JSON.stringify({
|
|
206
|
+
success: false,
|
|
207
|
+
error:
|
|
208
|
+
"This tool call was never completed — the session was reloaded or the history was trimmed.",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Repair tool-call pairing in place, and report how many messages it touched.
|
|
213
|
+
*
|
|
214
|
+
* `toMessagesArray()` serializes the array verbatim: an assistant message with `tool_calls` becomes
|
|
215
|
+
* a `tool_calls` request, and a `tool` message becomes a reply carrying `tool_call_id`. Providers
|
|
216
|
+
* require the two to come in pairs, and **three ordinary things break the pairing**:
|
|
217
|
+
*
|
|
218
|
+
* 1. `saveSession` persists `msgs.slice(-MAX_PERSIST_MESSAGES)` and `restoreChat` pushes the result
|
|
219
|
+
* back, so a session longer than the cap can be restored starting mid-pair.
|
|
220
|
+
* 2. {@link trimContext} splices from the front for the token budget, with no more regard for pairs.
|
|
221
|
+
* 3. A turn that stopped while a tool was still running — which `ask_user` makes routine, because its
|
|
222
|
+
* tool is _designed_ to be outstanding — leaves a request with no reply.
|
|
223
|
+
*
|
|
224
|
+
* Each of those produced a 400 on the NEXT send, from a conversation the user could see was fine.
|
|
225
|
+
* So the repair belongs on the send path, after trimming, rather than at any one of the three
|
|
226
|
+
* sites: a request with no reply gets a synthesized failure (keeping the assistant's text, which
|
|
227
|
+
* dropping the message would lose), and a reply with no request is dropped.
|
|
228
|
+
*
|
|
229
|
+
* @param {ReturnType<typeof createChatState>} chatState
|
|
230
|
+
* @returns {{ sealed: number; dropped: number }}
|
|
231
|
+
*/
|
|
232
|
+
export function pruneOrphanToolMessages(chatState: ReturnType<typeof createChatState>): {
|
|
233
|
+
sealed: number;
|
|
234
|
+
dropped: number;
|
|
235
|
+
} {
|
|
236
|
+
const { messages } = chatState;
|
|
237
|
+
|
|
238
|
+
const requested = new Set<string>();
|
|
239
|
+
const answered = new Set<string>();
|
|
240
|
+
for (const msg of messages) {
|
|
241
|
+
if (msg.role === "assistant" && msg.toolCalls) {
|
|
242
|
+
for (const call of msg.toolCalls) {
|
|
243
|
+
requested.add(call.id);
|
|
244
|
+
}
|
|
245
|
+
} else if (msg.role === "tool" && msg.toolCallId) {
|
|
246
|
+
answered.add(msg.toolCallId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Replies with no request, and replies with no id at all — neither can be serialized into a
|
|
251
|
+
// Well-formed pair, and a reply is the half that carries no author text worth keeping.
|
|
252
|
+
let dropped = 0;
|
|
253
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
254
|
+
const msg = messages[i]!;
|
|
255
|
+
if (msg.role === "tool" && (!msg.toolCallId || !requested.has(msg.toolCallId))) {
|
|
256
|
+
messages.splice(i, 1);
|
|
257
|
+
dropped += 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Requests with no reply. Walk backwards so each insertion is past the indices still to visit,
|
|
262
|
+
and seal immediately after the requesting message — `toMessagesArray` emits in array order, so
|
|
263
|
+
that is the only position the pair is adjacent in. */
|
|
264
|
+
let sealed = 0;
|
|
265
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
266
|
+
const msg = messages[i]!;
|
|
267
|
+
if (msg.role !== "assistant" || !msg.toolCalls) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const missing = msg.toolCalls.filter((call) => !answered.has(call.id));
|
|
271
|
+
if (missing.length === 0) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
messages.splice(
|
|
275
|
+
i + 1,
|
|
276
|
+
0,
|
|
277
|
+
...missing.map((call, n) => ({
|
|
278
|
+
content: UNANSWERED_TOOL_RESULT,
|
|
279
|
+
id: `sealed_${call.id}_${n}`,
|
|
280
|
+
role: "tool" as const,
|
|
281
|
+
timestamp: msg.timestamp,
|
|
282
|
+
toolCallId: call.id,
|
|
283
|
+
})),
|
|
284
|
+
);
|
|
285
|
+
sealed += missing.length;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { dropped, sealed };
|
|
289
|
+
}
|
|
@@ -20,14 +20,18 @@ import type { Tab } from "../tabs/tab";
|
|
|
20
20
|
import { componentRegistry } from "../files/components";
|
|
21
21
|
import { activeRegistry } from "../commands/active-registry";
|
|
22
22
|
import { registerAiTools } from "./ai-tools";
|
|
23
|
+
import { cancelAsk, registerAskTool, resetAsk } from "./ai-ask";
|
|
23
24
|
import { registerProjectTools } from "./ai-project-tools";
|
|
25
|
+
import { registerImportTools, resetImportGuard } from "./ai-import-tools";
|
|
24
26
|
import { createGatedToolRegistry } from "./gated-registry";
|
|
25
27
|
import type { ToolAvailability } from "./gated-registry";
|
|
26
28
|
import { adoptProject } from "./project-adoption";
|
|
29
|
+
import { abortImportRun, resetImportRuns } from "./import-run";
|
|
27
30
|
import { runAgentLoop } from "./tool-executor";
|
|
28
|
-
import { AI_TOOL_TIERS, buildSystemPrompt,
|
|
29
|
-
import { getBaseUrl,
|
|
30
|
-
import {
|
|
31
|
+
import { AI_TOOL_TIERS, buildSystemPrompt, toolActive } from "./ai-system-prompt";
|
|
32
|
+
import { getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
33
|
+
import { preferredModel } from "./ai-models";
|
|
34
|
+
import { pruneOrphanToolMessages, trimContext } from "./context-manager";
|
|
31
35
|
import { renderCheck } from "./render-critic";
|
|
32
36
|
import { openFileInTab, reloadFileInTab } from "../files/files";
|
|
33
37
|
import { refreshExtensionUi } from "../format/format-host";
|
|
@@ -58,7 +62,7 @@ function projectRoot() {
|
|
|
58
62
|
* }}
|
|
59
63
|
*/
|
|
60
64
|
export function createDocumentAssistant() {
|
|
61
|
-
const chatState = createChatState({ model:
|
|
65
|
+
const chatState = createChatState({ model: preferredModel() });
|
|
62
66
|
|
|
63
67
|
/** The persisted session backing the live chat; null = fresh unsaved chat. */
|
|
64
68
|
let sessionId: string | null = null;
|
|
@@ -76,6 +80,8 @@ export function createDocumentAssistant() {
|
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
const innerRegistry = createToolRegistry();
|
|
83
|
+
// Ungated: a question is not a function of what happens to be open.
|
|
84
|
+
registerAskTool(innerRegistry);
|
|
79
85
|
registerAiTools(innerRegistry, {
|
|
80
86
|
getTab: () => activeTab.value,
|
|
81
87
|
saveFile: async (relPath: string, content: string) => {
|
|
@@ -90,6 +96,17 @@ export function createDocumentAssistant() {
|
|
|
90
96
|
findOpenTab,
|
|
91
97
|
reloadTab: reloadFileInTab,
|
|
92
98
|
});
|
|
99
|
+
registerImportTools(innerRegistry, {
|
|
100
|
+
adoptProject,
|
|
101
|
+
getTab: () => activeTab.value,
|
|
102
|
+
// Same re-keying as `create_project`: an import bootstraps a project, so the bootstrap
|
|
103
|
+
// Conversation has to follow it out of the unscoped store.
|
|
104
|
+
onProjectAdopted: (root: string) => {
|
|
105
|
+
if (sessionId) {
|
|
106
|
+
sessionStore.moveSession("", root, sessionId);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
93
110
|
registerProjectTools(innerRegistry, {
|
|
94
111
|
getTab: () => activeTab.value,
|
|
95
112
|
renderCheck: renderCheck as (
|
|
@@ -125,6 +142,9 @@ export function createDocumentAssistant() {
|
|
|
125
142
|
* every agent-loop round, so a mid-loop create_project unlocks the higher tiers immediately.
|
|
126
143
|
*/
|
|
127
144
|
const TIER_REQUIREMENTS = {
|
|
145
|
+
/* Never refused, so the sentence is unreachable — it exists because the map is derived from
|
|
146
|
+
the tier union, and a tier without one would not compile. */
|
|
147
|
+
always: "nothing",
|
|
128
148
|
"no-project": "no project to be open (it bootstraps one)",
|
|
129
149
|
project: "an open project",
|
|
130
150
|
document: "an open document (use open_document first)",
|
|
@@ -154,12 +174,18 @@ export function createDocumentAssistant() {
|
|
|
154
174
|
t.name,
|
|
155
175
|
{
|
|
156
176
|
when: () =>
|
|
157
|
-
|
|
177
|
+
toolActive(t, {
|
|
178
|
+
/* The platform's own answer, read live rather than captured: the assistant is
|
|
179
|
+
constructed at module load, before a platform is registered in some hosts. */
|
|
180
|
+
canImport: Boolean(getPlatform().importSite),
|
|
158
181
|
hasDocument: Boolean(activeTab.value),
|
|
159
182
|
hasProject: Boolean(workspace.projectRoot),
|
|
160
183
|
treeEditable: treeEditable(),
|
|
161
184
|
}),
|
|
162
|
-
requires:
|
|
185
|
+
requires:
|
|
186
|
+
t.capability === "importSite"
|
|
187
|
+
? "a platform with a site-import backend"
|
|
188
|
+
: TIER_REQUIREMENTS[t.tier],
|
|
163
189
|
},
|
|
164
190
|
]),
|
|
165
191
|
);
|
|
@@ -181,6 +207,7 @@ export function createDocumentAssistant() {
|
|
|
181
207
|
components: componentRegistry.length > 0 ? componentRegistry : undefined,
|
|
182
208
|
projectRoot: workspace.projectRoot || undefined,
|
|
183
209
|
hasProject: Boolean(workspace.projectRoot),
|
|
210
|
+
canImport: Boolean(getPlatform().importSite),
|
|
184
211
|
...(inventory && inventory.length > 0 ? { fileInventory: inventory } : {}),
|
|
185
212
|
});
|
|
186
213
|
}
|
|
@@ -204,6 +231,11 @@ export function createDocumentAssistant() {
|
|
|
204
231
|
chatState.setTokenCount(trimmed.estimatedTokens);
|
|
205
232
|
}
|
|
206
233
|
|
|
234
|
+
/* AFTER the trim, not before: the trim splices from the front and is itself one of the three
|
|
235
|
+
ways a tool_calls request loses its reply (see pruneOrphanToolMessages). Repairing first
|
|
236
|
+
would leave the very pair the trim then broke. */
|
|
237
|
+
pruneOrphanToolMessages(chatState);
|
|
238
|
+
|
|
207
239
|
// Persist after trimming so the saved history reflects what's actually sent.
|
|
208
240
|
persistChat();
|
|
209
241
|
|
|
@@ -212,7 +244,7 @@ export function createDocumentAssistant() {
|
|
|
212
244
|
const chatUrl = await Promise.resolve(plat.aiChatUrl());
|
|
213
245
|
// Re-read the persisted model each send: the session is constructed once at module load
|
|
214
246
|
// (before the user sets a key/model), so the picker's choice must be picked up here.
|
|
215
|
-
chatState.setModel(
|
|
247
|
+
chatState.setModel(preferredModel());
|
|
216
248
|
const streamingClient = createProxyStreamingClient({
|
|
217
249
|
chatUrl,
|
|
218
250
|
model: chatState.model,
|
|
@@ -247,11 +279,19 @@ export function createDocumentAssistant() {
|
|
|
247
279
|
|
|
248
280
|
function stop() {
|
|
249
281
|
controller?.abort();
|
|
282
|
+
/* The abort settles an outstanding question through the turn signal, but only when the turn
|
|
283
|
+
HAS one — the evals runner and several tests drive the loop without. Settling here as well
|
|
284
|
+
is what makes Stop terminal in every case rather than in most of them. */
|
|
285
|
+
cancelAsk();
|
|
286
|
+
abortImportRun();
|
|
250
287
|
chatState.cancelStream();
|
|
251
288
|
}
|
|
252
289
|
|
|
253
290
|
function newChat() {
|
|
254
291
|
stop();
|
|
292
|
+
resetAsk();
|
|
293
|
+
resetImportRuns();
|
|
294
|
+
resetImportGuard();
|
|
255
295
|
chatState.clearChat();
|
|
256
296
|
sessionId = null;
|
|
257
297
|
sessionStore.setActiveSession(projectRoot(), null);
|
|
@@ -271,6 +311,7 @@ export function createDocumentAssistant() {
|
|
|
271
311
|
return;
|
|
272
312
|
}
|
|
273
313
|
stop();
|
|
314
|
+
resetAsk();
|
|
274
315
|
chatState.clearChat();
|
|
275
316
|
pushRestoredMessages(msgs);
|
|
276
317
|
sessionId = id;
|
|
@@ -282,6 +323,7 @@ export function createDocumentAssistant() {
|
|
|
282
323
|
sessionStore.deleteSession(projectRoot(), id);
|
|
283
324
|
if (sessionId === id) {
|
|
284
325
|
stop();
|
|
326
|
+
resetAsk();
|
|
285
327
|
chatState.clearChat();
|
|
286
328
|
sessionId = null;
|
|
287
329
|
}
|
package/src/services/idle.ts
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
* duplicate of `platform`: an activity spans a whole operation (an install, a deploy, a clone)
|
|
28
28
|
* across many PAL calls, so the gaps between them look quiet at the seam while the operation is
|
|
29
29
|
* plainly mid-flight. A finished entry never blocks — the list stays on screen forever.
|
|
30
|
+
* 7. `grid-idle.gridIdleBlockers()` — tables still building, or built but not yet showing the
|
|
31
|
+
* selection range `selectableRange: 1` gives them. Nothing else covered Tabulator: a grid
|
|
32
|
+
* command resolves when the panel mounts, which is several frames before the table is drawn.
|
|
30
33
|
*
|
|
31
34
|
* The consumers are the `packages/studio:verify` skill, the screenshot runner and P4.2's Activity
|
|
32
35
|
* tracker (this is a read-only projection of the same in-flight set that dock renders).
|
|
@@ -38,6 +41,7 @@ import { pendingSchedulers } from "../panels/panel-scheduler";
|
|
|
38
41
|
import { canvasIdleBlockers } from "../canvas/iframe-host";
|
|
39
42
|
import { platformInFlight } from "../platform";
|
|
40
43
|
import { overlayIdleBlockers } from "../ui/layers";
|
|
44
|
+
import { gridIdleBlockers } from "../grid/grid-idle";
|
|
41
45
|
|
|
42
46
|
/** One subsystem that can be outstanding, and its account of why. */
|
|
43
47
|
export interface IdleSource {
|
|
@@ -69,7 +73,7 @@ export class NotIdleError extends Error {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* The
|
|
76
|
+
* The seven live sources.
|
|
73
77
|
*
|
|
74
78
|
* Built per call rather than held in a module constant so a test can substitute one without
|
|
75
79
|
* unpicking the others, and so a second window's hosts are never read through the first window's
|
|
@@ -101,6 +105,7 @@ export function defaultIdleSources(): IdleSource[] {
|
|
|
101
105
|
},
|
|
102
106
|
{ blockers: overlayIdleBlockers, name: "overlay" },
|
|
103
107
|
{ blockers: activityIdleBlockers, name: "activity" },
|
|
108
|
+
{ blockers: gridIdleBlockers, name: "grid" },
|
|
104
109
|
];
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
* streaming fetch to some URL" — the dev server posts to its own origin, the desktop platforms post
|
|
4
4
|
* to their token-gated loopback servers — so the request/parse/settle logic lives here once.
|
|
5
5
|
*
|
|
6
|
-
* The endpoint emits one JSON object per line: `progress` lines (forwarded to onProgress),
|
|
7
|
-
* `
|
|
6
|
+
* The endpoint emits one JSON object per line: `progress` lines (forwarded to onProgress), a
|
|
7
|
+
* `ready` line the moment the destination is an openable project (forwarded to onReady),
|
|
8
|
+
* `heartbeat` keep-alives (ignored), and a terminal `done` ({root, config, result}) or `error`
|
|
9
|
+
* line. `result` is optional — a backend that does not send one is not broken, and every caller
|
|
10
|
+
* treats its absence as "the run happened, and it did not say what it found". `ready` is optional
|
|
11
|
+
* for the same reason: an older backend simply never sends one, and the caller opens the project at
|
|
12
|
+
* the end as it always did.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
ImportProgressEvent,
|
|
17
|
+
ImportReadyEvent,
|
|
18
|
+
ImportSiteOptions,
|
|
19
|
+
ImportSiteSummary,
|
|
20
|
+
} from "../types";
|
|
11
21
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
12
22
|
|
|
13
23
|
/**
|
|
@@ -27,6 +37,7 @@ interface StreamLine {
|
|
|
27
37
|
total?: number;
|
|
28
38
|
root?: string;
|
|
29
39
|
config?: ProjectConfig;
|
|
40
|
+
result?: ImportSiteSummary;
|
|
30
41
|
error?: string;
|
|
31
42
|
}
|
|
32
43
|
|
|
@@ -37,13 +48,17 @@ interface StreamLine {
|
|
|
37
48
|
* @param {ImportSiteOptions} opts — the import request; `apiKey`/`baseUrl` travel as headers
|
|
38
49
|
* @param {(evt: ImportProgressEvent) => void} onProgress
|
|
39
50
|
* @param {AbortSignal} [signal]
|
|
51
|
+
* @param {(evt: ImportReadyEvent) => void} [onReady] — fires once, as soon as the destination holds
|
|
52
|
+
* an openable project. Minutes before `done` on a real crawl, which is the whole reason it
|
|
53
|
+
* exists.
|
|
40
54
|
*/
|
|
41
55
|
export async function streamImport(
|
|
42
56
|
endpoint: string,
|
|
43
57
|
opts: ImportSiteOptions,
|
|
44
58
|
onProgress: (evt: ImportProgressEvent) => void,
|
|
45
59
|
signal?: AbortSignal,
|
|
46
|
-
|
|
60
|
+
onReady?: (evt: ImportReadyEvent) => void,
|
|
61
|
+
): Promise<{ root: string; config: ProjectConfig; result?: ImportSiteSummary }> {
|
|
47
62
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
48
63
|
if (opts.apiKey) {
|
|
49
64
|
headers["X-Api-Key"] = opts.apiKey;
|
|
@@ -59,7 +74,13 @@ export async function streamImport(
|
|
|
59
74
|
depth: opts.depth,
|
|
60
75
|
maxPages: opts.maxPages,
|
|
61
76
|
aiComponents: opts.aiComponents,
|
|
77
|
+
...(opts.breakpoints === undefined ? {} : { breakpoints: opts.breakpoints }),
|
|
62
78
|
...(opts.model === undefined ? {} : { aiModel: opts.model }),
|
|
79
|
+
...(opts.verify === undefined ? {} : { verify: opts.verify }),
|
|
80
|
+
...(opts.verifyThreshold === undefined ? {} : { verifyThreshold: opts.verifyThreshold }),
|
|
81
|
+
...(opts.verifyMinFidelity === undefined
|
|
82
|
+
? {}
|
|
83
|
+
: { verifyMinFidelity: opts.verifyMinFidelity }),
|
|
63
84
|
}),
|
|
64
85
|
headers,
|
|
65
86
|
method: "POST",
|
|
@@ -85,7 +106,7 @@ export async function streamImport(
|
|
|
85
106
|
const reader = res.body.getReader();
|
|
86
107
|
const decoder = new TextDecoder();
|
|
87
108
|
let buffer = "";
|
|
88
|
-
let result: { root: string; config: ProjectConfig } | null = null;
|
|
109
|
+
let result: { root: string; config: ProjectConfig; result?: ImportSiteSummary } | null = null;
|
|
89
110
|
/*
|
|
90
111
|
* Lines the parser could not read. Tolerating one is right — an import that ran for two minutes
|
|
91
112
|
* should not die on a garbled progress line — but doing it *silently* is not: the import then
|
|
@@ -113,8 +134,14 @@ export async function streamImport(
|
|
|
113
134
|
...(parsed.current === undefined ? {} : { current: parsed.current }),
|
|
114
135
|
...(parsed.total === undefined ? {} : { total: parsed.total }),
|
|
115
136
|
});
|
|
137
|
+
} else if (parsed.type === "ready" && parsed.root !== undefined) {
|
|
138
|
+
onReady?.({ root: parsed.root });
|
|
116
139
|
} else if (parsed.type === "done" && parsed.root !== undefined && parsed.config) {
|
|
117
|
-
result = {
|
|
140
|
+
result = {
|
|
141
|
+
root: parsed.root,
|
|
142
|
+
config: parsed.config,
|
|
143
|
+
...(parsed.result === undefined ? {} : { result: parsed.result }),
|
|
144
|
+
};
|
|
118
145
|
} else if (parsed.type === "error") {
|
|
119
146
|
throw new Error(parsed.error || "Import failed");
|
|
120
147
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import-run.ts — the live state of a site import, for the transcript to draw.
|
|
3
|
+
*
|
|
4
|
+
* An import takes minutes and reports a line at a time. Those lines used to render into the New
|
|
5
|
+
* Project modal and die with it: a successful import destroyed its own account of what it had done
|
|
6
|
+
* at the moment it handed off. Now the run belongs to a tool call, so the record is keyed by that
|
|
7
|
+
* call's id and the chip and its progress are one thing rather than two.
|
|
8
|
+
*
|
|
9
|
+
* Reactive, so `panels/ai-panel.ts`'s watcher repaints on it through the same rAF coalescer that
|
|
10
|
+
* carries streaming tokens — a crawl emitting a line per page cannot outrun the frame rate.
|
|
11
|
+
*
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { reactive } from "../reactivity";
|
|
16
|
+
|
|
17
|
+
import type { ImportProgressEvent } from "../types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lines kept per run.
|
|
21
|
+
*
|
|
22
|
+
* A wide crawl emits one per page plus one per asset batch, and every line is also held in the
|
|
23
|
+
* session's localStorage budget once the tool result lands — so there is a ceiling, and it is here.
|
|
24
|
+
* It was 200 back when the view showed the last SIX of them and the rest existed only to be
|
|
25
|
+
* summarised; the transcript now renders the whole log in a scroller the reader owns, so the number
|
|
26
|
+
* that matters is how far back they can scroll.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_LOG_LINES = 500;
|
|
29
|
+
|
|
30
|
+
export interface ImportRunRecord {
|
|
31
|
+
/** The tool-call id — the join key with the chip the transcript draws. */
|
|
32
|
+
id: string;
|
|
33
|
+
url: string;
|
|
34
|
+
directory: string;
|
|
35
|
+
phase: string;
|
|
36
|
+
message: string;
|
|
37
|
+
current: number | null;
|
|
38
|
+
total: number | null;
|
|
39
|
+
log: ImportProgressEvent[];
|
|
40
|
+
/** Lines the pipeline marked with `⚠`, kept apart so the summary can name them. */
|
|
41
|
+
warnings: string[];
|
|
42
|
+
status: "running" | "done" | "failed" | "stopped";
|
|
43
|
+
error: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One run at a time, because one project is created at a time and `import_site` is `no-project`
|
|
48
|
+
* tiered. Keyed rather than bare so a finished run still renders under its own chip after the next
|
|
49
|
+
* one starts, which is what makes the transcript readable after two attempts.
|
|
50
|
+
*/
|
|
51
|
+
const runs = reactive<{ records: ImportRunRecord[] }>({ records: [] });
|
|
52
|
+
|
|
53
|
+
/** Runs kept before the oldest is dropped. Two attempts and a retry is the realistic ceiling. */
|
|
54
|
+
const MAX_RUNS = 5;
|
|
55
|
+
|
|
56
|
+
/** Aborts the in-flight run. Owned here because `ToolRegistry.execute` cannot be handed a signal. */
|
|
57
|
+
let controller: AbortController | null = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Open a run record and return the signal its request should carry.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} id - The tool-call id.
|
|
63
|
+
* @param {{ url: string; directory: string }} init
|
|
64
|
+
* @returns {AbortSignal}
|
|
65
|
+
*/
|
|
66
|
+
export function beginImportRun(id: string, init: { url: string; directory: string }): AbortSignal {
|
|
67
|
+
controller?.abort();
|
|
68
|
+
controller = new AbortController();
|
|
69
|
+
runs.records.push({
|
|
70
|
+
current: null,
|
|
71
|
+
directory: init.directory,
|
|
72
|
+
error: "",
|
|
73
|
+
id,
|
|
74
|
+
log: [],
|
|
75
|
+
message: "Starting…",
|
|
76
|
+
phase: "start",
|
|
77
|
+
status: "running",
|
|
78
|
+
total: null,
|
|
79
|
+
url: init.url,
|
|
80
|
+
warnings: [],
|
|
81
|
+
});
|
|
82
|
+
if (runs.records.length > MAX_RUNS) {
|
|
83
|
+
runs.records.shift();
|
|
84
|
+
}
|
|
85
|
+
return controller.signal;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Record one progress line against a run.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} id
|
|
92
|
+
* @param {ImportProgressEvent} evt
|
|
93
|
+
*/
|
|
94
|
+
export function recordImportProgress(id: string, evt: ImportProgressEvent): void {
|
|
95
|
+
const record = runs.records.find((r) => r.id === id);
|
|
96
|
+
if (!record) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
record.phase = evt.phase;
|
|
100
|
+
record.message = evt.message;
|
|
101
|
+
record.current = evt.current ?? null;
|
|
102
|
+
record.total = evt.total ?? null;
|
|
103
|
+
record.log.push(evt);
|
|
104
|
+
if (record.log.length > MAX_LOG_LINES) {
|
|
105
|
+
record.log.shift();
|
|
106
|
+
}
|
|
107
|
+
/* The pipeline reports a soft failure by prefixing its progress line, and `ImportSiteResult`'s
|
|
108
|
+
own `warnings` array does not cross the HTTP boundary — so this is the only place they can be
|
|
109
|
+
recovered from, and the summary needs them to be honest about what was skipped. */
|
|
110
|
+
if (evt.message.startsWith("⚠")) {
|
|
111
|
+
record.warnings.push(evt.message.replace(/^⚠\s*/, ""));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Close a run.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} id
|
|
119
|
+
* @param {{ status: Exclude<ImportRunRecord["status"], "running">; error?: string }} outcome
|
|
120
|
+
*/
|
|
121
|
+
export function finishImportRun(
|
|
122
|
+
id: string,
|
|
123
|
+
outcome: { status: Exclude<ImportRunRecord["status"], "running">; error?: string },
|
|
124
|
+
): void {
|
|
125
|
+
const record = runs.records.find((r) => r.id === id);
|
|
126
|
+
if (record) {
|
|
127
|
+
record.status = outcome.status;
|
|
128
|
+
record.error = outcome.error ?? "";
|
|
129
|
+
}
|
|
130
|
+
controller = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The record for one tool call, or null. Reactive — read it inside an `effect()` to repaint on it.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} id
|
|
137
|
+
* @returns {ImportRunRecord | null}
|
|
138
|
+
*/
|
|
139
|
+
export function importRun(id: string): ImportRunRecord | null {
|
|
140
|
+
return runs.records.find((r) => r.id === id) ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The run still going, or null. What the panel's watcher tracks. */
|
|
144
|
+
export function activeImportRun(): ImportRunRecord | null {
|
|
145
|
+
return runs.records.find((r) => r.status === "running") ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Abort the in-flight run. Called from `assistant.stop`, which cannot reach the tool directly. */
|
|
149
|
+
export function abortImportRun(): void {
|
|
150
|
+
controller?.abort();
|
|
151
|
+
controller = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Drop every record. For New Chat and for tests. */
|
|
155
|
+
export function resetImportRuns(): void {
|
|
156
|
+
abortImportRun();
|
|
157
|
+
runs.records.length = 0;
|
|
158
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import-seed.ts — the New Project Import form's hand-off to the assistant.
|
|
3
|
+
*
|
|
4
|
+
* The wizard collects a URL, crawl options, a model and a brief, then closes; the assistant takes
|
|
5
|
+
* the turn and runs the import as a tool call. This holds what the form gathered for the one turn
|
|
6
|
+
* that consumes it.
|
|
7
|
+
*
|
|
8
|
+
* **Module state, and deliberately NOT a copy of `agent-seed.ts`.** That one uses localStorage keyed
|
|
9
|
+
* by project root because the project it seeds opens in a NEW WINDOW, and the key is the
|
|
10
|
+
* cross-window channel. Neither half of that applies here: the project does not exist yet at
|
|
11
|
+
* hand-off time, so there is no root to key on, and the assistant taking the turn is in this window
|
|
12
|
+
* — `import_site` is `no-project` tiered precisely because it runs before there is a project. A
|
|
13
|
+
* localStorage clone would be a channel with nothing on the other end and a key that cannot be
|
|
14
|
+
* computed.
|
|
15
|
+
*
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ImportBreakpointPolicy } from "../types";
|
|
20
|
+
|
|
21
|
+
/** What the wizard gathered, as the tool needs it. */
|
|
22
|
+
export interface ImportBrief {
|
|
23
|
+
/** Absolute http(s) URL of the site to clone. */
|
|
24
|
+
url: string;
|
|
25
|
+
/** Human project name (the wizard prefills it from the hostname). */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Absolute destination directory, already resolved from Location + folder name. */
|
|
28
|
+
directory: string;
|
|
29
|
+
depth: number;
|
|
30
|
+
maxPages: number;
|
|
31
|
+
/**
|
|
32
|
+
* How many of the site's declared breakpoints the project keeps.
|
|
33
|
+
*
|
|
34
|
+
* A real site declares as many as it has accumulated frameworks — nine is ordinary — and each one
|
|
35
|
+
* is a canvas size in Studio and a column in every style editor. The wizard defaults this to
|
|
36
|
+
* three because that is the complaint it answers, not because three is a magic number.
|
|
37
|
+
*/
|
|
38
|
+
breakpoints: ImportBreakpointPolicy;
|
|
39
|
+
aiComponents: boolean;
|
|
40
|
+
/** Build and screenshot-diff the result. Off by default — it roughly doubles the run. */
|
|
41
|
+
verify: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The average fidelity, 0..100, the clone has to reach for `verify` to report success.
|
|
44
|
+
*
|
|
45
|
+
* Only meaningful when `verify` is on. A finding rather than a failure here: the project is
|
|
46
|
+
* written and opened either way, and the assistant says the bar was missed.
|
|
47
|
+
*/
|
|
48
|
+
minFidelity: number;
|
|
49
|
+
/** Empty means "whatever the assistant would use" (`preferredModel()`). */
|
|
50
|
+
model: string;
|
|
51
|
+
/** What the user wants done with the site once it is cloned. May be empty. */
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let _brief: ImportBrief | null = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store the brief the assistant's next turn will act on.
|
|
59
|
+
*
|
|
60
|
+
* @param {ImportBrief} brief
|
|
61
|
+
*/
|
|
62
|
+
export function setPendingImportBrief(brief: ImportBrief): void {
|
|
63
|
+
_brief = brief;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The brief without consuming it — what `import_site` reads to fill in a destination the model was
|
|
68
|
+
* never told and must not invent.
|
|
69
|
+
*
|
|
70
|
+
* @returns {ImportBrief | null}
|
|
71
|
+
*/
|
|
72
|
+
export function pendingImportBrief(): ImportBrief | null {
|
|
73
|
+
return _brief;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Forget the brief.
|
|
78
|
+
*
|
|
79
|
+
* There is deliberately no consume-on-read here, unlike `agent-seed.ts`. TWO readers need the same
|
|
80
|
+
* brief and they read at different moments: the panel, to compose the turn, and `import_site`, for
|
|
81
|
+
* the destination — which is minutes later, after the model has decided to call it. A read that
|
|
82
|
+
* cleared would leave the tool inventing a path the user never chose. The tool clears it once the
|
|
83
|
+
* import has actually run.
|
|
84
|
+
*/
|
|
85
|
+
export function clearPendingImportBrief(): void {
|
|
86
|
+
_brief = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── The hand-off itself ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The assistant's side of the hand-off, registered at boot.
|
|
93
|
+
*
|
|
94
|
+
* A late-bound slot for the same reason `services/project-adoption.ts` is one: the New Project
|
|
95
|
+
* wizard must not import `panels/ai-panel.ts`. That import is not just a layering preference — the
|
|
96
|
+
* panel constructs a `DocumentAssistant` singleton at module load, so reaching for it from
|
|
97
|
+
* `new-project/` drags a live assistant into every surface that opens the wizard.
|
|
98
|
+
*/
|
|
99
|
+
let _handoff: ((brief: ImportBrief) => Promise<void>) | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register the assistant's hand-off (studio.ts boot).
|
|
103
|
+
*
|
|
104
|
+
* @param {(brief: ImportBrief) => Promise<void>} fn
|
|
105
|
+
*/
|
|
106
|
+
export function setImportHandoff(fn: (brief: ImportBrief) => Promise<void>): void {
|
|
107
|
+
_handoff = fn;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start the import as an assistant turn.
|
|
112
|
+
*
|
|
113
|
+
* A no-op with nothing registered — a reduced host (a test fixture, an embedder without the
|
|
114
|
+
* assistant) would otherwise crash on a button the wizard should simply not have offered.
|
|
115
|
+
*
|
|
116
|
+
* @param {ImportBrief} brief
|
|
117
|
+
*/
|
|
118
|
+
export async function runImportHandoff(brief: ImportBrief): Promise<void> {
|
|
119
|
+
await _handoff?.(brief);
|
|
120
|
+
}
|