@aigne/afs-ui 1.11.0-beta.12

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.
Files changed (196) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/aup-protocol.cjs +235 -0
  5. package/dist/aup-protocol.d.cts +78 -0
  6. package/dist/aup-protocol.d.cts.map +1 -0
  7. package/dist/aup-protocol.d.mts +78 -0
  8. package/dist/aup-protocol.d.mts.map +1 -0
  9. package/dist/aup-protocol.mjs +235 -0
  10. package/dist/aup-protocol.mjs.map +1 -0
  11. package/dist/aup-registry.cjs +2489 -0
  12. package/dist/aup-registry.mjs +2487 -0
  13. package/dist/aup-registry.mjs.map +1 -0
  14. package/dist/aup-spec.cjs +1467 -0
  15. package/dist/aup-spec.mjs +1466 -0
  16. package/dist/aup-spec.mjs.map +1 -0
  17. package/dist/aup-types.cjs +165 -0
  18. package/dist/aup-types.d.cts +157 -0
  19. package/dist/aup-types.d.cts.map +1 -0
  20. package/dist/aup-types.d.mts +157 -0
  21. package/dist/aup-types.d.mts.map +1 -0
  22. package/dist/aup-types.mjs +157 -0
  23. package/dist/aup-types.mjs.map +1 -0
  24. package/dist/backend.cjs +14 -0
  25. package/dist/backend.d.cts +104 -0
  26. package/dist/backend.d.cts.map +1 -0
  27. package/dist/backend.d.mts +104 -0
  28. package/dist/backend.d.mts.map +1 -0
  29. package/dist/backend.mjs +13 -0
  30. package/dist/backend.mjs.map +1 -0
  31. package/dist/degradation.cjs +85 -0
  32. package/dist/degradation.d.cts +17 -0
  33. package/dist/degradation.d.cts.map +1 -0
  34. package/dist/degradation.d.mts +17 -0
  35. package/dist/degradation.d.mts.map +1 -0
  36. package/dist/degradation.mjs +84 -0
  37. package/dist/degradation.mjs.map +1 -0
  38. package/dist/index.cjs +36 -0
  39. package/dist/index.d.cts +12 -0
  40. package/dist/index.d.mts +12 -0
  41. package/dist/index.mjs +13 -0
  42. package/dist/runtime.cjs +117 -0
  43. package/dist/runtime.d.cts +59 -0
  44. package/dist/runtime.d.cts.map +1 -0
  45. package/dist/runtime.d.mts +59 -0
  46. package/dist/runtime.d.mts.map +1 -0
  47. package/dist/runtime.mjs +118 -0
  48. package/dist/runtime.mjs.map +1 -0
  49. package/dist/session.cjs +159 -0
  50. package/dist/session.d.cts +80 -0
  51. package/dist/session.d.cts.map +1 -0
  52. package/dist/session.d.mts +80 -0
  53. package/dist/session.d.mts.map +1 -0
  54. package/dist/session.mjs +159 -0
  55. package/dist/session.mjs.map +1 -0
  56. package/dist/snapshot.cjs +162 -0
  57. package/dist/snapshot.mjs +163 -0
  58. package/dist/snapshot.mjs.map +1 -0
  59. package/dist/term-page.cjs +264 -0
  60. package/dist/term-page.mjs +264 -0
  61. package/dist/term-page.mjs.map +1 -0
  62. package/dist/term.cjs +295 -0
  63. package/dist/term.d.cts +84 -0
  64. package/dist/term.d.cts.map +1 -0
  65. package/dist/term.d.mts +84 -0
  66. package/dist/term.d.mts.map +1 -0
  67. package/dist/term.mjs +296 -0
  68. package/dist/term.mjs.map +1 -0
  69. package/dist/tty.cjs +136 -0
  70. package/dist/tty.d.cts +53 -0
  71. package/dist/tty.d.cts.map +1 -0
  72. package/dist/tty.d.mts +53 -0
  73. package/dist/tty.d.mts.map +1 -0
  74. package/dist/tty.mjs +135 -0
  75. package/dist/tty.mjs.map +1 -0
  76. package/dist/ui-provider.cjs +4615 -0
  77. package/dist/ui-provider.d.cts +307 -0
  78. package/dist/ui-provider.d.cts.map +1 -0
  79. package/dist/ui-provider.d.mts +307 -0
  80. package/dist/ui-provider.d.mts.map +1 -0
  81. package/dist/ui-provider.mjs +4616 -0
  82. package/dist/ui-provider.mjs.map +1 -0
  83. package/dist/web-page/core.cjs +1388 -0
  84. package/dist/web-page/core.mjs +1387 -0
  85. package/dist/web-page/core.mjs.map +1 -0
  86. package/dist/web-page/css.cjs +1699 -0
  87. package/dist/web-page/css.mjs +1698 -0
  88. package/dist/web-page/css.mjs.map +1 -0
  89. package/dist/web-page/icons.cjs +248 -0
  90. package/dist/web-page/icons.mjs +248 -0
  91. package/dist/web-page/icons.mjs.map +1 -0
  92. package/dist/web-page/overlay-themes.cjs +514 -0
  93. package/dist/web-page/overlay-themes.mjs +513 -0
  94. package/dist/web-page/overlay-themes.mjs.map +1 -0
  95. package/dist/web-page/renderers/action.cjs +72 -0
  96. package/dist/web-page/renderers/action.mjs +72 -0
  97. package/dist/web-page/renderers/action.mjs.map +1 -0
  98. package/dist/web-page/renderers/broadcast.cjs +160 -0
  99. package/dist/web-page/renderers/broadcast.mjs +160 -0
  100. package/dist/web-page/renderers/broadcast.mjs.map +1 -0
  101. package/dist/web-page/renderers/calendar.cjs +137 -0
  102. package/dist/web-page/renderers/calendar.mjs +137 -0
  103. package/dist/web-page/renderers/calendar.mjs.map +1 -0
  104. package/dist/web-page/renderers/canvas.cjs +173 -0
  105. package/dist/web-page/renderers/canvas.mjs +173 -0
  106. package/dist/web-page/renderers/canvas.mjs.map +1 -0
  107. package/dist/web-page/renderers/cdn-loader.cjs +25 -0
  108. package/dist/web-page/renderers/cdn-loader.mjs +25 -0
  109. package/dist/web-page/renderers/cdn-loader.mjs.map +1 -0
  110. package/dist/web-page/renderers/chart.cjs +101 -0
  111. package/dist/web-page/renderers/chart.mjs +101 -0
  112. package/dist/web-page/renderers/chart.mjs.map +1 -0
  113. package/dist/web-page/renderers/deck.cjs +390 -0
  114. package/dist/web-page/renderers/deck.mjs +390 -0
  115. package/dist/web-page/renderers/deck.mjs.map +1 -0
  116. package/dist/web-page/renderers/device.cjs +1015 -0
  117. package/dist/web-page/renderers/device.mjs +1015 -0
  118. package/dist/web-page/renderers/device.mjs.map +1 -0
  119. package/dist/web-page/renderers/editor.cjs +127 -0
  120. package/dist/web-page/renderers/editor.mjs +127 -0
  121. package/dist/web-page/renderers/editor.mjs.map +1 -0
  122. package/dist/web-page/renderers/finance-chart.cjs +178 -0
  123. package/dist/web-page/renderers/finance-chart.mjs +178 -0
  124. package/dist/web-page/renderers/finance-chart.mjs.map +1 -0
  125. package/dist/web-page/renderers/frame.cjs +274 -0
  126. package/dist/web-page/renderers/frame.mjs +274 -0
  127. package/dist/web-page/renderers/frame.mjs.map +1 -0
  128. package/dist/web-page/renderers/globe.cjs +119 -0
  129. package/dist/web-page/renderers/globe.mjs +119 -0
  130. package/dist/web-page/renderers/globe.mjs.map +1 -0
  131. package/dist/web-page/renderers/input.cjs +137 -0
  132. package/dist/web-page/renderers/input.mjs +137 -0
  133. package/dist/web-page/renderers/input.mjs.map +1 -0
  134. package/dist/web-page/renderers/list.cjs +1243 -0
  135. package/dist/web-page/renderers/list.mjs +1243 -0
  136. package/dist/web-page/renderers/list.mjs.map +1 -0
  137. package/dist/web-page/renderers/map.cjs +126 -0
  138. package/dist/web-page/renderers/map.mjs +126 -0
  139. package/dist/web-page/renderers/map.mjs.map +1 -0
  140. package/dist/web-page/renderers/media.cjs +106 -0
  141. package/dist/web-page/renderers/media.mjs +106 -0
  142. package/dist/web-page/renderers/media.mjs.map +1 -0
  143. package/dist/web-page/renderers/moonphase.cjs +105 -0
  144. package/dist/web-page/renderers/moonphase.mjs +105 -0
  145. package/dist/web-page/renderers/moonphase.mjs.map +1 -0
  146. package/dist/web-page/renderers/natal-chart.cjs +222 -0
  147. package/dist/web-page/renderers/natal-chart.mjs +222 -0
  148. package/dist/web-page/renderers/natal-chart.mjs.map +1 -0
  149. package/dist/web-page/renderers/overlay.cjs +531 -0
  150. package/dist/web-page/renderers/overlay.mjs +531 -0
  151. package/dist/web-page/renderers/overlay.mjs.map +1 -0
  152. package/dist/web-page/renderers/table.cjs +74 -0
  153. package/dist/web-page/renderers/table.mjs +74 -0
  154. package/dist/web-page/renderers/table.mjs.map +1 -0
  155. package/dist/web-page/renderers/terminal.cjs +30 -0
  156. package/dist/web-page/renderers/terminal.mjs +30 -0
  157. package/dist/web-page/renderers/terminal.mjs.map +1 -0
  158. package/dist/web-page/renderers/text.cjs +109 -0
  159. package/dist/web-page/renderers/text.mjs +109 -0
  160. package/dist/web-page/renderers/text.mjs.map +1 -0
  161. package/dist/web-page/renderers/ticker.cjs +133 -0
  162. package/dist/web-page/renderers/ticker.mjs +133 -0
  163. package/dist/web-page/renderers/ticker.mjs.map +1 -0
  164. package/dist/web-page/renderers/time.cjs +69 -0
  165. package/dist/web-page/renderers/time.mjs +69 -0
  166. package/dist/web-page/renderers/time.mjs.map +1 -0
  167. package/dist/web-page/renderers/unknown.cjs +20 -0
  168. package/dist/web-page/renderers/unknown.mjs +20 -0
  169. package/dist/web-page/renderers/unknown.mjs.map +1 -0
  170. package/dist/web-page/renderers/view.cjs +161 -0
  171. package/dist/web-page/renderers/view.mjs +161 -0
  172. package/dist/web-page/renderers/view.mjs.map +1 -0
  173. package/dist/web-page/renderers/wm.cjs +669 -0
  174. package/dist/web-page/renderers/wm.mjs +669 -0
  175. package/dist/web-page/renderers/wm.mjs.map +1 -0
  176. package/dist/web-page/skeleton.cjs +103 -0
  177. package/dist/web-page/skeleton.mjs +103 -0
  178. package/dist/web-page/skeleton.mjs.map +1 -0
  179. package/dist/web-page.cjs +114 -0
  180. package/dist/web-page.d.cts +19 -0
  181. package/dist/web-page.d.cts.map +1 -0
  182. package/dist/web-page.d.mts +19 -0
  183. package/dist/web-page.d.mts.map +1 -0
  184. package/dist/web-page.mjs +115 -0
  185. package/dist/web-page.mjs.map +1 -0
  186. package/dist/web.cjs +827 -0
  187. package/dist/web.d.cts +144 -0
  188. package/dist/web.d.cts.map +1 -0
  189. package/dist/web.d.mts +144 -0
  190. package/dist/web.d.mts.map +1 -0
  191. package/dist/web.mjs +828 -0
  192. package/dist/web.mjs.map +1 -0
  193. package/dist/wm-state.cjs +172 -0
  194. package/dist/wm-state.mjs +171 -0
  195. package/dist/wm-state.mjs.map +1 -0
  196. package/package.json +59 -0
