@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
|
@@ -41,6 +41,8 @@ import { renderMarkdown } from "./chat-markdown";
|
|
|
41
41
|
import { summarizeWrites, writesForTurn } from "../../services/ai-writes";
|
|
42
42
|
import { activeRegistry } from "../../commands/active-registry";
|
|
43
43
|
|
|
44
|
+
import type { ImportRunRecord } from "../../services/import-run";
|
|
45
|
+
|
|
44
46
|
// ─── Helpers (moved from ai-panel.ts) ────────────────────────────────────────
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -255,15 +257,93 @@ function renderUserMessage(msg: Message): TemplateResult {
|
|
|
255
257
|
`;
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
/** The tool whose chip is a question card rather than a chip. */
|
|
261
|
+
const ASK_TOOL = "ask_user";
|
|
262
|
+
|
|
263
|
+
/** The tool whose chip grows a live progress line while it runs. */
|
|
264
|
+
const IMPORT_TOOL = "import_site";
|
|
265
|
+
|
|
266
|
+
/** Log lines drawn under a running import. The tail is what a reader wants. */
|
|
267
|
+
/**
|
|
268
|
+
* How close to the bottom counts as "following along".
|
|
269
|
+
*
|
|
270
|
+
* The same threshold and the same reasoning as the transcript's own scroller
|
|
271
|
+
* (`panels/ai-panel.ts`): a reader who has scrolled up is reading something, and yanking them back
|
|
272
|
+
* on every new line is the behaviour that makes a live log unreadable.
|
|
273
|
+
*/
|
|
274
|
+
const IMPORT_STICK_THRESHOLD = 24;
|
|
275
|
+
|
|
258
276
|
/**
|
|
259
|
-
*
|
|
277
|
+
* Keep an import log pinned to its newest line, unless the reader has scrolled away from it.
|
|
278
|
+
*
|
|
279
|
+
* The log is a scroller now rather than a six-line tail, and a scroller that does not follow is a
|
|
280
|
+
* box that shows the same six lines it started with while the interesting ones pile up below.
|
|
281
|
+
*/
|
|
282
|
+
function stickToBottom(element: Element | undefined): void {
|
|
283
|
+
if (!(element instanceof HTMLElement)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
287
|
+
if (distance <= IMPORT_STICK_THRESHOLD || element.scrollTop === 0) {
|
|
288
|
+
element.scrollTop = element.scrollHeight;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** What `ask_user` was called with, as far as the arguments actually parse. */
|
|
293
|
+
interface AskArgs {
|
|
294
|
+
question: string;
|
|
295
|
+
options: string[];
|
|
296
|
+
context: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read a question out of a tool call's arguments.
|
|
301
|
+
*
|
|
302
|
+
* Tolerant on purpose: the arguments are assembled from streamed fragments, so a chip can be asked
|
|
303
|
+
* to render a call whose JSON is still half-written. A card with an empty question renders as an
|
|
304
|
+
* ordinary chip rather than as an empty box.
|
|
260
305
|
*
|
|
261
306
|
* @param {ToolCallRecord} tc
|
|
262
|
-
* @returns {
|
|
307
|
+
* @returns {AskArgs | null}
|
|
263
308
|
*/
|
|
264
|
-
export function
|
|
309
|
+
export function parseAsk(tc: ToolCallRecord): AskArgs | null {
|
|
310
|
+
let parsed: { question?: unknown; options?: unknown; context?: unknown };
|
|
311
|
+
try {
|
|
312
|
+
parsed = tc.arguments ? (JSON.parse(tc.arguments) as typeof parsed) : {};
|
|
313
|
+
} catch {
|
|
314
|
+
return null; // Still streaming, or malformed — the generic chip is the honest fallback.
|
|
315
|
+
}
|
|
316
|
+
if (typeof parsed.question !== "string" || !parsed.question.trim()) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
context: typeof parsed.context === "string" ? parsed.context : "",
|
|
321
|
+
options: Array.isArray(parsed.options)
|
|
322
|
+
? parsed.options.filter((o): o is string => typeof o === "string" && o.trim().length > 0)
|
|
323
|
+
: [],
|
|
324
|
+
question: parsed.question,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* What became of one tool call.
|
|
330
|
+
*
|
|
331
|
+
* `"unanswered"` is the fourth value and it exists for `ask_user`. A question's promise lives in
|
|
332
|
+
* memory, so a reload kills it while the transcript survives: without this the restored card is
|
|
333
|
+
* indistinguishable from a live one and spins forever waiting for a loop that is gone. It is a
|
|
334
|
+
* property of the RUN, not of the record, so the caller supplies whether this call is still live.
|
|
335
|
+
*
|
|
336
|
+
* @param {ToolCallRecord} tc
|
|
337
|
+
* @param {boolean} [live] - Whether the loop is still waiting on this call. Only consulted for a
|
|
338
|
+
* result-less interactive call; every other pending call belongs to a turn still in flight.
|
|
339
|
+
* @returns {"pending" | "unanswered" | "ok" | "failed"}
|
|
340
|
+
*/
|
|
341
|
+
export function toolOutcome(
|
|
342
|
+
tc: ToolCallRecord,
|
|
343
|
+
live = true,
|
|
344
|
+
): "pending" | "unanswered" | "ok" | "failed" {
|
|
265
345
|
if (!tc.result) {
|
|
266
|
-
return "pending";
|
|
346
|
+
return tc.name === ASK_TOOL && !live ? "unanswered" : "pending";
|
|
267
347
|
}
|
|
268
348
|
return tc.result.success ? "ok" : "failed";
|
|
269
349
|
}
|
|
@@ -286,27 +366,180 @@ export function toolOutcomeText(tc: ToolCallRecord): string {
|
|
|
286
366
|
return (result.success ? result.summary : result.error) ?? "";
|
|
287
367
|
}
|
|
288
368
|
|
|
289
|
-
|
|
369
|
+
/**
|
|
370
|
+
* A running import, under the chip that started it.
|
|
371
|
+
*
|
|
372
|
+
* An import takes minutes and says a line at a time. Rendering it here rather than in the modal it
|
|
373
|
+
* used to live in is what stops a successful run destroying its own account of what it did: the
|
|
374
|
+
* chip and the progress are one thing, joined by the tool-call id.
|
|
375
|
+
*/
|
|
376
|
+
function renderImportProgress(record: ImportRunRecord): TemplateResult {
|
|
377
|
+
const running = record.status === "running";
|
|
378
|
+
const determinate = record.total !== null && record.current !== null;
|
|
379
|
+
const outcome =
|
|
380
|
+
record.status === "done"
|
|
381
|
+
? `Imported ${record.url}`
|
|
382
|
+
: record.status === "failed"
|
|
383
|
+
? `Import failed — ${record.error || "the run did not say why"}`
|
|
384
|
+
: record.status === "stopped"
|
|
385
|
+
? "Import stopped"
|
|
386
|
+
: record.message;
|
|
387
|
+
return html`
|
|
388
|
+
<details class="ai-import-progress" ?open=${running}>
|
|
389
|
+
<summary class="ai-import-head">
|
|
390
|
+
${
|
|
391
|
+
running
|
|
392
|
+
? determinate
|
|
393
|
+
? html`<sp-progress-circle
|
|
394
|
+
size="s"
|
|
395
|
+
progress=${Math.round((record.current! / Math.max(record.total!, 1)) * 100)}
|
|
396
|
+
></sp-progress-circle>`
|
|
397
|
+
: html`<sp-progress-circle indeterminate size="s"></sp-progress-circle>`
|
|
398
|
+
: nothing
|
|
399
|
+
}
|
|
400
|
+
${running ? html`<span class="ai-import-phase">${record.phase}</span>` : nothing}
|
|
401
|
+
<span class="ai-import-message">${outcome}</span>
|
|
402
|
+
<span class="ai-import-count">${record.log.length}</span>
|
|
403
|
+
</summary>
|
|
404
|
+
<div class="ai-import-log" ${ref(stickToBottom)}>
|
|
405
|
+
${repeat(
|
|
406
|
+
record.log,
|
|
407
|
+
(evt, index) => `${index}:${evt.phase}`,
|
|
408
|
+
(evt) => html`
|
|
409
|
+
<div class="ai-import-log-line">
|
|
410
|
+
<span class="ai-import-phase">${evt.phase}</span>${evt.message}
|
|
411
|
+
</div>
|
|
412
|
+
`,
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
</details>
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** How a question card reaches the store that resolves it. */
|
|
420
|
+
export interface AskHandlers {
|
|
421
|
+
/** The id of the question the loop is still waiting on, or null. */
|
|
422
|
+
pendingId?: string | null;
|
|
423
|
+
onAnswer?: (text: string) => void;
|
|
424
|
+
onSkip?: () => void;
|
|
425
|
+
/** The live record for an `import_site` call, if this renderer's host has one. */
|
|
426
|
+
importRun?: (id: string) => ImportRunRecord | null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* A question, as the last thing in the transcript, waiting on the reader.
|
|
431
|
+
*
|
|
432
|
+
* The card IS the tool chip rather than a row beside it: the question, its options and its answer
|
|
433
|
+
* are all facts about one tool call, and rendering them anywhere else would give the transcript two
|
|
434
|
+
* accounts of the same event that can disagree after a reload.
|
|
435
|
+
*
|
|
436
|
+
* The options are a shortcut, never the whole answer — the composer is always live beneath, and its
|
|
437
|
+
* placeholder says so. A question whose real answer is "neither, do this instead" must stay
|
|
438
|
+
* answerable.
|
|
439
|
+
*/
|
|
440
|
+
function renderAskCard(
|
|
441
|
+
tc: ToolCallRecord,
|
|
442
|
+
ask: AskArgs,
|
|
443
|
+
outcome: ReturnType<typeof toolOutcome>,
|
|
444
|
+
handlers: AskHandlers,
|
|
445
|
+
): TemplateResult {
|
|
446
|
+
const answered = tc.result?.success
|
|
447
|
+
? ((tc.result.data as { answer?: string | null; skipped?: boolean } | undefined) ?? null)
|
|
448
|
+
: null;
|
|
449
|
+
return html`
|
|
450
|
+
<div class="ai-ask" data-outcome=${outcome}>
|
|
451
|
+
<div class="ai-ask-question">${ask.question}</div>
|
|
452
|
+
${ask.context ? html`<div class="ai-ask-context">${ask.context}</div>` : nothing}
|
|
453
|
+
${
|
|
454
|
+
outcome === "pending"
|
|
455
|
+
? html`
|
|
456
|
+
<div class="ai-ask-options">
|
|
457
|
+
${ask.options.map(
|
|
458
|
+
(option) => html`
|
|
459
|
+
<sp-button
|
|
460
|
+
size="s"
|
|
461
|
+
variant="secondary"
|
|
462
|
+
treatment="outline"
|
|
463
|
+
@click=${() => handlers.onAnswer?.(option)}
|
|
464
|
+
>
|
|
465
|
+
${option}
|
|
466
|
+
</sp-button>
|
|
467
|
+
`,
|
|
468
|
+
)}
|
|
469
|
+
<sp-action-button
|
|
470
|
+
size="s"
|
|
471
|
+
quiet
|
|
472
|
+
class="ai-ask-skip"
|
|
473
|
+
title="Let the assistant decide"
|
|
474
|
+
@click=${() => handlers.onSkip?.()}
|
|
475
|
+
>
|
|
476
|
+
You decide
|
|
477
|
+
</sp-action-button>
|
|
478
|
+
</div>
|
|
479
|
+
`
|
|
480
|
+
: nothing
|
|
481
|
+
}
|
|
482
|
+
${
|
|
483
|
+
outcome === "unanswered"
|
|
484
|
+
? html`<div class="ai-ask-outcome">
|
|
485
|
+
This question was still open when the session was reloaded — reply below to carry on.
|
|
486
|
+
</div>`
|
|
487
|
+
: nothing
|
|
488
|
+
}
|
|
489
|
+
${
|
|
490
|
+
answered
|
|
491
|
+
? html`<div class="ai-ask-answer">
|
|
492
|
+
${answered.skipped ? "You decide" : answered.answer}
|
|
493
|
+
</div>`
|
|
494
|
+
: nothing
|
|
495
|
+
}
|
|
496
|
+
${
|
|
497
|
+
outcome === "failed"
|
|
498
|
+
? html`<div class="ai-ask-outcome">${toolOutcomeText(tc)}</div>`
|
|
499
|
+
: nothing
|
|
500
|
+
}
|
|
501
|
+
</div>
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function renderToolChips(
|
|
506
|
+
toolCalls: ToolCallRecord[],
|
|
507
|
+
handlers: AskHandlers = {},
|
|
508
|
+
): TemplateResult | typeof nothing {
|
|
290
509
|
if (toolCalls.length === 0) {
|
|
291
510
|
return nothing;
|
|
292
511
|
}
|
|
293
512
|
return html`
|
|
294
513
|
<div class="ai-msg-tools">
|
|
295
514
|
${toolCalls.map((tc) => {
|
|
296
|
-
const
|
|
515
|
+
const ask = tc.name === ASK_TOOL ? parseAsk(tc) : null;
|
|
516
|
+
const outcome = toolOutcome(tc, tc.id === handlers.pendingId || Boolean(tc.result));
|
|
517
|
+
if (ask) {
|
|
518
|
+
return renderAskCard(tc, ask, outcome, handlers);
|
|
519
|
+
}
|
|
297
520
|
const text = toolOutcomeText(tc);
|
|
521
|
+
/*
|
|
522
|
+
* For EVERY import chip, not only a running one.
|
|
523
|
+
*
|
|
524
|
+
* A finished run used to lose its whole account of itself at the instant it finished — the
|
|
525
|
+
* record was fetched only while the call was pending, so the log vanished on success. That
|
|
526
|
+
* is the failure the hand-off from the wizard to the assistant was made to fix, reproduced
|
|
527
|
+
* one layer in. It stays, collapsed, under the chip that produced it.
|
|
528
|
+
*/
|
|
529
|
+
const run = tc.name === IMPORT_TOOL ? (handlers.importRun?.(tc.id) ?? null) : null;
|
|
298
530
|
return html`
|
|
299
531
|
<span class="ai-tool-chip" data-outcome=${outcome} title=${text || formatToolLabel(tc)}>
|
|
300
532
|
<sp-icon-gears size="xs"></sp-icon-gears>
|
|
301
533
|
<span class="ai-tool-chip-name">${formatToolLabel(tc)}</span>
|
|
302
534
|
${
|
|
303
|
-
outcome === "pending"
|
|
535
|
+
outcome === "pending" || outcome === "unanswered"
|
|
304
536
|
? nothing
|
|
305
537
|
: html`<span class="ai-tool-chip-outcome"
|
|
306
538
|
>${outcome === "ok" ? "✓" : "✗"} ${text}</span
|
|
307
539
|
>`
|
|
308
540
|
}
|
|
309
541
|
</span>
|
|
542
|
+
${run ? renderImportProgress(run) : nothing}
|
|
310
543
|
`;
|
|
311
544
|
})}
|
|
312
545
|
</div>
|
|
@@ -373,6 +606,7 @@ function renderChangedFiles(
|
|
|
373
606
|
function renderAssistantMessage(
|
|
374
607
|
msg: Message,
|
|
375
608
|
onRestore?: (messageId: string) => void,
|
|
609
|
+
ask: AskHandlers = {},
|
|
376
610
|
): TemplateResult | typeof nothing {
|
|
377
611
|
const toolCalls = msg.toolCalls ?? [];
|
|
378
612
|
if (!msg.content && toolCalls.length === 0) {
|
|
@@ -380,8 +614,8 @@ function renderAssistantMessage(
|
|
|
380
614
|
}
|
|
381
615
|
return html`
|
|
382
616
|
<div class="ai-msg-assistant">
|
|
383
|
-
${msg.content ? renderMarkdown(msg.id, msg.content) : nothing}
|
|
384
|
-
${renderChangedFiles(msg, onRestore)}
|
|
617
|
+
${msg.content ? renderMarkdown(msg.id, msg.content) : nothing}
|
|
618
|
+
${renderToolChips(toolCalls, ask)} ${renderChangedFiles(msg, onRestore)}
|
|
385
619
|
</div>
|
|
386
620
|
`;
|
|
387
621
|
}
|
|
@@ -396,7 +630,7 @@ function renderToolMessage(msg: Message): TemplateResult | typeof nothing {
|
|
|
396
630
|
return html`<div class="ai-msg-tool-error">⚠️ ${parsed.error || "Tool call failed"}</div>`;
|
|
397
631
|
}
|
|
398
632
|
|
|
399
|
-
function renderStreamingTail(msg: Message): TemplateResult {
|
|
633
|
+
function renderStreamingTail(msg: Message, ask: AskHandlers = {}): TemplateResult {
|
|
400
634
|
if (!msg.content) {
|
|
401
635
|
return html`
|
|
402
636
|
<div class="ai-msg-typing">
|
|
@@ -409,7 +643,7 @@ function renderStreamingTail(msg: Message): TemplateResult {
|
|
|
409
643
|
return html`
|
|
410
644
|
<div class="ai-msg-assistant">
|
|
411
645
|
<span class="ai-msg-streaming">${msg.content}</span>
|
|
412
|
-
${renderToolChips(msg.toolCalls ?? [])}
|
|
646
|
+
${renderToolChips(msg.toolCalls ?? [], ask)}
|
|
413
647
|
</div>
|
|
414
648
|
`;
|
|
415
649
|
}
|
|
@@ -426,11 +660,19 @@ export interface MessageListOptions {
|
|
|
426
660
|
listRef: (el: Element | undefined) => void;
|
|
427
661
|
/** Undo everything one turn changed. Offered only for turns whose changes are all transactional. */
|
|
428
662
|
onRestore?: ((messageId: string) => void) | undefined;
|
|
663
|
+
/**
|
|
664
|
+
* The outstanding question and how to settle it.
|
|
665
|
+
*
|
|
666
|
+
* Passed down rather than read from the store here, because this module renders templates and
|
|
667
|
+
* holds no state — the same reason the three header buttons are command records.
|
|
668
|
+
*/
|
|
669
|
+
ask?: AskHandlers | undefined;
|
|
429
670
|
}
|
|
430
671
|
|
|
431
672
|
/** The scrollable message list — THE scroller of the chat view. */
|
|
432
673
|
export function renderMessageList(opts: MessageListOptions): TemplateResult {
|
|
433
674
|
const { messages, status } = opts;
|
|
675
|
+
const ask = opts.ask ?? {};
|
|
434
676
|
const lastIdx = messages.length - 1;
|
|
435
677
|
return html`
|
|
436
678
|
<div class="ai-chat-messages" ${ref(opts.listRef)} @scroll=${opts.onScroll}>
|
|
@@ -462,9 +704,9 @@ export function renderMessageList(opts: MessageListOptions): TemplateResult {
|
|
|
462
704
|
}
|
|
463
705
|
if (msg.role === "assistant") {
|
|
464
706
|
if (status === "streaming" && i === lastIdx) {
|
|
465
|
-
return renderStreamingTail(msg);
|
|
707
|
+
return renderStreamingTail(msg, ask);
|
|
466
708
|
}
|
|
467
|
-
return renderAssistantMessage(msg, opts.onRestore);
|
|
709
|
+
return renderAssistantMessage(msg, opts.onRestore, ask);
|
|
468
710
|
}
|
|
469
711
|
return nothing;
|
|
470
712
|
},
|
|
@@ -17,21 +17,17 @@
|
|
|
17
17
|
import { html, nothing } from "lit-html";
|
|
18
18
|
import { displayTagName } from "@jxsuite/schema/guards";
|
|
19
19
|
import type { TemplateResult } from "lit-html";
|
|
20
|
-
import { live } from "lit-html/directives/live.js";
|
|
21
20
|
import { ref } from "lit-html/directives/ref.js";
|
|
22
21
|
import { getNodeAtPath } from "../../state";
|
|
23
|
-
import {
|
|
24
|
-
import { getModel, setModel } from "../../services/ai-settings";
|
|
22
|
+
import { createModelPicker } from "../../ui/ai-model-picker";
|
|
25
23
|
import { activeTab } from "../../workspace/workspace";
|
|
26
24
|
import { primarySelection } from "../../tabs/selection";
|
|
27
25
|
import { buildMessageWithContext } from "./attached-context";
|
|
28
26
|
import type { ContextChip } from "./attached-context";
|
|
29
|
-
import type { AiModel } from "../../services/ai-models";
|
|
30
27
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
31
28
|
|
|
32
29
|
/** Tallest the textarea auto-grows before it scrolls internally. */
|
|
33
30
|
const MAX_INPUT_HEIGHT = 120;
|
|
34
|
-
const RETRY_MODELS = "__retry_models__";
|
|
35
31
|
|
|
36
32
|
export interface ComposerOptions {
|
|
37
33
|
/** Receives the full message content (typed text + serialized context). */
|
|
@@ -40,6 +36,13 @@ export interface ComposerOptions {
|
|
|
40
36
|
/** Opens the credentials form. */
|
|
41
37
|
onOpenSettings: () => void;
|
|
42
38
|
isStreaming: () => boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the turn is suspended on a question. The composer becomes the answer field: Enter still
|
|
41
|
+
* sends, but the host routes it to the pending question instead of opening a new turn.
|
|
42
|
+
*
|
|
43
|
+
* Optional so the evals harness and tests that predate `ask_user` keep compiling.
|
|
44
|
+
*/
|
|
45
|
+
isAwaiting?: () => boolean;
|
|
43
46
|
/** Host re-render scheduler — called whenever composer state changes. */
|
|
44
47
|
requestRender: () => void;
|
|
45
48
|
}
|
|
@@ -71,77 +74,10 @@ export function createComposer(opts: ComposerOptions): Composer {
|
|
|
71
74
|
let hasText = false;
|
|
72
75
|
let chips: ContextChip[] = [];
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// ── Model picker ──────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
function ensureModels(force = false) {
|
|
82
|
-
if (modelsLoading || (models !== null && !force)) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
modelsLoading = true;
|
|
86
|
-
modelsError = "";
|
|
87
|
-
fetchAvailableModels({ force })
|
|
88
|
-
.then((list) => {
|
|
89
|
-
models = list;
|
|
90
|
-
})
|
|
91
|
-
.catch((error: unknown) => {
|
|
92
|
-
models = [];
|
|
93
|
-
modelsError = (error as Error).message || "Failed to fetch models";
|
|
94
|
-
})
|
|
95
|
-
.finally(() => {
|
|
96
|
-
modelsLoading = false;
|
|
97
|
-
opts.requestRender();
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function onModelChange(e: Event) {
|
|
102
|
-
const { value } = e.target as HTMLInputElement;
|
|
103
|
-
if (value === RETRY_MODELS) {
|
|
104
|
-
models = null;
|
|
105
|
-
ensureModels(true);
|
|
106
|
-
// Re-render so the picker snaps back to the stored model instead of "Retry".
|
|
107
|
-
opts.requestRender();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (value) {
|
|
111
|
-
setModel(value);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function renderModelPicker(): TemplateResult {
|
|
116
|
-
ensureModels();
|
|
117
|
-
const current = getModel();
|
|
118
|
-
const listed = models ?? [];
|
|
119
|
-
const items = listed.some((m) => m.id === current)
|
|
120
|
-
? listed
|
|
121
|
-
: [{ id: current, name: current }, ...listed];
|
|
122
|
-
return html`
|
|
123
|
-
<sp-picker
|
|
124
|
-
size="s"
|
|
125
|
-
quiet
|
|
126
|
-
class="ai-model-picker"
|
|
127
|
-
title=${modelsError ? `Couldn't load models: ${modelsError}` : "Model"}
|
|
128
|
-
.value=${live(current)}
|
|
129
|
-
@change=${onModelChange}
|
|
130
|
-
>
|
|
131
|
-
${items.map((m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`)}
|
|
132
|
-
${
|
|
133
|
-
modelsLoading
|
|
134
|
-
? html`<sp-menu-item disabled value="__loading__">Loading models…</sp-menu-item>`
|
|
135
|
-
: nothing
|
|
136
|
-
}
|
|
137
|
-
${
|
|
138
|
-
modelsError
|
|
139
|
-
? html`<sp-menu-item value=${RETRY_MODELS}>Retry loading models</sp-menu-item>`
|
|
140
|
-
: nothing
|
|
141
|
-
}
|
|
142
|
-
</sp-picker>
|
|
143
|
-
`;
|
|
144
|
-
}
|
|
77
|
+
/* The model picker is `ui/ai-model-picker.ts` now, not forty lines here: the New Project Import
|
|
78
|
+
source chooses a model too, and the "never hold the list privately" invariant is the kind that
|
|
79
|
+
only breaks in the copy nobody looked at. */
|
|
80
|
+
const modelPicker = createModelPicker({ requestRender: opts.requestRender });
|
|
145
81
|
|
|
146
82
|
// ── Context attach ────────────────────────────────────────────────────
|
|
147
83
|
|
|
@@ -300,13 +236,18 @@ export function createComposer(opts: ComposerOptions): Composer {
|
|
|
300
236
|
|
|
301
237
|
function render(): TemplateResult {
|
|
302
238
|
const streaming = opts.isStreaming();
|
|
239
|
+
const awaiting = opts.isAwaiting?.() ?? false;
|
|
303
240
|
return html`
|
|
304
|
-
<div class="ai-composer">
|
|
241
|
+
<div class="ai-composer" ?data-awaiting=${awaiting}>
|
|
305
242
|
${renderChips()}
|
|
306
243
|
<textarea
|
|
307
244
|
class="ai-composer-input"
|
|
308
245
|
rows="1"
|
|
309
|
-
placeholder
|
|
246
|
+
placeholder=${
|
|
247
|
+
awaiting
|
|
248
|
+
? "Answer the assistant… (Enter to reply)"
|
|
249
|
+
: "Ask the assistant… (Enter to send)"
|
|
250
|
+
}
|
|
310
251
|
${ref((el) => {
|
|
311
252
|
textareaEl = (el as HTMLTextAreaElement | null) || null;
|
|
312
253
|
})}
|
|
@@ -314,7 +255,7 @@ export function createComposer(opts: ComposerOptions): Composer {
|
|
|
314
255
|
@keydown=${onKeydown}
|
|
315
256
|
></textarea>
|
|
316
257
|
<div class="ai-composer-row">
|
|
317
|
-
${renderAttachMenu()} ${
|
|
258
|
+
${renderAttachMenu()} ${modelPicker.render()}
|
|
318
259
|
<span class="ai-header-spacer"></span>
|
|
319
260
|
<sp-action-button size="s" quiet title="API key & endpoint" @click=${opts.onOpenSettings}>
|
|
320
261
|
<sp-icon-settings slot="icon"></sp-icon-settings>
|
|
@@ -330,7 +271,7 @@ export function createComposer(opts: ComposerOptions): Composer {
|
|
|
330
271
|
<sp-action-button
|
|
331
272
|
size="s"
|
|
332
273
|
class="ai-send-btn"
|
|
333
|
-
title
|
|
274
|
+
title=${awaiting ? "Answer" : "Send"}
|
|
334
275
|
?disabled=${!hasText}
|
|
335
276
|
@click=${trySend}
|
|
336
277
|
>
|