acrylblends 1.1.0 → 1.2.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 +9 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +1980 -27
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +1 -1
- package/docs/framework/workshop/README.md +138 -152
- package/docs/framework/workshop/facilitator.md +69 -101
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +42 -7
- package/lib/cordis/door.js +204 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/src/host/channel.ts +86 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +53 -23
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +2 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ACRYL frame's view as the app page sees it (ide/acryl-blends-frame/src/client/frame-view.ts): the frame's canvas hides the app's own build-time
|
|
3
|
+
* controls in Build and shows what production shows in Preview. The page is same-origin with the frame, so it reads the handle the frame publishes on
|
|
4
|
+
* its window through `window.parent`; a page opened directly has no such parent and keeps its own behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const FRAME_VIEW_KEY: symbol = Symbol.for('acryl.frame.view')
|
|
8
|
+
export const FRAME_MODE_EVENT = 'acryl:frame-mode'
|
|
9
|
+
|
|
10
|
+
/** What the app page reads from `window.parent[FRAME_VIEW_KEY]`. */
|
|
11
|
+
export interface FrameViewHandle {
|
|
12
|
+
readonly mode: 'build' | 'preview'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The frame's view handle when this page runs in the frame's canvas.
|
|
17
|
+
* @param win - the page's window.
|
|
18
|
+
* @returns the handle, or undefined when the page is the top window or its parent is cross-origin.
|
|
19
|
+
*/
|
|
20
|
+
export function frameViewOf(win: Window = window): FrameViewHandle | undefined {
|
|
21
|
+
try {
|
|
22
|
+
if (win.parent === win) return undefined
|
|
23
|
+
return (win.parent as unknown as Record<symbol, FrameViewHandle | undefined>)[FRAME_VIEW_KEY]
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the panel is the production Assistant: the Host itself is the Assistant, or the app's production environment names it (ACRYL_ASSISTANT=on)
|
|
31
|
+
* and the page runs in the frame, whose Preview shows the app as production would.
|
|
32
|
+
* @param session - the panel's session from its Host.
|
|
33
|
+
* @param frame - the frame's view handle, if any.
|
|
34
|
+
*/
|
|
35
|
+
export function panelVariant(
|
|
36
|
+
session: { readonly assistant?: boolean; readonly assistantInProduction?: boolean },
|
|
37
|
+
frame: FrameViewHandle | undefined,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (session.assistant === true) return true
|
|
40
|
+
return frame !== undefined && session.assistantInProduction === true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the panel shows at all: on a page opened directly, yes; in the frame, only in Preview, and only as the Assistant. Build is the builder's
|
|
45
|
+
* screen, where the app's own panel has no place over the canvas.
|
|
46
|
+
* @param frame - the frame's view handle, if any.
|
|
47
|
+
* @param assistant - whether this panel is the production Assistant (see {@link panelVariant}).
|
|
48
|
+
*/
|
|
49
|
+
export function panelVisible(frame: FrameViewHandle | undefined, assistant: boolean): boolean {
|
|
50
|
+
return frame === undefined || (frame.mode === 'preview' && assistant)
|
|
51
|
+
}
|
|
@@ -1,13 +1,69 @@
|
|
|
1
1
|
// The panel in the page: a button at the corner opens a side panel (a shadow root, so the app's CSS cannot touch it and it cannot touch the app's). It asks the app's
|
|
2
|
-
// agent through POST
|
|
3
|
-
|
|
2
|
+
// agent through POST <prefix>/ask, shows the answer as it arrives, and reloads the page after a run that installed or removed a plugin. `panel.js` calls
|
|
3
|
+
// `start` with the prefix the app is served under (the root of its own server, or `/app` when the ACRYL frame hosts it).
|
|
4
|
+
import { describePageStep, describeTool } from './describe.ts'
|
|
5
|
+
import { createPickGeneration } from './driver-pick.ts'
|
|
6
|
+
import { FRAME_MODE_EVENT, frameViewOf, panelVariant, panelVisible } from './frame.ts'
|
|
4
7
|
import { renderMarkdown } from './markdown.ts'
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
let PREFIX = '/api/agent-panel'
|
|
10
|
+
/** Where the composer's draft is kept across a page reload or a remount. */
|
|
11
|
+
const DRAFT_KEY = 'agent-panel-draft'
|
|
12
|
+
/** Set when a visitor closed the Assistant, so it stays closed on the next page. */
|
|
13
|
+
const ASSISTANT_CLOSED_KEY = 'agent-panel-assistant-closed'
|
|
14
|
+
/** What the Assistant tells a visitor first: what it can do, and that it cannot change the app. */
|
|
15
|
+
export const ASSISTANT_NOTE =
|
|
16
|
+
'The assistant can use the app for you: open pages, filter, fill in forms. It cannot change the app.'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Why a submit does not send: nothing typed, or a run still going. The panel says it in its log instead of returning silently - a Send that does nothing and
|
|
20
|
+
* says nothing reads as a broken button, and one was observed doing exactly that in the frame gate (empty log, enabled Send, no run).
|
|
21
|
+
*/
|
|
22
|
+
export type SendRefusal = 'empty' | 'busy'
|
|
23
|
+
export function sendRefusal(message: string, following: boolean): SendRefusal | undefined {
|
|
24
|
+
if (message.trim() === '') return 'empty'
|
|
25
|
+
if (following) return 'busy'
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the page may be replaced to show a change a run made. Asked at the moment of reloading, not earlier: text in the composer or the focus in it
|
|
31
|
+
* belongs to the person, and a run still going must not lose its page either.
|
|
32
|
+
*/
|
|
33
|
+
export function reloadDecision(state: {
|
|
34
|
+
readonly value: string
|
|
35
|
+
readonly focused: boolean
|
|
36
|
+
readonly following: boolean
|
|
37
|
+
}): boolean {
|
|
38
|
+
return state.value.trim() === '' && !state.focused && !state.following
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Where the last panel-initiated reload is recorded, for the page after it and for a gate looking at a failure. */
|
|
42
|
+
export const RELOAD_MARKER = 'agent-panel-reload'
|
|
43
|
+
|
|
44
|
+
/** Replace the page, saying why first: a warning the page's own error report and the frame gate both see, and a marker the reloaded page keeps. */
|
|
45
|
+
function reloadPage(reason: string): void {
|
|
46
|
+
try {
|
|
47
|
+
sessionStorage.setItem(RELOAD_MARKER, JSON.stringify({ reason, at: Date.now() }))
|
|
48
|
+
} catch {
|
|
49
|
+
// No storage: the warning below still names it.
|
|
50
|
+
}
|
|
51
|
+
console.warn(`agent-panel: reloading the page (${reason})`)
|
|
52
|
+
location.reload()
|
|
53
|
+
}
|
|
7
54
|
|
|
8
55
|
interface Session {
|
|
9
56
|
token: string
|
|
10
|
-
|
|
57
|
+
/** The production Assistant: the app's own agent drives the page for a visitor. */
|
|
58
|
+
assistant?: boolean
|
|
59
|
+
/** The app's production environment names the Assistant (ACRYL_ASSISTANT=on); the frame's Preview shows it even though the frame host runs in development. */
|
|
60
|
+
assistantInProduction?: boolean
|
|
61
|
+
drivers: Array<{
|
|
62
|
+
name: string
|
|
63
|
+
kind: 'chat' | 'terminal'
|
|
64
|
+
/** A terminal driver says where its view module is and which prefix its endpoints live under (the driver's own plugin serves both). */
|
|
65
|
+
view?: { module: string; prefix: string }
|
|
66
|
+
}>
|
|
11
67
|
}
|
|
12
68
|
interface PanelEvent {
|
|
13
69
|
event: string
|
|
@@ -160,33 +216,86 @@ textarea { width: 100%; min-height: 64px; resize: vertical; padding: 8px; border
|
|
|
160
216
|
.row button, header button { padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer }
|
|
161
217
|
.row button.send { background: #1f2937; color: #fff; border-color: #1f2937 }
|
|
162
218
|
.row button:disabled { opacity: 0.45; cursor: not-allowed }
|
|
219
|
+
.notice, .steps, .tab { display: none }
|
|
220
|
+
/* The Assistant (production): docked on the right in the app's dark surface, one tab, a note, the conversation and one line to ask. */
|
|
221
|
+
:host([data-assistant]) section { width: min(330px, 100vw); background: #18181b; color: #e4e4e7; border-left: 1px solid #27272a; box-shadow: none }
|
|
222
|
+
:host([data-assistant]) header { padding: 0 12px; border-bottom: 1px solid #27272a; min-height: 38px }
|
|
223
|
+
:host([data-assistant]) header select, :host([data-assistant]) header strong { display: none }
|
|
224
|
+
:host([data-assistant]) .tab { display: block; flex: 1; align-self: stretch }
|
|
225
|
+
:host([data-assistant]) .tab span { display: inline-block; height: 100%; padding: 10px 6px 8px; font-size: 12.5px; font-weight: 600; color: #fafafa; border-bottom: 2px solid #5468ff }
|
|
226
|
+
:host([data-assistant]) header button.close { border: 0; background: none; color: #a1a1aa; font-size: 18px; padding: 2px 6px }
|
|
227
|
+
:host([data-assistant]) .notice { display: block; margin: 8px 10px 0; padding: 7px 9px; border-radius: 6px; background: #15291e; color: #a7f3c0; font-size: 12px; line-height: 1.5 }
|
|
228
|
+
:host([data-assistant]) #log { padding: 10px; gap: 10px }
|
|
229
|
+
:host([data-assistant]) .msg { background: #232326; color: #e4e4e7; border-radius: 8px; padding: 9px 11px; line-height: 1.45; max-width: 92% }
|
|
230
|
+
:host([data-assistant]) .me { background: #23264f; color: #eef0ff; align-self: flex-end }
|
|
231
|
+
:host([data-assistant]) .err { background: #3b1d1d; color: #fecaca }
|
|
232
|
+
:host([data-assistant]) .tool, :host([data-assistant]) .working { background: none; color: #a1a1aa; padding: 0 2px }
|
|
233
|
+
:host([data-assistant]) .steps { display: block; margin-top: 6px; font: 11.5px ui-monospace, monospace; color: #a1a1aa; white-space: normal }
|
|
234
|
+
:host([data-assistant]) .steps:empty { display: none }
|
|
235
|
+
:host([data-assistant]) .md code { background: #2e2e33; color: #e4e4e7 }
|
|
236
|
+
:host([data-assistant]) form { border-top: 0; padding: 10px }
|
|
237
|
+
:host([data-assistant]) textarea { min-height: 0; height: 38px; resize: none; background: #18181b; color: #e4e4e7; border: 1px solid #3f3f46; border-radius: 8px; padding: 9px 11px; font-size: 13px }
|
|
238
|
+
:host([data-assistant]) textarea:focus { outline: none; border-color: #5468ff }
|
|
239
|
+
:host([data-assistant]) .row { display: none }
|
|
240
|
+
:host([data-assistant][data-following]) .row { display: flex; justify-content: flex-end }
|
|
241
|
+
:host([data-assistant]) .row .send { display: none }
|
|
242
|
+
:host([data-assistant]) .row button { background: none; color: #e4e4e7; border-color: #3f3f46 }
|
|
243
|
+
:host([data-assistant]) button.open { background: #18181b; color: #fafafa; border: 1px solid #3f3f46 }
|
|
163
244
|
`
|
|
164
245
|
|
|
165
|
-
|
|
246
|
+
/** Start the panel against the app served at `prefix` (`/api/agent-panel` at the root, `/app/api/agent-panel` in the frame). */
|
|
247
|
+
export async function start(prefix: string = '/api/agent-panel'): Promise<void> {
|
|
248
|
+
PREFIX = prefix
|
|
249
|
+
// A panel that is mounted again replaces the previous one: one live host, and the readiness mark then belongs to the instance being wired now. A
|
|
250
|
+
// duplicated or reused host once let a submit reach a form that no live instance was listening to (the frame gate's empty-log failure, 2026-11-02).
|
|
251
|
+
// The previous instance's run socket goes with it, so its watch ends instead of writing into a detached log.
|
|
252
|
+
activeSocket?.close()
|
|
253
|
+
activeSocket = undefined
|
|
254
|
+
document.getElementById('agent-panel')?.remove()
|
|
166
255
|
const reply = await fetch(`${PREFIX}/session`)
|
|
167
256
|
if (!reply.ok) return
|
|
168
257
|
let session = (await reply.json()) as Session
|
|
169
258
|
|
|
259
|
+
// The frame's Build is the builder's screen, and its Preview shows the app as production would: the panel takes the Assistant's variant only where
|
|
260
|
+
// production has it, and it is hidden everywhere the canvas should be the app alone. A direct page (no frame) is unchanged.
|
|
261
|
+
const frame = frameViewOf()
|
|
262
|
+
const assistant = panelVariant(session, frame)
|
|
170
263
|
const host = document.createElement('div')
|
|
171
264
|
host.id = 'agent-panel'
|
|
265
|
+
if (assistant) host.dataset.assistant = ''
|
|
172
266
|
const shadow = host.attachShadow({ mode: 'open' })
|
|
173
267
|
shadow.innerHTML = `<style>${css}</style>
|
|
174
|
-
<button class="open" type="button"
|
|
175
|
-
<section aria-label="Agent panel">
|
|
176
|
-
<header><strong>Agent</strong><select aria-label="Agent">${session.drivers
|
|
268
|
+
<button class="open" type="button">${assistant ? 'Assistant' : 'Ask the agent'}</button>
|
|
269
|
+
<section aria-label="${assistant ? 'Assistant' : 'Agent panel'}">
|
|
270
|
+
<header><strong>Agent</strong><div class="tab"><span>Assistant</span></div><select aria-label="Agent">${session.drivers
|
|
177
271
|
.map((d) => `<option>${d.name}</option>`)
|
|
178
272
|
.join(
|
|
179
273
|
'',
|
|
180
274
|
)}</select><button class="termstop" type="button" hidden>Stop terminal</button><button class="close" type="button" aria-label="Close">×</button></header>
|
|
275
|
+
<p class="notice">${ASSISTANT_NOTE}</p>
|
|
181
276
|
<div id="log" role="log"></div>
|
|
182
277
|
<div id="term" hidden></div>
|
|
183
|
-
<form><textarea aria-label="Message" placeholder="What should the app do?"></textarea>
|
|
278
|
+
<form><textarea aria-label="Message" placeholder="${assistant ? 'Ask the assistant…' : 'What should the app do?'}"${assistant ? ' rows="1"' : ''}></textarea>
|
|
184
279
|
<div class="row"><button class="send" type="submit">Send</button><button class="stop" type="button" disabled>Stop</button></div></form>
|
|
185
280
|
</section>`
|
|
186
281
|
const $ = <T extends Element>(selector: string): T => shadow.querySelector<T>(selector)!
|
|
187
282
|
const panel = $<HTMLElement>('section')
|
|
188
283
|
const log = $<HTMLElement>('#log')
|
|
189
284
|
const input = $<HTMLTextAreaElement>('textarea')
|
|
285
|
+
// The composer survives the page being replaced (a run that changes files reloads the page, and a mount replaces this panel): the draft is kept
|
|
286
|
+
// beside the page, restored into this textarea before the form is wired, and cleared when a submit sends it.
|
|
287
|
+
try {
|
|
288
|
+
input.value = sessionStorage.getItem(DRAFT_KEY) ?? ''
|
|
289
|
+
} catch {
|
|
290
|
+
// No storage: the draft lives as long as this document.
|
|
291
|
+
}
|
|
292
|
+
input.addEventListener('input', () => {
|
|
293
|
+
try {
|
|
294
|
+
sessionStorage.setItem(DRAFT_KEY, input.value)
|
|
295
|
+
} catch {
|
|
296
|
+
// No storage: the draft lives as long as this document.
|
|
297
|
+
}
|
|
298
|
+
})
|
|
190
299
|
const send = $<HTMLButtonElement>('.send')
|
|
191
300
|
const stop = $<HTMLButtonElement>('.stop')
|
|
192
301
|
const driver = $<HTMLSelectElement>('select')
|
|
@@ -195,35 +304,52 @@ async function start(): Promise<void> {
|
|
|
195
304
|
termStop.addEventListener('click', () => void terminal?.stop())
|
|
196
305
|
const form = $<HTMLFormElement>('form')
|
|
197
306
|
document.body.append(host)
|
|
307
|
+
const applyFrame = (): void => {
|
|
308
|
+
host.style.display = panelVisible(frame, assistant) ? '' : 'none'
|
|
309
|
+
}
|
|
310
|
+
window.addEventListener(FRAME_MODE_EVENT, applyFrame)
|
|
311
|
+
applyFrame()
|
|
198
312
|
const kindOf = (name: string): 'chat' | 'terminal' =>
|
|
199
313
|
session.drivers.find((d) => d.name === name)?.kind ?? 'chat'
|
|
200
314
|
let terminal: { detach(): void; stop(): Promise<void> } | undefined
|
|
315
|
+
// A pick can land while an earlier pick's terminal is still loading (the page also restores a remembered terminal at
|
|
316
|
+
// start). Only the latest pick may apply its result; a stale terminal must never replace the form the person chose.
|
|
317
|
+
const picks = createPickGeneration()
|
|
201
318
|
// A terminal driver swaps the chat for the driver's own terminal (loaded from the plugin that provides it, only when asked for).
|
|
202
319
|
const choose = async (): Promise<void> => {
|
|
320
|
+
const generation = picks.begin()
|
|
321
|
+
const name = driver.value
|
|
203
322
|
terminal?.detach()
|
|
204
323
|
terminal = undefined
|
|
205
|
-
const isTerminal = kindOf(
|
|
324
|
+
const isTerminal = kindOf(name) === 'terminal'
|
|
206
325
|
log.hidden = isTerminal
|
|
207
326
|
form.hidden = isTerminal
|
|
208
327
|
term.hidden = !isTerminal
|
|
209
328
|
termStop.hidden = !isTerminal
|
|
210
329
|
if (!isTerminal) return
|
|
330
|
+
const selected = session.drivers.find((entry) => entry.name === name)
|
|
331
|
+
if (selected?.view === undefined) {
|
|
332
|
+
line('This terminal driver does not provide its view.', 'err')
|
|
333
|
+
return
|
|
334
|
+
}
|
|
211
335
|
try {
|
|
212
|
-
const url =
|
|
336
|
+
const url = selected.view.module
|
|
213
337
|
const { openTerminal } = (await import(url)) as {
|
|
214
338
|
openTerminal(options: {
|
|
215
339
|
container: HTMLElement
|
|
216
340
|
token: string
|
|
217
341
|
driver: string
|
|
342
|
+
prefix: string
|
|
218
343
|
status(text: string): void
|
|
219
344
|
onChange(change: { extension: string; action: string }): void
|
|
220
345
|
}): Promise<{ detach(): void; stop(): Promise<void> }>
|
|
221
346
|
}
|
|
222
347
|
await refreshSession()
|
|
223
|
-
|
|
348
|
+
const opened = await openTerminal({
|
|
224
349
|
container: term,
|
|
225
350
|
token: session.token,
|
|
226
|
-
driver:
|
|
351
|
+
driver: name,
|
|
352
|
+
prefix: selected.view.prefix,
|
|
227
353
|
status: (text) => line(text, 'tool'),
|
|
228
354
|
onChange: (change) => {
|
|
229
355
|
line(`plugin ${change.extension}: ${change.action}`, 'tool')
|
|
@@ -231,7 +357,13 @@ async function start(): Promise<void> {
|
|
|
231
357
|
setTimeout(() => location.reload(), 900)
|
|
232
358
|
},
|
|
233
359
|
})
|
|
360
|
+
if (!picks.isCurrent(generation)) {
|
|
361
|
+
opened.detach()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
terminal = opened
|
|
234
365
|
} catch (error) {
|
|
366
|
+
if (!picks.isCurrent(generation)) return
|
|
235
367
|
line(
|
|
236
368
|
`The terminal could not open: ${error instanceof Error ? error.message : String(error)}`,
|
|
237
369
|
'err',
|
|
@@ -252,9 +384,10 @@ async function start(): Promise<void> {
|
|
|
252
384
|
root.style.getPropertyValue(property),
|
|
253
385
|
root.style.getPropertyPriority(property),
|
|
254
386
|
])
|
|
387
|
+
// The panel's own width (the Assistant's is narrower than the builder's), so the page meets its edge with no gap.
|
|
255
388
|
root.style.setProperty(
|
|
256
389
|
'margin-right',
|
|
257
|
-
`${String(Math.
|
|
390
|
+
`${String(Math.ceil(panel.getBoundingClientRect().width))}px`,
|
|
258
391
|
'important',
|
|
259
392
|
)
|
|
260
393
|
} else {
|
|
@@ -266,14 +399,39 @@ async function start(): Promise<void> {
|
|
|
266
399
|
saved.clear()
|
|
267
400
|
}
|
|
268
401
|
}
|
|
269
|
-
const show = (open: boolean): void => {
|
|
402
|
+
const show = (open: boolean, focus = true): void => {
|
|
270
403
|
const was = panel.classList.contains('shown')
|
|
271
404
|
panel.classList.toggle('shown', open)
|
|
405
|
+
$<HTMLButtonElement>('button.open').hidden = open && assistant
|
|
272
406
|
if (open !== was) dock(open)
|
|
273
|
-
if (open) input.focus()
|
|
407
|
+
if (open && focus) input.focus()
|
|
408
|
+
// Other corner buttons of the page (the feedback button) stand clear of the panel.
|
|
409
|
+
window.dispatchEvent(new Event('agent-panel:layout'))
|
|
410
|
+
}
|
|
411
|
+
const remember = (closed: boolean): void => {
|
|
412
|
+
try {
|
|
413
|
+
if (closed) sessionStorage.setItem(ASSISTANT_CLOSED_KEY, '1')
|
|
414
|
+
else sessionStorage.removeItem(ASSISTANT_CLOSED_KEY)
|
|
415
|
+
} catch {
|
|
416
|
+
// No storage: the Assistant opens again on the next page.
|
|
417
|
+
}
|
|
274
418
|
}
|
|
275
|
-
$('button.open').addEventListener('click', () =>
|
|
276
|
-
|
|
419
|
+
$('button.open').addEventListener('click', () => {
|
|
420
|
+
show(true)
|
|
421
|
+
if (assistant) remember(false)
|
|
422
|
+
})
|
|
423
|
+
$('button.close').addEventListener('click', () => {
|
|
424
|
+
show(false)
|
|
425
|
+
if (assistant) remember(true)
|
|
426
|
+
})
|
|
427
|
+
// Enter asks (Shift+Enter is a new line): the Assistant has one line to ask in and no Send button.
|
|
428
|
+
if (assistant)
|
|
429
|
+
input.addEventListener('keydown', (event) => {
|
|
430
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
431
|
+
event.preventDefault()
|
|
432
|
+
form.requestSubmit()
|
|
433
|
+
}
|
|
434
|
+
})
|
|
277
435
|
const line = (text: string, kind = ''): HTMLElement => {
|
|
278
436
|
const el = document.createElement('div')
|
|
279
437
|
el.className = `msg ${kind}`
|
|
@@ -369,6 +527,10 @@ async function start(): Promise<void> {
|
|
|
369
527
|
let changed = false
|
|
370
528
|
let answer: HTMLElement | undefined
|
|
371
529
|
let answered = ''
|
|
530
|
+
// The Assistant's steps on the page, shown under its answer in one line, as the person reads what it did.
|
|
531
|
+
const steps: string[] = []
|
|
532
|
+
const stepsLine = document.createElement('div')
|
|
533
|
+
stepsLine.className = 'steps'
|
|
372
534
|
// A coding agent started headless says little until it is done, so the panel says it is working, and for how long (from when the run began, not from when this page loaded).
|
|
373
535
|
let started = Date.now()
|
|
374
536
|
let working: HTMLElement | undefined
|
|
@@ -384,6 +546,7 @@ async function start(): Promise<void> {
|
|
|
384
546
|
const begin = (): void => {
|
|
385
547
|
if (working !== undefined) return
|
|
386
548
|
following = true
|
|
549
|
+
host.dataset.following = '1'
|
|
387
550
|
send.disabled = true
|
|
388
551
|
stop.disabled = false
|
|
389
552
|
working = line('', 'working')
|
|
@@ -412,8 +575,17 @@ async function start(): Promise<void> {
|
|
|
412
575
|
answer ??= line('', 'md')
|
|
413
576
|
answered += String(data.text ?? '')
|
|
414
577
|
answer.replaceChildren(renderMarkdown(answered, document))
|
|
578
|
+
if (assistant) {
|
|
579
|
+
stepsLine.textContent = steps.join(' · ')
|
|
580
|
+
answer.append(stepsLine)
|
|
581
|
+
}
|
|
415
582
|
log.scrollTop = log.scrollHeight
|
|
416
583
|
} else if (event === 'tool') {
|
|
584
|
+
if (assistant) {
|
|
585
|
+
const step = describePageStep(data)
|
|
586
|
+
if (step !== undefined) steps.push(step)
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
417
589
|
const described = describeTool(data)
|
|
418
590
|
if (described !== undefined) line(described, 'tool')
|
|
419
591
|
} else if (event === 'change') {
|
|
@@ -432,22 +604,49 @@ async function start(): Promise<void> {
|
|
|
432
604
|
if (working !== undefined) {
|
|
433
605
|
working.remove()
|
|
434
606
|
following = false
|
|
607
|
+
delete host.dataset.following
|
|
435
608
|
send.disabled = false
|
|
436
609
|
stop.disabled = true
|
|
437
610
|
}
|
|
438
611
|
}
|
|
439
|
-
// A plugin was installed or removed while this page watched: the new feature is served by the running app, so the page only needs to be loaded again
|
|
440
|
-
//
|
|
441
|
-
|
|
612
|
+
// A plugin was installed or removed while this page watched: the new feature is served by the running app, so the page only needs to be loaded again.
|
|
613
|
+
// The panel lives inside the app's own page (it cannot refresh the frame's canvas from here), so the page reload is the app's refresh - but the decision
|
|
614
|
+
// to reload is made at the moment of reloading, never on a check made a second earlier: a person (or the frame gate) can start typing in between, and a
|
|
615
|
+
// reload then replaces the page under their text (the frame gate's empty-composer dump, 2026-11-02).
|
|
616
|
+
if (live && changed) {
|
|
442
617
|
line('Reloading the page to show it.', 'tool')
|
|
443
|
-
setTimeout(() =>
|
|
618
|
+
setTimeout(() => {
|
|
619
|
+
if (
|
|
620
|
+
reloadDecision({
|
|
621
|
+
value: input.value,
|
|
622
|
+
focused: host.shadowRoot?.activeElement === input,
|
|
623
|
+
following,
|
|
624
|
+
})
|
|
625
|
+
)
|
|
626
|
+
reloadPage('a plugin changed')
|
|
627
|
+
}, 900)
|
|
444
628
|
}
|
|
445
629
|
}
|
|
446
630
|
form.addEventListener('submit', async (submitted) => {
|
|
447
631
|
submitted.preventDefault()
|
|
632
|
+
const refusal = sendRefusal(input.value, following)
|
|
633
|
+
if (refusal !== undefined) {
|
|
634
|
+
line(
|
|
635
|
+
refusal === 'empty' ? 'Type a message first.' : 'A run is already going; stop it first.',
|
|
636
|
+
'err',
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
}
|
|
448
640
|
const message = input.value.trim()
|
|
449
|
-
if (message === '' || following) return
|
|
450
641
|
input.value = ''
|
|
642
|
+
try {
|
|
643
|
+
sessionStorage.removeItem(DRAFT_KEY)
|
|
644
|
+
} catch {
|
|
645
|
+
// No storage.
|
|
646
|
+
}
|
|
647
|
+
// The message is sent: the composer is empty and the person is not typing, so it gives the focus back. A run that installs a plugin reloads the page
|
|
648
|
+
// afterwards, and that reload must not be refused for a focus nobody is using.
|
|
649
|
+
input.blur()
|
|
451
650
|
line(message, 'me')
|
|
452
651
|
try {
|
|
453
652
|
const run = await openRun({ t: 'ask', message, driver: driver.value, ...doorPage() })
|
|
@@ -461,7 +660,22 @@ async function start(): Promise<void> {
|
|
|
461
660
|
line(`The request failed: ${error instanceof Error ? error.message : String(error)}`, 'err')
|
|
462
661
|
}
|
|
463
662
|
})
|
|
464
|
-
// The
|
|
663
|
+
// The Assistant is open beside the page unless the visitor closed it.
|
|
664
|
+
if (assistant) {
|
|
665
|
+
let closed = false
|
|
666
|
+
try {
|
|
667
|
+
closed = sessionStorage.getItem(ASSISTANT_CLOSED_KEY) === '1'
|
|
668
|
+
} catch {
|
|
669
|
+
// No storage: open.
|
|
670
|
+
}
|
|
671
|
+
show(!closed, false)
|
|
672
|
+
}
|
|
673
|
+
// The form is wired: a script (the frame gate included) waits for this instead of guessing that the panel can be submitted, and a panel that never got
|
|
674
|
+
// this far says so by the mark's absence - together with the visible guards above, no submit can vanish without a trace.
|
|
675
|
+
host.dataset.ready = '1'
|
|
676
|
+
// The latest run, if there is one: shown again after a reload (its question, its events), and followed if it is still going. The server records a run
|
|
677
|
+
// before it answers the ask that started it (startRun sets the current run, then follow answers), so a page that asks after that answer always finds
|
|
678
|
+
// it - no client poll is needed. A run the app server itself restarted away (a file change the run made) no longer exists for that server to show.
|
|
465
679
|
try {
|
|
466
680
|
const latest = await openRun({ t: 'follow' })
|
|
467
681
|
if (latest !== undefined && !('error' in latest)) void watch(latest, false)
|
|
@@ -482,5 +696,3 @@ async function start(): Promise<void> {
|
|
|
482
696
|
// No storage: nothing to show again.
|
|
483
697
|
}
|
|
484
698
|
}
|
|
485
|
-
|
|
486
|
-
void start()
|
|
@@ -16,8 +16,8 @@ export type ChatEvent =
|
|
|
16
16
|
export interface ChatDriver {
|
|
17
17
|
readonly kind: 'chat'
|
|
18
18
|
readonly name: string
|
|
19
|
-
/** Runs one message. Aborting `signal` stops the run. */
|
|
20
|
-
start(options: { message: string; signal: AbortSignal }): AsyncIterable<ChatEvent>
|
|
19
|
+
/** Runs one message. Aborting `signal` stops the run. `page` is the door's id of the page whose panel asked, when it sent one: a driver that uses the page drives that one. */
|
|
20
|
+
start(options: { message: string; signal: AbortSignal; page?: string }): AsyncIterable<ChatEvent>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** A live pseudo-terminal session. */
|
|
@@ -34,6 +34,11 @@ export interface TerminalSession {
|
|
|
34
34
|
export interface TerminalDriver {
|
|
35
35
|
readonly kind: 'terminal'
|
|
36
36
|
readonly name: string
|
|
37
|
+
/**
|
|
38
|
+
* Where the panel loads the terminal view from: the browser module that draws it, and the URL prefix the view's endpoints live under. The driver
|
|
39
|
+
* plugin serves both, so the view follows wherever the app is mounted (the root of its own server, or the prefix the ACRYL frame serves it at).
|
|
40
|
+
*/
|
|
41
|
+
readonly view: { readonly module: string; readonly prefix: string }
|
|
37
42
|
start(options: {
|
|
38
43
|
cwd: string
|
|
39
44
|
env: NodeJS.ProcessEnv
|
|
@@ -112,8 +112,11 @@ export function agentDriver(ctx: Context): ChatDriver {
|
|
|
112
112
|
return {
|
|
113
113
|
kind: 'chat',
|
|
114
114
|
name: 'agent',
|
|
115
|
-
async *start({ message, signal }) {
|
|
116
|
-
const result = await ctx.agent.run(message, {
|
|
115
|
+
async *start({ message, signal, page }) {
|
|
116
|
+
const result = await ctx.agent.run(message, {
|
|
117
|
+
signal,
|
|
118
|
+
...(page === undefined ? {} : { page }),
|
|
119
|
+
})
|
|
117
120
|
for (const call of result.trace)
|
|
118
121
|
yield {
|
|
119
122
|
event: 'tool',
|