@@ -0,0 +1,4616 @@
1
+ import { DEVICE_CAPS_TERM, DEVICE_CAPS_TTY, DEVICE_CAPS_WEB_FULL, validateDeviceCaps, validateNode } from "./aup-types.mjs";
2
+ import { AUPNodeStore, AUPSceneManager } from "./aup-protocol.mjs";
3
+ import { isAUPTransport, isSessionAware } from "./backend.mjs";
4
+ import { degradeTree } from "./degradation.mjs";
5
+ import { SessionManager } from "./session.mjs";
6
+ import { TTYBackend } from "./tty.mjs";
7
+ import { TermBackend } from "./term.mjs";
8
+ import { OVERLAY_THEMES, PRIMITIVES, THEMES } from "./aup-registry.mjs";
9
+ import { AUP_EXAMPLES, AUP_SPEC } from "./aup-spec.mjs";
10
+ import { generateSnapshot } from "./snapshot.mjs";
11
+ import { WebBackend } from "./web.mjs";
12
+ import { PANEL_PRESETS, autoArrange } from "./wm-state.mjs";
13
+ import { __decorate } from "./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs";
14
+ import { joinURL } from "ufo";
15
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { AFSNotFoundError } from "@aigne/afs";
18
+ import { AFSBaseProvider, Actions, Delete, Explain, List, Meta, Read, Stat, Write } from "@aigne/afs/provider";
19
+ import { z } from "zod";
20
+
21
+ //#region src/ui-provider.ts
22
+ const VALID_ACCESS_LEVELS = new Set([
23
+ "guest",
24
+ "link-only",
25
+ "user",
26
+ "admin"
27
+ ]);
28
+ const VALID_SHARING_MODES = new Set(["static", "live"]);
29
+ const afsUISchema = z.object({
30
+ backend: z.enum([
31
+ "tty",
32
+ "web",
33
+ "term"
34
+ ]).default("tty"),
35
+ port: z.coerce.number().optional()
36
+ });
37
+ var AFSUIProvider = class extends AFSBaseProvider {
38
+ name;
39
+ description;
40
+ accessMode = "readwrite";
41
+ backend;
42
+ inputTimeout;
43
+ pages = /* @__PURE__ */ new Map();
44
+ pagesDir;
45
+ initPromise = null;
46
+ serverUrl = null;
47
+ /** Web sharing entries: slug -> SharingEntry */
48
+ sharingEntries = /* @__PURE__ */ new Map();
49
+ /** Session management */
50
+ sessions = new SessionManager();
51
+ /** Per-session AUP node stores */
52
+ aupStores = /* @__PURE__ */ new Map();
53
+ /** Per-session AUP scene managers (for stage-to-live dual buffer) */
54
+ aupManagers = /* @__PURE__ */ new Map();
55
+ /** AFS root reference for auto-dispatching AUP events to afs.exec() */
56
+ afsRoot = null;
57
+ /** Endpoint type derived from backend (tty, web, term) */
58
+ endpointType;
59
+ /** External callback for AUP events — allows app code to handle navigation etc. */
60
+ onAupEvent;
61
+ constructor(options = {}) {
62
+ super();
63
+ this.name = options.name ?? "ui";
64
+ this.description = options.description ?? "Interactive UI device";
65
+ this.inputTimeout = options.inputTimeout ?? 0;
66
+ this.pagesDir = options.pagesDir ?? "";
67
+ if (typeof options.backend === "object") {
68
+ this.backend = options.backend;
69
+ if ("url" in this.backend && typeof this.backend.url === "string") this.serverUrl = this.backend.url;
70
+ } else if (options.backend === "web") {
71
+ const webOpts = { ...options.webOptions };
72
+ if (options.port != null) webOpts.port = options.port;
73
+ const web = new WebBackend(webOpts);
74
+ this.backend = web;
75
+ this.initPromise = web.listen().then(() => {
76
+ this.serverUrl = web.url;
77
+ });
78
+ } else if (options.backend === "term") {
79
+ const termOpts = { ...options.termOptions };
80
+ if (options.port != null) termOpts.port = options.port;
81
+ const term = new TermBackend(termOpts);
82
+ this.backend = term;
83
+ this.initPromise = term.listen().then(() => {
84
+ this.serverUrl = term.url;
85
+ });
86
+ } else this.backend = new TTYBackend({
87
+ ...options.ttyOptions,
88
+ inputTimeout: this.inputTimeout
89
+ });
90
+ this.endpointType = this.backend.type;
91
+ if (isSessionAware(this.backend)) {
92
+ const defaultCaps = this.backend.type === "term" ? DEVICE_CAPS_TERM : DEVICE_CAPS_WEB_FULL;
93
+ this.backend.setSessionFactory((endpoint, requestedSid, requestedSessionToken, rawCaps) => {
94
+ const caps = rawCaps && !validateDeviceCaps(rawCaps) ? rawCaps : defaultCaps;
95
+ if (requestedSid && this.sessions.has(requestedSid)) {
96
+ const existing = this.sessions.get(requestedSid);
97
+ if (requestedSessionToken && requestedSessionToken === existing.resumeToken) {
98
+ existing.touch();
99
+ existing.setDeviceCaps(caps);
100
+ return {
101
+ sessionId: requestedSid,
102
+ sessionToken: existing.resumeToken
103
+ };
104
+ }
105
+ const rotated = this.sessions.create(endpoint, caps);
106
+ return {
107
+ sessionId: rotated.id,
108
+ sessionToken: rotated.resumeToken
109
+ };
110
+ }
111
+ const fresh = this.sessions.create(endpoint, caps);
112
+ return {
113
+ sessionId: fresh.id,
114
+ sessionToken: fresh.resumeToken
115
+ };
116
+ });
117
+ }
118
+ if (isAUPTransport(this.backend)) {
119
+ this.backend.setAupEventHandler(async (msg, sessionId, channelId) => {
120
+ const storeId = channelId ? this.channelAupKey(channelId) : sessionId;
121
+ if (!storeId) throw new Error("No session or channel for AUP event");
122
+ return this.handleAupEvent(storeId, msg.nodeId, msg.event, msg.data);
123
+ });
124
+ this.backend.setChannelJoinHandler((channelId, send) => {
125
+ const store = this.aupStores.get(this.channelAupKey(channelId));
126
+ const root = store?.getRoot();
127
+ if (store && root) {
128
+ const opts = store.renderOptions;
129
+ const msg = {
130
+ type: "aup",
131
+ action: "render",
132
+ root,
133
+ treeVersion: store.version
134
+ };
135
+ if (opts.fullPage) msg.fullPage = true;
136
+ if (opts.chrome) msg.chrome = true;
137
+ if (opts.theme) msg.theme = opts.theme;
138
+ if (opts.style) msg.style = opts.style;
139
+ if (opts.locale) msg.locale = opts.locale;
140
+ send(msg);
141
+ }
142
+ });
143
+ this.backend.setSessionJoinHandler((sessionId, clientVersion, send) => {
144
+ const session = this.sessions.has(sessionId) ? this.sessions.get(sessionId) : null;
145
+ const activeScene = this.aupManagers.get(sessionId)?.getActiveScene();
146
+ if (activeScene) {
147
+ const { store: store$1 } = activeScene;
148
+ const root$1 = store$1.getRoot();
149
+ if (root$1) {
150
+ if (clientVersion > 0 && clientVersion >= store$1.version) return;
151
+ const degraded$1 = session ? this.degradeForSession(root$1, session) : root$1;
152
+ const opts$1 = store$1.renderOptions;
153
+ const msg$1 = {
154
+ type: "aup",
155
+ action: "stage",
156
+ sceneId: activeScene.sceneId,
157
+ root: degraded$1,
158
+ treeVersion: store$1.version
159
+ };
160
+ if (opts$1.fullPage) msg$1.fullPage = true;
161
+ if (opts$1.chrome) msg$1.chrome = true;
162
+ if (opts$1.theme) msg$1.theme = opts$1.theme;
163
+ if (opts$1.style) msg$1.style = opts$1.style;
164
+ if (opts$1.locale) msg$1.locale = opts$1.locale;
165
+ send(msg$1);
166
+ send({
167
+ type: "aup",
168
+ action: "take",
169
+ sceneId: activeScene.sceneId
170
+ });
171
+ return;
172
+ }
173
+ }
174
+ const store = this.aupStores.get(sessionId);
175
+ if (!store) return;
176
+ const root = store.getRoot();
177
+ if (!root) return;
178
+ if (clientVersion > 0 && clientVersion >= store.version) return;
179
+ const degraded = session ? this.degradeForSession(root, session) : root;
180
+ const opts = store.renderOptions;
181
+ const msg = {
182
+ type: "aup",
183
+ action: "render",
184
+ root: degraded,
185
+ treeVersion: store.version
186
+ };
187
+ if (opts.fullPage) msg.fullPage = true;
188
+ if (opts.chrome) msg.chrome = true;
189
+ if (opts.theme) msg.theme = opts.theme;
190
+ if (opts.style) msg.style = opts.style;
191
+ if (opts.locale) msg.locale = opts.locale;
192
+ send(msg);
193
+ });
194
+ this.backend.setPageResolver(async (pageId, sessionId, sessionToken) => {
195
+ if (sessionId && sessionToken && this.sessions.has(sessionId)) {
196
+ const session = this.sessions.get(sessionId);
197
+ if (session.resumeToken === sessionToken) {
198
+ const page$1 = session.getPage(pageId);
199
+ if (page$1) return {
200
+ content: page$1.content,
201
+ format: page$1.format
202
+ };
203
+ }
204
+ }
205
+ const page = this.pages.get(pageId);
206
+ if (page) return {
207
+ content: page.content,
208
+ format: page.format
209
+ };
210
+ return null;
211
+ });
212
+ if ("setSnapshotResolver" in this.backend) this.backend.setSnapshotResolver((slug) => {
213
+ const entry = this.sharingEntries.get(slug);
214
+ if (!entry?.snapshot) return null;
215
+ return entry.snapshot;
216
+ });
217
+ }
218
+ if (this.endpointType === "tty") this.sessions.create("tty", DEVICE_CAPS_TTY);
219
+ }
220
+ /** Inject AFS root into AUP transport for browser AFS proxy + discover pages dir. */
221
+ onMount(root, _mountPath) {
222
+ this.afsRoot = root;
223
+ if (isAUPTransport(this.backend)) this.backend.setAFS(root);
224
+ if (this.pagesDir !== false) this.discoverAndLoadPages(root);
225
+ }
226
+ /** Discover workspace path from AFS root meta, then load persisted pages. */
227
+ async discoverAndLoadPages(root) {
228
+ try {
229
+ if (this.pagesDir) {
230
+ this.loadPagesFromDisk(this.pagesDir);
231
+ return;
232
+ }
233
+ if (root.read) try {
234
+ const storagePath = (await root.read("/.meta"))?.data?.content?.storagePath;
235
+ if (storagePath) {
236
+ this.pagesDir = join(storagePath, ".afs-ui", "pages");
237
+ this.loadPagesFromDisk(this.pagesDir);
238
+ return;
239
+ }
240
+ } catch {}
241
+ this.pagesDir = join(process.cwd(), ".afs-ui", "pages");
242
+ this.loadPagesFromDisk(this.pagesDir);
243
+ } catch {}
244
+ }
245
+ /** Load persisted page files from disk into the pages Map. */
246
+ loadPagesFromDisk(dir) {
247
+ if (!existsSync(dir)) return;
248
+ for (const file of readdirSync(dir)) {
249
+ if (file.startsWith(".")) continue;
250
+ try {
251
+ const content = readFileSync(join(dir, file), "utf-8");
252
+ const now = Date.now();
253
+ this.pages.set(file, {
254
+ content,
255
+ format: "html",
256
+ createdAt: now,
257
+ updatedAt: now
258
+ });
259
+ } catch {}
260
+ }
261
+ }
262
+ /** Persist a page to disk as a plain file (best-effort). */
263
+ persistPageToDisk(pageId, data) {
264
+ if (!this.pagesDir) return;
265
+ try {
266
+ if (!existsSync(this.pagesDir)) mkdirSync(this.pagesDir, { recursive: true });
267
+ writeFileSync(join(this.pagesDir, pageId), data.content, "utf-8");
268
+ } catch {}
269
+ }
270
+ /** Remove a persisted page from disk (best-effort). */
271
+ removePageFromDisk(pageId) {
272
+ if (!this.pagesDir) return;
273
+ try {
274
+ const filePath = join(this.pagesDir, pageId);
275
+ if (existsSync(filePath)) unlinkSync(filePath);
276
+ } catch {}
277
+ }
278
+ /** Assert the endpoint param matches this provider's type */
279
+ assertEndpoint(ctx) {
280
+ const endpoint = ctx.params.endpoint;
281
+ if (endpoint !== this.endpointType) throw new AFSNotFoundError(`/${endpoint}`);
282
+ return endpoint;
283
+ }
284
+ /** Resolve session from route params, throwing if not found */
285
+ resolveSession(ctx) {
286
+ this.assertEndpoint(ctx);
287
+ const sid = ctx.params.sid;
288
+ return this.sessions.get(sid);
289
+ }
290
+ /** Get or create AUP node store for a session */
291
+ getAupStore(sessionId) {
292
+ let store = this.aupStores.get(sessionId);
293
+ if (!store) {
294
+ store = new AUPNodeStore();
295
+ this.aupStores.set(sessionId, store);
296
+ }
297
+ return store;
298
+ }
299
+ /** Get or create AUP scene manager for a session */
300
+ getAupManager(sessionId) {
301
+ let mgr = this.aupManagers.get(sessionId);
302
+ if (!mgr) {
303
+ mgr = new AUPSceneManager();
304
+ this.aupManagers.set(sessionId, mgr);
305
+ }
306
+ return mgr;
307
+ }
308
+ /** Get the WM node from the AUP tree, or null if it doesn't exist yet. */
309
+ getWmNode(sessionId) {
310
+ const store = this.aupStores.get(sessionId);
311
+ if (!store) return null;
312
+ const wmNode = store.findNode("wm");
313
+ if (!wmNode) return null;
314
+ return {
315
+ store,
316
+ wmNode
317
+ };
318
+ }
319
+ /**
320
+ * Ensure a WM node exists in the session's AUP tree.
321
+ * Creates a minimal root + wm node if the tree is empty.
322
+ */
323
+ ensureWmNode(sessionId, strategy = "floating") {
324
+ const store = this.getAupStore(sessionId);
325
+ let wmNode = store.findNode("wm");
326
+ if (wmNode) return {
327
+ store,
328
+ wmNode
329
+ };
330
+ if (!store.getRoot()) {
331
+ store.setRoot({
332
+ id: "root",
333
+ type: "view",
334
+ children: [{
335
+ id: "wm",
336
+ type: "wm",
337
+ props: { strategy }
338
+ }]
339
+ });
340
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(sessionId, {
341
+ type: "aup",
342
+ action: "render",
343
+ root: store.getRoot(),
344
+ treeVersion: store.version,
345
+ fullPage: true
346
+ });
347
+ } else store.applyPatch([{
348
+ op: "create",
349
+ id: "wm",
350
+ parentId: store.getRoot().id,
351
+ node: {
352
+ id: "wm",
353
+ type: "wm",
354
+ props: { strategy }
355
+ }
356
+ }]);
357
+ wmNode = store.findNode("wm");
358
+ return {
359
+ store,
360
+ wmNode
361
+ };
362
+ }
363
+ /**
364
+ * Apply WM-related AUP patch ops and notify the client.
365
+ */
366
+ applyWmPatch(sessionId, ctx, ops) {
367
+ const store = this.getAupStore(sessionId);
368
+ store.applyPatch(ops);
369
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(sessionId, {
370
+ type: "aup",
371
+ action: "patch",
372
+ ops,
373
+ treeVersion: store.version
374
+ });
375
+ const endpoint = ctx.params.endpoint;
376
+ const treePath = joinURL("/", endpoint, "sessions", sessionId, "tree");
377
+ this.emit({
378
+ type: "afs:write",
379
+ path: treePath,
380
+ data: { root: store.getRoot() }
381
+ });
382
+ return store;
383
+ }
384
+ /** Find a WM surface child node by surface name. */
385
+ findWmSurface(wmNode, name) {
386
+ return (wmNode.children || []).find((c) => c.type === "wm-surface" && (c.props?.surfaceName === name || c.id === `wm-surface-${name}`));
387
+ }
388
+ /**
389
+ * Handle built-in WM events from client-side panel interactions.
390
+ * Returns undefined if the event is not a WM-specific event.
391
+ */
392
+ handleWmEvent(sessionId, event, data) {
393
+ const wm = this.getWmNode(sessionId);
394
+ if (!wm) return void 0;
395
+ const panels = (wm.wmNode.props ?? {}).panels ?? [];
396
+ if (event === "panel-collapse") {
397
+ const panelId = data.panelId;
398
+ if (!panelId) return void 0;
399
+ const updated = panels.map((p) => p.id === panelId ? {
400
+ ...p,
401
+ collapsed: true
402
+ } : p);
403
+ this.applyWmPatchDirect(sessionId, [{
404
+ op: "update",
405
+ id: "wm",
406
+ props: { panels: updated }
407
+ }]);
408
+ return {
409
+ success: true,
410
+ panelId,
411
+ collapsed: true
412
+ };
413
+ }
414
+ if (event === "panel-expand") {
415
+ const panelId = data.panelId;
416
+ if (!panelId) return void 0;
417
+ const updated = panels.map((p) => p.id === panelId ? {
418
+ ...p,
419
+ collapsed: false
420
+ } : p);
421
+ this.applyWmPatchDirect(sessionId, [{
422
+ op: "update",
423
+ id: "wm",
424
+ props: { panels: updated }
425
+ }]);
426
+ return {
427
+ success: true,
428
+ panelId,
429
+ collapsed: false
430
+ };
431
+ }
432
+ if (event === "panel-resize") {
433
+ const panelId = data.panelId;
434
+ const size = data.size;
435
+ if (!panelId || !size) return void 0;
436
+ const updated = panels.map((p) => p.id === panelId ? {
437
+ ...p,
438
+ defaultSize: size
439
+ } : p);
440
+ this.applyWmPatchDirect(sessionId, [{
441
+ op: "update",
442
+ id: "wm",
443
+ props: { panels: updated }
444
+ }]);
445
+ return {
446
+ success: true,
447
+ panelId,
448
+ size
449
+ };
450
+ }
451
+ }
452
+ /**
453
+ * Apply WM patch directly without needing a RouteContext.
454
+ * Used for client-initiated WM events.
455
+ */
456
+ applyWmPatchDirect(sessionId, ops) {
457
+ const store = this.getAupStore(sessionId);
458
+ store.applyPatch(ops);
459
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(sessionId, {
460
+ type: "aup",
461
+ action: "patch",
462
+ ops,
463
+ treeVersion: store.version
464
+ });
465
+ }
466
+ /** AUP store key for a live channel */
467
+ channelAupKey(channelId) {
468
+ return `live:${channelId}`;
469
+ }
470
+ /** Get or create AUP node store for a live channel */
471
+ getChannelAupStore(channelId) {
472
+ const key = this.channelAupKey(channelId);
473
+ let store = this.aupStores.get(key);
474
+ if (!store) {
475
+ store = new AUPNodeStore();
476
+ this.aupStores.set(key, store);
477
+ }
478
+ return store;
479
+ }
480
+ /** Degrade an AUP root for a session's device capabilities (D14). */
481
+ degradeForSession(root, session) {
482
+ return degradeTree(root, session.deviceCaps);
483
+ }
484
+ /** Count active live channels (stores with live: prefix that have content) */
485
+ getActiveChannelCount() {
486
+ let count = 0;
487
+ for (const key of this.aupStores.keys()) if (key.startsWith("live:")) count++;
488
+ return count;
489
+ }
490
+ /** List active channel IDs */
491
+ getActiveChannelIds() {
492
+ const ids = [];
493
+ for (const key of this.aupStores.keys()) if (key.startsWith("live:")) ids.push(key.slice(5));
494
+ if (isAUPTransport(this.backend)) {
495
+ for (const id of this.backend.getActiveChannelIds()) if (!ids.includes(id)) ids.push(id);
496
+ }
497
+ return ids;
498
+ }
499
+ /** Handle an AUP event from a client — resolve exec path and call it */
500
+ async handleAupEvent(sessionId, nodeId, event, data) {
501
+ const node = this.getAupStore(sessionId).findNode(nodeId);
502
+ if (!node) throw new Error(`AUP node not found: ${nodeId}`);
503
+ if (node.type === "wm") {
504
+ const wmResult = this.handleWmEvent(sessionId, event, data ?? {});
505
+ if (wmResult !== void 0) return wmResult;
506
+ }
507
+ const evtConfig = node.events?.[event];
508
+ if (!evtConfig) {
509
+ if (this.onAupEvent) {
510
+ const config$1 = {
511
+ exec: event,
512
+ args: data ?? {}
513
+ };
514
+ const result = await this.onAupEvent(sessionId, nodeId, event, config$1);
515
+ if (result !== void 0) return result;
516
+ }
517
+ if (!node.events) throw new Error(`Node '${nodeId}' has no events`);
518
+ throw new Error(`Node '${nodeId}' has no '${event}' event`);
519
+ }
520
+ const execPath = evtConfig.exec;
521
+ if (!execPath || typeof execPath !== "string") throw new Error("Event exec path is required");
522
+ if (execPath.includes("..")) throw new Error("Event exec path cannot contain '..'");
523
+ const config = {
524
+ exec: execPath,
525
+ args: {
526
+ ...evtConfig.args ?? {},
527
+ ...data ?? {}
528
+ }
529
+ };
530
+ if (this.onAupEvent) {
531
+ const result = await this.onAupEvent(sessionId, nodeId, event, config);
532
+ if (result !== void 0) return result;
533
+ }
534
+ if (this.afsRoot?.exec) return (await this.afsRoot.exec(config.exec, config.args, {}))?.data;
535
+ return {
536
+ ...config,
537
+ nodeId,
538
+ event
539
+ };
540
+ }
541
+ static schema() {
542
+ return afsUISchema;
543
+ }
544
+ static manifest() {
545
+ return {
546
+ name: "ui",
547
+ description: "Interactive UI device — terminal (xterm.js), web chat, or messaging",
548
+ uriTemplate: "ui://{backend}",
549
+ category: "device",
550
+ schema: afsUISchema,
551
+ tags: [
552
+ "ui",
553
+ "device",
554
+ "interactive",
555
+ "term"
556
+ ],
557
+ capabilityTags: [
558
+ "read-write",
559
+ "auth:none",
560
+ "local"
561
+ ],
562
+ security: {
563
+ riskLevel: "local",
564
+ resourceAccess: ["local-network"],
565
+ notes: ["Interacts with local terminal, web, or messaging backend"]
566
+ },
567
+ capabilities: { network: {
568
+ egress: true,
569
+ ingress: true
570
+ } }
571
+ };
572
+ }
573
+ static treeSchema() {
574
+ return {
575
+ operations: [
576
+ "list",
577
+ "read",
578
+ "write",
579
+ "delete",
580
+ "exec",
581
+ "stat",
582
+ "explain"
583
+ ],
584
+ tree: {
585
+ "/": {
586
+ kind: "device",
587
+ operations: [
588
+ "list",
589
+ "read",
590
+ "exec"
591
+ ],
592
+ actions: [
593
+ "prompt",
594
+ "clear",
595
+ "notify",
596
+ "navigate",
597
+ "dialog",
598
+ "progress",
599
+ "form",
600
+ "table",
601
+ "toast"
602
+ ]
603
+ },
604
+ "/input": {
605
+ kind: "input-channel",
606
+ operations: ["read"]
607
+ },
608
+ "/output": {
609
+ kind: "output-channel",
610
+ operations: ["read", "write"]
611
+ },
612
+ "/pages": {
613
+ kind: "pages-directory",
614
+ operations: ["list", "read"]
615
+ },
616
+ "/pages/{id}": {
617
+ kind: "page",
618
+ operations: [
619
+ "read",
620
+ "write",
621
+ "delete"
622
+ ]
623
+ }
624
+ },
625
+ auth: { type: "none" },
626
+ bestFor: [
627
+ "user interaction",
628
+ "terminal UI",
629
+ "web chat"
630
+ ],
631
+ notFor: ["data storage"]
632
+ };
633
+ }
634
+ async readCapabilities(_ctx) {
635
+ return {
636
+ id: "/.meta/.capabilities",
637
+ path: "/.meta/.capabilities",
638
+ content: {
639
+ schemaVersion: 1,
640
+ provider: this.name,
641
+ description: this.description,
642
+ tools: [],
643
+ actions: [],
644
+ operations: this.getOperationsDeclaration()
645
+ },
646
+ meta: { kind: "afs:capabilities" }
647
+ };
648
+ }
649
+ async metaRoot(_ctx) {
650
+ if (this.initPromise) await this.initPromise;
651
+ const viewport = this.backend.getViewport();
652
+ const meta = {
653
+ kind: "device",
654
+ backend: this.backend.type,
655
+ supportedFormats: this.backend.supportedFormats,
656
+ capabilities: this.backend.capabilities,
657
+ viewport,
658
+ childrenCount: 10
659
+ };
660
+ if (this.serverUrl) meta.url = this.serverUrl;
661
+ return {
662
+ id: ".meta",
663
+ path: "/.meta",
664
+ meta
665
+ };
666
+ }
667
+ async metaInput(_ctx) {
668
+ return {
669
+ id: ".meta",
670
+ path: "/input/.meta",
671
+ meta: {
672
+ kind: "input-channel",
673
+ childrenCount: 0,
674
+ description: "Read user input from this device",
675
+ pending: this.backend.hasPendingInput()
676
+ }
677
+ };
678
+ }
679
+ async metaOutput(_ctx) {
680
+ return {
681
+ id: ".meta",
682
+ path: "/output/.meta",
683
+ meta: {
684
+ kind: "output-channel",
685
+ childrenCount: 0,
686
+ description: "Write content to this device"
687
+ }
688
+ };
689
+ }
690
+ async metaPages(_ctx) {
691
+ return {
692
+ id: ".meta",
693
+ path: "/pages/.meta",
694
+ meta: {
695
+ kind: "pages-directory",
696
+ childrenCount: this.pages.size,
697
+ description: "Managed pages for full-page rendering"
698
+ }
699
+ };
700
+ }
701
+ async metaPage(ctx) {
702
+ const pageId = ctx.params.id;
703
+ const page = this.pages.get(pageId);
704
+ if (!page) throw new Error(`Page not found: ${pageId}`);
705
+ return {
706
+ id: ".meta",
707
+ path: joinURL("/pages", pageId, ".meta"),
708
+ meta: {
709
+ kind: "page",
710
+ format: page.format,
711
+ childrenCount: 0,
712
+ createdAt: page.createdAt,
713
+ updatedAt: page.updatedAt
714
+ }
715
+ };
716
+ }
717
+ async listInput(_ctx) {
718
+ return { data: [] };
719
+ }
720
+ async listOutput(_ctx) {
721
+ return { data: [] };
722
+ }
723
+ async listPages(_ctx) {
724
+ const entries = [];
725
+ for (const [id, page] of this.pages) entries.push({
726
+ id,
727
+ path: joinURL("/pages", id),
728
+ meta: {
729
+ kind: "page",
730
+ format: page.format,
731
+ childrenCount: 0,
732
+ createdAt: page.createdAt,
733
+ updatedAt: page.updatedAt
734
+ }
735
+ });
736
+ return { data: entries };
737
+ }
738
+ async listRoot(_ctx) {
739
+ return { data: [
740
+ {
741
+ id: "spec",
742
+ path: "/spec",
743
+ meta: {
744
+ kind: "aup:spec",
745
+ description: "AUP document format specification — start here"
746
+ }
747
+ },
748
+ {
749
+ id: "primitives",
750
+ path: "/primitives",
751
+ meta: {
752
+ kind: "primitives-directory",
753
+ childrenCount: Object.keys(PRIMITIVES).length,
754
+ description: "Available UI components with full props, events, and examples"
755
+ }
756
+ },
757
+ {
758
+ id: "themes",
759
+ path: "/themes",
760
+ meta: {
761
+ kind: "themes-directory",
762
+ childrenCount: Object.keys(THEMES).length,
763
+ description: "Visual themes with color tokens"
764
+ }
765
+ },
766
+ {
767
+ id: "overlay-themes",
768
+ path: "/overlay-themes",
769
+ meta: {
770
+ kind: "overlay-themes-directory",
771
+ childrenCount: Object.keys(OVERLAY_THEMES).length,
772
+ description: "Broadcast overlay themes (graphics packages) for overlay-grid"
773
+ }
774
+ },
775
+ {
776
+ id: "examples",
777
+ path: "/examples",
778
+ meta: {
779
+ kind: "examples-directory",
780
+ childrenCount: Object.keys(AUP_EXAMPLES).length,
781
+ description: "Complete working AUP documents to learn from"
782
+ }
783
+ },
784
+ {
785
+ id: this.endpointType,
786
+ path: joinURL("/", this.endpointType),
787
+ meta: {
788
+ kind: "endpoint",
789
+ childrenCount: 1,
790
+ description: `${this.endpointType} endpoint with session management`
791
+ }
792
+ },
793
+ {
794
+ id: "input",
795
+ path: "/input",
796
+ meta: {
797
+ kind: "input-channel",
798
+ childrenCount: 0,
799
+ description: "Read user input (legacy — use sessions/:id/messages instead)"
800
+ }
801
+ },
802
+ {
803
+ id: "output",
804
+ path: "/output",
805
+ meta: {
806
+ kind: "output-channel",
807
+ childrenCount: 0,
808
+ description: "Write content (legacy — use sessions/:id/messages instead)"
809
+ }
810
+ },
811
+ {
812
+ id: "pages",
813
+ path: "/pages",
814
+ meta: {
815
+ kind: "pages-directory",
816
+ childrenCount: this.pages.size,
817
+ description: "Managed pages (legacy — use sessions/:id/pages instead)"
818
+ }
819
+ },
820
+ {
821
+ id: "sharing",
822
+ path: "/sharing",
823
+ meta: {
824
+ kind: "sharing-directory",
825
+ childrenCount: this.sharingEntries.size,
826
+ description: "Web sharing entries — publish AFS subtrees as public URLs"
827
+ }
828
+ }
829
+ ] };
830
+ }
831
+ async readRoot(_ctx) {
832
+ return {
833
+ id: this.name,
834
+ path: "/",
835
+ content: `UI Device (${this.backend.type})`,
836
+ meta: {
837
+ kind: "device",
838
+ childrenCount: 10,
839
+ backend: this.backend.type
840
+ }
841
+ };
842
+ }
843
+ async readInput(_ctx) {
844
+ return {
845
+ id: "input",
846
+ path: "/input",
847
+ content: await this.backend.read({ timeout: this.inputTimeout })
848
+ };
849
+ }
850
+ async readOutput(_ctx) {
851
+ return {
852
+ id: "output",
853
+ path: "/output",
854
+ content: "",
855
+ meta: { kind: "output-channel" }
856
+ };
857
+ }
858
+ async readPages(_ctx) {
859
+ return {
860
+ id: "pages",
861
+ path: "/pages",
862
+ content: "",
863
+ meta: {
864
+ kind: "pages-directory",
865
+ childrenCount: this.pages.size
866
+ }
867
+ };
868
+ }
869
+ async readPage(ctx) {
870
+ const pageId = ctx.params.id;
871
+ const page = this.pages.get(pageId);
872
+ if (!page) throw new Error(`Page not found: ${pageId}`);
873
+ return {
874
+ id: pageId,
875
+ path: joinURL("/pages", pageId),
876
+ content: page.content,
877
+ meta: {
878
+ kind: "page",
879
+ format: page.format,
880
+ layout: page.layout
881
+ }
882
+ };
883
+ }
884
+ async listPrimitives(_ctx) {
885
+ return { data: Object.values(PRIMITIVES).map((p) => ({
886
+ id: p.name,
887
+ name: p.name,
888
+ path: joinURL("/primitives", p.name),
889
+ meta: {
890
+ kind: "aup:primitive",
891
+ category: p.category,
892
+ description: p.description
893
+ }
894
+ })) };
895
+ }
896
+ async readPrimitivesDir(_ctx) {
897
+ return {
898
+ id: "primitives",
899
+ path: "/primitives",
900
+ content: `AUP Primitive Registry (${Object.keys(PRIMITIVES).length} primitives)`,
901
+ meta: {
902
+ kind: "primitives-directory",
903
+ childrenCount: Object.keys(PRIMITIVES).length
904
+ }
905
+ };
906
+ }
907
+ async statPrimitivesDir(_ctx) {
908
+ return { data: {
909
+ id: "primitives",
910
+ path: "/primitives",
911
+ meta: {
912
+ kind: "primitives-directory",
913
+ childrenCount: Object.keys(PRIMITIVES).length
914
+ }
915
+ } };
916
+ }
917
+ async metaPrimitivesDir(_ctx) {
918
+ return {
919
+ id: "primitives",
920
+ path: "/primitives/.meta",
921
+ meta: {
922
+ kind: "primitives-directory",
923
+ childrenCount: Object.keys(PRIMITIVES).length
924
+ }
925
+ };
926
+ }
927
+ async readPrimitive(ctx) {
928
+ const name = ctx.params.name;
929
+ const prim = PRIMITIVES[name];
930
+ if (!prim) throw new AFSNotFoundError(joinURL("/primitives", name));
931
+ return {
932
+ id: prim.name,
933
+ path: joinURL("/primitives", prim.name),
934
+ content: prim,
935
+ meta: {
936
+ kind: "aup:primitive",
937
+ category: prim.category,
938
+ description: prim.description
939
+ }
940
+ };
941
+ }
942
+ async statPrimitive(ctx) {
943
+ const name = ctx.params.name;
944
+ const prim = PRIMITIVES[name];
945
+ if (!prim) throw new AFSNotFoundError(joinURL("/primitives", name));
946
+ return { data: {
947
+ id: prim.name,
948
+ path: joinURL("/primitives", prim.name),
949
+ meta: {
950
+ kind: "aup:primitive",
951
+ category: prim.category,
952
+ description: prim.description
953
+ }
954
+ } };
955
+ }
956
+ async metaPrimitive(ctx) {
957
+ const name = ctx.params.name;
958
+ const prim = PRIMITIVES[name];
959
+ if (!prim) throw new AFSNotFoundError(joinURL("/primitives", name));
960
+ return {
961
+ id: prim.name,
962
+ path: joinURL("/primitives", prim.name, ".meta"),
963
+ meta: {
964
+ kind: "aup:primitive",
965
+ category: prim.category,
966
+ description: prim.description
967
+ }
968
+ };
969
+ }
970
+ async listThemes(_ctx) {
971
+ return { data: Object.values(THEMES).map((t) => ({
972
+ id: t.name,
973
+ name: t.name,
974
+ path: joinURL("/themes", t.name),
975
+ meta: {
976
+ kind: "aup:theme",
977
+ description: t.description
978
+ }
979
+ })) };
980
+ }
981
+ async readThemesDir(_ctx) {
982
+ return {
983
+ id: "themes",
984
+ path: "/themes",
985
+ content: `AUP Theme Registry (${Object.keys(THEMES).length} themes)`,
986
+ meta: {
987
+ kind: "themes-directory",
988
+ childrenCount: Object.keys(THEMES).length
989
+ }
990
+ };
991
+ }
992
+ async statThemesDir(_ctx) {
993
+ return { data: {
994
+ id: "themes",
995
+ path: "/themes",
996
+ meta: {
997
+ kind: "themes-directory",
998
+ childrenCount: Object.keys(THEMES).length
999
+ }
1000
+ } };
1001
+ }
1002
+ async metaThemesDir(_ctx) {
1003
+ return {
1004
+ id: "themes",
1005
+ path: "/themes/.meta",
1006
+ meta: {
1007
+ kind: "themes-directory",
1008
+ childrenCount: Object.keys(THEMES).length
1009
+ }
1010
+ };
1011
+ }
1012
+ async readTheme(ctx) {
1013
+ const name = ctx.params.name;
1014
+ const theme = THEMES[name];
1015
+ if (!theme) throw new AFSNotFoundError(joinURL("/themes", name));
1016
+ return {
1017
+ id: theme.name,
1018
+ path: joinURL("/themes", theme.name),
1019
+ content: theme,
1020
+ meta: {
1021
+ kind: "aup:theme",
1022
+ description: theme.description
1023
+ }
1024
+ };
1025
+ }
1026
+ async statTheme(ctx) {
1027
+ const name = ctx.params.name;
1028
+ const theme = THEMES[name];
1029
+ if (!theme) throw new AFSNotFoundError(joinURL("/themes", name));
1030
+ return { data: {
1031
+ id: theme.name,
1032
+ path: joinURL("/themes", theme.name),
1033
+ meta: {
1034
+ kind: "aup:theme",
1035
+ description: theme.description
1036
+ }
1037
+ } };
1038
+ }
1039
+ async metaTheme(ctx) {
1040
+ const name = ctx.params.name;
1041
+ const theme = THEMES[name];
1042
+ if (!theme) throw new AFSNotFoundError(joinURL("/themes", name));
1043
+ return {
1044
+ id: theme.name,
1045
+ path: joinURL("/themes", theme.name, ".meta"),
1046
+ meta: {
1047
+ kind: "aup:theme",
1048
+ description: theme.description
1049
+ }
1050
+ };
1051
+ }
1052
+ async listOverlayThemes(_ctx) {
1053
+ return { data: Object.values(OVERLAY_THEMES).map((t) => ({
1054
+ id: t.name,
1055
+ name: t.name,
1056
+ path: joinURL("/overlay-themes", t.name),
1057
+ meta: {
1058
+ kind: "aup:overlay-theme",
1059
+ description: t.description
1060
+ }
1061
+ })) };
1062
+ }
1063
+ async readOverlayThemesDir(_ctx) {
1064
+ return {
1065
+ id: "overlay-themes",
1066
+ path: "/overlay-themes",
1067
+ content: `AUP Overlay Theme Registry (${Object.keys(OVERLAY_THEMES).length} themes)\n\nBroadcast "graphics packages" for overlay-grid layouts. Apply via theme prop on view with layout: "overlay-grid".\n\nAvailable: ${Object.keys(OVERLAY_THEMES).join(", ")}`,
1068
+ meta: {
1069
+ kind: "overlay-themes-directory",
1070
+ childrenCount: Object.keys(OVERLAY_THEMES).length
1071
+ }
1072
+ };
1073
+ }
1074
+ async statOverlayThemesDir(_ctx) {
1075
+ return { data: {
1076
+ id: "overlay-themes",
1077
+ path: "/overlay-themes",
1078
+ meta: {
1079
+ kind: "overlay-themes-directory",
1080
+ childrenCount: Object.keys(OVERLAY_THEMES).length
1081
+ }
1082
+ } };
1083
+ }
1084
+ async metaOverlayThemesDir(_ctx) {
1085
+ return {
1086
+ id: "overlay-themes",
1087
+ path: "/overlay-themes/.meta",
1088
+ meta: {
1089
+ kind: "overlay-themes-directory",
1090
+ childrenCount: Object.keys(OVERLAY_THEMES).length
1091
+ }
1092
+ };
1093
+ }
1094
+ async readOverlayTheme(ctx) {
1095
+ const name = ctx.params.name;
1096
+ const theme = OVERLAY_THEMES[name];
1097
+ if (!theme) throw new AFSNotFoundError(joinURL("/overlay-themes", name));
1098
+ return {
1099
+ id: theme.name,
1100
+ path: joinURL("/overlay-themes", theme.name),
1101
+ content: theme,
1102
+ meta: {
1103
+ kind: "aup:overlay-theme",
1104
+ description: theme.description
1105
+ }
1106
+ };
1107
+ }
1108
+ async statOverlayTheme(ctx) {
1109
+ const name = ctx.params.name;
1110
+ const theme = OVERLAY_THEMES[name];
1111
+ if (!theme) throw new AFSNotFoundError(joinURL("/overlay-themes", name));
1112
+ return { data: {
1113
+ id: theme.name,
1114
+ path: joinURL("/overlay-themes", theme.name),
1115
+ meta: {
1116
+ kind: "aup:overlay-theme",
1117
+ description: theme.description
1118
+ }
1119
+ } };
1120
+ }
1121
+ async metaOverlayTheme(ctx) {
1122
+ const name = ctx.params.name;
1123
+ const theme = OVERLAY_THEMES[name];
1124
+ if (!theme) throw new AFSNotFoundError(joinURL("/overlay-themes", name));
1125
+ return {
1126
+ id: theme.name,
1127
+ path: joinURL("/overlay-themes", theme.name, ".meta"),
1128
+ meta: {
1129
+ kind: "aup:overlay-theme",
1130
+ description: theme.description
1131
+ }
1132
+ };
1133
+ }
1134
+ async listOverlayExamples(ctx) {
1135
+ const name = ctx.params.name;
1136
+ const theme = OVERLAY_THEMES[name];
1137
+ if (!theme) throw new AFSNotFoundError(joinURL("/overlay-themes", name));
1138
+ return { data: (theme.examples || []).map((ex) => ({
1139
+ id: ex.name,
1140
+ name: ex.name,
1141
+ path: joinURL("/overlay-themes", name, "examples", ex.name),
1142
+ meta: {
1143
+ kind: "aup:overlay-example",
1144
+ description: ex.description
1145
+ }
1146
+ })) };
1147
+ }
1148
+ async readOverlayExample(ctx) {
1149
+ const name = ctx.params.name;
1150
+ const exName = ctx.params.example;
1151
+ const theme = OVERLAY_THEMES[name];
1152
+ if (!theme) throw new AFSNotFoundError(joinURL("/overlay-themes", name));
1153
+ const ex = (theme.examples || []).find((e) => e.name === exName);
1154
+ if (!ex) throw new AFSNotFoundError(joinURL("/overlay-themes", name, "examples", exName));
1155
+ return {
1156
+ id: ex.name,
1157
+ path: joinURL("/overlay-themes", name, "examples", ex.name),
1158
+ content: ex.tree,
1159
+ meta: {
1160
+ kind: "aup:overlay-example",
1161
+ description: ex.description
1162
+ }
1163
+ };
1164
+ }
1165
+ async readSpec(_ctx) {
1166
+ return {
1167
+ id: "spec",
1168
+ path: "/spec",
1169
+ content: AUP_SPEC,
1170
+ meta: {
1171
+ kind: "aup:spec",
1172
+ version: AUP_SPEC.version
1173
+ }
1174
+ };
1175
+ }
1176
+ async statSpec(_ctx) {
1177
+ return { data: {
1178
+ id: "spec",
1179
+ path: "/spec",
1180
+ meta: {
1181
+ kind: "aup:spec",
1182
+ version: AUP_SPEC.version
1183
+ }
1184
+ } };
1185
+ }
1186
+ async listSpec(_ctx) {
1187
+ return { data: [] };
1188
+ }
1189
+ async metaSpec(_ctx) {
1190
+ return {
1191
+ id: ".meta",
1192
+ path: "/spec/.meta",
1193
+ meta: {
1194
+ kind: "aup:spec",
1195
+ version: AUP_SPEC.version,
1196
+ childrenCount: 0
1197
+ }
1198
+ };
1199
+ }
1200
+ async explainSpec(_ctx) {
1201
+ return {
1202
+ format: "markdown",
1203
+ content: [
1204
+ "# AUP Document Specification",
1205
+ "",
1206
+ "Read `/spec` to get the full AUP wire format definition,",
1207
+ "including node fields, spatial intent system, sizing, event model, and workflow.",
1208
+ "",
1209
+ "This is the starting point for any agent building UI with AUP."
1210
+ ].join("\n")
1211
+ };
1212
+ }
1213
+ async listExamples(_ctx) {
1214
+ return { data: Object.values(AUP_EXAMPLES).map((ex) => ({
1215
+ id: ex.name,
1216
+ name: ex.name,
1217
+ path: joinURL("/examples", ex.name),
1218
+ meta: {
1219
+ kind: "aup:example",
1220
+ title: ex.title,
1221
+ description: ex.description,
1222
+ concepts: ex.concepts
1223
+ }
1224
+ })) };
1225
+ }
1226
+ async readExamplesDir(_ctx) {
1227
+ return {
1228
+ id: "examples",
1229
+ path: "/examples",
1230
+ content: `AUP Examples (${Object.keys(AUP_EXAMPLES).length} examples)`,
1231
+ meta: {
1232
+ kind: "examples-directory",
1233
+ childrenCount: Object.keys(AUP_EXAMPLES).length
1234
+ }
1235
+ };
1236
+ }
1237
+ async statExamplesDir(_ctx) {
1238
+ return { data: {
1239
+ id: "examples",
1240
+ path: "/examples",
1241
+ meta: {
1242
+ kind: "examples-directory",
1243
+ childrenCount: Object.keys(AUP_EXAMPLES).length
1244
+ }
1245
+ } };
1246
+ }
1247
+ async metaExamplesDir(_ctx) {
1248
+ return {
1249
+ id: "examples",
1250
+ path: "/examples/.meta",
1251
+ meta: {
1252
+ kind: "examples-directory",
1253
+ childrenCount: Object.keys(AUP_EXAMPLES).length
1254
+ }
1255
+ };
1256
+ }
1257
+ async readExample(ctx) {
1258
+ const name = ctx.params.name;
1259
+ const ex = AUP_EXAMPLES[name];
1260
+ if (!ex) throw new AFSNotFoundError(joinURL("/examples", name));
1261
+ return {
1262
+ id: ex.name,
1263
+ path: joinURL("/examples", ex.name),
1264
+ content: ex,
1265
+ meta: {
1266
+ kind: "aup:example",
1267
+ title: ex.title,
1268
+ description: ex.description,
1269
+ concepts: ex.concepts
1270
+ }
1271
+ };
1272
+ }
1273
+ async statExample(ctx) {
1274
+ const name = ctx.params.name;
1275
+ const ex = AUP_EXAMPLES[name];
1276
+ if (!ex) throw new AFSNotFoundError(joinURL("/examples", name));
1277
+ return { data: {
1278
+ id: ex.name,
1279
+ path: joinURL("/examples", ex.name),
1280
+ meta: {
1281
+ kind: "aup:example",
1282
+ title: ex.title,
1283
+ description: ex.description
1284
+ }
1285
+ } };
1286
+ }
1287
+ async metaExample(ctx) {
1288
+ const name = ctx.params.name;
1289
+ const ex = AUP_EXAMPLES[name];
1290
+ if (!ex) throw new AFSNotFoundError(joinURL("/examples", name));
1291
+ return {
1292
+ id: ex.name,
1293
+ path: joinURL("/examples", ex.name, ".meta"),
1294
+ meta: {
1295
+ kind: "aup:example",
1296
+ title: ex.title,
1297
+ description: ex.description
1298
+ }
1299
+ };
1300
+ }
1301
+ async writeOutput(_ctx, payload) {
1302
+ const content = typeof payload.content === "string" ? payload.content : String(payload.content ?? "");
1303
+ const format = payload.meta?.format ?? void 0;
1304
+ const component = payload.meta?.component ?? void 0;
1305
+ const componentProps = payload.meta?.componentProps ?? void 0;
1306
+ await this.backend.write(content, {
1307
+ format,
1308
+ component,
1309
+ componentProps
1310
+ });
1311
+ return { data: {
1312
+ id: "output",
1313
+ path: "/output",
1314
+ content,
1315
+ meta: { kind: "output-channel" }
1316
+ } };
1317
+ }
1318
+ async writePage(ctx, payload) {
1319
+ const pageId = ctx.params.id;
1320
+ const content = typeof payload.content === "string" ? payload.content : String(payload.content ?? "");
1321
+ const format = payload.meta?.format ?? "html";
1322
+ const layout = payload.meta?.layout ?? void 0;
1323
+ const now = Date.now();
1324
+ const pageData = {
1325
+ content,
1326
+ format,
1327
+ layout,
1328
+ createdAt: this.pages.get(pageId)?.createdAt ?? now,
1329
+ updatedAt: now
1330
+ };
1331
+ this.pages.set(pageId, pageData);
1332
+ this.persistPageToDisk(pageId, pageData);
1333
+ return { data: {
1334
+ id: pageId,
1335
+ path: joinURL("/pages", pageId),
1336
+ content,
1337
+ meta: {
1338
+ kind: "page",
1339
+ format
1340
+ }
1341
+ } };
1342
+ }
1343
+ async deletePage(ctx) {
1344
+ const pageId = ctx.params.id;
1345
+ if (!this.pages.has(pageId)) throw new Error(`Page not found: ${pageId}`);
1346
+ this.pages.delete(pageId);
1347
+ this.removePageFromDisk(pageId);
1348
+ return { message: `Deleted page: ${pageId}` };
1349
+ }
1350
+ /** Validate and parse a sharing entry payload */
1351
+ parseSharingPayload(payload, existingCreatedAt) {
1352
+ const raw = payload.content;
1353
+ if (!raw || typeof raw !== "object") throw new Error("Sharing entry requires a content object");
1354
+ const target = raw.target;
1355
+ if (!target || typeof target !== "string") throw new Error("Sharing entry requires 'target' field");
1356
+ const access = raw.access ?? "guest";
1357
+ if (!VALID_ACCESS_LEVELS.has(access)) throw new Error(`Invalid access level: '${access}'. Must be one of: ${[...VALID_ACCESS_LEVELS].join(", ")}`);
1358
+ const mode = raw.mode ?? "static";
1359
+ if (!VALID_SHARING_MODES.has(mode)) throw new Error(`Invalid sharing mode: '${mode}'. Must be one of: ${[...VALID_SHARING_MODES].join(", ")}`);
1360
+ const now = Date.now();
1361
+ return {
1362
+ target,
1363
+ access,
1364
+ mode,
1365
+ slug: "",
1366
+ meta: raw.meta,
1367
+ createdAt: existingCreatedAt ?? now,
1368
+ updatedAt: now
1369
+ };
1370
+ }
1371
+ async writeSharingEntry(ctx, payload) {
1372
+ const slug = ctx.params.slug;
1373
+ const existing = this.sharingEntries.get(slug);
1374
+ const entry = this.parseSharingPayload(payload, existing?.createdAt);
1375
+ entry.slug = slug;
1376
+ this.sharingEntries.set(slug, entry);
1377
+ return { data: {
1378
+ id: slug,
1379
+ path: joinURL("/sharing", slug),
1380
+ content: entry,
1381
+ meta: { kind: "sharing-entry" }
1382
+ } };
1383
+ }
1384
+ async readSharingEntry(ctx) {
1385
+ const slug = ctx.params.slug;
1386
+ const entry = this.sharingEntries.get(slug);
1387
+ if (!entry) throw new AFSNotFoundError(joinURL("/sharing", slug));
1388
+ return {
1389
+ id: slug,
1390
+ path: joinURL("/sharing", slug),
1391
+ content: entry,
1392
+ meta: {
1393
+ kind: "sharing-entry",
1394
+ access: entry.access,
1395
+ mode: entry.mode
1396
+ }
1397
+ };
1398
+ }
1399
+ async readSharingDir(_ctx) {
1400
+ return {
1401
+ id: "sharing",
1402
+ path: "/sharing",
1403
+ content: `Web Sharing Entries (${this.sharingEntries.size} entries)`,
1404
+ meta: {
1405
+ kind: "sharing-directory",
1406
+ childrenCount: this.sharingEntries.size
1407
+ }
1408
+ };
1409
+ }
1410
+ async listSharingEntries(_ctx) {
1411
+ const entries = [];
1412
+ for (const [slug, entry] of this.sharingEntries) entries.push({
1413
+ id: slug,
1414
+ path: joinURL("/sharing", slug),
1415
+ meta: {
1416
+ kind: "sharing-entry",
1417
+ target: entry.target,
1418
+ access: entry.access,
1419
+ mode: entry.mode
1420
+ }
1421
+ });
1422
+ return { data: entries };
1423
+ }
1424
+ async deleteSharingEntry(ctx) {
1425
+ const slug = ctx.params.slug;
1426
+ if (!this.sharingEntries.has(slug)) throw new AFSNotFoundError(joinURL("/sharing", slug));
1427
+ this.sharingEntries.delete(slug);
1428
+ return { message: `Deleted sharing entry: ${slug}` };
1429
+ }
1430
+ async metaSharingDir(_ctx) {
1431
+ return {
1432
+ id: "sharing",
1433
+ path: "/sharing/.meta",
1434
+ meta: {
1435
+ kind: "sharing-directory",
1436
+ childrenCount: this.sharingEntries.size
1437
+ }
1438
+ };
1439
+ }
1440
+ async metaSharingEntry(ctx) {
1441
+ const slug = ctx.params.slug;
1442
+ const entry = this.sharingEntries.get(slug);
1443
+ if (!entry) throw new AFSNotFoundError(joinURL("/sharing", slug));
1444
+ return {
1445
+ id: slug,
1446
+ path: joinURL("/sharing", slug, ".meta"),
1447
+ meta: {
1448
+ kind: "sharing-entry",
1449
+ access: entry.access,
1450
+ mode: entry.mode
1451
+ }
1452
+ };
1453
+ }
1454
+ async statSharingDir(_ctx) {
1455
+ return { data: {
1456
+ id: "sharing",
1457
+ path: "/sharing",
1458
+ meta: {
1459
+ kind: "sharing-directory",
1460
+ childrenCount: this.sharingEntries.size
1461
+ }
1462
+ } };
1463
+ }
1464
+ async statSharingEntry(ctx) {
1465
+ const slug = ctx.params.slug;
1466
+ const entry = this.sharingEntries.get(slug);
1467
+ if (!entry) throw new AFSNotFoundError(joinURL("/sharing", slug));
1468
+ return { data: {
1469
+ id: slug,
1470
+ path: joinURL("/sharing", slug),
1471
+ meta: {
1472
+ kind: "sharing-entry",
1473
+ access: entry.access,
1474
+ mode: entry.mode
1475
+ }
1476
+ } };
1477
+ }
1478
+ async listSharingActions(ctx) {
1479
+ const slug = ctx.params.slug;
1480
+ const entry = this.sharingEntries.get(slug);
1481
+ if (!entry) throw new AFSNotFoundError(joinURL("/sharing", slug));
1482
+ const basePath = joinURL("/sharing", slug, ".actions");
1483
+ const actions = [];
1484
+ if (entry.mode === "static") actions.push({
1485
+ id: "snapshot",
1486
+ path: joinURL(basePath, "snapshot"),
1487
+ meta: {
1488
+ kind: "afs:executable",
1489
+ description: "Freeze the AUP tree from a session into a static HTML snapshot",
1490
+ inputSchema: {
1491
+ type: "object",
1492
+ required: ["sessionId"],
1493
+ properties: { sessionId: {
1494
+ type: "string",
1495
+ description: "Session ID whose AUP tree to snapshot"
1496
+ } }
1497
+ }
1498
+ }
1499
+ });
1500
+ return { data: actions };
1501
+ }
1502
+ async execSharingSnapshot(ctx, args) {
1503
+ const slug = ctx.params.slug;
1504
+ const entry = this.sharingEntries.get(slug);
1505
+ if (!entry) throw new AFSNotFoundError(joinURL("/sharing", slug));
1506
+ if (entry.mode !== "static") throw new Error("Static snapshot not available for live mode entries");
1507
+ const sessionId = args.sessionId;
1508
+ if (!sessionId) throw new Error("snapshot action requires 'sessionId' argument");
1509
+ const store = this.aupStores.get(sessionId);
1510
+ const tree = store?.getRoot();
1511
+ if (!tree) throw new Error("No AUP tree found for the specified session — render content first");
1512
+ const html = generateSnapshot({
1513
+ tree,
1514
+ slug,
1515
+ meta: entry.meta,
1516
+ style: store.renderOptions.style,
1517
+ locale: store.renderOptions.locale
1518
+ });
1519
+ entry.snapshot = html;
1520
+ entry.updatedAt = Date.now();
1521
+ return {
1522
+ success: true,
1523
+ data: {
1524
+ slug,
1525
+ size: html.length
1526
+ }
1527
+ };
1528
+ }
1529
+ async statRoot(_ctx) {
1530
+ if (this.initPromise) await this.initPromise;
1531
+ const meta = {
1532
+ kind: "device",
1533
+ childrenCount: 10,
1534
+ backend: this.backend.type
1535
+ };
1536
+ if (this.serverUrl) meta.url = this.serverUrl;
1537
+ return { data: {
1538
+ id: this.name,
1539
+ path: "/",
1540
+ meta
1541
+ } };
1542
+ }
1543
+ async statInput(_ctx) {
1544
+ return { data: {
1545
+ id: "input",
1546
+ path: "/input",
1547
+ meta: {
1548
+ kind: "input-channel",
1549
+ childrenCount: 0,
1550
+ pending: this.backend.hasPendingInput()
1551
+ }
1552
+ } };
1553
+ }
1554
+ async statOutput(_ctx) {
1555
+ return { data: {
1556
+ id: "output",
1557
+ path: "/output",
1558
+ meta: {
1559
+ kind: "output-channel",
1560
+ childrenCount: 0
1561
+ }
1562
+ } };
1563
+ }
1564
+ async statPages(_ctx) {
1565
+ return { data: {
1566
+ id: "pages",
1567
+ path: "/pages",
1568
+ meta: {
1569
+ kind: "pages-directory",
1570
+ childrenCount: this.pages.size
1571
+ }
1572
+ } };
1573
+ }
1574
+ async statPage(ctx) {
1575
+ const pageId = ctx.params.id;
1576
+ const page = this.pages.get(pageId);
1577
+ if (!page) throw new Error(`Page not found: ${pageId}`);
1578
+ return { data: {
1579
+ id: pageId,
1580
+ path: joinURL("/pages", pageId),
1581
+ meta: {
1582
+ kind: "page",
1583
+ format: page.format,
1584
+ childrenCount: 0
1585
+ }
1586
+ } };
1587
+ }
1588
+ async explainRoot(_ctx) {
1589
+ return {
1590
+ format: "markdown",
1591
+ content: [
1592
+ `# UI Device (${this.backend.type})`,
1593
+ "",
1594
+ "You can build **any UI** by composing AUP (Agentic UI Protocol) component trees.",
1595
+ "",
1596
+ "## Quick Start",
1597
+ "",
1598
+ "1. **Read `/spec`** — Full AUP document format, node fields, event model, and workflow",
1599
+ `2. **Read \`/primitives\`** — ${Object.keys(PRIMITIVES).length} available components with complete props, events, and examples`,
1600
+ `3. **Read \`/themes\`** — ${Object.keys(THEMES).length} visual themes with color tokens`,
1601
+ `4. **Read \`/overlay-themes\`** — ${Object.keys(OVERLAY_THEMES).length} broadcast overlay themes (graphics packages) for overlay-grid`,
1602
+ `5. **Read \`/examples\`** — ${Object.keys(AUP_EXAMPLES).length} complete working documents to learn from`,
1603
+ "",
1604
+ "## Creating a UI",
1605
+ "",
1606
+ "```",
1607
+ "// 1. Build a node tree",
1608
+ "{ id: \"root\", type: \"view\", props: { layout: \"column\" }, children: [...] }",
1609
+ "",
1610
+ "// 2. Render it via session action",
1611
+ `exec('/${this.endpointType}/sessions/:sid/.actions/aup_render', {`,
1612
+ " root: nodeTree,",
1613
+ " fullPage: true,",
1614
+ " style: 'midnight'",
1615
+ "})",
1616
+ "",
1617
+ "// 3. Handle events (user clicks, input changes)",
1618
+ "// Events call your AFS exec paths defined in node.events",
1619
+ "",
1620
+ "// 4. Update UI incrementally",
1621
+ `exec('/${this.endpointType}/sessions/:sid/.actions/aup_patch', {`,
1622
+ " ops: [{ op: \"update\", id: \"node-id\", props: { content: \"new text\" } }]",
1623
+ "})",
1624
+ "```",
1625
+ "",
1626
+ "## Key Concepts",
1627
+ "",
1628
+ `- **${Object.keys(PRIMITIVES).length} primitives**: view, text, action, input, media, overlay, table, time, chart, map, calendar, moonphase, natal-chart, chat, terminal, editor, explorer, canvas, globe, finance-chart, rtc`,
1629
+ "- **Events**: click, change, send, sort, select, confirm, cancel — each triggers an AFS exec call",
1630
+ "- **Live data**: Set `node.src` to an AFS path for auto-updating charts, maps, etc.",
1631
+ "- **Themes**: midnight, clean, glass, brutalist, soft, cyber, editorial",
1632
+ "",
1633
+ "## Session Actions",
1634
+ "",
1635
+ "| Action | Description |",
1636
+ "|--------|-------------|",
1637
+ "| `aup_render` | Render a full AUP component tree |",
1638
+ "| `aup_patch` | Incremental updates (create/update/remove/reorder) |",
1639
+ "| `aup_stage` | Stage a scene off-screen (pre-render for instant switching) |",
1640
+ "| `aup_take` | Take a staged scene live (CSS swap, zero DOM teardown) |",
1641
+ "| `aup_release` | Release a staged scene's resources |",
1642
+ "| `aup_save` | Save current graph to a session page |",
1643
+ "| `aup_load` | Load a saved graph and render it |",
1644
+ "| `prompt` | Ask user a question (text/password/confirm/select) |",
1645
+ "| `dialog` | Show dialog with custom buttons |",
1646
+ "| `toast` | Lightweight toast notification |",
1647
+ "| `form` | Collect structured input |",
1648
+ "| `navigate` | Navigate to a page |",
1649
+ "",
1650
+ `## Capabilities: ${this.backend.capabilities.join(", ")}`,
1651
+ `## Formats: ${this.backend.supportedFormats.join(", ")}`
1652
+ ].join("\n")
1653
+ };
1654
+ }
1655
+ async explainInput(_ctx) {
1656
+ return {
1657
+ format: "text",
1658
+ content: "Input channel — use `read('/input')` to wait for user input. Use `stat('/input')` to check if input is pending without blocking."
1659
+ };
1660
+ }
1661
+ async explainOutput(_ctx) {
1662
+ return {
1663
+ format: "text",
1664
+ content: "Output channel — use `write('/output', { content: '...' })` to display content to the user."
1665
+ };
1666
+ }
1667
+ async explainPages(_ctx) {
1668
+ return {
1669
+ format: "text",
1670
+ content: "Pages directory — use `write('/pages/:id', { content, meta: { format } })` to create pages. Use `exec('/.actions/navigate', { page: ':id' })` to display a page."
1671
+ };
1672
+ }
1673
+ async readEndpoint(ctx) {
1674
+ const endpoint = this.assertEndpoint(ctx);
1675
+ const sessions = this.sessions.list(endpoint);
1676
+ const isWeb = endpoint === "web";
1677
+ return {
1678
+ id: endpoint,
1679
+ path: joinURL("/", endpoint),
1680
+ content: `${endpoint} endpoint — ${sessions.length} active session(s)`,
1681
+ meta: {
1682
+ kind: "endpoint",
1683
+ childrenCount: isWeb ? 2 : 1
1684
+ }
1685
+ };
1686
+ }
1687
+ async statEndpoint(ctx) {
1688
+ const endpoint = this.assertEndpoint(ctx);
1689
+ const isWeb = endpoint === "web";
1690
+ return { data: {
1691
+ id: endpoint,
1692
+ path: joinURL("/", endpoint),
1693
+ meta: {
1694
+ kind: "endpoint",
1695
+ childrenCount: isWeb ? 2 : 1
1696
+ }
1697
+ } };
1698
+ }
1699
+ async listEndpoint(ctx) {
1700
+ const endpoint = this.assertEndpoint(ctx);
1701
+ const sessions = this.sessions.list(endpoint);
1702
+ const entries = [{
1703
+ id: "sessions",
1704
+ path: joinURL("/", endpoint, "sessions"),
1705
+ meta: {
1706
+ kind: "sessions-directory",
1707
+ childrenCount: sessions.length
1708
+ }
1709
+ }];
1710
+ if (endpoint === "web") {
1711
+ const channelCount = this.getActiveChannelCount();
1712
+ entries.push({
1713
+ id: "live",
1714
+ path: joinURL("/", endpoint, "live"),
1715
+ meta: {
1716
+ kind: "live-directory",
1717
+ childrenCount: channelCount
1718
+ }
1719
+ });
1720
+ }
1721
+ return { data: entries };
1722
+ }
1723
+ async metaEndpoint(ctx) {
1724
+ const endpoint = this.assertEndpoint(ctx);
1725
+ const isWeb = endpoint === "web";
1726
+ return {
1727
+ id: endpoint,
1728
+ path: joinURL("/", endpoint, ".meta"),
1729
+ meta: {
1730
+ kind: "endpoint",
1731
+ childrenCount: isWeb ? 2 : 1
1732
+ }
1733
+ };
1734
+ }
1735
+ async readSessions(ctx) {
1736
+ const endpoint = this.assertEndpoint(ctx);
1737
+ const sessions = this.sessions.list(endpoint);
1738
+ return {
1739
+ id: "sessions",
1740
+ path: joinURL("/", endpoint, "sessions"),
1741
+ content: `${sessions.length} active session(s)`,
1742
+ meta: {
1743
+ kind: "sessions-directory",
1744
+ childrenCount: sessions.length
1745
+ }
1746
+ };
1747
+ }
1748
+ async metaSessions(ctx) {
1749
+ const endpoint = this.assertEndpoint(ctx);
1750
+ const sessions = this.sessions.list(endpoint);
1751
+ return {
1752
+ id: "sessions",
1753
+ path: joinURL("/", endpoint, "sessions", ".meta"),
1754
+ meta: {
1755
+ kind: "sessions-directory",
1756
+ childrenCount: sessions.length
1757
+ }
1758
+ };
1759
+ }
1760
+ async listSessions(_ctx) {
1761
+ const endpoint = this.assertEndpoint(_ctx);
1762
+ return { data: this.sessions.list(endpoint).map((s) => ({
1763
+ id: s.id,
1764
+ path: joinURL("/", endpoint, "sessions", s.id),
1765
+ meta: {
1766
+ ...s.toMeta(),
1767
+ kind: "session",
1768
+ childrenCount: -1
1769
+ }
1770
+ })) };
1771
+ }
1772
+ async listSession(ctx) {
1773
+ const session = this.resolveSession(ctx);
1774
+ const endpoint = ctx.params.endpoint;
1775
+ const basePath = joinURL("/", endpoint, "sessions", session.id);
1776
+ return { data: [
1777
+ {
1778
+ id: "messages",
1779
+ path: joinURL(basePath, "messages"),
1780
+ meta: {
1781
+ kind: "messages-directory",
1782
+ childrenCount: session.listMessages().length
1783
+ }
1784
+ },
1785
+ {
1786
+ id: "pages",
1787
+ path: joinURL(basePath, "pages"),
1788
+ meta: {
1789
+ kind: "pages-directory",
1790
+ childrenCount: session.listPages().length
1791
+ }
1792
+ },
1793
+ {
1794
+ id: "tree",
1795
+ path: joinURL(basePath, "tree"),
1796
+ meta: {
1797
+ kind: "aup:tree",
1798
+ childrenCount: 0
1799
+ }
1800
+ },
1801
+ {
1802
+ id: "wm",
1803
+ path: joinURL(basePath, "wm"),
1804
+ meta: {
1805
+ kind: "wm",
1806
+ childrenCount: 3
1807
+ }
1808
+ }
1809
+ ] };
1810
+ }
1811
+ async readSession(ctx) {
1812
+ const session = this.resolveSession(ctx);
1813
+ const endpoint = ctx.params.endpoint;
1814
+ return {
1815
+ id: session.id,
1816
+ path: joinURL("/", endpoint, "sessions", session.id),
1817
+ content: `Session ${session.id} (${session.endpoint})`,
1818
+ meta: {
1819
+ kind: "session",
1820
+ ...session.toMeta()
1821
+ }
1822
+ };
1823
+ }
1824
+ async metaSession(ctx) {
1825
+ const session = this.resolveSession(ctx);
1826
+ const endpoint = ctx.params.endpoint;
1827
+ return {
1828
+ id: session.id,
1829
+ path: joinURL("/", endpoint, "sessions", session.id, ".meta"),
1830
+ meta: {
1831
+ kind: "session",
1832
+ ...session.toMeta(),
1833
+ capabilities: {
1834
+ supportedTypes: [
1835
+ "text",
1836
+ "table",
1837
+ "form",
1838
+ "select",
1839
+ "confirm",
1840
+ "dialog",
1841
+ "progress",
1842
+ "notification",
1843
+ "markdown",
1844
+ "code"
1845
+ ],
1846
+ interactive: [
1847
+ "prompt",
1848
+ "select",
1849
+ "confirm",
1850
+ "form",
1851
+ "dialog"
1852
+ ],
1853
+ pages: true,
1854
+ fallback: "degrade_to_text"
1855
+ },
1856
+ viewport: this.backend.getViewport()
1857
+ }
1858
+ };
1859
+ }
1860
+ async statSession(ctx) {
1861
+ const session = this.resolveSession(ctx);
1862
+ const endpoint = ctx.params.endpoint;
1863
+ return { data: {
1864
+ id: session.id,
1865
+ path: joinURL("/", endpoint, "sessions", session.id),
1866
+ meta: {
1867
+ kind: "session",
1868
+ ...session.toMeta()
1869
+ }
1870
+ } };
1871
+ }
1872
+ async listSessionMessages(ctx) {
1873
+ const session = this.resolveSession(ctx);
1874
+ const endpoint = ctx.params.endpoint;
1875
+ return { data: session.listMessages().map((m) => ({
1876
+ id: m.id,
1877
+ path: joinURL("/", endpoint, "sessions", session.id, "messages", m.id),
1878
+ content: m,
1879
+ meta: {
1880
+ kind: "message",
1881
+ type: m.type,
1882
+ from: m.from
1883
+ }
1884
+ })) };
1885
+ }
1886
+ async readSessionMessage(ctx) {
1887
+ const session = this.resolveSession(ctx);
1888
+ const mid = ctx.params.mid;
1889
+ const endpoint = ctx.params.endpoint;
1890
+ const msg = session.findMessage(mid);
1891
+ if (!msg) throw new Error(`Message not found: ${mid}`);
1892
+ return {
1893
+ id: msg.id,
1894
+ path: joinURL("/", endpoint, "sessions", session.id, "messages", mid),
1895
+ content: msg,
1896
+ meta: {
1897
+ kind: "message",
1898
+ type: msg.type,
1899
+ from: msg.from
1900
+ }
1901
+ };
1902
+ }
1903
+ async writeSessionMessage(ctx, payload) {
1904
+ const session = this.resolveSession(ctx);
1905
+ const endpoint = ctx.params.endpoint;
1906
+ const msgData = typeof payload.content === "object" && payload.content !== null ? payload.content : {
1907
+ type: "text",
1908
+ from: "agent",
1909
+ content: String(payload.content ?? "")
1910
+ };
1911
+ const msg = session.addMessage(msgData);
1912
+ if (msg.type === "text" && msg.from === "agent" && typeof msg.content === "string") await this.backend.write(msg.content);
1913
+ return { data: {
1914
+ id: msg.id,
1915
+ path: joinURL("/", endpoint, "sessions", session.id, "messages", msg.id),
1916
+ content: msg,
1917
+ meta: {
1918
+ kind: "message",
1919
+ type: msg.type
1920
+ }
1921
+ } };
1922
+ }
1923
+ async listSessionPages(ctx) {
1924
+ const session = this.resolveSession(ctx);
1925
+ const endpoint = ctx.params.endpoint;
1926
+ return { data: session.listPages().map(({ id, page }) => ({
1927
+ id,
1928
+ path: joinURL("/", endpoint, "sessions", session.id, "pages", id),
1929
+ meta: {
1930
+ kind: "page",
1931
+ format: page.format,
1932
+ childrenCount: 0
1933
+ }
1934
+ })) };
1935
+ }
1936
+ async readSessionPage(ctx) {
1937
+ const session = this.resolveSession(ctx);
1938
+ const pid = ctx.params.pid;
1939
+ const endpoint = ctx.params.endpoint;
1940
+ const page = session.getPage(pid);
1941
+ if (!page) throw new Error(`Page not found: ${pid}`);
1942
+ return {
1943
+ id: pid,
1944
+ path: joinURL("/", endpoint, "sessions", session.id, "pages", pid),
1945
+ content: page.content,
1946
+ meta: {
1947
+ kind: "page",
1948
+ format: page.format,
1949
+ layout: page.layout
1950
+ }
1951
+ };
1952
+ }
1953
+ async writeSessionPage(ctx, payload) {
1954
+ const session = this.resolveSession(ctx);
1955
+ const pid = ctx.params.pid;
1956
+ const endpoint = ctx.params.endpoint;
1957
+ const content = typeof payload.content === "string" ? payload.content : String(payload.content ?? "");
1958
+ const format = payload.meta?.format ?? "html";
1959
+ const layout = payload.meta?.layout ?? void 0;
1960
+ session.setPage(pid, {
1961
+ content,
1962
+ format,
1963
+ layout
1964
+ });
1965
+ return { data: {
1966
+ id: pid,
1967
+ path: joinURL("/", endpoint, "sessions", session.id, "pages", pid),
1968
+ content,
1969
+ meta: {
1970
+ kind: "page",
1971
+ format
1972
+ }
1973
+ } };
1974
+ }
1975
+ async readSessionCaps(ctx) {
1976
+ const session = this.resolveSession(ctx);
1977
+ const endpoint = ctx.params.endpoint;
1978
+ return {
1979
+ id: ".caps",
1980
+ path: joinURL("/", endpoint, "sessions", session.id, ".caps"),
1981
+ content: session.deviceCaps,
1982
+ meta: { kind: "aup:device-caps" }
1983
+ };
1984
+ }
1985
+ async writeSessionCaps(ctx, payload) {
1986
+ const session = this.resolveSession(ctx);
1987
+ const endpoint = ctx.params.endpoint;
1988
+ const err = session.setDeviceCaps(payload.content);
1989
+ if (err) throw new Error(`Invalid DeviceCaps: ${err}`);
1990
+ const capsPath = joinURL("/", endpoint, "sessions", session.id, ".caps");
1991
+ return { data: {
1992
+ id: capsPath,
1993
+ path: capsPath
1994
+ } };
1995
+ }
1996
+ async readSessionTree(ctx) {
1997
+ const session = this.resolveSession(ctx);
1998
+ const endpoint = ctx.params.endpoint;
1999
+ const root = this.aupStores.get(session.id)?.getRoot() ?? null;
2000
+ return {
2001
+ id: "tree",
2002
+ path: joinURL("/", endpoint, "sessions", session.id, "tree"),
2003
+ content: root,
2004
+ meta: { kind: "aup:tree" }
2005
+ };
2006
+ }
2007
+ async writeSessionTree(ctx, payload) {
2008
+ const session = this.resolveSession(ctx);
2009
+ const endpoint = ctx.params.endpoint;
2010
+ const root = payload.content;
2011
+ if (!root || typeof root !== "object") throw new Error("tree write requires a node object as content");
2012
+ const err = validateNode(root);
2013
+ if (err) throw new Error(`Invalid AUP node: ${err}`);
2014
+ const store = this.getAupStore(session.id);
2015
+ store.setRoot(root);
2016
+ const meta = payload.meta ?? {};
2017
+ const fullPage = meta.fullPage === true;
2018
+ const chrome = meta.chrome === true;
2019
+ const theme = typeof meta.theme === "string" ? meta.theme : void 0;
2020
+ const style = typeof meta.style === "string" ? meta.style : void 0;
2021
+ const locale = typeof meta.locale === "string" ? meta.locale : void 0;
2022
+ if (isAUPTransport(this.backend)) {
2023
+ const msg = {
2024
+ type: "aup",
2025
+ action: "render",
2026
+ root: this.degradeForSession(store.getRoot(), session)
2027
+ };
2028
+ if (fullPage) msg.fullPage = true;
2029
+ if (chrome) msg.chrome = true;
2030
+ if (theme) msg.theme = theme;
2031
+ if (style) msg.style = style;
2032
+ if (locale) msg.locale = locale;
2033
+ this.backend.sendToSession(session.id, msg);
2034
+ }
2035
+ const treePath = joinURL("/", endpoint, "sessions", session.id, "tree");
2036
+ this.emit({
2037
+ type: "afs:write",
2038
+ path: treePath,
2039
+ data: { root: store.getRoot() }
2040
+ });
2041
+ return { data: {
2042
+ id: "tree",
2043
+ path: treePath,
2044
+ content: store.getRoot(),
2045
+ meta: { kind: "aup:tree" }
2046
+ } };
2047
+ }
2048
+ async readSessionWm(ctx) {
2049
+ const session = this.resolveSession(ctx);
2050
+ const endpoint = ctx.params.endpoint;
2051
+ const wm = this.getWmNode(session.id);
2052
+ const wmProps = wm?.wmNode.props ?? {};
2053
+ const wmState = wm?.wmNode.state ?? {};
2054
+ const surfaces = (wm?.wmNode.children ?? []).filter((c) => c.type === "wm-surface");
2055
+ return {
2056
+ id: "wm",
2057
+ path: joinURL("/", endpoint, "sessions", session.id, "wm"),
2058
+ content: {
2059
+ strategy: wmProps.strategy ?? "single",
2060
+ active: wmState.active ?? null,
2061
+ surfaceCount: surfaces.length
2062
+ },
2063
+ meta: { kind: "wm" }
2064
+ };
2065
+ }
2066
+ async listSessionWm(ctx) {
2067
+ const session = this.resolveSession(ctx);
2068
+ const endpoint = ctx.params.endpoint;
2069
+ const basePath = joinURL("/", endpoint, "sessions", session.id, "wm");
2070
+ return { data: [
2071
+ {
2072
+ id: "strategy",
2073
+ path: joinURL(basePath, "strategy"),
2074
+ meta: { kind: "wm:strategy" }
2075
+ },
2076
+ {
2077
+ id: "layout",
2078
+ path: joinURL(basePath, "layout"),
2079
+ meta: { kind: "wm:layout" }
2080
+ },
2081
+ {
2082
+ id: "surfaces",
2083
+ path: joinURL(basePath, "surfaces"),
2084
+ meta: { kind: "wm:surfaces" }
2085
+ }
2086
+ ] };
2087
+ }
2088
+ async readWmStrategy(ctx) {
2089
+ const session = this.resolveSession(ctx);
2090
+ const endpoint = ctx.params.endpoint;
2091
+ const strategy = (this.getWmNode(session.id)?.wmNode.props ?? {}).strategy ?? "single";
2092
+ return {
2093
+ id: "strategy",
2094
+ path: joinURL("/", endpoint, "sessions", session.id, "wm", "strategy"),
2095
+ content: strategy,
2096
+ meta: { kind: "wm:strategy" }
2097
+ };
2098
+ }
2099
+ async writeWmStrategy(ctx, payload) {
2100
+ const session = this.resolveSession(ctx);
2101
+ const endpoint = ctx.params.endpoint;
2102
+ const value = payload.content;
2103
+ if (typeof value !== "string") throw new Error("WM strategy must be a string");
2104
+ const { wmNode } = this.ensureWmNode(session.id, value);
2105
+ this.applyWmPatch(session.id, ctx, [{
2106
+ op: "update",
2107
+ id: wmNode.id,
2108
+ props: { strategy: value }
2109
+ }]);
2110
+ return { data: {
2111
+ id: "strategy",
2112
+ path: joinURL("/", endpoint, "sessions", session.id, "wm", "strategy"),
2113
+ content: value,
2114
+ meta: { kind: "wm:strategy" }
2115
+ } };
2116
+ }
2117
+ async readWmLayout(ctx) {
2118
+ const session = this.resolveSession(ctx);
2119
+ const endpoint = ctx.params.endpoint;
2120
+ const wm = this.getWmNode(session.id);
2121
+ const wmProps = wm?.wmNode.props ?? {};
2122
+ const wmState = wm?.wmNode.state ?? {};
2123
+ return {
2124
+ id: "layout",
2125
+ path: joinURL("/", endpoint, "sessions", session.id, "wm", "layout"),
2126
+ content: {
2127
+ active: wmState.active ?? null,
2128
+ panels: wmProps.panels ?? [],
2129
+ preset: wmProps.layoutPreset ?? null,
2130
+ panelActives: wmState.panelActives ?? null
2131
+ },
2132
+ meta: { kind: "wm:layout" }
2133
+ };
2134
+ }
2135
+ async listWmSurfaces(ctx) {
2136
+ const session = this.resolveSession(ctx);
2137
+ const endpoint = ctx.params.endpoint;
2138
+ const surfaces = (this.getWmNode(session.id)?.wmNode.children ?? []).filter((c) => c.type === "wm-surface");
2139
+ const basePath = joinURL("/", endpoint, "sessions", session.id, "wm", "surfaces");
2140
+ return { data: surfaces.map((c) => {
2141
+ const cp = c.props ?? {};
2142
+ const name = cp.surfaceName || c.id.replace(/^wm-surface-/, "");
2143
+ return {
2144
+ id: name,
2145
+ path: joinURL(basePath, name),
2146
+ meta: {
2147
+ kind: "wm:surface",
2148
+ title: cp.title ?? name,
2149
+ panel: cp.panel || void 0
2150
+ }
2151
+ };
2152
+ }) };
2153
+ }
2154
+ async readWmSurface(ctx) {
2155
+ const session = this.resolveSession(ctx);
2156
+ const endpoint = ctx.params.endpoint;
2157
+ const surfaceName = ctx.params.surfaceName;
2158
+ const wm = this.getWmNode(session.id);
2159
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, surfaceName) : void 0;
2160
+ if (!surfNode) throw new AFSNotFoundError(`Surface not found: ${surfaceName}`);
2161
+ const cp = surfNode.props ?? {};
2162
+ return {
2163
+ id: surfaceName,
2164
+ path: joinURL("/", endpoint, "sessions", session.id, "wm", "surfaces", surfaceName),
2165
+ content: {
2166
+ session: surfNode.src,
2167
+ panel: cp.panel ?? void 0,
2168
+ title: cp.title ?? cp.surfaceName ?? surfaceName,
2169
+ position: cp.position,
2170
+ size: cp.size,
2171
+ docked: !!cp.docked,
2172
+ originalPanel: cp.originalPanel ?? void 0
2173
+ },
2174
+ meta: {
2175
+ kind: "wm:surface",
2176
+ title: cp.title ?? surfaceName
2177
+ }
2178
+ };
2179
+ }
2180
+ async listWmActions(ctx) {
2181
+ const session = this.resolveSession(ctx);
2182
+ const endpoint = ctx.params.endpoint;
2183
+ const basePath = joinURL("/", endpoint, "sessions", session.id, "wm", ".actions");
2184
+ return { data: [
2185
+ {
2186
+ id: "open-surface",
2187
+ path: joinURL(basePath, "open-surface"),
2188
+ meta: {
2189
+ kind: "afs:executable",
2190
+ description: "Open (or replace) a WM surface. Universal: src can be ws:// URL, AFS path, or omitted for inline content.",
2191
+ inputSchema: {
2192
+ type: "object",
2193
+ required: ["name"],
2194
+ properties: {
2195
+ name: {
2196
+ type: "string",
2197
+ description: "Unique surface name"
2198
+ },
2199
+ src: {
2200
+ type: "string",
2201
+ description: "Source: ws:// URL, http:// URL, or /afs/path"
2202
+ },
2203
+ session: {
2204
+ type: "string",
2205
+ description: "Alias for src (backward compat)"
2206
+ },
2207
+ content: {
2208
+ type: "object",
2209
+ description: "Initial inline AUP tree (when no src)"
2210
+ },
2211
+ panel: {
2212
+ type: "string",
2213
+ description: "Panel assignment"
2214
+ },
2215
+ title: {
2216
+ type: "string",
2217
+ description: "Display title"
2218
+ }
2219
+ }
2220
+ }
2221
+ }
2222
+ },
2223
+ {
2224
+ id: "set-surface-content",
2225
+ path: joinURL(basePath, "set-surface-content"),
2226
+ meta: {
2227
+ kind: "afs:executable",
2228
+ description: "Replace the inline AUP content of a surface.",
2229
+ inputSchema: {
2230
+ type: "object",
2231
+ required: ["surface", "content"],
2232
+ properties: {
2233
+ surface: {
2234
+ type: "string",
2235
+ description: "Surface name"
2236
+ },
2237
+ content: {
2238
+ type: "object",
2239
+ description: "AUP node tree to render"
2240
+ }
2241
+ }
2242
+ }
2243
+ }
2244
+ },
2245
+ {
2246
+ id: "close-surface",
2247
+ path: joinURL(basePath, "close-surface"),
2248
+ meta: {
2249
+ kind: "afs:executable",
2250
+ description: "Close a WM surface.",
2251
+ inputSchema: {
2252
+ type: "object",
2253
+ required: ["name"],
2254
+ properties: { name: {
2255
+ type: "string",
2256
+ description: "Surface name to close"
2257
+ } }
2258
+ }
2259
+ }
2260
+ },
2261
+ {
2262
+ id: "set-strategy",
2263
+ path: joinURL(basePath, "set-strategy"),
2264
+ meta: {
2265
+ kind: "afs:executable",
2266
+ description: "Change WM strategy.",
2267
+ inputSchema: {
2268
+ type: "object",
2269
+ required: ["strategy"],
2270
+ properties: { strategy: {
2271
+ type: "string",
2272
+ enum: [
2273
+ "single",
2274
+ "panels",
2275
+ "floating",
2276
+ "virtual"
2277
+ ],
2278
+ description: "WM strategy"
2279
+ } }
2280
+ }
2281
+ }
2282
+ },
2283
+ {
2284
+ id: "set-active",
2285
+ path: joinURL(basePath, "set-active"),
2286
+ meta: {
2287
+ kind: "afs:executable",
2288
+ description: "Set the active (focused) surface.",
2289
+ inputSchema: {
2290
+ type: "object",
2291
+ required: ["name"],
2292
+ properties: { name: {
2293
+ type: "string",
2294
+ description: "Surface name to activate"
2295
+ } }
2296
+ }
2297
+ }
2298
+ },
2299
+ {
2300
+ id: "pin-to-dock",
2301
+ path: joinURL(basePath, "pin-to-dock"),
2302
+ meta: {
2303
+ kind: "afs:executable",
2304
+ description: "Pin a floating surface to the dock strip.",
2305
+ inputSchema: {
2306
+ type: "object",
2307
+ required: ["name"],
2308
+ properties: { name: {
2309
+ type: "string",
2310
+ description: "Surface name to dock"
2311
+ } }
2312
+ }
2313
+ }
2314
+ },
2315
+ {
2316
+ id: "unpin-from-dock",
2317
+ path: joinURL(basePath, "unpin-from-dock"),
2318
+ meta: {
2319
+ kind: "afs:executable",
2320
+ description: "Unpin a surface from the dock, restore to floating.",
2321
+ inputSchema: {
2322
+ type: "object",
2323
+ required: ["name"],
2324
+ properties: { name: {
2325
+ type: "string",
2326
+ description: "Surface name to undock"
2327
+ } }
2328
+ }
2329
+ }
2330
+ },
2331
+ {
2332
+ id: "set-layout",
2333
+ path: joinURL(basePath, "set-layout"),
2334
+ meta: {
2335
+ kind: "afs:executable",
2336
+ description: "Set panel layout preset (explorer, settings, simple, miller).",
2337
+ inputSchema: {
2338
+ type: "object",
2339
+ required: ["preset"],
2340
+ properties: { preset: {
2341
+ type: "string",
2342
+ description: "Panel preset name (explorer, settings, simple, miller). Unknown names fall back to simple."
2343
+ } }
2344
+ }
2345
+ }
2346
+ },
2347
+ {
2348
+ id: "collapse-panel",
2349
+ path: joinURL(basePath, "collapse-panel"),
2350
+ meta: {
2351
+ kind: "afs:executable",
2352
+ description: "Collapse a panel in the layout.",
2353
+ inputSchema: {
2354
+ type: "object",
2355
+ required: ["panelId"],
2356
+ properties: { panelId: {
2357
+ type: "string",
2358
+ description: "Panel ID to collapse"
2359
+ } }
2360
+ }
2361
+ }
2362
+ },
2363
+ {
2364
+ id: "expand-panel",
2365
+ path: joinURL(basePath, "expand-panel"),
2366
+ meta: {
2367
+ kind: "afs:executable",
2368
+ description: "Expand a collapsed panel.",
2369
+ inputSchema: {
2370
+ type: "object",
2371
+ required: ["panelId"],
2372
+ properties: { panelId: {
2373
+ type: "string",
2374
+ description: "Panel ID to expand"
2375
+ } }
2376
+ }
2377
+ }
2378
+ },
2379
+ {
2380
+ id: "move-surface",
2381
+ path: joinURL(basePath, "move-surface"),
2382
+ meta: {
2383
+ kind: "afs:executable",
2384
+ description: "Move a surface to a different panel.",
2385
+ inputSchema: {
2386
+ type: "object",
2387
+ required: ["name", "panel"],
2388
+ properties: {
2389
+ name: {
2390
+ type: "string",
2391
+ description: "Surface name to move"
2392
+ },
2393
+ panel: {
2394
+ type: "string",
2395
+ description: "Target panel ID"
2396
+ }
2397
+ }
2398
+ }
2399
+ }
2400
+ },
2401
+ {
2402
+ id: "add-to-panel",
2403
+ path: joinURL(basePath, "add-to-panel"),
2404
+ meta: {
2405
+ kind: "afs:executable",
2406
+ description: "Add a surface to a panel as a new tab.",
2407
+ inputSchema: {
2408
+ type: "object",
2409
+ required: [
2410
+ "name",
2411
+ "session",
2412
+ "panel"
2413
+ ],
2414
+ properties: {
2415
+ name: {
2416
+ type: "string",
2417
+ description: "Surface name"
2418
+ },
2419
+ session: {
2420
+ type: "string",
2421
+ description: "AUP session ID for this surface"
2422
+ },
2423
+ panel: {
2424
+ type: "string",
2425
+ description: "Target panel ID"
2426
+ },
2427
+ title: {
2428
+ type: "string",
2429
+ description: "Display title for the tab"
2430
+ }
2431
+ }
2432
+ }
2433
+ }
2434
+ },
2435
+ {
2436
+ id: "set-panel-active",
2437
+ path: joinURL(basePath, "set-panel-active"),
2438
+ meta: {
2439
+ kind: "afs:executable",
2440
+ description: "Set the active tab (surface) within a specific panel.",
2441
+ inputSchema: {
2442
+ type: "object",
2443
+ required: ["panel", "surface"],
2444
+ properties: {
2445
+ panel: {
2446
+ type: "string",
2447
+ description: "Panel ID"
2448
+ },
2449
+ surface: {
2450
+ type: "string",
2451
+ description: "Surface name to activate in the panel"
2452
+ }
2453
+ }
2454
+ }
2455
+ }
2456
+ },
2457
+ {
2458
+ id: "popout",
2459
+ path: joinURL(basePath, "popout"),
2460
+ meta: {
2461
+ kind: "afs:executable",
2462
+ description: "Detach a surface from its panel to a floating overlay.",
2463
+ inputSchema: {
2464
+ type: "object",
2465
+ required: ["name"],
2466
+ properties: { name: {
2467
+ type: "string",
2468
+ description: "Surface name to pop out"
2469
+ } }
2470
+ }
2471
+ }
2472
+ },
2473
+ {
2474
+ id: "redock",
2475
+ path: joinURL(basePath, "redock"),
2476
+ meta: {
2477
+ kind: "afs:executable",
2478
+ description: "Return a floating overlay surface to its original panel.",
2479
+ inputSchema: {
2480
+ type: "object",
2481
+ required: ["name"],
2482
+ properties: { name: {
2483
+ type: "string",
2484
+ description: "Surface name to redock"
2485
+ } }
2486
+ }
2487
+ }
2488
+ },
2489
+ {
2490
+ id: "auto-arrange",
2491
+ path: joinURL(basePath, "auto-arrange"),
2492
+ meta: {
2493
+ kind: "afs:executable",
2494
+ description: "Tile all floating surfaces evenly within the viewport. Computes non-overlapping positions using an optimal grid layout.",
2495
+ inputSchema: {
2496
+ type: "object",
2497
+ required: ["viewport"],
2498
+ properties: {
2499
+ viewport: {
2500
+ type: "object",
2501
+ description: "Desktop viewport size",
2502
+ properties: {
2503
+ width: {
2504
+ type: "number",
2505
+ description: "Viewport width in px"
2506
+ },
2507
+ height: {
2508
+ type: "number",
2509
+ description: "Viewport height in px"
2510
+ }
2511
+ },
2512
+ required: ["width", "height"]
2513
+ },
2514
+ gap: {
2515
+ type: "number",
2516
+ description: "Gap between windows in px (default 8)"
2517
+ },
2518
+ padding: {
2519
+ type: "number",
2520
+ description: "Padding from viewport edges in px (default 8)"
2521
+ },
2522
+ preserveAspect: {
2523
+ type: "boolean",
2524
+ description: "Preserve each window's aspect ratio (default false)"
2525
+ }
2526
+ }
2527
+ }
2528
+ }
2529
+ }
2530
+ ] };
2531
+ }
2532
+ async execWmOpenSurface(ctx, args) {
2533
+ const session = this.resolveSession(ctx);
2534
+ const name = args.name;
2535
+ if (!name) throw new Error("open-surface action requires 'name' argument");
2536
+ const sessionRef = args.session;
2537
+ const src = args.src || sessionRef;
2538
+ const { wmNode } = this.ensureWmNode(session.id);
2539
+ const surfId = `wm-surface-${name}`;
2540
+ const existing = this.findWmSurface(wmNode, name);
2541
+ const surfProps = {
2542
+ surfaceName: name,
2543
+ title: typeof args.title === "string" ? args.title : name
2544
+ };
2545
+ if (typeof args.panel === "string") surfProps.panel = args.panel;
2546
+ if (args.position) surfProps.position = args.position;
2547
+ if (args.size) surfProps.size = args.size;
2548
+ const surfNode = {
2549
+ id: surfId,
2550
+ type: "wm-surface",
2551
+ props: surfProps
2552
+ };
2553
+ if (src) surfNode.src = src;
2554
+ const content = args.content;
2555
+ if (content && typeof content === "object" && content.type) surfNode.children = [content];
2556
+ const ops = [];
2557
+ if (existing) ops.push({
2558
+ op: "remove",
2559
+ id: existing.id
2560
+ });
2561
+ ops.push({
2562
+ op: "create",
2563
+ id: surfId,
2564
+ parentId: "wm",
2565
+ node: surfNode
2566
+ });
2567
+ ops.push({
2568
+ op: "update",
2569
+ id: "wm",
2570
+ state: { active: name }
2571
+ });
2572
+ this.applyWmPatch(session.id, ctx, ops);
2573
+ return {
2574
+ success: true,
2575
+ data: {
2576
+ name,
2577
+ active: name
2578
+ }
2579
+ };
2580
+ }
2581
+ async execWmSetSurfaceContent(ctx, args) {
2582
+ const session = this.resolveSession(ctx);
2583
+ const surfaceName = args.surface;
2584
+ if (!surfaceName) throw new Error("set-surface-content requires 'surface' argument");
2585
+ const content = args.content;
2586
+ const wm = this.getWmNode(session.id);
2587
+ if (!wm) throw new Error(`No WM node for session ${session.id}`);
2588
+ const surfNode = this.findWmSurface(wm.wmNode, surfaceName);
2589
+ if (!surfNode) throw new Error(`Surface not found: ${surfaceName}`);
2590
+ if (content && typeof content === "object" && content.type) surfNode.children = [content];
2591
+ else surfNode.children = [];
2592
+ const store = this.getAupStore(session.id);
2593
+ const root = store.getRoot();
2594
+ if (root) store.setRoot(root);
2595
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(session.id, {
2596
+ type: "aup",
2597
+ action: "surface-update",
2598
+ surfaceId: surfNode.id,
2599
+ children: surfNode.children || [],
2600
+ treeVersion: store.version
2601
+ });
2602
+ return {
2603
+ success: true,
2604
+ data: { surface: surfaceName }
2605
+ };
2606
+ }
2607
+ async execWmCloseSurface(ctx, args) {
2608
+ const session = this.resolveSession(ctx);
2609
+ const name = args.name;
2610
+ if (!name) throw new Error("close-surface action requires 'name' argument");
2611
+ const wm = this.getWmNode(session.id);
2612
+ if (!wm) throw new Error(`Surface not found: ${name}`);
2613
+ const surfNode = this.findWmSurface(wm.wmNode, name);
2614
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2615
+ const ops = [{
2616
+ op: "remove",
2617
+ id: surfNode.id
2618
+ }];
2619
+ const wmState = wm.wmNode.state ?? {};
2620
+ if (wmState.active === name) {
2621
+ const remaining = (wm.wmNode.children ?? []).filter((c) => c.type === "wm-surface" && c.id !== surfNode.id);
2622
+ const next = remaining.length > 0 ? remaining[0].props?.surfaceName || remaining[0].id.replace(/^wm-surface-/, "") : null;
2623
+ ops.push({
2624
+ op: "update",
2625
+ id: "wm",
2626
+ state: { active: next }
2627
+ });
2628
+ }
2629
+ const surfPanel = (surfNode.props ?? {}).panel;
2630
+ const panelActives = wmState.panelActives ?? {};
2631
+ if (surfPanel && panelActives[surfPanel] === name) {
2632
+ const remainingInPanel = (wm.wmNode.children ?? []).filter((c) => c.type === "wm-surface" && c.id !== surfNode.id && c.props?.panel === surfPanel);
2633
+ const nextTab = remainingInPanel.length > 0 ? remainingInPanel[0].props?.surfaceName || remainingInPanel[0].id.replace(/^wm-surface-/, "") : void 0;
2634
+ const updatedPanelActives = { ...panelActives };
2635
+ if (nextTab) updatedPanelActives[surfPanel] = nextTab;
2636
+ else delete updatedPanelActives[surfPanel];
2637
+ ops.push({
2638
+ op: "update",
2639
+ id: "wm",
2640
+ state: { panelActives: updatedPanelActives }
2641
+ });
2642
+ }
2643
+ this.applyWmPatch(session.id, ctx, ops);
2644
+ return {
2645
+ success: true,
2646
+ data: { active: (wm.wmNode.state ?? {}).active ?? null }
2647
+ };
2648
+ }
2649
+ async execWmSetStrategy(ctx, args) {
2650
+ const session = this.resolveSession(ctx);
2651
+ const strategy = args.strategy;
2652
+ if (!strategy) throw new Error("set-strategy action requires 'strategy' argument");
2653
+ const validStrategies = [
2654
+ "single",
2655
+ "panels",
2656
+ "floating",
2657
+ "virtual"
2658
+ ];
2659
+ if (!validStrategies.includes(strategy)) throw new Error(`Invalid WM strategy: ${strategy}. Must be one of: ${validStrategies.join(", ")}`);
2660
+ this.ensureWmNode(session.id, strategy);
2661
+ this.applyWmPatch(session.id, ctx, [{
2662
+ op: "update",
2663
+ id: "wm",
2664
+ props: { strategy }
2665
+ }]);
2666
+ return {
2667
+ success: true,
2668
+ data: { strategy }
2669
+ };
2670
+ }
2671
+ async execWmSetActive(ctx, args) {
2672
+ const session = this.resolveSession(ctx);
2673
+ const name = args.name;
2674
+ if (!name) throw new Error("set-active action requires 'name' argument");
2675
+ this.ensureWmNode(session.id);
2676
+ this.applyWmPatch(session.id, ctx, [{
2677
+ op: "update",
2678
+ id: "wm",
2679
+ state: { active: name }
2680
+ }]);
2681
+ return {
2682
+ success: true,
2683
+ data: { active: name }
2684
+ };
2685
+ }
2686
+ async execWmPinToDock(ctx, args) {
2687
+ const session = this.resolveSession(ctx);
2688
+ const name = args.name;
2689
+ if (!name) throw new Error("pin-to-dock action requires 'name' argument");
2690
+ const wm = this.getWmNode(session.id);
2691
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, name) : void 0;
2692
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2693
+ this.applyWmPatch(session.id, ctx, [{
2694
+ op: "update",
2695
+ id: surfNode.id,
2696
+ props: { docked: true }
2697
+ }]);
2698
+ return {
2699
+ success: true,
2700
+ data: {
2701
+ name,
2702
+ docked: true
2703
+ }
2704
+ };
2705
+ }
2706
+ async execWmUnpinFromDock(ctx, args) {
2707
+ const session = this.resolveSession(ctx);
2708
+ const name = args.name;
2709
+ if (!name) throw new Error("unpin-from-dock action requires 'name' argument");
2710
+ const wm = this.getWmNode(session.id);
2711
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, name) : void 0;
2712
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2713
+ this.applyWmPatch(session.id, ctx, [{
2714
+ op: "update",
2715
+ id: surfNode.id,
2716
+ props: { docked: false }
2717
+ }]);
2718
+ return {
2719
+ success: true,
2720
+ data: {
2721
+ name,
2722
+ docked: false
2723
+ }
2724
+ };
2725
+ }
2726
+ async execWmSetLayout(ctx, args) {
2727
+ const session = this.resolveSession(ctx);
2728
+ const preset = args.preset;
2729
+ if (!preset) throw new Error("set-layout action requires 'preset' argument");
2730
+ const resolved = PANEL_PRESETS[preset] ?? PANEL_PRESETS.simple;
2731
+ const effectivePreset = PANEL_PRESETS[preset] ? preset : "simple";
2732
+ this.ensureWmNode(session.id, "panels");
2733
+ this.applyWmPatch(session.id, ctx, [{
2734
+ op: "update",
2735
+ id: "wm",
2736
+ props: {
2737
+ strategy: "panels",
2738
+ layoutPreset: effectivePreset,
2739
+ panels: resolved
2740
+ }
2741
+ }]);
2742
+ return {
2743
+ success: true,
2744
+ data: { preset: effectivePreset }
2745
+ };
2746
+ }
2747
+ async execWmCollapsePanel(ctx, args) {
2748
+ const session = this.resolveSession(ctx);
2749
+ const panelId = args.panelId;
2750
+ if (!panelId) throw new Error("collapse-panel action requires 'panelId' argument");
2751
+ const wm = this.getWmNode(session.id);
2752
+ if (!wm) throw new Error(`Panel not found: ${panelId}`);
2753
+ const panels = (wm.wmNode.props ?? {}).panels ?? [];
2754
+ if (!panels.find((p) => p.id === panelId)) throw new Error(`Panel not found: ${panelId}`);
2755
+ const updated = panels.map((p) => p.id === panelId ? {
2756
+ ...p,
2757
+ collapsed: true
2758
+ } : p);
2759
+ this.applyWmPatch(session.id, ctx, [{
2760
+ op: "update",
2761
+ id: "wm",
2762
+ props: { panels: updated }
2763
+ }]);
2764
+ return {
2765
+ success: true,
2766
+ data: {
2767
+ panelId,
2768
+ collapsed: true
2769
+ }
2770
+ };
2771
+ }
2772
+ async execWmExpandPanel(ctx, args) {
2773
+ const session = this.resolveSession(ctx);
2774
+ const panelId = args.panelId;
2775
+ if (!panelId) throw new Error("expand-panel action requires 'panelId' argument");
2776
+ const wm = this.getWmNode(session.id);
2777
+ if (!wm) throw new Error(`Panel not found: ${panelId}`);
2778
+ const panels = (wm.wmNode.props ?? {}).panels ?? [];
2779
+ if (!panels.find((p) => p.id === panelId)) throw new Error(`Panel not found: ${panelId}`);
2780
+ const updated = panels.map((p) => p.id === panelId ? {
2781
+ ...p,
2782
+ collapsed: false
2783
+ } : p);
2784
+ this.applyWmPatch(session.id, ctx, [{
2785
+ op: "update",
2786
+ id: "wm",
2787
+ props: { panels: updated }
2788
+ }]);
2789
+ return {
2790
+ success: true,
2791
+ data: {
2792
+ panelId,
2793
+ collapsed: false
2794
+ }
2795
+ };
2796
+ }
2797
+ async execWmMoveSurface(ctx, args) {
2798
+ const session = this.resolveSession(ctx);
2799
+ const name = args.name;
2800
+ if (!name) throw new Error("move-surface action requires 'name' argument");
2801
+ const panel = args.panel;
2802
+ if (!panel) throw new Error("move-surface action requires 'panel' argument");
2803
+ const wm = this.getWmNode(session.id);
2804
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, name) : void 0;
2805
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2806
+ const panels = (wm.wmNode.props ?? {}).panels ?? [];
2807
+ if (panels.length > 0 && !panels.some((p) => p.id === panel)) throw new Error(`Panel not found: ${panel}`);
2808
+ this.applyWmPatch(session.id, ctx, [{
2809
+ op: "update",
2810
+ id: surfNode.id,
2811
+ props: { panel }
2812
+ }]);
2813
+ return {
2814
+ success: true,
2815
+ data: {
2816
+ name,
2817
+ panel
2818
+ }
2819
+ };
2820
+ }
2821
+ async execWmAddToPanel(ctx, args) {
2822
+ const session = this.resolveSession(ctx);
2823
+ const name = args.name;
2824
+ if (!name) throw new Error("add-to-panel action requires 'name' argument");
2825
+ const sessionRef = args.session;
2826
+ if (!sessionRef) throw new Error("add-to-panel action requires 'session' argument");
2827
+ const panel = args.panel;
2828
+ if (!panel) throw new Error("add-to-panel action requires 'panel' argument");
2829
+ this.ensureWmNode(session.id, "panels");
2830
+ const panels = (this.getWmNode(session.id).wmNode.props ?? {}).panels ?? [];
2831
+ if (panels.length > 0 && !panels.some((p) => p.id === panel)) throw new Error(`Panel not found: ${panel}`);
2832
+ const surfId = `wm-surface-${name}`;
2833
+ const ops = [{
2834
+ op: "create",
2835
+ id: surfId,
2836
+ parentId: "wm",
2837
+ node: {
2838
+ id: surfId,
2839
+ type: "wm-surface",
2840
+ src: sessionRef,
2841
+ props: {
2842
+ surfaceName: name,
2843
+ title: typeof args.title === "string" ? args.title : name,
2844
+ panel
2845
+ }
2846
+ }
2847
+ }, {
2848
+ op: "update",
2849
+ id: "wm",
2850
+ state: { active: name }
2851
+ }];
2852
+ this.applyWmPatch(session.id, ctx, ops);
2853
+ return {
2854
+ success: true,
2855
+ data: {
2856
+ name,
2857
+ panel,
2858
+ active: name
2859
+ }
2860
+ };
2861
+ }
2862
+ async execWmSetPanelActive(ctx, args) {
2863
+ const session = this.resolveSession(ctx);
2864
+ const panel = args.panel;
2865
+ if (!panel) throw new Error("set-panel-active action requires 'panel' argument");
2866
+ const surface = args.surface;
2867
+ if (!surface) throw new Error("set-panel-active action requires 'surface' argument");
2868
+ this.ensureWmNode(session.id);
2869
+ const wm = this.getWmNode(session.id);
2870
+ if (!this.findWmSurface(wm.wmNode, surface)) throw new Error(`Surface not found: ${surface}`);
2871
+ const panelActives = { ...(wm.wmNode.state ?? {}).panelActives ?? {} };
2872
+ panelActives[panel] = surface;
2873
+ this.applyWmPatch(session.id, ctx, [{
2874
+ op: "update",
2875
+ id: "wm",
2876
+ state: { panelActives }
2877
+ }]);
2878
+ return {
2879
+ success: true,
2880
+ data: {
2881
+ panel,
2882
+ surface
2883
+ }
2884
+ };
2885
+ }
2886
+ async execWmPopout(ctx, args) {
2887
+ const session = this.resolveSession(ctx);
2888
+ const name = args.name;
2889
+ if (!name) throw new Error("popout action requires 'name' argument");
2890
+ const wm = this.getWmNode(session.id);
2891
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, name) : void 0;
2892
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2893
+ const originalPanel = (surfNode.props ?? {}).panel;
2894
+ if (!originalPanel) throw new Error(`Surface "${name}" has no panel to pop out from`);
2895
+ this.applyWmPatch(session.id, ctx, [{
2896
+ op: "update",
2897
+ id: surfNode.id,
2898
+ props: {
2899
+ panel: null,
2900
+ originalPanel
2901
+ }
2902
+ }]);
2903
+ return {
2904
+ success: true,
2905
+ data: {
2906
+ name,
2907
+ originalPanel
2908
+ }
2909
+ };
2910
+ }
2911
+ async execWmRedock(ctx, args) {
2912
+ const session = this.resolveSession(ctx);
2913
+ const name = args.name;
2914
+ if (!name) throw new Error("redock action requires 'name' argument");
2915
+ const wm = this.getWmNode(session.id);
2916
+ const surfNode = wm ? this.findWmSurface(wm.wmNode, name) : void 0;
2917
+ if (!surfNode) throw new Error(`Surface not found: ${name}`);
2918
+ const panel = (surfNode.props ?? {}).originalPanel;
2919
+ if (!panel) throw new Error(`Surface "${name}" was not popped out`);
2920
+ this.applyWmPatch(session.id, ctx, [{
2921
+ op: "update",
2922
+ id: surfNode.id,
2923
+ props: {
2924
+ panel,
2925
+ originalPanel: null
2926
+ }
2927
+ }]);
2928
+ return {
2929
+ success: true,
2930
+ data: {
2931
+ name,
2932
+ panel
2933
+ }
2934
+ };
2935
+ }
2936
+ async execWmAutoArrange(ctx, args) {
2937
+ const session = this.resolveSession(ctx);
2938
+ const viewport = args.viewport;
2939
+ if (!viewport || typeof viewport.width !== "number" || typeof viewport.height !== "number") throw new Error("auto-arrange action requires 'viewport' with width and height");
2940
+ const wm = this.getWmNode(session.id);
2941
+ if (!wm) return {
2942
+ success: true,
2943
+ data: { arranged: [] }
2944
+ };
2945
+ const arranged = autoArrange((wm.wmNode.children ?? []).filter((c) => c.type === "wm-surface" && !c.props?.docked).map((c) => {
2946
+ const cp = c.props ?? {};
2947
+ return {
2948
+ name: cp.surfaceName || c.id.replace(/^wm-surface-/, ""),
2949
+ size: cp.size
2950
+ };
2951
+ }), viewport, {
2952
+ gap: typeof args.gap === "number" ? args.gap : void 0,
2953
+ padding: typeof args.padding === "number" ? args.padding : void 0,
2954
+ preserveAspect: typeof args.preserveAspect === "boolean" ? args.preserveAspect : void 0
2955
+ });
2956
+ const ops = [];
2957
+ for (const a of arranged) {
2958
+ const surfNode = this.findWmSurface(wm.wmNode, a.name);
2959
+ if (surfNode) ops.push({
2960
+ op: "update",
2961
+ id: surfNode.id,
2962
+ props: {
2963
+ position: {
2964
+ x: a.x,
2965
+ y: a.y
2966
+ },
2967
+ size: {
2968
+ width: a.width,
2969
+ height: a.height
2970
+ }
2971
+ }
2972
+ });
2973
+ }
2974
+ if (ops.length > 0) this.applyWmPatch(session.id, ctx, ops);
2975
+ return {
2976
+ success: true,
2977
+ data: { arranged }
2978
+ };
2979
+ }
2980
+ async explainSession(ctx) {
2981
+ const session = this.resolveSession(ctx);
2982
+ return {
2983
+ format: "markdown",
2984
+ content: [
2985
+ `# Session ${session.id}`,
2986
+ "",
2987
+ `Endpoint: ${session.endpoint}`,
2988
+ `Messages: ${session.listMessages().length}`,
2989
+ `Pages: ${session.listPages().length}`,
2990
+ "",
2991
+ "## Paths",
2992
+ "- `messages/` — Message stream (read/write)",
2993
+ "- `pages/:id` — Managed pages (CRUD)",
2994
+ "- `.actions/*` — Interactive actions"
2995
+ ].join("\n")
2996
+ };
2997
+ }
2998
+ /** Assert web endpoint and return channelId from params */
2999
+ assertLiveChannel(ctx) {
3000
+ const endpoint = ctx.params.endpoint;
3001
+ if (endpoint !== "web") throw new AFSNotFoundError(`/${endpoint}/live`);
3002
+ this.assertEndpoint(ctx);
3003
+ return ctx.params.channelId;
3004
+ }
3005
+ async listLiveChannels(ctx) {
3006
+ const endpoint = ctx.params.endpoint;
3007
+ if (endpoint !== "web") throw new AFSNotFoundError(`/${endpoint}/live`);
3008
+ this.assertEndpoint(ctx);
3009
+ return { data: this.getActiveChannelIds().map((id) => ({
3010
+ id,
3011
+ path: joinURL("/", endpoint, "live", id),
3012
+ meta: {
3013
+ kind: "live-channel",
3014
+ childrenCount: 1
3015
+ }
3016
+ })) };
3017
+ }
3018
+ async readLiveDirectory(ctx) {
3019
+ const endpoint = ctx.params.endpoint;
3020
+ if (endpoint !== "web") throw new AFSNotFoundError(`/${endpoint}/live`);
3021
+ this.assertEndpoint(ctx);
3022
+ const ids = this.getActiveChannelIds();
3023
+ return {
3024
+ id: "live",
3025
+ path: joinURL("/", endpoint, "live"),
3026
+ content: `${ids.length} active live channel(s)`,
3027
+ meta: {
3028
+ kind: "live-directory",
3029
+ childrenCount: ids.length
3030
+ }
3031
+ };
3032
+ }
3033
+ async metaLiveDirectory(ctx) {
3034
+ const endpoint = ctx.params.endpoint;
3035
+ if (endpoint !== "web") throw new AFSNotFoundError(`/${endpoint}/live`);
3036
+ this.assertEndpoint(ctx);
3037
+ const ids = this.getActiveChannelIds();
3038
+ return {
3039
+ id: "live",
3040
+ path: joinURL("/", endpoint, "live", ".meta"),
3041
+ meta: {
3042
+ kind: "live-directory",
3043
+ childrenCount: ids.length
3044
+ }
3045
+ };
3046
+ }
3047
+ async readLiveChannel(ctx) {
3048
+ const channelId = this.assertLiveChannel(ctx);
3049
+ const endpoint = ctx.params.endpoint;
3050
+ const hasTree = !!this.aupStores.get(this.channelAupKey(channelId))?.getRoot();
3051
+ return {
3052
+ id: channelId,
3053
+ path: joinURL("/", endpoint, "live", channelId),
3054
+ content: `Live channel: ${channelId}`,
3055
+ meta: {
3056
+ kind: "live-channel",
3057
+ hasTree,
3058
+ childrenCount: 1
3059
+ }
3060
+ };
3061
+ }
3062
+ async metaLiveChannel(ctx) {
3063
+ const channelId = this.assertLiveChannel(ctx);
3064
+ const endpoint = ctx.params.endpoint;
3065
+ return {
3066
+ id: channelId,
3067
+ path: joinURL("/", endpoint, "live", channelId, ".meta"),
3068
+ meta: {
3069
+ kind: "live-channel",
3070
+ childrenCount: 1
3071
+ }
3072
+ };
3073
+ }
3074
+ async listLiveChannel(ctx) {
3075
+ const channelId = this.assertLiveChannel(ctx);
3076
+ const endpoint = ctx.params.endpoint;
3077
+ return { data: [{
3078
+ id: "tree",
3079
+ path: joinURL("/", endpoint, "live", channelId, "tree"),
3080
+ meta: {
3081
+ kind: "aup:tree",
3082
+ childrenCount: 0
3083
+ }
3084
+ }] };
3085
+ }
3086
+ async readLiveChannelTree(ctx) {
3087
+ const channelId = this.assertLiveChannel(ctx);
3088
+ const endpoint = ctx.params.endpoint;
3089
+ const root = this.aupStores.get(this.channelAupKey(channelId))?.getRoot() ?? null;
3090
+ return {
3091
+ id: "tree",
3092
+ path: joinURL("/", endpoint, "live", channelId, "tree"),
3093
+ content: root,
3094
+ meta: { kind: "aup:tree" }
3095
+ };
3096
+ }
3097
+ async metaLiveChannelTree(ctx) {
3098
+ const channelId = this.assertLiveChannel(ctx);
3099
+ const endpoint = ctx.params.endpoint;
3100
+ return {
3101
+ id: "tree",
3102
+ path: joinURL("/", endpoint, "live", channelId, "tree", ".meta"),
3103
+ meta: { kind: "aup:tree" }
3104
+ };
3105
+ }
3106
+ async writeLiveChannelTree(ctx, payload) {
3107
+ const channelId = this.assertLiveChannel(ctx);
3108
+ const endpoint = ctx.params.endpoint;
3109
+ const root = payload.content;
3110
+ if (!root || typeof root !== "object") throw new Error("tree write requires a node object as content");
3111
+ const err = validateNode(root);
3112
+ if (err) throw new Error(`Invalid AUP node: ${err}`);
3113
+ const store = this.getChannelAupStore(channelId);
3114
+ store.setRoot(root);
3115
+ const meta = payload.meta ?? {};
3116
+ const fullPage = meta.fullPage !== false;
3117
+ const chrome = meta.chrome === true;
3118
+ const theme = typeof meta.theme === "string" ? meta.theme : void 0;
3119
+ const style = typeof meta.style === "string" ? meta.style : void 0;
3120
+ const locale = typeof meta.locale === "string" ? meta.locale : void 0;
3121
+ if (isAUPTransport(this.backend)) {
3122
+ const msg = {
3123
+ type: "aup",
3124
+ action: "render",
3125
+ root: store.getRoot()
3126
+ };
3127
+ if (fullPage) msg.fullPage = true;
3128
+ if (chrome) msg.chrome = true;
3129
+ if (theme) msg.theme = theme;
3130
+ if (style) msg.style = style;
3131
+ if (locale) msg.locale = locale;
3132
+ this.backend.sendToLiveChannel(channelId, msg);
3133
+ }
3134
+ const treePath = joinURL("/", endpoint, "live", channelId, "tree");
3135
+ this.emit({
3136
+ type: "afs:write",
3137
+ path: treePath,
3138
+ data: { root: store.getRoot() }
3139
+ });
3140
+ return { data: {
3141
+ id: "tree",
3142
+ path: treePath,
3143
+ content: store.getRoot(),
3144
+ meta: { kind: "aup:tree" }
3145
+ } };
3146
+ }
3147
+ async listLiveChannelActions(ctx) {
3148
+ const channelId = this.assertLiveChannel(ctx);
3149
+ const endpoint = ctx.params.endpoint;
3150
+ const basePath = joinURL("/", endpoint, "live", channelId, ".actions");
3151
+ return { data: [{
3152
+ id: "aup_render",
3153
+ path: joinURL(basePath, "aup_render"),
3154
+ meta: {
3155
+ kind: "afs:executable",
3156
+ description: "Render AUP tree to all live channel viewers",
3157
+ inputSchema: {
3158
+ type: "object",
3159
+ required: ["root"],
3160
+ properties: {
3161
+ root: {
3162
+ type: "object",
3163
+ description: "AUP node tree"
3164
+ },
3165
+ fullPage: {
3166
+ type: "boolean",
3167
+ default: true
3168
+ },
3169
+ chrome: {
3170
+ type: "boolean",
3171
+ description: "Show lang/theme/mode toolbar"
3172
+ },
3173
+ theme: {
3174
+ type: "string",
3175
+ enum: [
3176
+ "dark",
3177
+ "light",
3178
+ "auto"
3179
+ ]
3180
+ },
3181
+ style: { type: "string" },
3182
+ locale: { type: "string" }
3183
+ }
3184
+ }
3185
+ }
3186
+ }, {
3187
+ id: "aup_patch",
3188
+ path: joinURL(basePath, "aup_patch"),
3189
+ meta: {
3190
+ kind: "afs:executable",
3191
+ description: "Patch AUP tree for all live channel viewers",
3192
+ inputSchema: {
3193
+ type: "object",
3194
+ required: ["ops"],
3195
+ properties: { ops: {
3196
+ type: "array",
3197
+ description: "Patch operations to apply",
3198
+ items: {
3199
+ type: "object",
3200
+ required: ["op", "id"],
3201
+ properties: {
3202
+ op: {
3203
+ type: "string",
3204
+ enum: [
3205
+ "create",
3206
+ "update",
3207
+ "remove",
3208
+ "reorder"
3209
+ ],
3210
+ description: "Operation type"
3211
+ },
3212
+ id: {
3213
+ type: "string",
3214
+ description: "Target node id (for update/remove/reorder) or new node id (for create)"
3215
+ },
3216
+ props: {
3217
+ type: "object",
3218
+ description: "Props to merge into node (for update)"
3219
+ },
3220
+ parentId: {
3221
+ type: "string",
3222
+ description: "Parent node id (for create)"
3223
+ },
3224
+ node: {
3225
+ type: "object",
3226
+ description: "Full AUP node { id, type, props, children } (for create)"
3227
+ },
3228
+ index: {
3229
+ type: "number",
3230
+ description: "Position index (for create/reorder)"
3231
+ }
3232
+ }
3233
+ }
3234
+ } }
3235
+ }
3236
+ }
3237
+ }] };
3238
+ }
3239
+ async execLiveChannelAupRender(ctx, args) {
3240
+ const channelId = this.assertLiveChannel(ctx);
3241
+ const endpoint = ctx.params.endpoint;
3242
+ const root = args.root;
3243
+ if (!root) throw new Error("aup_render action requires 'root' argument");
3244
+ const fullPage = args.fullPage !== false;
3245
+ const chrome = args.chrome === true;
3246
+ const theme = typeof args.theme === "string" ? args.theme : void 0;
3247
+ const style = typeof args.style === "string" ? args.style : void 0;
3248
+ const locale = typeof args.locale === "string" ? args.locale : void 0;
3249
+ const err = validateNode(root);
3250
+ if (err) throw new Error(`Invalid AUP node: ${err}`);
3251
+ const store = this.getChannelAupStore(channelId);
3252
+ store.setRoot(root);
3253
+ store.setRenderOptions({
3254
+ fullPage,
3255
+ chrome,
3256
+ theme,
3257
+ style,
3258
+ locale
3259
+ });
3260
+ if (isAUPTransport(this.backend)) {
3261
+ const msg = {
3262
+ type: "aup",
3263
+ action: "render",
3264
+ root: store.getRoot(),
3265
+ treeVersion: store.version
3266
+ };
3267
+ if (fullPage) msg.fullPage = true;
3268
+ if (chrome) msg.chrome = true;
3269
+ if (theme) msg.theme = theme;
3270
+ if (style) msg.style = style;
3271
+ if (locale) msg.locale = locale;
3272
+ this.backend.sendToLiveChannel(channelId, msg);
3273
+ }
3274
+ const treePath = joinURL("/", endpoint, "live", channelId, "tree");
3275
+ this.emit({
3276
+ type: "afs:write",
3277
+ path: treePath,
3278
+ data: { root: store.getRoot() }
3279
+ });
3280
+ return { success: true };
3281
+ }
3282
+ async execLiveChannelAupPatch(ctx, args) {
3283
+ const channelId = this.assertLiveChannel(ctx);
3284
+ const endpoint = ctx.params.endpoint;
3285
+ const ops = args.ops;
3286
+ if (!ops || !Array.isArray(ops)) throw new Error("aup_patch action requires 'ops' array argument");
3287
+ const store = this.getChannelAupStore(channelId);
3288
+ store.applyPatch(ops);
3289
+ if (isAUPTransport(this.backend)) this.backend.sendToLiveChannel(channelId, {
3290
+ type: "aup",
3291
+ action: "patch",
3292
+ ops,
3293
+ treeVersion: store.version
3294
+ });
3295
+ const treePath = joinURL("/", endpoint, "live", channelId, "tree");
3296
+ this.emit({
3297
+ type: "afs:write",
3298
+ path: treePath,
3299
+ data: { root: store.getRoot() }
3300
+ });
3301
+ return { success: true };
3302
+ }
3303
+ sessionActionEntries(basePath) {
3304
+ return [
3305
+ {
3306
+ id: "prompt",
3307
+ path: joinURL(basePath, "prompt"),
3308
+ meta: {
3309
+ kind: "afs:executable",
3310
+ description: "Ask user a question",
3311
+ inputSchema: {
3312
+ type: "object",
3313
+ required: ["message"],
3314
+ properties: {
3315
+ message: { type: "string" },
3316
+ type: {
3317
+ type: "string",
3318
+ enum: [
3319
+ "text",
3320
+ "password",
3321
+ "confirm",
3322
+ "select",
3323
+ "multiselect"
3324
+ ],
3325
+ default: "text"
3326
+ },
3327
+ options: {
3328
+ type: "array",
3329
+ items: { type: "string" }
3330
+ }
3331
+ }
3332
+ }
3333
+ }
3334
+ },
3335
+ {
3336
+ id: "clear",
3337
+ path: joinURL(basePath, "clear"),
3338
+ meta: {
3339
+ kind: "afs:executable",
3340
+ description: "Clear screen",
3341
+ inputSchema: {
3342
+ type: "object",
3343
+ properties: {}
3344
+ }
3345
+ }
3346
+ },
3347
+ {
3348
+ id: "notify",
3349
+ path: joinURL(basePath, "notify"),
3350
+ meta: {
3351
+ kind: "afs:executable",
3352
+ description: "Send notification",
3353
+ inputSchema: {
3354
+ type: "object",
3355
+ required: ["message"],
3356
+ properties: { message: { type: "string" } }
3357
+ }
3358
+ }
3359
+ },
3360
+ {
3361
+ id: "navigate",
3362
+ path: joinURL(basePath, "navigate"),
3363
+ meta: {
3364
+ kind: "afs:executable",
3365
+ description: "Navigate to a managed page",
3366
+ inputSchema: {
3367
+ type: "object",
3368
+ required: ["page"],
3369
+ properties: { page: { type: "string" } }
3370
+ }
3371
+ }
3372
+ },
3373
+ {
3374
+ id: "dialog",
3375
+ path: joinURL(basePath, "dialog"),
3376
+ meta: {
3377
+ kind: "afs:executable",
3378
+ description: "Show dialog with custom buttons",
3379
+ inputSchema: {
3380
+ type: "object",
3381
+ required: [
3382
+ "title",
3383
+ "content",
3384
+ "buttons"
3385
+ ],
3386
+ properties: {
3387
+ title: { type: "string" },
3388
+ content: { type: "string" },
3389
+ buttons: {
3390
+ type: "array",
3391
+ items: { type: "string" }
3392
+ }
3393
+ }
3394
+ }
3395
+ }
3396
+ },
3397
+ {
3398
+ id: "progress",
3399
+ path: joinURL(basePath, "progress"),
3400
+ meta: {
3401
+ kind: "afs:executable",
3402
+ description: "Display/update progress indicator",
3403
+ inputSchema: {
3404
+ type: "object",
3405
+ required: [
3406
+ "label",
3407
+ "value",
3408
+ "max"
3409
+ ],
3410
+ properties: {
3411
+ label: { type: "string" },
3412
+ value: { type: "number" },
3413
+ max: { type: "number" }
3414
+ }
3415
+ }
3416
+ }
3417
+ },
3418
+ {
3419
+ id: "form",
3420
+ path: joinURL(basePath, "form"),
3421
+ meta: {
3422
+ kind: "afs:executable",
3423
+ description: "Collect structured input via form",
3424
+ inputSchema: {
3425
+ type: "object",
3426
+ required: ["fields"],
3427
+ properties: {
3428
+ title: { type: "string" },
3429
+ fields: {
3430
+ type: "array",
3431
+ items: { type: "object" }
3432
+ }
3433
+ }
3434
+ }
3435
+ }
3436
+ },
3437
+ {
3438
+ id: "table",
3439
+ path: joinURL(basePath, "table"),
3440
+ meta: {
3441
+ kind: "afs:executable",
3442
+ description: "Display tabular data",
3443
+ inputSchema: {
3444
+ type: "object",
3445
+ required: ["headers", "rows"],
3446
+ properties: {
3447
+ headers: {
3448
+ type: "array",
3449
+ items: { type: "string" }
3450
+ },
3451
+ rows: {
3452
+ type: "array",
3453
+ items: {
3454
+ type: "array",
3455
+ items: { type: "string" }
3456
+ }
3457
+ }
3458
+ }
3459
+ }
3460
+ }
3461
+ },
3462
+ {
3463
+ id: "toast",
3464
+ path: joinURL(basePath, "toast"),
3465
+ meta: {
3466
+ kind: "afs:executable",
3467
+ description: "Show lightweight toast notification",
3468
+ inputSchema: {
3469
+ type: "object",
3470
+ required: ["message"],
3471
+ properties: {
3472
+ message: { type: "string" },
3473
+ toastType: {
3474
+ type: "string",
3475
+ enum: [
3476
+ "info",
3477
+ "success",
3478
+ "warning",
3479
+ "error"
3480
+ ],
3481
+ default: "info"
3482
+ }
3483
+ }
3484
+ }
3485
+ }
3486
+ },
3487
+ {
3488
+ id: "aup_render",
3489
+ path: joinURL(basePath, "aup_render"),
3490
+ meta: {
3491
+ kind: "afs:executable",
3492
+ description: "Render an AUP node graph (full replacement)",
3493
+ inputSchema: {
3494
+ type: "object",
3495
+ required: ["root"],
3496
+ properties: {
3497
+ root: {
3498
+ type: "object",
3499
+ description: "AUP node tree (id, type, props, children)"
3500
+ },
3501
+ fullPage: {
3502
+ type: "boolean",
3503
+ description: "When true, renders AUP as full-page (no chat chrome)"
3504
+ },
3505
+ chrome: {
3506
+ type: "boolean",
3507
+ description: "When true, shows lang/theme/mode toolbar. Hidden by default."
3508
+ },
3509
+ theme: {
3510
+ type: "string",
3511
+ enum: [
3512
+ "dark",
3513
+ "light",
3514
+ "auto"
3515
+ ],
3516
+ description: "Color mode (default: auto, follows system preference)"
3517
+ },
3518
+ style: {
3519
+ type: "string",
3520
+ enum: [
3521
+ "midnight",
3522
+ "clean",
3523
+ "glass",
3524
+ "brutalist",
3525
+ "soft",
3526
+ "cyber"
3527
+ ],
3528
+ description: "Structural style (default: midnight)"
3529
+ },
3530
+ locale: {
3531
+ type: "string",
3532
+ description: "Display locale for UI chrome (e.g. 'en', 'zh', 'ja')"
3533
+ }
3534
+ }
3535
+ }
3536
+ }
3537
+ },
3538
+ {
3539
+ id: "aup_patch",
3540
+ path: joinURL(basePath, "aup_patch"),
3541
+ meta: {
3542
+ kind: "afs:executable",
3543
+ description: "Apply incremental patches to the AUP graph",
3544
+ inputSchema: {
3545
+ type: "object",
3546
+ required: ["ops"],
3547
+ properties: { ops: {
3548
+ type: "array",
3549
+ description: "Patch operations to apply",
3550
+ items: {
3551
+ type: "object",
3552
+ required: ["op", "id"],
3553
+ properties: {
3554
+ op: {
3555
+ type: "string",
3556
+ enum: [
3557
+ "create",
3558
+ "update",
3559
+ "remove",
3560
+ "reorder"
3561
+ ],
3562
+ description: "Operation type"
3563
+ },
3564
+ id: {
3565
+ type: "string",
3566
+ description: "Target node id (for update/remove/reorder) or new node id (for create)"
3567
+ },
3568
+ props: {
3569
+ type: "object",
3570
+ description: "Props to merge into node (for update)"
3571
+ },
3572
+ parentId: {
3573
+ type: "string",
3574
+ description: "Parent node id (for create)"
3575
+ },
3576
+ node: {
3577
+ type: "object",
3578
+ description: "Full AUP node { id, type, props, children } (for create)"
3579
+ },
3580
+ index: {
3581
+ type: "number",
3582
+ description: "Position index (for create/reorder)"
3583
+ }
3584
+ }
3585
+ }
3586
+ } }
3587
+ }
3588
+ }
3589
+ },
3590
+ {
3591
+ id: "aup_save",
3592
+ path: joinURL(basePath, "aup_save"),
3593
+ meta: {
3594
+ kind: "afs:executable",
3595
+ description: "Save current AUP graph to a session page",
3596
+ inputSchema: {
3597
+ type: "object",
3598
+ required: ["pageId"],
3599
+ properties: { pageId: {
3600
+ type: "string",
3601
+ description: "Page ID to save the graph under"
3602
+ } }
3603
+ }
3604
+ }
3605
+ },
3606
+ {
3607
+ id: "aup_load",
3608
+ path: joinURL(basePath, "aup_load"),
3609
+ meta: {
3610
+ kind: "afs:executable",
3611
+ description: "Load a previously saved AUP graph from a session page",
3612
+ inputSchema: {
3613
+ type: "object",
3614
+ required: ["pageId"],
3615
+ properties: {
3616
+ pageId: {
3617
+ type: "string",
3618
+ description: "Page ID to load the graph from"
3619
+ },
3620
+ fullPage: {
3621
+ type: "boolean",
3622
+ description: "When true, renders AUP as full-page (no chat chrome)"
3623
+ },
3624
+ chrome: {
3625
+ type: "boolean",
3626
+ description: "When true, shows lang/theme/mode toolbar. Hidden by default."
3627
+ },
3628
+ theme: {
3629
+ type: "string",
3630
+ enum: [
3631
+ "dark",
3632
+ "light",
3633
+ "auto"
3634
+ ],
3635
+ description: "Color mode (default: auto, follows system preference)"
3636
+ },
3637
+ style: {
3638
+ type: "string",
3639
+ enum: [
3640
+ "midnight",
3641
+ "clean",
3642
+ "glass",
3643
+ "brutalist",
3644
+ "soft",
3645
+ "cyber"
3646
+ ],
3647
+ description: "Structural style (default: midnight)"
3648
+ },
3649
+ locale: {
3650
+ type: "string",
3651
+ description: "Display locale for UI chrome (e.g. 'en', 'zh', 'ja')"
3652
+ }
3653
+ }
3654
+ }
3655
+ }
3656
+ },
3657
+ {
3658
+ id: "aup_stage",
3659
+ path: joinURL(basePath, "aup_stage"),
3660
+ meta: {
3661
+ kind: "afs:executable",
3662
+ description: "Stage an AUP scene (pre-render off-screen). Use aup_take to swap it live.",
3663
+ inputSchema: {
3664
+ type: "object",
3665
+ required: ["sceneId", "root"],
3666
+ properties: {
3667
+ sceneId: {
3668
+ type: "string",
3669
+ description: "Unique scene identifier"
3670
+ },
3671
+ root: {
3672
+ type: "object",
3673
+ description: "AUP node tree"
3674
+ },
3675
+ fullPage: { type: "boolean" },
3676
+ chrome: { type: "boolean" },
3677
+ theme: { type: "string" },
3678
+ style: { type: "string" },
3679
+ locale: { type: "string" }
3680
+ }
3681
+ }
3682
+ }
3683
+ },
3684
+ {
3685
+ id: "aup_take",
3686
+ path: joinURL(basePath, "aup_take"),
3687
+ meta: {
3688
+ kind: "afs:executable",
3689
+ description: "Take a staged scene live (CSS swap, zero DOM teardown).",
3690
+ inputSchema: {
3691
+ type: "object",
3692
+ required: ["sceneId"],
3693
+ properties: {
3694
+ sceneId: {
3695
+ type: "string",
3696
+ description: "Scene to take live"
3697
+ },
3698
+ transition: {
3699
+ type: "string",
3700
+ enum: ["cut", "dissolve"],
3701
+ description: "Transition type (default: cut)"
3702
+ },
3703
+ duration: {
3704
+ type: "number",
3705
+ description: "Transition duration in ms"
3706
+ }
3707
+ }
3708
+ }
3709
+ }
3710
+ },
3711
+ {
3712
+ id: "aup_release",
3713
+ path: joinURL(basePath, "aup_release"),
3714
+ meta: {
3715
+ kind: "afs:executable",
3716
+ description: "Release a staged scene's resources. Cannot release the active scene.",
3717
+ inputSchema: {
3718
+ type: "object",
3719
+ required: ["sceneId"],
3720
+ properties: { sceneId: {
3721
+ type: "string",
3722
+ description: "Scene to release"
3723
+ } }
3724
+ }
3725
+ }
3726
+ }
3727
+ ];
3728
+ }
3729
+ async listSessionActions(ctx) {
3730
+ const session = this.resolveSession(ctx);
3731
+ const endpoint = ctx.params.endpoint;
3732
+ const basePath = joinURL("/", endpoint, "sessions", session.id, ".actions");
3733
+ return { data: this.sessionActionEntries(basePath) };
3734
+ }
3735
+ async execSessionPrompt(ctx, args) {
3736
+ const session = this.resolveSession(ctx);
3737
+ const message = args.message;
3738
+ if (!message) throw new Error("prompt action requires 'message' argument");
3739
+ const type = args.type ?? "text";
3740
+ const options = args.options;
3741
+ const promptMsg = session.addMessage({
3742
+ type: "prompt",
3743
+ from: "agent",
3744
+ message,
3745
+ promptType: type,
3746
+ ...options ? { options } : {}
3747
+ });
3748
+ const result = await this.backend.prompt({
3749
+ message,
3750
+ type,
3751
+ options
3752
+ });
3753
+ session.addMessage({
3754
+ type: "prompt.response",
3755
+ from: "user",
3756
+ ref: promptMsg.id,
3757
+ value: result
3758
+ });
3759
+ return {
3760
+ success: true,
3761
+ data: { response: result }
3762
+ };
3763
+ }
3764
+ async execSessionForm(ctx, args) {
3765
+ const session = this.resolveSession(ctx);
3766
+ const fields = args.fields;
3767
+ if (!fields || fields.length === 0) throw new Error("form action requires non-empty 'fields' argument");
3768
+ const title = args.title;
3769
+ const formMsg = session.addMessage({
3770
+ type: "form",
3771
+ from: "agent",
3772
+ ...title ? { title } : {},
3773
+ fields
3774
+ });
3775
+ const values = {};
3776
+ for (const field of fields) {
3777
+ const promptType = field.type === "password" ? "password" : "text";
3778
+ const result = await this.backend.prompt({
3779
+ message: `${field.label}:`,
3780
+ type: promptType
3781
+ });
3782
+ values[field.name] = result;
3783
+ }
3784
+ session.addMessage({
3785
+ type: "form.response",
3786
+ from: "user",
3787
+ ref: formMsg.id,
3788
+ data: values
3789
+ });
3790
+ return {
3791
+ success: true,
3792
+ data: { values }
3793
+ };
3794
+ }
3795
+ async execSessionDialog(ctx, args) {
3796
+ const session = this.resolveSession(ctx);
3797
+ const title = args.title;
3798
+ if (!title) throw new Error("dialog action requires 'title' argument");
3799
+ const content = args.content ?? "";
3800
+ const buttons = args.buttons;
3801
+ if (!buttons || buttons.length === 0) throw new Error("dialog action requires 'buttons' argument");
3802
+ const dialogMsg = session.addMessage({
3803
+ type: "dialog",
3804
+ from: "agent",
3805
+ title,
3806
+ content,
3807
+ buttons
3808
+ });
3809
+ const result = await this.backend.prompt({
3810
+ message: `${title}\n${content}`,
3811
+ type: "select",
3812
+ options: buttons
3813
+ });
3814
+ session.addMessage({
3815
+ type: "dialog.response",
3816
+ from: "user",
3817
+ ref: dialogMsg.id,
3818
+ selected: result
3819
+ });
3820
+ return {
3821
+ success: true,
3822
+ data: { selection: result }
3823
+ };
3824
+ }
3825
+ async execSessionTable(ctx, args) {
3826
+ const session = this.resolveSession(ctx);
3827
+ const headers = args.headers;
3828
+ if (!headers || headers.length === 0) throw new Error("table action requires 'headers' argument");
3829
+ const rows = args.rows ?? [];
3830
+ session.addMessage({
3831
+ type: "table",
3832
+ from: "agent",
3833
+ headers,
3834
+ rows
3835
+ });
3836
+ const colWidths = headers.map((h, i) => {
3837
+ let max = h.length;
3838
+ for (const row of rows) if (row[i] && row[i].length > max) max = row[i].length;
3839
+ return max;
3840
+ });
3841
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
3842
+ const headerLine = headers.map((h, i) => pad(h, colWidths[i])).join(" | ");
3843
+ const separator = colWidths.map((w) => "-".repeat(w)).join("-+-");
3844
+ const dataLines = rows.map((row) => headers.map((_, i) => pad(row[i] ?? "", colWidths[i])).join(" | "));
3845
+ await this.backend.write([
3846
+ headerLine,
3847
+ separator,
3848
+ ...dataLines
3849
+ ].join("\n"));
3850
+ return { success: true };
3851
+ }
3852
+ async execSessionToast(ctx, args) {
3853
+ const session = this.resolveSession(ctx);
3854
+ const message = args.message;
3855
+ if (!message) throw new Error("toast action requires 'message' argument");
3856
+ session.addMessage({
3857
+ type: "notification",
3858
+ from: "agent",
3859
+ message
3860
+ });
3861
+ await this.backend.notify(message);
3862
+ return { success: true };
3863
+ }
3864
+ async execSessionProgress(ctx, args) {
3865
+ const session = this.resolveSession(ctx);
3866
+ const label = args.label ?? "";
3867
+ const value = args.value;
3868
+ if (value === void 0 || value === null) throw new Error("progress action requires 'value' argument");
3869
+ const max = args.max ?? 100;
3870
+ const pct = Math.round(value / max * 100);
3871
+ session.addMessage({
3872
+ type: "progress",
3873
+ from: "agent",
3874
+ label,
3875
+ value,
3876
+ max
3877
+ });
3878
+ await this.backend.notify(`[${pct}%] ${label}`);
3879
+ return {
3880
+ success: true,
3881
+ data: {
3882
+ value,
3883
+ max,
3884
+ percent: pct
3885
+ }
3886
+ };
3887
+ }
3888
+ async execSessionClear(ctx, _args) {
3889
+ this.resolveSession(ctx);
3890
+ await this.backend.clear();
3891
+ return { success: true };
3892
+ }
3893
+ async execSessionNavigate(ctx, args) {
3894
+ const session = this.resolveSession(ctx);
3895
+ const pageId = args.page;
3896
+ if (!pageId) throw new Error("navigate action requires 'page' argument");
3897
+ const page = session.getPage(pageId) ?? this.pages.get(pageId);
3898
+ if (!page) throw new Error(`Page not found: ${pageId}`);
3899
+ if (this.backend.navigate) await this.backend.navigate(pageId, page.content, page.format, page.layout);
3900
+ else await this.backend.write(page.content);
3901
+ return { success: true };
3902
+ }
3903
+ async execSessionNotify(ctx, args) {
3904
+ const session = this.resolveSession(ctx);
3905
+ const message = args.message;
3906
+ if (!message) throw new Error("notify action requires 'message' argument");
3907
+ session.addMessage({
3908
+ type: "notification",
3909
+ from: "agent",
3910
+ message
3911
+ });
3912
+ await this.backend.notify(message);
3913
+ return { success: true };
3914
+ }
3915
+ async execSessionAupRender(ctx, args) {
3916
+ const session = this.resolveSession(ctx);
3917
+ const root = args.root;
3918
+ if (!root) throw new Error("aup_render action requires 'root' argument");
3919
+ const fullPage = args.fullPage === true;
3920
+ const chrome = args.chrome === true;
3921
+ const theme = typeof args.theme === "string" ? args.theme : void 0;
3922
+ const style = typeof args.style === "string" ? args.style : void 0;
3923
+ const locale = typeof args.locale === "string" ? args.locale : void 0;
3924
+ const err = validateNode(root);
3925
+ if (err) throw new Error(`Invalid AUP node: ${err}`);
3926
+ const store = this.getAupStore(session.id);
3927
+ store.setRoot(root);
3928
+ store.setRenderOptions({
3929
+ fullPage,
3930
+ chrome,
3931
+ theme,
3932
+ style,
3933
+ locale
3934
+ });
3935
+ if (isAUPTransport(this.backend)) {
3936
+ const msg = {
3937
+ type: "aup",
3938
+ action: "render",
3939
+ root: this.degradeForSession(store.getRoot(), session),
3940
+ treeVersion: store.version
3941
+ };
3942
+ if (fullPage) msg.fullPage = true;
3943
+ if (chrome) msg.chrome = true;
3944
+ if (theme) msg.theme = theme;
3945
+ if (style) msg.style = style;
3946
+ if (locale) msg.locale = locale;
3947
+ this.backend.sendToSession(session.id, msg);
3948
+ }
3949
+ const endpoint = ctx.params.endpoint;
3950
+ const treePath = joinURL("/", endpoint, "sessions", session.id, "tree");
3951
+ this.emit({
3952
+ type: "afs:write",
3953
+ path: treePath,
3954
+ data: { root: store.getRoot() }
3955
+ });
3956
+ return { success: true };
3957
+ }
3958
+ async execSessionAupPatch(ctx, args) {
3959
+ const session = this.resolveSession(ctx);
3960
+ const ops = args.ops;
3961
+ if (!ops || !Array.isArray(ops)) throw new Error("aup_patch action requires 'ops' array argument");
3962
+ const store = this.getAupStore(session.id);
3963
+ store.applyPatch(ops);
3964
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(session.id, {
3965
+ type: "aup",
3966
+ action: "patch",
3967
+ ops,
3968
+ treeVersion: store.version
3969
+ });
3970
+ const endpoint = ctx.params.endpoint;
3971
+ const treePath = joinURL("/", endpoint, "sessions", session.id, "tree");
3972
+ this.emit({
3973
+ type: "afs:write",
3974
+ path: treePath,
3975
+ data: { root: store.getRoot() }
3976
+ });
3977
+ return { success: true };
3978
+ }
3979
+ async execSessionAupSave(ctx, args) {
3980
+ const session = this.resolveSession(ctx);
3981
+ const pageId = args.pageId;
3982
+ if (!pageId) throw new Error("aup_save action requires 'pageId' argument");
3983
+ const root = this.aupStores.get(session.id)?.getRoot();
3984
+ if (!root) throw new Error("No active AUP graph to save");
3985
+ const graphJson = JSON.stringify(root);
3986
+ session.setPage(pageId, {
3987
+ content: graphJson,
3988
+ format: "application/json"
3989
+ });
3990
+ return { success: true };
3991
+ }
3992
+ async execSessionAupLoad(ctx, args) {
3993
+ const session = this.resolveSession(ctx);
3994
+ const pageId = args.pageId;
3995
+ if (!pageId) throw new Error("aup_load action requires 'pageId' argument");
3996
+ const fullPage = args.fullPage === true;
3997
+ const chrome = args.chrome === true;
3998
+ const theme = typeof args.theme === "string" ? args.theme : void 0;
3999
+ const style = typeof args.style === "string" ? args.style : void 0;
4000
+ const locale = typeof args.locale === "string" ? args.locale : void 0;
4001
+ const page = session.getPage(pageId);
4002
+ if (!page) throw new Error(`AUP page not found: ${pageId}`);
4003
+ const root = JSON.parse(page.content);
4004
+ const err = validateNode(root);
4005
+ if (err) throw new Error(`Invalid stored AUP graph: ${err}`);
4006
+ const store = this.getAupStore(session.id);
4007
+ store.setRoot(root);
4008
+ if (isAUPTransport(this.backend)) {
4009
+ const msg = {
4010
+ type: "aup",
4011
+ action: "render",
4012
+ root: this.degradeForSession(store.getRoot(), session)
4013
+ };
4014
+ if (fullPage) msg.fullPage = true;
4015
+ if (chrome) msg.chrome = true;
4016
+ if (theme) msg.theme = theme;
4017
+ if (style) msg.style = style;
4018
+ if (locale) msg.locale = locale;
4019
+ this.backend.sendToSession(session.id, msg);
4020
+ }
4021
+ return { success: true };
4022
+ }
4023
+ async execSessionAupStage(ctx, args) {
4024
+ const session = this.resolveSession(ctx);
4025
+ const sceneId = args.sceneId;
4026
+ if (!sceneId) throw new Error("aup_stage action requires 'sceneId' argument");
4027
+ const root = args.root;
4028
+ if (!root) throw new Error("aup_stage action requires 'root' argument");
4029
+ const fullPage = args.fullPage === true;
4030
+ const chrome = args.chrome === true;
4031
+ const theme = typeof args.theme === "string" ? args.theme : void 0;
4032
+ const style = typeof args.style === "string" ? args.style : void 0;
4033
+ const locale = typeof args.locale === "string" ? args.locale : void 0;
4034
+ const err = validateNode(root);
4035
+ if (err) throw new Error(`Invalid AUP node: ${err}`);
4036
+ const store = this.getAupManager(session.id).stage(sceneId, root, {
4037
+ fullPage,
4038
+ chrome,
4039
+ theme,
4040
+ style,
4041
+ locale
4042
+ });
4043
+ if (isAUPTransport(this.backend)) {
4044
+ const msg = {
4045
+ type: "aup",
4046
+ action: "stage",
4047
+ sceneId,
4048
+ root: this.degradeForSession(store.getRoot(), session),
4049
+ treeVersion: store.version
4050
+ };
4051
+ if (fullPage) msg.fullPage = true;
4052
+ if (chrome) msg.chrome = true;
4053
+ if (theme) msg.theme = theme;
4054
+ if (style) msg.style = style;
4055
+ if (locale) msg.locale = locale;
4056
+ this.backend.sendToSession(session.id, msg);
4057
+ }
4058
+ return { success: true };
4059
+ }
4060
+ async execSessionAupTake(ctx, args) {
4061
+ const session = this.resolveSession(ctx);
4062
+ const sceneId = args.sceneId;
4063
+ if (!sceneId) throw new Error("aup_take action requires 'sceneId' argument");
4064
+ const transition = typeof args.transition === "string" ? args.transition : void 0;
4065
+ const duration = typeof args.duration === "number" ? args.duration : void 0;
4066
+ this.getAupManager(session.id).take(sceneId);
4067
+ if (isAUPTransport(this.backend)) {
4068
+ const msg = {
4069
+ type: "aup",
4070
+ action: "take",
4071
+ sceneId
4072
+ };
4073
+ if (transition) msg.transition = transition;
4074
+ if (duration) msg.duration = duration;
4075
+ this.backend.sendToSession(session.id, msg);
4076
+ }
4077
+ return { success: true };
4078
+ }
4079
+ async execSessionAupRelease(ctx, args) {
4080
+ const session = this.resolveSession(ctx);
4081
+ const sceneId = args.sceneId;
4082
+ if (!sceneId) throw new Error("aup_release action requires 'sceneId' argument");
4083
+ this.getAupManager(session.id).release(sceneId);
4084
+ if (isAUPTransport(this.backend)) this.backend.sendToSession(session.id, {
4085
+ type: "aup",
4086
+ action: "release",
4087
+ sceneId
4088
+ });
4089
+ return { success: true };
4090
+ }
4091
+ async listActions(_ctx) {
4092
+ return { data: [
4093
+ {
4094
+ id: "prompt",
4095
+ path: "/.actions/prompt",
4096
+ meta: {
4097
+ kind: "afs:executable",
4098
+ description: "Ask user a question",
4099
+ inputSchema: {
4100
+ type: "object",
4101
+ required: ["message"],
4102
+ properties: {
4103
+ message: {
4104
+ type: "string",
4105
+ description: "The question to ask"
4106
+ },
4107
+ type: {
4108
+ type: "string",
4109
+ enum: [
4110
+ "text",
4111
+ "password",
4112
+ "confirm",
4113
+ "select",
4114
+ "multiselect"
4115
+ ],
4116
+ default: "text",
4117
+ description: "Input type"
4118
+ },
4119
+ options: {
4120
+ type: "array",
4121
+ items: { type: "string" },
4122
+ description: "Options for select/multiselect"
4123
+ }
4124
+ }
4125
+ }
4126
+ }
4127
+ },
4128
+ {
4129
+ id: "clear",
4130
+ path: "/.actions/clear",
4131
+ meta: {
4132
+ kind: "afs:executable",
4133
+ description: "Clear screen",
4134
+ inputSchema: {
4135
+ type: "object",
4136
+ properties: {}
4137
+ }
4138
+ }
4139
+ },
4140
+ {
4141
+ id: "notify",
4142
+ path: "/.actions/notify",
4143
+ meta: {
4144
+ kind: "afs:executable",
4145
+ description: "Send notification",
4146
+ inputSchema: {
4147
+ type: "object",
4148
+ required: ["message"],
4149
+ properties: { message: {
4150
+ type: "string",
4151
+ description: "Notification message"
4152
+ } }
4153
+ }
4154
+ }
4155
+ },
4156
+ {
4157
+ id: "navigate",
4158
+ path: "/.actions/navigate",
4159
+ meta: {
4160
+ kind: "afs:executable",
4161
+ description: "Navigate to a managed page",
4162
+ inputSchema: {
4163
+ type: "object",
4164
+ required: ["page"],
4165
+ properties: { page: {
4166
+ type: "string",
4167
+ description: "Page ID to navigate to"
4168
+ } }
4169
+ }
4170
+ }
4171
+ },
4172
+ {
4173
+ id: "dialog",
4174
+ path: "/.actions/dialog",
4175
+ meta: {
4176
+ kind: "afs:executable",
4177
+ description: "Show dialog with custom buttons",
4178
+ inputSchema: {
4179
+ type: "object",
4180
+ required: [
4181
+ "title",
4182
+ "content",
4183
+ "buttons"
4184
+ ],
4185
+ properties: {
4186
+ title: {
4187
+ type: "string",
4188
+ description: "Dialog title"
4189
+ },
4190
+ content: {
4191
+ type: "string",
4192
+ description: "Dialog content"
4193
+ },
4194
+ buttons: {
4195
+ type: "array",
4196
+ items: { type: "string" },
4197
+ description: "Button labels"
4198
+ }
4199
+ }
4200
+ }
4201
+ }
4202
+ },
4203
+ {
4204
+ id: "progress",
4205
+ path: "/.actions/progress",
4206
+ meta: {
4207
+ kind: "afs:executable",
4208
+ description: "Display/update progress indicator",
4209
+ inputSchema: {
4210
+ type: "object",
4211
+ required: [
4212
+ "label",
4213
+ "value",
4214
+ "max"
4215
+ ],
4216
+ properties: {
4217
+ label: {
4218
+ type: "string",
4219
+ description: "Progress label"
4220
+ },
4221
+ value: {
4222
+ type: "number",
4223
+ description: "Current value"
4224
+ },
4225
+ max: {
4226
+ type: "number",
4227
+ description: "Maximum value"
4228
+ }
4229
+ }
4230
+ }
4231
+ }
4232
+ },
4233
+ {
4234
+ id: "form",
4235
+ path: "/.actions/form",
4236
+ meta: {
4237
+ kind: "afs:executable",
4238
+ description: "Collect structured input via form",
4239
+ inputSchema: {
4240
+ type: "object",
4241
+ required: ["fields"],
4242
+ properties: {
4243
+ title: {
4244
+ type: "string",
4245
+ description: "Form title"
4246
+ },
4247
+ fields: {
4248
+ type: "array",
4249
+ items: {
4250
+ type: "object",
4251
+ required: [
4252
+ "name",
4253
+ "label",
4254
+ "type"
4255
+ ],
4256
+ properties: {
4257
+ name: { type: "string" },
4258
+ label: { type: "string" },
4259
+ type: {
4260
+ type: "string",
4261
+ enum: [
4262
+ "text",
4263
+ "number",
4264
+ "password",
4265
+ "select",
4266
+ "checkbox",
4267
+ "textarea"
4268
+ ]
4269
+ },
4270
+ options: {
4271
+ type: "array",
4272
+ items: { type: "string" }
4273
+ },
4274
+ required: { type: "boolean" }
4275
+ }
4276
+ },
4277
+ description: "Form field definitions"
4278
+ }
4279
+ }
4280
+ }
4281
+ }
4282
+ },
4283
+ {
4284
+ id: "table",
4285
+ path: "/.actions/table",
4286
+ meta: {
4287
+ kind: "afs:executable",
4288
+ description: "Display tabular data",
4289
+ inputSchema: {
4290
+ type: "object",
4291
+ required: ["headers", "rows"],
4292
+ properties: {
4293
+ headers: {
4294
+ type: "array",
4295
+ items: { type: "string" },
4296
+ description: "Column headers"
4297
+ },
4298
+ rows: {
4299
+ type: "array",
4300
+ items: {
4301
+ type: "array",
4302
+ items: { type: "string" }
4303
+ },
4304
+ description: "Table rows"
4305
+ }
4306
+ }
4307
+ }
4308
+ }
4309
+ },
4310
+ {
4311
+ id: "toast",
4312
+ path: "/.actions/toast",
4313
+ meta: {
4314
+ kind: "afs:executable",
4315
+ description: "Show lightweight toast notification",
4316
+ inputSchema: {
4317
+ type: "object",
4318
+ required: ["message"],
4319
+ properties: {
4320
+ message: {
4321
+ type: "string",
4322
+ description: "Toast message"
4323
+ },
4324
+ toastType: {
4325
+ type: "string",
4326
+ enum: [
4327
+ "info",
4328
+ "success",
4329
+ "warning",
4330
+ "error"
4331
+ ],
4332
+ default: "info",
4333
+ description: "Toast type"
4334
+ }
4335
+ }
4336
+ }
4337
+ }
4338
+ }
4339
+ ] };
4340
+ }
4341
+ async execPrompt(_ctx, args) {
4342
+ const message = args.message;
4343
+ if (!message) throw new Error("prompt action requires 'message' argument");
4344
+ const type = args.type ?? "text";
4345
+ const validTypes = [
4346
+ "text",
4347
+ "password",
4348
+ "confirm",
4349
+ "select",
4350
+ "multiselect"
4351
+ ];
4352
+ if (!validTypes.includes(type)) throw new Error(`Invalid prompt type: ${type}. Must be one of: ${validTypes.join(", ")}`);
4353
+ const options = args.options;
4354
+ return {
4355
+ success: true,
4356
+ data: { response: await this.backend.prompt({
4357
+ message,
4358
+ type,
4359
+ options
4360
+ }) }
4361
+ };
4362
+ }
4363
+ async execClear(_ctx, _args) {
4364
+ await this.backend.clear();
4365
+ return { success: true };
4366
+ }
4367
+ async execNotify(_ctx, args) {
4368
+ const message = args.message;
4369
+ if (!message) throw new Error("notify action requires 'message' argument");
4370
+ await this.backend.notify(message);
4371
+ return { success: true };
4372
+ }
4373
+ async execNavigate(_ctx, args) {
4374
+ const pageId = args.page;
4375
+ if (!pageId) throw new Error("navigate action requires 'page' argument");
4376
+ const page = this.pages.get(pageId);
4377
+ if (!page) throw new Error(`Page not found: ${pageId}`);
4378
+ if (this.backend.navigate) await this.backend.navigate(pageId, page.content, page.format, page.layout);
4379
+ else await this.backend.write(page.content);
4380
+ return { success: true };
4381
+ }
4382
+ async execDialog(_ctx, args) {
4383
+ const title = args.title;
4384
+ if (!title) throw new Error("dialog action requires 'title' argument");
4385
+ const content = args.content ?? "";
4386
+ const buttons = args.buttons;
4387
+ if (!buttons || buttons.length === 0) throw new Error("dialog action requires 'buttons' argument");
4388
+ return {
4389
+ success: true,
4390
+ data: { selection: await this.backend.prompt({
4391
+ message: `${title}\n${content}`,
4392
+ type: "select",
4393
+ options: buttons
4394
+ }) }
4395
+ };
4396
+ }
4397
+ async execProgress(_ctx, args) {
4398
+ const label = args.label ?? "";
4399
+ const value = args.value;
4400
+ if (value === void 0 || value === null) throw new Error("progress action requires 'value' argument");
4401
+ const max = args.max ?? 100;
4402
+ const pct = Math.round(value / max * 100);
4403
+ await this.backend.notify(`[${pct}%] ${label}`);
4404
+ return {
4405
+ success: true,
4406
+ data: {
4407
+ value,
4408
+ max,
4409
+ percent: pct
4410
+ }
4411
+ };
4412
+ }
4413
+ async execForm(_ctx, args) {
4414
+ const fields = args.fields;
4415
+ if (!fields || fields.length === 0) throw new Error("form action requires non-empty 'fields' argument");
4416
+ const values = {};
4417
+ for (const field of fields) {
4418
+ const promptType = field.type === "password" ? "password" : "text";
4419
+ const result = await this.backend.prompt({
4420
+ message: `${field.label}:`,
4421
+ type: promptType
4422
+ });
4423
+ values[field.name] = result;
4424
+ }
4425
+ return {
4426
+ success: true,
4427
+ data: { values }
4428
+ };
4429
+ }
4430
+ async execTable(_ctx, args) {
4431
+ const headers = args.headers;
4432
+ if (!headers || headers.length === 0) throw new Error("table action requires 'headers' argument");
4433
+ const rows = args.rows ?? [];
4434
+ const colWidths = headers.map((h, i) => {
4435
+ let max = h.length;
4436
+ for (const row of rows) if (row[i] && row[i].length > max) max = row[i].length;
4437
+ return max;
4438
+ });
4439
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
4440
+ const tableText = [
4441
+ headers.map((h, i) => pad(h, colWidths[i])).join(" | "),
4442
+ colWidths.map((w) => "-".repeat(w)).join("-+-"),
4443
+ ...rows.map((row) => headers.map((_, i) => pad(row[i] ?? "", colWidths[i])).join(" | "))
4444
+ ].join("\n");
4445
+ await this.backend.write(tableText);
4446
+ return { success: true };
4447
+ }
4448
+ async execToast(_ctx, args) {
4449
+ const message = args.message;
4450
+ if (!message) throw new Error("toast action requires 'message' argument");
4451
+ await this.backend.notify(message);
4452
+ return { success: true };
4453
+ }
4454
+ };
4455
+ __decorate([Read("/.meta/.capabilities")], AFSUIProvider.prototype, "readCapabilities", null);
4456
+ __decorate([Meta("/")], AFSUIProvider.prototype, "metaRoot", null);
4457
+ __decorate([Meta("/input")], AFSUIProvider.prototype, "metaInput", null);
4458
+ __decorate([Meta("/output")], AFSUIProvider.prototype, "metaOutput", null);
4459
+ __decorate([Meta("/pages")], AFSUIProvider.prototype, "metaPages", null);
4460
+ __decorate([Meta("/pages/:id")], AFSUIProvider.prototype, "metaPage", null);
4461
+ __decorate([List("/input")], AFSUIProvider.prototype, "listInput", null);
4462
+ __decorate([List("/output")], AFSUIProvider.prototype, "listOutput", null);
4463
+ __decorate([List("/pages")], AFSUIProvider.prototype, "listPages", null);
4464
+ __decorate([List("/")], AFSUIProvider.prototype, "listRoot", null);
4465
+ __decorate([Read("/")], AFSUIProvider.prototype, "readRoot", null);
4466
+ __decorate([Read("/input")], AFSUIProvider.prototype, "readInput", null);
4467
+ __decorate([Read("/output")], AFSUIProvider.prototype, "readOutput", null);
4468
+ __decorate([Read("/pages")], AFSUIProvider.prototype, "readPages", null);
4469
+ __decorate([Read("/pages/:id")], AFSUIProvider.prototype, "readPage", null);
4470
+ __decorate([List("/primitives")], AFSUIProvider.prototype, "listPrimitives", null);
4471
+ __decorate([Read("/primitives")], AFSUIProvider.prototype, "readPrimitivesDir", null);
4472
+ __decorate([Stat("/primitives")], AFSUIProvider.prototype, "statPrimitivesDir", null);
4473
+ __decorate([Meta("/primitives")], AFSUIProvider.prototype, "metaPrimitivesDir", null);
4474
+ __decorate([Read("/primitives/:name")], AFSUIProvider.prototype, "readPrimitive", null);
4475
+ __decorate([Stat("/primitives/:name")], AFSUIProvider.prototype, "statPrimitive", null);
4476
+ __decorate([Meta("/primitives/:name")], AFSUIProvider.prototype, "metaPrimitive", null);
4477
+ __decorate([List("/themes")], AFSUIProvider.prototype, "listThemes", null);
4478
+ __decorate([Read("/themes")], AFSUIProvider.prototype, "readThemesDir", null);
4479
+ __decorate([Stat("/themes")], AFSUIProvider.prototype, "statThemesDir", null);
4480
+ __decorate([Meta("/themes")], AFSUIProvider.prototype, "metaThemesDir", null);
4481
+ __decorate([Read("/themes/:name")], AFSUIProvider.prototype, "readTheme", null);
4482
+ __decorate([Stat("/themes/:name")], AFSUIProvider.prototype, "statTheme", null);
4483
+ __decorate([Meta("/themes/:name")], AFSUIProvider.prototype, "metaTheme", null);
4484
+ __decorate([List("/overlay-themes")], AFSUIProvider.prototype, "listOverlayThemes", null);
4485
+ __decorate([Read("/overlay-themes")], AFSUIProvider.prototype, "readOverlayThemesDir", null);
4486
+ __decorate([Stat("/overlay-themes")], AFSUIProvider.prototype, "statOverlayThemesDir", null);
4487
+ __decorate([Meta("/overlay-themes")], AFSUIProvider.prototype, "metaOverlayThemesDir", null);
4488
+ __decorate([Read("/overlay-themes/:name")], AFSUIProvider.prototype, "readOverlayTheme", null);
4489
+ __decorate([Stat("/overlay-themes/:name")], AFSUIProvider.prototype, "statOverlayTheme", null);
4490
+ __decorate([Meta("/overlay-themes/:name")], AFSUIProvider.prototype, "metaOverlayTheme", null);
4491
+ __decorate([List("/overlay-themes/:name/examples")], AFSUIProvider.prototype, "listOverlayExamples", null);
4492
+ __decorate([Read("/overlay-themes/:name/examples/:example")], AFSUIProvider.prototype, "readOverlayExample", null);
4493
+ __decorate([Read("/spec")], AFSUIProvider.prototype, "readSpec", null);
4494
+ __decorate([Stat("/spec")], AFSUIProvider.prototype, "statSpec", null);
4495
+ __decorate([List("/spec")], AFSUIProvider.prototype, "listSpec", null);
4496
+ __decorate([Meta("/spec")], AFSUIProvider.prototype, "metaSpec", null);
4497
+ __decorate([Explain("/spec")], AFSUIProvider.prototype, "explainSpec", null);
4498
+ __decorate([List("/examples")], AFSUIProvider.prototype, "listExamples", null);
4499
+ __decorate([Read("/examples")], AFSUIProvider.prototype, "readExamplesDir", null);
4500
+ __decorate([Stat("/examples")], AFSUIProvider.prototype, "statExamplesDir", null);
4501
+ __decorate([Meta("/examples")], AFSUIProvider.prototype, "metaExamplesDir", null);
4502
+ __decorate([Read("/examples/:name")], AFSUIProvider.prototype, "readExample", null);
4503
+ __decorate([Stat("/examples/:name")], AFSUIProvider.prototype, "statExample", null);
4504
+ __decorate([Meta("/examples/:name")], AFSUIProvider.prototype, "metaExample", null);
4505
+ __decorate([Write("/output")], AFSUIProvider.prototype, "writeOutput", null);
4506
+ __decorate([Write("/pages/:id")], AFSUIProvider.prototype, "writePage", null);
4507
+ __decorate([Delete("/pages/:id")], AFSUIProvider.prototype, "deletePage", null);
4508
+ __decorate([Write("/sharing/:slug")], AFSUIProvider.prototype, "writeSharingEntry", null);
4509
+ __decorate([Read("/sharing/:slug")], AFSUIProvider.prototype, "readSharingEntry", null);
4510
+ __decorate([Read("/sharing")], AFSUIProvider.prototype, "readSharingDir", null);
4511
+ __decorate([List("/sharing")], AFSUIProvider.prototype, "listSharingEntries", null);
4512
+ __decorate([Delete("/sharing/:slug")], AFSUIProvider.prototype, "deleteSharingEntry", null);
4513
+ __decorate([Meta("/sharing")], AFSUIProvider.prototype, "metaSharingDir", null);
4514
+ __decorate([Meta("/sharing/:slug")], AFSUIProvider.prototype, "metaSharingEntry", null);
4515
+ __decorate([Stat("/sharing")], AFSUIProvider.prototype, "statSharingDir", null);
4516
+ __decorate([Stat("/sharing/:slug")], AFSUIProvider.prototype, "statSharingEntry", null);
4517
+ __decorate([Actions("/sharing/:slug")], AFSUIProvider.prototype, "listSharingActions", null);
4518
+ __decorate([Actions.Exec("/sharing/:slug", "snapshot")], AFSUIProvider.prototype, "execSharingSnapshot", null);
4519
+ __decorate([Stat("/")], AFSUIProvider.prototype, "statRoot", null);
4520
+ __decorate([Stat("/input")], AFSUIProvider.prototype, "statInput", null);
4521
+ __decorate([Stat("/output")], AFSUIProvider.prototype, "statOutput", null);
4522
+ __decorate([Stat("/pages")], AFSUIProvider.prototype, "statPages", null);
4523
+ __decorate([Stat("/pages/:id")], AFSUIProvider.prototype, "statPage", null);
4524
+ __decorate([Explain("/")], AFSUIProvider.prototype, "explainRoot", null);
4525
+ __decorate([Explain("/input")], AFSUIProvider.prototype, "explainInput", null);
4526
+ __decorate([Explain("/output")], AFSUIProvider.prototype, "explainOutput", null);
4527
+ __decorate([Explain("/pages")], AFSUIProvider.prototype, "explainPages", null);
4528
+ __decorate([Read("/:endpoint")], AFSUIProvider.prototype, "readEndpoint", null);
4529
+ __decorate([Stat("/:endpoint")], AFSUIProvider.prototype, "statEndpoint", null);
4530
+ __decorate([List("/:endpoint")], AFSUIProvider.prototype, "listEndpoint", null);
4531
+ __decorate([Meta("/:endpoint")], AFSUIProvider.prototype, "metaEndpoint", null);
4532
+ __decorate([Read("/:endpoint/sessions")], AFSUIProvider.prototype, "readSessions", null);
4533
+ __decorate([Meta("/:endpoint/sessions")], AFSUIProvider.prototype, "metaSessions", null);
4534
+ __decorate([List("/:endpoint/sessions")], AFSUIProvider.prototype, "listSessions", null);
4535
+ __decorate([List("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "listSession", null);
4536
+ __decorate([Read("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "readSession", null);
4537
+ __decorate([Meta("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "metaSession", null);
4538
+ __decorate([Stat("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "statSession", null);
4539
+ __decorate([List("/:endpoint/sessions/:sid/messages")], AFSUIProvider.prototype, "listSessionMessages", null);
4540
+ __decorate([Read("/:endpoint/sessions/:sid/messages/:mid")], AFSUIProvider.prototype, "readSessionMessage", null);
4541
+ __decorate([Write("/:endpoint/sessions/:sid/messages")], AFSUIProvider.prototype, "writeSessionMessage", null);
4542
+ __decorate([List("/:endpoint/sessions/:sid/pages")], AFSUIProvider.prototype, "listSessionPages", null);
4543
+ __decorate([Read("/:endpoint/sessions/:sid/pages/:pid")], AFSUIProvider.prototype, "readSessionPage", null);
4544
+ __decorate([Write("/:endpoint/sessions/:sid/pages/:pid")], AFSUIProvider.prototype, "writeSessionPage", null);
4545
+ __decorate([Read("/:endpoint/sessions/:sid/.caps")], AFSUIProvider.prototype, "readSessionCaps", null);
4546
+ __decorate([Write("/:endpoint/sessions/:sid/.caps")], AFSUIProvider.prototype, "writeSessionCaps", null);
4547
+ __decorate([Read("/:endpoint/sessions/:sid/tree")], AFSUIProvider.prototype, "readSessionTree", null);
4548
+ __decorate([Write("/:endpoint/sessions/:sid/tree")], AFSUIProvider.prototype, "writeSessionTree", null);
4549
+ __decorate([Read("/:endpoint/sessions/:sid/wm")], AFSUIProvider.prototype, "readSessionWm", null);
4550
+ __decorate([List("/:endpoint/sessions/:sid/wm")], AFSUIProvider.prototype, "listSessionWm", null);
4551
+ __decorate([Read("/:endpoint/sessions/:sid/wm/strategy")], AFSUIProvider.prototype, "readWmStrategy", null);
4552
+ __decorate([Write("/:endpoint/sessions/:sid/wm/strategy")], AFSUIProvider.prototype, "writeWmStrategy", null);
4553
+ __decorate([Read("/:endpoint/sessions/:sid/wm/layout")], AFSUIProvider.prototype, "readWmLayout", null);
4554
+ __decorate([List("/:endpoint/sessions/:sid/wm/surfaces")], AFSUIProvider.prototype, "listWmSurfaces", null);
4555
+ __decorate([Read("/:endpoint/sessions/:sid/wm/surfaces/:surfaceName")], AFSUIProvider.prototype, "readWmSurface", null);
4556
+ __decorate([Actions("/:endpoint/sessions/:sid/wm")], AFSUIProvider.prototype, "listWmActions", null);
4557
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "open-surface")], AFSUIProvider.prototype, "execWmOpenSurface", null);
4558
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "set-surface-content")], AFSUIProvider.prototype, "execWmSetSurfaceContent", null);
4559
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "close-surface")], AFSUIProvider.prototype, "execWmCloseSurface", null);
4560
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "set-strategy")], AFSUIProvider.prototype, "execWmSetStrategy", null);
4561
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "set-active")], AFSUIProvider.prototype, "execWmSetActive", null);
4562
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "pin-to-dock")], AFSUIProvider.prototype, "execWmPinToDock", null);
4563
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "unpin-from-dock")], AFSUIProvider.prototype, "execWmUnpinFromDock", null);
4564
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "set-layout")], AFSUIProvider.prototype, "execWmSetLayout", null);
4565
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "collapse-panel")], AFSUIProvider.prototype, "execWmCollapsePanel", null);
4566
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "expand-panel")], AFSUIProvider.prototype, "execWmExpandPanel", null);
4567
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "move-surface")], AFSUIProvider.prototype, "execWmMoveSurface", null);
4568
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "add-to-panel")], AFSUIProvider.prototype, "execWmAddToPanel", null);
4569
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "set-panel-active")], AFSUIProvider.prototype, "execWmSetPanelActive", null);
4570
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "popout")], AFSUIProvider.prototype, "execWmPopout", null);
4571
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "redock")], AFSUIProvider.prototype, "execWmRedock", null);
4572
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid/wm", "auto-arrange")], AFSUIProvider.prototype, "execWmAutoArrange", null);
4573
+ __decorate([Explain("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "explainSession", null);
4574
+ __decorate([List("/:endpoint/live")], AFSUIProvider.prototype, "listLiveChannels", null);
4575
+ __decorate([Read("/:endpoint/live")], AFSUIProvider.prototype, "readLiveDirectory", null);
4576
+ __decorate([Meta("/:endpoint/live")], AFSUIProvider.prototype, "metaLiveDirectory", null);
4577
+ __decorate([Read("/:endpoint/live/:channelId")], AFSUIProvider.prototype, "readLiveChannel", null);
4578
+ __decorate([Meta("/:endpoint/live/:channelId")], AFSUIProvider.prototype, "metaLiveChannel", null);
4579
+ __decorate([List("/:endpoint/live/:channelId")], AFSUIProvider.prototype, "listLiveChannel", null);
4580
+ __decorate([Read("/:endpoint/live/:channelId/tree")], AFSUIProvider.prototype, "readLiveChannelTree", null);
4581
+ __decorate([Meta("/:endpoint/live/:channelId/tree")], AFSUIProvider.prototype, "metaLiveChannelTree", null);
4582
+ __decorate([Write("/:endpoint/live/:channelId/tree")], AFSUIProvider.prototype, "writeLiveChannelTree", null);
4583
+ __decorate([Actions("/:endpoint/live/:channelId")], AFSUIProvider.prototype, "listLiveChannelActions", null);
4584
+ __decorate([Actions.Exec("/:endpoint/live/:channelId", "aup_render")], AFSUIProvider.prototype, "execLiveChannelAupRender", null);
4585
+ __decorate([Actions.Exec("/:endpoint/live/:channelId", "aup_patch")], AFSUIProvider.prototype, "execLiveChannelAupPatch", null);
4586
+ __decorate([Actions("/:endpoint/sessions/:sid")], AFSUIProvider.prototype, "listSessionActions", null);
4587
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "prompt")], AFSUIProvider.prototype, "execSessionPrompt", null);
4588
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "form")], AFSUIProvider.prototype, "execSessionForm", null);
4589
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "dialog")], AFSUIProvider.prototype, "execSessionDialog", null);
4590
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "table")], AFSUIProvider.prototype, "execSessionTable", null);
4591
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "toast")], AFSUIProvider.prototype, "execSessionToast", null);
4592
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "progress")], AFSUIProvider.prototype, "execSessionProgress", null);
4593
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "clear")], AFSUIProvider.prototype, "execSessionClear", null);
4594
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "navigate")], AFSUIProvider.prototype, "execSessionNavigate", null);
4595
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "notify")], AFSUIProvider.prototype, "execSessionNotify", null);
4596
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_render")], AFSUIProvider.prototype, "execSessionAupRender", null);
4597
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_patch")], AFSUIProvider.prototype, "execSessionAupPatch", null);
4598
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_save")], AFSUIProvider.prototype, "execSessionAupSave", null);
4599
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_load")], AFSUIProvider.prototype, "execSessionAupLoad", null);
4600
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_stage")], AFSUIProvider.prototype, "execSessionAupStage", null);
4601
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_take")], AFSUIProvider.prototype, "execSessionAupTake", null);
4602
+ __decorate([Actions.Exec("/:endpoint/sessions/:sid", "aup_release")], AFSUIProvider.prototype, "execSessionAupRelease", null);
4603
+ __decorate([Actions("/")], AFSUIProvider.prototype, "listActions", null);
4604
+ __decorate([Actions.Exec("/", "prompt")], AFSUIProvider.prototype, "execPrompt", null);
4605
+ __decorate([Actions.Exec("/", "clear")], AFSUIProvider.prototype, "execClear", null);
4606
+ __decorate([Actions.Exec("/", "notify")], AFSUIProvider.prototype, "execNotify", null);
4607
+ __decorate([Actions.Exec("/", "navigate")], AFSUIProvider.prototype, "execNavigate", null);
4608
+ __decorate([Actions.Exec("/", "dialog")], AFSUIProvider.prototype, "execDialog", null);
4609
+ __decorate([Actions.Exec("/", "progress")], AFSUIProvider.prototype, "execProgress", null);
4610
+ __decorate([Actions.Exec("/", "form")], AFSUIProvider.prototype, "execForm", null);
4611
+ __decorate([Actions.Exec("/", "table")], AFSUIProvider.prototype, "execTable", null);
4612
+ __decorate([Actions.Exec("/", "toast")], AFSUIProvider.prototype, "execToast", null);
4613
+
4614
+ //#endregion
4615
+ export { AFSUIProvider };
4616
+ //# sourceMappingURL=ui-provider.mjs.map