@jxsuite/studio 5.5.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/chunks/{2020-g8mwpbkq.js → 2020-cs2sb9t1.js} +3 -3
- package/dist/chunks/adapter-y5gdq19c.js +4 -0
- package/dist/chunks/cf-account-picker-882a7ecm.js +4 -0
- package/dist/chunks/cf-settings-20dznc3j.js +4 -0
- package/dist/chunks/components-q037dya7.js +4 -0
- package/dist/chunks/content-source-37x3vsap.js +4 -0
- package/dist/chunks/convert-to-repeater-grfep2w8.js +4 -0
- package/dist/chunks/editor-j5sq149q.js +4 -0
- package/dist/chunks/entry-commands-c5h3nqck.js +4 -0
- package/dist/chunks/extension-sections-dp8endhx.js +4 -0
- package/dist/chunks/file-ops-937cvqk6.js +4 -0
- package/dist/chunks/files-tznwe1p1.js +4 -0
- package/dist/chunks/format-host-e2pjvd25.js +4 -0
- package/dist/chunks/fs-events-fv64ak3s.js +4 -0
- package/dist/chunks/git-panel-gb713ttc.js +4 -0
- package/dist/chunks/grid-view-5amhg604.js +51 -0
- package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
- package/dist/chunks/iframe-host-t819mcg5.js +4 -0
- package/dist/chunks/{index-arg188jq.js → index-9ydfe04y.js} +3 -3
- package/dist/chunks/index-m0x5ztax.js +4 -0
- package/dist/chunks/{javascript-zcdmye2k.js → javascript-d9pqgc2p.js} +2 -2
- package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-800yvbem.js} +5 -5
- package/dist/chunks/jx-validate-zc9zp77s.js +4 -0
- package/dist/chunks/layers-4bsr82wz.js +4 -0
- package/dist/chunks/layers-panel-b6shnaax.js +4 -0
- package/dist/chunks/lit-html-cfn61dn7.js +4 -0
- package/dist/chunks/media-picker-ej0kdh1j.js +4 -0
- package/dist/chunks/menu-rtxf9chb.js +4 -0
- package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-62dtmgbf.js} +3 -3
- package/dist/chunks/monaco-buffer-nn4dtx27.js +4 -0
- package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-tj13ndtm.js} +3 -3
- package/dist/chunks/notify-ahs57a2z.js +4 -0
- package/dist/chunks/platform-v9xyfkbx.js +4 -0
- package/dist/chunks/publish-panel-bq36xcmj.js +4 -0
- package/dist/chunks/quick-search-zm00h9dh.js +4 -0
- package/dist/chunks/references-99ws0b0m.js +4 -0
- package/dist/chunks/right-panel-pq2f71q3.js +4 -0
- package/dist/chunks/section-registry-b2yzp9sw.js +4 -0
- package/dist/chunks/studio-01kbbqg3.js +928 -0
- package/dist/chunks/{studio-hf4qdjc8.js → studio-01qza1n3.js} +4 -4
- package/dist/chunks/{studio-pkkw32j0.js → studio-03wwfvyt.js} +4 -4
- package/dist/chunks/{studio-7gqyhjns.js → studio-0bbxhhce.js} +4 -4
- package/dist/chunks/studio-0k9mvpgj.js +14 -0
- package/dist/chunks/studio-0scebvb9.js +6 -0
- package/dist/chunks/studio-1a99w39s.js +5 -0
- package/dist/chunks/studio-1wvfxkyy.js +5 -0
- package/dist/chunks/studio-1x8x26k3.js +5 -0
- package/dist/chunks/studio-222rqyyc.js +5 -0
- package/dist/chunks/studio-4xppwncr.js +4 -0
- package/dist/chunks/{studio-xcng8nx7.js → studio-54g077x3.js} +5 -5
- package/dist/chunks/studio-56z7evx4.js +10 -0
- package/dist/chunks/studio-5e9zsamh.js +9 -0
- package/dist/chunks/studio-65fhspdd.js +5 -0
- package/dist/chunks/studio-693nhyky.js +5 -0
- package/dist/chunks/{studio-7jazdy6y.js → studio-7pgy3xaf.js} +4 -4
- package/dist/chunks/studio-888knnkc.js +5 -0
- package/dist/chunks/studio-9mqfamps.js +5 -0
- package/dist/chunks/studio-a7xqhc3n.js +5 -0
- package/dist/chunks/studio-aje966s9.js +5 -0
- package/dist/chunks/studio-aqmph6k4.js +5 -0
- package/dist/chunks/studio-c6w3tq7t.js +5 -0
- package/dist/chunks/{studio-6j2jn55a.js → studio-c6zdm25z.js} +8 -8
- package/dist/chunks/{studio-f8jyp45j.js → studio-c9d3pxf7.js} +4 -4
- package/dist/chunks/studio-ceb34dxj.js +5 -0
- package/dist/chunks/studio-cvbkkfqb.js +5 -0
- package/dist/chunks/studio-cxt150pj.js +7 -0
- package/dist/chunks/studio-dkn3x3hz.js +5 -0
- package/dist/chunks/studio-e6re3rk1.js +5 -0
- package/dist/chunks/{studio-5n1km79v.js → studio-edbtvcd1.js} +4 -4
- package/dist/chunks/{studio-xc8ebsts.js → studio-eh0rac07.js} +4 -4
- package/dist/chunks/{studio-625ezz59.js → studio-evwgp1ym.js} +4 -4
- package/dist/chunks/{studio-he2fhrgp.js → studio-f195h2hq.js} +4 -4
- package/dist/chunks/studio-fqq890ap.js +4 -0
- package/dist/chunks/studio-frb0qbf7.js +6 -0
- package/dist/chunks/studio-gfn1kpxj.js +6 -0
- package/dist/chunks/studio-gh97gngt.js +5 -0
- package/dist/chunks/{studio-awqghqxv.js → studio-gp02nqnw.js} +4 -4
- package/dist/chunks/{studio-x9sagq2d.js → studio-hjbg8d5y.js} +7 -7
- package/dist/chunks/studio-j6gnhvaa.js +5 -0
- package/dist/chunks/studio-mwxgyn9h.js +7 -0
- package/dist/chunks/{studio-x5esgzct.js → studio-n78gjfy8.js} +4 -4
- package/dist/chunks/studio-nbxqcgvf.js +5 -0
- package/dist/chunks/{studio-p1eggxcf.js → studio-nt06yybj.js} +4 -4
- package/dist/chunks/{studio-hc9q4j24.js → studio-p7c1agz0.js} +5 -5
- package/dist/chunks/{studio-8r4y2x0z.js → studio-pwqfmt4p.js} +4 -4
- package/dist/chunks/{studio-5wfrgee9.js → studio-r3kaka4d.js} +4 -4
- package/dist/chunks/studio-semd1s8s.js +5 -0
- package/dist/chunks/studio-skzanb0k.js +5 -0
- package/dist/chunks/studio-spf2skgc.js +5 -0
- package/dist/chunks/{studio-b4epb26c.js → studio-st75kn99.js} +7 -7
- package/dist/chunks/{studio-eq27rrr0.js → studio-sxc0zvpt.js} +3 -3
- package/dist/chunks/studio-t1eedsyx.js +4 -0
- package/dist/chunks/{studio-73czgk6a.js → studio-tp642n5g.js} +4 -4
- package/dist/chunks/studio-tw7rre6s.js +5 -0
- package/dist/chunks/{studio-7by9a93k.js → studio-tzegmy4w.js} +4 -4
- package/dist/chunks/studio-vavmwshr.js +5 -0
- package/dist/chunks/studio-wma2c6g8.js +5 -0
- package/dist/chunks/studio-x92tgnee.js +64 -0
- package/dist/chunks/studio-xk0z6msk.js +5 -0
- package/dist/chunks/{studio-c4pnt1c6.js → studio-xry7tk3z.js} +5 -5
- package/dist/chunks/studio-xta2vxp6.js +9 -0
- package/dist/chunks/{studio-21enxdr8.js → studio-xy3tv3az.js} +4 -4
- package/dist/chunks/studio-y7jehjrp.js +5 -0
- package/dist/chunks/{studio-k1eaqyzm.js → studio-yr04mv2j.js} +4 -4
- package/dist/chunks/{studio-mhhc9btt.js → studio-zd4kayn2.js} +4 -4
- package/dist/chunks/studio-zpnk016a.js +16 -0
- package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-k9s9fcag.js} +5 -5
- package/dist/chunks/workspace-b69mgm0e.js +4 -0
- package/dist/chunks/ws-client-0ja08haa.js +4 -0
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +62 -59
- package/dist/studio.css +1 -1
- package/dist/studio.js +121 -123
- package/package.json +5 -5
- package/src/account/action-flow.ts +294 -0
- package/src/account/action-required.ts +68 -0
- package/src/account-status.ts +29 -1
- package/src/browse/library-model.ts +3 -1
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +3 -1
- package/src/canvas/asset-resolve.ts +79 -1
- package/src/canvas/iframe-entry.ts +38 -1
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/canvas/stage-scrollbar.ts +7 -7
- package/src/files/asset-versions.ts +294 -0
- package/src/files/file-ops.ts +5 -8
- package/src/files/files.ts +61 -9
- package/src/files/fs-events.ts +279 -28
- package/src/files/media-meta.ts +2 -1
- package/src/files/media-paths.ts +5 -2
- package/src/files/media-upload.ts +5 -1
- package/src/new-project/add-repo-modal.ts +12 -1
- package/src/new-project/new-project-modal.ts +12 -1
- package/src/panels/git-panel.ts +25 -2
- package/src/panels/jump-bar.ts +132 -158
- package/src/panels/pane-context.ts +8 -8
- package/src/panels/pane-grid.ts +18 -26
- package/src/panels/style-panel.ts +2 -2
- package/src/platform-errors.ts +61 -1
- package/src/platforms/cloud.ts +389 -50
- package/src/platforms/devserver.ts +17 -1
- package/src/publish/pages-service.ts +17 -7
- package/src/services/ai-models.ts +30 -6
- package/src/services/ai-system-prompt.ts +81 -51
- package/src/services/context-manager.ts +41 -70
- package/src/services/document-assistant.ts +12 -3
- package/src/services/harness/chat-reducer.ts +115 -0
- package/src/services/harness/turn-hooks.ts +50 -0
- package/src/services/tool-executor.ts +69 -288
- package/src/services/tool-outcomes.ts +12 -10
- package/src/settings/preferences-accounts.ts +42 -3
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/studio.ts +20 -6
- package/src/surfaces/block-action-bar.json +6 -1
- package/src/surfaces/dialog.ts +5 -0
- package/src/surfaces/jump-bar.json +9 -12
- package/src/surfaces/jump-bar.ts +15 -16
- package/src/surfaces/pane-context.json +2 -2
- package/src/surfaces/pane-grid.json +8 -25
- package/src/surfaces/pane-grid.ts +7 -8
- package/src/surfaces/statusbar.json +71 -3
- package/src/surfaces/statusbar.ts +66 -139
- package/src/surfaces/style-panel.json +3 -3
- package/src/surfaces/style-panel.ts +11 -1
- package/src/surfaces/welcome.json +86 -1
- package/src/surfaces/welcome.ts +61 -2
- package/src/types.ts +84 -2
- package/src/ui/ai-managed-connect.ts +41 -4
- package/src/ui/media-picker.ts +2 -1
- package/dist/chunks/adapter-bsbs78x9.js +0 -4
- package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
- package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
- package/dist/chunks/components-s51h3b7c.js +0 -4
- package/dist/chunks/content-source-ty3be0qg.js +0 -4
- package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
- package/dist/chunks/editor-t002b41m.js +0 -4
- package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
- package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
- package/dist/chunks/file-ops-78zeytkm.js +0 -4
- package/dist/chunks/files-gatzrz8v.js +0 -4
- package/dist/chunks/format-host-r59b1651.js +0 -4
- package/dist/chunks/fs-events-f8h91ndt.js +0 -4
- package/dist/chunks/git-panel-y12zf7nj.js +0 -4
- package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
- package/dist/chunks/grid-view-tafenfm2.css +0 -1
- package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
- package/dist/chunks/index-5aghyrfh.js +0 -4
- package/dist/chunks/jx-validate-653krkrf.js +0 -4
- package/dist/chunks/layers-32p8gtae.js +0 -4
- package/dist/chunks/layers-panel-rs58m105.js +0 -4
- package/dist/chunks/lit-html-54bmkwz3.js +0 -4
- package/dist/chunks/media-picker-5bb04wct.js +0 -4
- package/dist/chunks/menu-cbha7w2e.js +0 -4
- package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
- package/dist/chunks/notify-dc1nz5ar.js +0 -4
- package/dist/chunks/platform-2m49s1zf.js +0 -4
- package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
- package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
- package/dist/chunks/references-vx7mbj9f.js +0 -4
- package/dist/chunks/right-panel-7jzpbxja.js +0 -4
- package/dist/chunks/section-registry-gjryse41.js +0 -4
- package/dist/chunks/studio-3znfhcxg.js +0 -5
- package/dist/chunks/studio-58qam27v.js +0 -4
- package/dist/chunks/studio-6j59fwaw.js +0 -5
- package/dist/chunks/studio-7z69g1gx.js +0 -5
- package/dist/chunks/studio-93j3v6zx.js +0 -5
- package/dist/chunks/studio-9gf6v8zy.js +0 -5
- package/dist/chunks/studio-9rhmettf.js +0 -4
- package/dist/chunks/studio-ac5aq677.js +0 -10
- package/dist/chunks/studio-as65vbns.js +0 -5
- package/dist/chunks/studio-bkv0sga4.js +0 -6
- package/dist/chunks/studio-bttmqef4.js +0 -5
- package/dist/chunks/studio-c3y9d41k.js +0 -5
- package/dist/chunks/studio-ccegp7gz.js +0 -9
- package/dist/chunks/studio-chxabn98.js +0 -6
- package/dist/chunks/studio-cktcdsxz.js +0 -16
- package/dist/chunks/studio-cnspfke5.js +0 -5
- package/dist/chunks/studio-de3p1ktm.js +0 -5
- package/dist/chunks/studio-fsbcn530.js +0 -928
- package/dist/chunks/studio-gftga7a3.js +0 -64
- package/dist/chunks/studio-hf1rpcq0.js +0 -4
- package/dist/chunks/studio-hz02hq70.js +0 -5
- package/dist/chunks/studio-jnb285we.js +0 -5
- package/dist/chunks/studio-jsv3s3s1.js +0 -5
- package/dist/chunks/studio-ks7cksh0.js +0 -5
- package/dist/chunks/studio-nptx0dbs.js +0 -5
- package/dist/chunks/studio-pg0z7k98.js +0 -5
- package/dist/chunks/studio-pve0b2f2.js +0 -5
- package/dist/chunks/studio-q7zmjv3b.js +0 -6
- package/dist/chunks/studio-rpmvw7kj.js +0 -5
- package/dist/chunks/studio-rwhss52s.js +0 -5
- package/dist/chunks/studio-sbg3g3h0.js +0 -5
- package/dist/chunks/studio-sq1bn60g.js +0 -14
- package/dist/chunks/studio-vwz7rma2.js +0 -5
- package/dist/chunks/studio-wa55e8gf.js +0 -5
- package/dist/chunks/studio-x0mzdha2.js +0 -5
- package/dist/chunks/studio-xrgpakvc.js +0 -9
- package/dist/chunks/studio-xt3djk1w.js +0 -7
- package/dist/chunks/studio-yh3nggx1.js +0 -7
- package/dist/chunks/studio-zmecp6hq.js +0 -5
- package/dist/chunks/workspace-g6dvaete.js +0 -4
- package/dist/chunks/ws-client-shf41j5p.js +0 -4
|
@@ -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
|
+
}
|
|
@@ -1,60 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool-executor.js —
|
|
2
|
+
* Tool-executor.js — the agent loop, as Studio runs it.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The loop itself is the turn engine in `@jxsuite/ai/harness` (specs/ai.md §3.8): it streams each
|
|
5
|
+
* round, runs the calls the model made, feeds their results back, and decides how the turn ends.
|
|
6
|
+
* What stays here is Studio's side of a turn, which no other host shares:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* summary, because a read returns a summary too, and a turn that only looked around was reported
|
|
18
|
-
* as having changed things.
|
|
19
|
-
* - **A turn that drew nothing says so.** A model that answered with neither text nor a tool call
|
|
20
|
-
* left the author's message sitting there with no reply at all. That turn ends on an error row,
|
|
21
|
-
* with Retry, instead.
|
|
22
|
-
* - **A batch belongs to the tab it edits.** `beginBatch(getTab())` ran once, against whichever tab
|
|
23
|
-
* happened to be active when the loop started. A turn that then moved to a second document closed
|
|
24
|
-
* its batch against the FIRST tab, so the second document's edits got neither a history snapshot
|
|
25
|
-
* nor a collab publish. The loop re-anchors the batch whenever the active tab changes under it.
|
|
8
|
+
* - **The chat store is the transcript of record.** The engine's events are applied to it as they
|
|
9
|
+
* happen (`services/harness/chat-reducer.ts`), and each round's request is projected from it, as
|
|
10
|
+
* it always was, so the provider sees the conversation the panel shows, with whatever the send
|
|
11
|
+
* path trimmed or sealed before the turn began.
|
|
12
|
+
* - **A batch belongs to the tab it edits** (§3.3). The undo batch opens on the active tab and is
|
|
13
|
+
* re-anchored after every call whose tools moved to another document, so a turn that edits two
|
|
14
|
+
* documents gives each its own history step and collab publish.
|
|
15
|
+
* - **The ledger is filed under the last message the turn drew** (§3.2), in this module's `finally`,
|
|
16
|
+
* so the transcript draws the turn's changed-files summary under it.
|
|
26
17
|
*
|
|
27
18
|
* @license MIT
|
|
28
19
|
*/
|
|
29
20
|
|
|
30
21
|
import type { createChatState } from "@jxsuite/ai/chat-state";
|
|
22
|
+
import { DEFAULT_TURN_POLICY, runTurn } from "@jxsuite/ai/harness";
|
|
23
|
+
import type { TurnRun } from "@jxsuite/ai/harness";
|
|
24
|
+
import { toChatMessages, toOpenAITools } from "@jxsuite/ai/messages";
|
|
31
25
|
import type { StreamingClient } from "@jxsuite/ai/streaming-client";
|
|
32
|
-
import { createSessionFacts
|
|
33
|
-
import type {
|
|
26
|
+
import { createSessionFacts } from "@jxsuite/ai/tools";
|
|
27
|
+
import type { SessionFacts, ToolRegistry } from "@jxsuite/ai/tools";
|
|
34
28
|
|
|
35
29
|
import type { Tab } from "../tabs/tab";
|
|
36
|
-
import type { ImportProgressEvent } from "../types";
|
|
37
30
|
import { batchTab, beginBatch, endBatch } from "../tabs/transact";
|
|
38
|
-
import { ensureProxyProbe, resetModelCache } from "./ai-models";
|
|
39
31
|
import { estimatePromptTokens } from "./context-manager";
|
|
40
32
|
import { fileTurn, openTurnLedger, turnAnchor } from "./ai-writes";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
const MAX_ROUNDS = 5;
|
|
33
|
+
import { applyHarnessEvent } from "./harness/chat-reducer";
|
|
34
|
+
import { studioTurnHooks } from "./harness/turn-hooks";
|
|
44
35
|
|
|
45
36
|
/** What a turn that drew nothing says: the model answered with neither text nor a tool call. */
|
|
46
|
-
export const EMPTY_TURN_TEXT =
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Hard ceiling on rounds of every kind, so the loop terminates whatever the model does.
|
|
50
|
-
*
|
|
51
|
-
* {@link MAX_ROUNDS} bounds AUTONOMOUS work, and a round that ends by blocking on a person is the
|
|
52
|
-
* opposite of runaway — it cannot advance without them, which is the property the cap was ever a
|
|
53
|
-
* proxy for. So an interactive round does not spend that budget. This one is the backstop that
|
|
54
|
-
* keeps `runAgentLoop` provably terminating anyway; a human answering ten questions will have
|
|
55
|
-
* stopped it long before.
|
|
56
|
-
*/
|
|
57
|
-
const MAX_TOTAL_ROUNDS = 25;
|
|
37
|
+
export const EMPTY_TURN_TEXT = DEFAULT_TURN_POLICY.emptyText;
|
|
58
38
|
|
|
59
39
|
interface RunAgentLoopOptions {
|
|
60
40
|
chatState: ReturnType<typeof createChatState>;
|
|
@@ -80,35 +60,15 @@ export async function runAgentLoop({
|
|
|
80
60
|
getTab,
|
|
81
61
|
session = createSessionFacts(),
|
|
82
62
|
}: RunAgentLoopOptions): Promise<void> {
|
|
83
|
-
const allErrors: string[] = [];
|
|
84
|
-
const appliedSummaries: string[] = [];
|
|
85
|
-
|
|
86
63
|
/* The ledger is filed under the turn's last DRAWN assistant message (see services/ai-writes), so
|
|
87
64
|
the scan starts at the message this turn answers. */
|
|
88
65
|
const turnUserId = chatState.messages.findLast((m) => m.role === "user")?.id;
|
|
89
|
-
/* What every call's context carries (specs/ai.md §3.7): this turn's ledger, the conversation's
|
|
90
|
-
facts, and who the calls act as. The signal and the call id are the call's own, below. */
|
|
91
66
|
const ledger = openTurnLedger(`turn:${chatState.messages.length}`);
|
|
92
|
-
const actor: Actor = {
|
|
93
|
-
id: `assistant:${session.sessionId ?? "local"}:${ledger.turnId}`,
|
|
94
|
-
kind: "assistant",
|
|
95
|
-
model: chatState.model,
|
|
96
|
-
sessionId: session.sessionId,
|
|
97
|
-
turnId: ledger.turnId,
|
|
98
|
-
};
|
|
99
67
|
|
|
100
68
|
// Batch all tool-call mutations into a single undo step, anchored on the tab being edited.
|
|
101
69
|
if (getTab) {
|
|
102
70
|
beginBatch(getTab());
|
|
103
71
|
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Close the batch and re-open it when the tools have moved to a different document.
|
|
107
|
-
*
|
|
108
|
-
* Runs after every tool execution rather than only inside `open_document`, because a tool is not
|
|
109
|
-
* the only thing that can change the active tab: project adoption replaces every tab in the
|
|
110
|
-
* workspace, and the user is free to click another tab while the model is still streaming.
|
|
111
|
-
*/
|
|
112
72
|
const reanchorBatch = () => {
|
|
113
73
|
if (!getTab) {
|
|
114
74
|
return;
|
|
@@ -120,237 +80,58 @@ export async function runAgentLoop({
|
|
|
120
80
|
}
|
|
121
81
|
};
|
|
122
82
|
|
|
83
|
+
/* The placeholder `sendMessage` pushed is round one's message, so the turn answers into it; the
|
|
84
|
+
conversation the turn continues is everything before it. */
|
|
85
|
+
const last = chatState.messages.at(-1);
|
|
86
|
+
const placeholder =
|
|
87
|
+
chatState.status === "streaming" && last?.role === "assistant" ? last : undefined;
|
|
88
|
+
const history = toChatMessages(
|
|
89
|
+
placeholder ? chatState.messages.slice(0, -1) : chatState.messages,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/* Once another chat has replaced the one this turn answers, nothing more of the turn is written
|
|
93
|
+
into the transcript: its events belong to a conversation that is no longer on screen. */
|
|
94
|
+
let superseded = false;
|
|
95
|
+
let run: TurnRun | undefined;
|
|
123
96
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
case "tool_call_delta": {
|
|
158
|
-
chatState.appendToolCallDelta(event.id, event.args);
|
|
159
|
-
const tc = toolCalls.get(event.id);
|
|
160
|
-
if (tc) {
|
|
161
|
-
tc.arguments += event.args;
|
|
162
|
-
}
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
case "tool_call_end": {
|
|
166
|
-
chatState.appendToolCallEnd(event.id);
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
case "usage": {
|
|
170
|
-
/* The provider's own count replaces the four-characters-per-token estimate. The prompt
|
|
171
|
-
it covered is recorded with it, because the next send rebuilds the prompt. */
|
|
172
|
-
chatState.recordUsage(event, { systemTokens: estimatePromptTokens(systemPrompt) });
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
case "done": {
|
|
176
|
-
({ stopReason } = event);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
case "error": {
|
|
180
|
-
streamError = event.message;
|
|
181
|
-
streamErrorCode = event.code;
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
default: {
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (streamError) {
|
|
191
|
-
/* A lapsed hosted grant is the one stream error that makes the app's OWN reading wrong.
|
|
192
|
-
The probe settles once at boot, so without this every gate keeps offering an assistant
|
|
193
|
-
that cannot answer, and the send that just failed is the only evidence anyone has. Drop
|
|
194
|
-
the reading and re-probe, so the next paint carries the Reconnect CTA instead. */
|
|
195
|
-
if (streamErrorCode === "cf_reconnect_required") {
|
|
196
|
-
resetModelCache();
|
|
197
|
-
ensureProxyProbe();
|
|
198
|
-
}
|
|
199
|
-
/* `setError` alone, never after `finishStream`: it removes the round's partial message,
|
|
200
|
-
and only while that message is still the streaming one. Finishing first let go of it, so
|
|
201
|
-
the partial (a tool call cut off mid-arguments, as often as not) stayed in the
|
|
202
|
-
transcript and went out on the next send. */
|
|
203
|
-
chatState.setError(streamError);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/* A turn that has drawn nothing by the end of a round with no calls ends here, as an error
|
|
208
|
-
the author can see and retry, rather than as silence under their message. `setError`
|
|
209
|
-
before `finishStream`, so it removes the empty reply. A stopped round is not empty: the
|
|
210
|
-
author ended it. */
|
|
211
|
-
const stopped = stopReason === "cancelled" || signal?.aborted === true;
|
|
212
|
-
if (toolCalls.size === 0 && !stopped && turnAnchor(chatState.messages, turnUserId) === null) {
|
|
213
|
-
chatState.setError(EMPTY_TURN_TEXT);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
chatState.finishStream(stopReason);
|
|
218
|
-
|
|
219
|
-
/* The calls the model streamed decide whether tools run, not the finish reason the provider
|
|
220
|
-
reported beside them. Some OpenAI-compatible backends (Workers AI among them) end a
|
|
221
|
-
tool-calling turn with `finish_reason: "stop"`, and gating on `tool_calls` dropped those
|
|
222
|
-
calls unrun — the turn ended looking finished, and the next send sealed them as failures.
|
|
223
|
-
A call cut off by `length` still runs: its arguments fail to parse, and the model reads
|
|
224
|
-
that as a tool error it can correct.
|
|
225
|
-
|
|
226
|
-
Cancellation is the exception, and it is not a finish reason at all: it is the author
|
|
227
|
-
pressing Stop. A stopped round runs nothing it streamed, including calls whose arguments
|
|
228
|
-
were already complete, because Stop is the one control that must not be outrun. */
|
|
229
|
-
if (toolCalls.size === 0 || stopReason === "cancelled" || signal?.aborted) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
let didWork = false;
|
|
234
|
-
for (const [id, call] of toolCalls) {
|
|
235
|
-
/* A Stop that lands between calls (while `ask_user` waits, or during a long import) stops
|
|
236
|
-
the calls after it too. They stay unanswered, and the send path seals them. */
|
|
237
|
-
if (signal?.aborted) {
|
|
238
|
-
return;
|
|
97
|
+
run = runTurn({
|
|
98
|
+
history,
|
|
99
|
+
system: [{ text: systemPrompt }],
|
|
100
|
+
/* Projected from the chat store, the transcript of record. The streaming client is read on
|
|
101
|
+
every round, and the turn's own signal is the host's, so a request is the one it was. */
|
|
102
|
+
model: (request) =>
|
|
103
|
+
streamingClient.streamChat(
|
|
104
|
+
chatState.toMessagesArray(),
|
|
105
|
+
toOpenAITools(request.tools),
|
|
106
|
+
systemPrompt,
|
|
107
|
+
signal as AbortSignal,
|
|
108
|
+
),
|
|
109
|
+
modelInfo: { family: "openai-compat", model: chatState.model },
|
|
110
|
+
tools: toolRegistry,
|
|
111
|
+
...(signal ? { signal } : {}),
|
|
112
|
+
turnId: ledger.turnId,
|
|
113
|
+
...(placeholder ? { firstMessageId: placeholder.id } : {}),
|
|
114
|
+
policy: { estimateTokens: estimatePromptTokens },
|
|
115
|
+
hooks: studioTurnHooks({
|
|
116
|
+
superseded: () =>
|
|
117
|
+
turnUserId !== undefined && !chatState.messages.some((m) => m.id === turnUserId),
|
|
118
|
+
onSuperseded: () => {
|
|
119
|
+
superseded = true;
|
|
120
|
+
run?.cancel("superseded");
|
|
121
|
+
},
|
|
122
|
+
reanchor: reanchorBatch,
|
|
123
|
+
}),
|
|
124
|
+
session,
|
|
125
|
+
ledger,
|
|
126
|
+
onEvent: (event) => {
|
|
127
|
+
if (!superseded) {
|
|
128
|
+
applyHarnessEvent(chatState, event);
|
|
239
129
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
/* The call's own signal: it aborts with the turn, so a tool waiting on a person or a crawl
|
|
244
|
-
learns of a Stop, and it is unlinked once the call settles, so a Stop later in the turn
|
|
245
|
-
reaches nothing that has already finished. */
|
|
246
|
-
const link = signal ? linkCallSignal(signal) : null;
|
|
247
|
-
const ctx = createToolContext({
|
|
248
|
-
actor,
|
|
249
|
-
callId: id,
|
|
250
|
-
ledger,
|
|
251
|
-
progress: (event) => {
|
|
252
|
-
recordImportProgress(id, event as unknown as ImportProgressEvent);
|
|
253
|
-
},
|
|
254
|
-
session,
|
|
255
|
-
...(link ? { signal: link.signal } : {}),
|
|
256
|
-
});
|
|
257
|
-
try {
|
|
258
|
-
result = await toolRegistry.execute(call.name, parseToolArguments(call.arguments), ctx);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
result = {
|
|
261
|
-
success: false,
|
|
262
|
-
error: `Failed to parse arguments: ${(error as Error).message}`,
|
|
263
|
-
};
|
|
264
|
-
} finally {
|
|
265
|
-
link?.release();
|
|
266
|
-
}
|
|
267
|
-
/* The transcript was replaced while the call ran: another chat was opened from Chat
|
|
268
|
-
History, which stops the turn. The reply belongs to a request that is no longer there,
|
|
269
|
-
so writing it would land a stray `tool` message in the other conversation, and could
|
|
270
|
-
overwrite a restored record's result where a provider reuses call ids. */
|
|
271
|
-
if (turnUserId !== undefined && !chatState.messages.some((m) => m.id === turnUserId)) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// A call that suspends the turn on a person does not spend its work budget.
|
|
275
|
-
if (toolRegistry.getDefinition(call.name)?.interactive !== true) {
|
|
276
|
-
didWork = true;
|
|
277
|
-
}
|
|
278
|
-
reanchorBatch();
|
|
279
|
-
if (!result.success && result.error) {
|
|
280
|
-
allErrors.push(result.error);
|
|
281
|
-
}
|
|
282
|
-
const wrote = ledger.writes.slice(writesBefore).some((write) => write.ok);
|
|
283
|
-
if (result.success && result.summary && wrote) {
|
|
284
|
-
appliedSummaries.push(result.summary);
|
|
285
|
-
}
|
|
286
|
-
chatState.appendToolResult(id, result);
|
|
287
|
-
chatState.pushToolResultMessage(id, JSON.stringify(result));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (didWork) {
|
|
291
|
-
workRounds += 1;
|
|
292
|
-
}
|
|
293
|
-
/* A Stop that landed during the round's last call. Nothing is left to stop in this round, so
|
|
294
|
-
without this the loop opened the next one: a placeholder pushed after the author stopped,
|
|
295
|
-
and a request streamed on the aborted signal. */
|
|
296
|
-
if (signal?.aborted) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (workRounds < MAX_ROUNDS && round < MAX_TOTAL_ROUNDS) {
|
|
300
|
-
chatState.beginAssistantTurn();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/*
|
|
305
|
-
* The round cap. Surface the actual errors so the user knows what went wrong, not just a
|
|
306
|
-
* generic "I couldn't do it" — and, when anything was applied, say it as the ASSISTANT rather
|
|
307
|
-
* than as a failure (`ai.md` §3.2). A partial success is not a failure.
|
|
308
|
-
*/
|
|
309
|
-
const uniqueErrors = [...new Set(allErrors)];
|
|
310
|
-
const applied =
|
|
311
|
-
appliedSummaries.length > 0
|
|
312
|
-
? `\n\nChanges applied so far:\n${appliedSummaries.map((s) => `- ${s}`).join("\n")}`
|
|
313
|
-
: "";
|
|
314
|
-
const errors =
|
|
315
|
-
uniqueErrors.length > 0
|
|
316
|
-
? `\n\nErrors encountered:\n${uniqueErrors.map((e) => `- ${e}`).join("\n")}`
|
|
317
|
-
: "";
|
|
318
|
-
const tail =
|
|
319
|
-
`I ran out of tool-call rounds (${MAX_ROUNDS}) before finishing.${applied}${errors}` +
|
|
320
|
-
`\n\nYou can continue by sending another message, or try a more specific request.`;
|
|
321
|
-
if (appliedSummaries.length > 0) {
|
|
322
|
-
chatState.beginAssistantTurn();
|
|
323
|
-
chatState.appendDelta(tail);
|
|
324
|
-
chatState.finishStream("length");
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
chatState.setError(tail);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
await run.outcome;
|
|
328
133
|
} finally {
|
|
329
134
|
endBatch();
|
|
330
135
|
fileTurn(turnAnchor(chatState.messages, turnUserId) ?? "", ledger.writes);
|
|
331
136
|
}
|
|
332
137
|
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* A call's arguments as the object a tool takes.
|
|
336
|
-
*
|
|
337
|
-
* An empty string is no arguments. JSON that parses to something other than an object (`null`, an
|
|
338
|
-
* array, a number) is refused here with its own sentence, rather than reaching the registry, where
|
|
339
|
-
* a validator reading a property of `null` threw a message about the validator. The prefix is the
|
|
340
|
-
* one a JSON syntax error already carries, so the model reads both as one kind of mistake.
|
|
341
|
-
*
|
|
342
|
-
* @param {string} text - The call's accumulated argument text
|
|
343
|
-
* @returns {object}
|
|
344
|
-
* @throws {SyntaxError | TypeError} Caught by the loop and returned to the model as a tool error
|
|
345
|
-
*/
|
|
346
|
-
function parseToolArguments(text: string): object {
|
|
347
|
-
if (!text) {
|
|
348
|
-
return {};
|
|
349
|
-
}
|
|
350
|
-
const parsed = JSON.parse(text) as unknown;
|
|
351
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
352
|
-
const type = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
|
|
353
|
-
throw new TypeError(`arguments must be a JSON object, got ${type}`);
|
|
354
|
-
}
|
|
355
|
-
return parsed;
|
|
356
|
-
}
|
|
@@ -4,27 +4,29 @@
|
|
|
4
4
|
* A tool call's outcome is recorded twice while a turn runs: as the `result` on its
|
|
5
5
|
* `ToolCallRecord`, which the chip renders, and as the `tool` message that answers it, which the
|
|
6
6
|
* provider reads. The second is the one that must survive, because it is the one the wire carries,
|
|
7
|
-
* and it is the one that always did: until the turn became accountable (specs/ai.md §3.2) the first
|
|
8
|
-
* finished call (the loop looked for the record after closing the
|
|
9
|
-
* `result` was `null`: every restored chip rendered as still pending,
|
|
10
|
-
* one still open (specs/ai.md §3.4).
|
|
7
|
+
* and it is the one that always did: until the turn became accountable (specs/ai.md §3.2) the first
|
|
8
|
+
* was never populated for a finished call (the loop looked for the record after closing the
|
|
9
|
+
* stream), so every persisted `result` was `null`: every restored chip rendered as still pending,
|
|
10
|
+
* and an answered question as one still open (specs/ai.md §3.4).
|
|
11
11
|
*
|
|
12
12
|
* So the tool message is the single source. A save strips `result` from every record, which keeps
|
|
13
13
|
* the payload free of a second, disagreeing copy; a restore backfills each record from the reply
|
|
14
|
-
* that answers it. A question nothing answered keeps no result, which is what makes it render
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* that answers it. A question nothing answered keeps no result, which is what makes it render inert
|
|
15
|
+
* rather than as one still waiting; an ordinary call nothing answered never completed, and says
|
|
16
|
+
* so.
|
|
17
17
|
*
|
|
18
18
|
* **A seal is not an answer.** When a question was left open, the send path seals its request with
|
|
19
19
|
* a synthesized failure so the wire stays well-formed (`pruneOrphanToolMessages`). For an ordinary
|
|
20
20
|
* call that failure IS the outcome to show: it never completed. For a question it is the absence of
|
|
21
21
|
* an answer, so a restore leaves it without a result, and the chip draws the inert "still open when
|
|
22
|
-
* the session was reloaded" card rather than a failure.
|
|
22
|
+
* the session was reloaded" card rather than a failure. Either seal counts, the one for a call that
|
|
23
|
+
* was cut off mid-arguments as well (`isSealContent`).
|
|
23
24
|
*
|
|
24
|
-
* @docs studio/ai/chat
|
|
25
25
|
* @license MIT
|
|
26
|
+
* @docs studio/ai/chat
|
|
26
27
|
*/
|
|
27
28
|
|
|
29
|
+
import { isSealContent } from "@jxsuite/ai/messages";
|
|
28
30
|
import type { ToolResult } from "@jxsuite/ai/tools";
|
|
29
31
|
import type { PersistedMessage } from "./ai-session-store";
|
|
30
32
|
import { UNANSWERED_TOOL_RESULT } from "./context-manager";
|
|
@@ -114,7 +116,7 @@ export function backfillToolResults(messages: readonly PersistedMessage[]): Pers
|
|
|
114
116
|
const result = parseToolResult(reply.content);
|
|
115
117
|
if (typeof reply.toolCallId === "string" && result && !replies.has(reply.toolCallId)) {
|
|
116
118
|
replies.set(reply.toolCallId, result);
|
|
117
|
-
if (reply.content
|
|
119
|
+
if (isSealContent(reply.content)) {
|
|
118
120
|
sealed.add(reply.toolCallId);
|
|
119
121
|
}
|
|
120
122
|
}
|
|
@@ -30,7 +30,9 @@ import { clearCfConnection, getCfAccountId, getCfToken } from "../services/cf-se
|
|
|
30
30
|
import { clearAiProvider, getBaseUrl, hasOpenAiKey } from "../services/ai-settings";
|
|
31
31
|
import { preferredModel, resetModelCache } from "../services/ai-models";
|
|
32
32
|
import { getPlatform, hasPlatform } from "../platform";
|
|
33
|
-
import
|
|
33
|
+
import { getAccountEntries } from "../account-status";
|
|
34
|
+
import { runOfferedAction } from "../account/action-flow";
|
|
35
|
+
import type { AccountEntry, CfConnection, OfferedAction } from "../types";
|
|
34
36
|
|
|
35
37
|
/** One thing a row can do besides existing. */
|
|
36
38
|
export interface AccountAction {
|
|
@@ -228,11 +230,47 @@ function localCloudflareRecord(): AccountRecord {
|
|
|
228
230
|
};
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
// ─── The platform's own rows ─────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One row the platform adds (desktop.md §10.4) — a membership, a role, a plan; Studio does not know
|
|
237
|
+
* which, and draws it in the platform's words.
|
|
238
|
+
*
|
|
239
|
+
* Its id is namespaced so no platform can shadow one of Studio's own rows by naming it `github`.
|
|
240
|
+
* `revoke` does nothing: whatever the row describes is not a credential this app holds, so there is
|
|
241
|
+
* nothing here to forget, and its verbs are the actions the platform offers with it.
|
|
242
|
+
*/
|
|
243
|
+
function platformAction(action: OfferedAction): AccountAction {
|
|
244
|
+
const verb: AccountAction = {
|
|
245
|
+
id: action.id,
|
|
246
|
+
label: action.label,
|
|
247
|
+
run: async () => {
|
|
248
|
+
await runOfferedAction(action);
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
if (action.primary) {
|
|
252
|
+
verb.variant = "accent";
|
|
253
|
+
}
|
|
254
|
+
return verb;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function platformRecord(entry: AccountEntry): AccountRecord {
|
|
258
|
+
return {
|
|
259
|
+
actions: (entry.actions ?? []).map((action) => platformAction(action)),
|
|
260
|
+
connected: entry.connected ?? false,
|
|
261
|
+
detail: entry.detail,
|
|
262
|
+
id: `platform:${entry.id}`,
|
|
263
|
+
label: entry.label,
|
|
264
|
+
revoke: () => {},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
231
268
|
/**
|
|
232
|
-
* The three accounts, always all three.
|
|
269
|
+
* The three accounts, always all three — then any rows the platform adds.
|
|
233
270
|
*
|
|
234
271
|
* A disconnected account still gets a row: "you are not signed in to GitHub" is information, and a
|
|
235
|
-
* list that hides what is absent cannot answer the question the section exists to answer.
|
|
272
|
+
* list that hides what is absent cannot answer the question the section exists to answer. The
|
|
273
|
+
* platform's rows are the platform's to include or leave out; Studio adds none of its own.
|
|
236
274
|
*/
|
|
237
275
|
export function listAccounts(): AccountRecord[] {
|
|
238
276
|
const githubToken = githubTokenStored();
|
|
@@ -260,6 +298,7 @@ export function listAccounts(): AccountRecord[] {
|
|
|
260
298
|
revoke: clearAiProvider,
|
|
261
299
|
},
|
|
262
300
|
platformBrokersCf() ? brokeredCloudflareRecord() : localCloudflareRecord(),
|
|
301
|
+
...getAccountEntries().map((entry) => platformRecord(entry)),
|
|
263
302
|
];
|
|
264
303
|
}
|
|
265
304
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* this, `clearGithubToken()` had zero callers: signing out of GitHub was not expressible.
|
|
20
20
|
* - **Keyboard** — GENERATED from `commands/reference.ts`'s `shortcutReference()`, the same
|
|
21
21
|
* projection `docs/studio/interface/shortcuts.md` is built from. The registry is the one place a
|
|
22
|
-
* chord is declared, so the sheet cannot drift from the app or from the docs. Per
|
|
22
|
+
* chord is declared, so the sheet cannot drift from the app or from the docs. Per AGENTS.md's
|
|
23
23
|
* Screenshot Policy there is deliberately no screenshot of it — photographing generated content
|
|
24
24
|
* is a bug. It is searchable two ways, because there are two questions ("what is the shortcut for
|
|
25
25
|
* X" and "what did I just press"), and rebindable — as a LAYER over the registry
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
|
|
45
45
|
import { CHROME_THEMES, setChromeTheme, shell } from "../shell";
|
|
46
46
|
import { layerHost } from "../ui/layers";
|
|
47
|
+
import { hydrateAccountStatus } from "../account-status";
|
|
48
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
47
49
|
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
48
50
|
import { createManagedConnect } from "../ui/ai-managed-connect";
|
|
49
51
|
import { activeRegistry } from "../commands/active-registry";
|
|
@@ -563,6 +565,11 @@ export function openPreferences(section?: string): Promise<null> {
|
|
|
563
565
|
grant can lapse between visits, and a row that repaints the previous answer first is stating
|
|
564
566
|
something it has no reason to believe. */
|
|
565
567
|
resetCfConnectionCache();
|
|
568
|
+
/* And the platform's own account rows, for the same reason: whatever they report can change
|
|
569
|
+
between visits. They repaint when the answer lands; until then they show the last one. */
|
|
570
|
+
if (hasPlatform() && getPlatform().getAccountStatus) {
|
|
571
|
+
void hydrateAccountStatus().then(repaint);
|
|
572
|
+
}
|
|
566
573
|
_section = section !== undefined && isPreferencesSection(section) ? section : _section;
|
|
567
574
|
if (_surface) {
|
|
568
575
|
repaint();
|