@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 @@
1
+ {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport type { AFSRoot, AFSUnsubscribe } from \"@aigne/afs\";\nimport { joinURL } from \"ufo\";\nimport { type WebSocket, WebSocketServer } from \"ws\";\nimport type {\n PromptOptions,\n PromptResult,\n ReadOptions,\n UIBackend,\n ViewportInfo,\n WriteOptions,\n} from \"./backend.js\";\nimport { createMockInputSource, type TTYInputSource } from \"./tty.js\";\nimport { WEB_CLIENT_HTML } from \"./web-page.js\";\n\nexport interface WebBackendOptions {\n /** Port to listen on (0 = OS-assigned random port) */\n port?: number;\n /** Host to bind to */\n host?: string;\n /** For testing: custom input source (bypasses WebSocket) */\n inputSource?: TTYInputSource & { push?: (line: string) => void };\n /** For testing: custom output handler (bypasses WebSocket) */\n stdout?: { write(data: string): boolean };\n}\n\n/**\n * WebBackend — HTTP + WebSocket based browser UI.\n *\n * Serves a web page and communicates with it via WebSocket.\n * For agent code, this behaves identically to TTYBackend — pure text I/O.\n *\n * For testing, accepts an inputSource/stdout to bypass WebSocket entirely.\n */\nexport class WebBackend implements UIBackend {\n readonly type = \"web\";\n readonly supportedFormats = [\"text\", \"html\", \"markdown\", \"component\"];\n readonly capabilities = [\"text\", \"html\", \"markdown\", \"component\"];\n\n private static readonly KNOWN_COMPONENTS = new Set([\"code-block\", \"table\", \"image\"]);\n\n private port: number;\n private host: string;\n private server: Server | null = null;\n private wss: WebSocketServer | null = null;\n private clients = new Set<WebSocket>();\n /** Track all TCP sockets so we can force-destroy on close */\n private sockets = new Set<Socket>();\n\n /** Input queue — fed by WebSocket messages or test inputSource */\n private inputSource: TTYInputSource & { push?: (line: string) => void };\n /** Output handler — sends to WebSocket clients or test stdout */\n private outputHandler: (data: string) => void;\n\n /** Queue for messages sent before any client connects */\n private pendingMessages: string[] = [];\n\n /** Pending prompt resolve — only one prompt at a time */\n private promptResolve: ((value: PromptResult) => void) | null = null;\n /** Pending prompt message — re-sent on client reconnect */\n private pendingPromptMessage: string | null = null;\n\n private testMode: boolean;\n private _url: string | null = null;\n\n /** Per-client session tracking */\n private sessionForClient = new Map<WebSocket, string>();\n /** Per-client session token tracking */\n private sessionTokenForClient = new Map<WebSocket, string>();\n private createSessionCallback:\n | ((\n endpoint: string,\n requestedSessionId?: string,\n requestedSessionToken?: string,\n caps?: Record<string, unknown>,\n ) => { sessionId: string; sessionToken?: string })\n | null = null;\n /** AUP event handler callback */\n private aupEventHandler:\n | ((\n msg: { nodeId: string; event: string; data?: Record<string, unknown> },\n sessionId?: string,\n channelId?: string,\n ) => Promise<unknown>)\n | null = null;\n\n /** Page resolver callback — returns page content for HTTP serving */\n private pageResolver:\n | ((\n pageId: string,\n sessionId?: string,\n sessionToken?: string,\n ) => Promise<{ content: string; format: string } | null>)\n | null = null;\n\n /** Snapshot resolver callback — returns snapshot HTML for a sharing slug */\n private snapshotResolver: ((slug: string) => string | null) | null = null;\n\n /** AFS root instance — injected via setAFS() when provider is mounted */\n private afs: AFSRoot | null = null;\n /** Per-client AFS event subscriptions: ws → subId → unsubscribe */\n private clientSubscriptions = new Map<WebSocket, Map<string, AFSUnsubscribe>>();\n\n /** Live channel subscribers: channelId → set of viewer WebSockets */\n private channelSubscribers = new Map<string, Set<WebSocket>>();\n /** Reverse map: ws → channelId (only for channel viewers) */\n private channelForClient = new Map<WebSocket, string>();\n /** Callback invoked when a viewer joins a channel — provider sends snapshot */\n private channelJoinHandler:\n | ((channelId: string, send: (msg: Record<string, unknown>) => void) => void)\n | null = null;\n /** Callback invoked when a client joins/reconnects a session — provider sends snapshot if stale */\n private sessionJoinHandler:\n | ((\n sessionId: string,\n clientVersion: number,\n send: (msg: Record<string, unknown>) => void,\n ) => void)\n | null = null;\n\n constructor(options: WebBackendOptions = {}) {\n this.port = options.port ?? 0;\n this.host = options.host ?? \"localhost\";\n\n if (options.inputSource) {\n this.testMode = true;\n this.inputSource = options.inputSource;\n this.outputHandler = options.stdout\n ? (data) => {\n options.stdout!.write(data);\n }\n : () => {};\n } else {\n this.testMode = false;\n const queue = createMockInputSource();\n this.inputSource = queue;\n this.outputHandler = (data) =>\n this.broadcast(JSON.stringify({ type: \"write\", content: data }));\n }\n }\n\n /** URL of the running server, or null if not started. */\n get url(): string | null {\n return this._url;\n }\n\n /** Register a factory that creates or reattaches a session for each new WebSocket client. */\n setSessionFactory(\n fn: (\n endpoint: string,\n requestedSessionId?: string,\n requestedSessionToken?: string,\n caps?: Record<string, unknown>,\n ) => { sessionId: string; sessionToken?: string },\n ): void {\n this.createSessionCallback = fn;\n }\n\n /** Broadcast a raw JSON-serializable message to all connected clients. */\n broadcastRaw(msg: Record<string, unknown>): void {\n if (this.testMode) return;\n this.broadcast(JSON.stringify(msg));\n }\n\n /** Send a raw JSON message to the client that owns a specific session. */\n sendToSession(sessionId: string, msg: Record<string, unknown>): void {\n if (this.testMode) return;\n const data = JSON.stringify(msg);\n for (const [ws, sid] of this.sessionForClient) {\n if (sid === sessionId && ws.readyState === 1 /* OPEN */) {\n ws.send(data);\n return;\n }\n }\n }\n\n /** Send a raw JSON message to all viewers of a live channel. */\n sendToLiveChannel(channelId: string, msg: Record<string, unknown>): void {\n if (this.testMode) return;\n const viewers = this.channelSubscribers.get(channelId);\n if (!viewers) return;\n const data = JSON.stringify(msg);\n for (const ws of viewers) {\n if (ws.readyState === 1) ws.send(data);\n }\n }\n\n /** Register a handler called when a viewer joins a channel (for snapshot delivery). */\n setChannelJoinHandler(\n fn: (channelId: string, send: (msg: Record<string, unknown>) => void) => void,\n ): void {\n this.channelJoinHandler = fn;\n }\n\n /** Register a handler called when a client joins/reconnects a session (for snapshot replay). */\n setSessionJoinHandler(\n fn: (\n sessionId: string,\n clientVersion: number,\n send: (msg: Record<string, unknown>) => void,\n ) => void,\n ): void {\n this.sessionJoinHandler = fn;\n }\n\n /** Return the set of active channel IDs. */\n getActiveChannelIds(): string[] {\n return [...this.channelSubscribers.keys()];\n }\n\n /** Register a handler for AUP events from clients. */\n setAupEventHandler(\n fn: (\n msg: { nodeId: string; event: string; data?: Record<string, unknown> },\n sessionId?: string,\n channelId?: string,\n ) => Promise<unknown>,\n ): void {\n this.aupEventHandler = fn;\n }\n\n /** Inject the AFS root for browser AFS proxy operations. */\n setAFS(afs: AFSRoot): void {\n this.afs = afs;\n }\n\n /** Register a resolver for serving pages via HTTP (/p/:id). */\n setPageResolver(\n fn: (\n pageId: string,\n sessionId?: string,\n sessionToken?: string,\n ) => Promise<{ content: string; format: string } | null>,\n ): void {\n this.pageResolver = fn;\n }\n\n /** Set the snapshot resolver for web sharing. */\n setSnapshotResolver(fn: (slug: string) => string | null): void {\n this.snapshotResolver = fn;\n }\n\n /** Start the HTTP + WebSocket server. */\n async listen(): Promise<{ port: number; host: string }> {\n return new Promise((resolve, reject) => {\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const requestUrl = req.url ?? \"\";\n const pathname = requestUrl.split(\"?\")[0] ?? \"\";\n if (pathname === \"/\" || pathname === \"/index.html\" || pathname.startsWith(\"/live/\")) {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(WEB_CLIENT_HTML);\n } else if (requestUrl.startsWith(\"/p/\")) {\n this.handlePageRequest(requestUrl, res);\n } else if (pathname.startsWith(\"/s/\") && this.snapshotResolver) {\n this.handleSnapshotRequest(pathname, res);\n } else {\n res.writeHead(404);\n res.end(\"Not Found\");\n }\n });\n\n // Track TCP sockets for force-close\n server.on(\"connection\", (socket: Socket) => {\n this.sockets.add(socket);\n socket.on(\"close\", () => this.sockets.delete(socket));\n });\n\n server.on(\"error\", (err) => {\n server.close();\n reject(err);\n });\n\n // Bind to 127.0.0.1 to avoid IPv6 address issues in URLs\n const bindHost = this.host === \"localhost\" ? \"127.0.0.1\" : this.host;\n server.listen({ port: this.port, host: bindHost, exclusive: true }, () => {\n this.server = server;\n\n const addr = server.address();\n if (typeof addr === \"object\" && addr) {\n this.port = addr.port;\n }\n\n this._url = `http://127.0.0.1:${this.port}`;\n\n // Create WebSocket server only after HTTP server is listening\n this.wss = new WebSocketServer({ server });\n this.wss.on(\"connection\", (ws, req) => this.onConnection(ws, req));\n\n resolve({ port: this.port, host: this.host });\n });\n });\n }\n\n /** Shut down the server and disconnect all clients. */\n async close(): Promise<void> {\n // Terminate all WebSocket clients\n for (const ws of this.clients) {\n ws.terminate();\n }\n this.clients.clear();\n this.sessionForClient.clear();\n this.sessionTokenForClient.clear();\n this.channelSubscribers.clear();\n this.channelForClient.clear();\n\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n if (this.server) {\n // Stop accepting new connections\n this.server.close();\n\n // Force-destroy all TCP sockets so server.close() completes immediately\n for (const socket of this.sockets) {\n socket.destroy();\n }\n this.sockets.clear();\n\n this.server = null;\n }\n\n this._url = null;\n }\n\n // ─── UIBackend Interface ──────────────────────────────────\n\n async write(content: string, options?: WriteOptions): Promise<void> {\n const format = options?.format ?? \"text\";\n\n if (!this.supportedFormats.includes(format)) {\n throw new Error(`Web backend does not support format: ${format}`);\n }\n\n if (format === \"component\") {\n if (!options?.component) {\n throw new Error(\"format 'component' requires a component type\");\n }\n if (!WebBackend.KNOWN_COMPONENTS.has(options.component)) {\n throw new Error(`Unknown component type: ${options.component}`);\n }\n }\n\n // Sanitize HTML content server-side before sending\n const sanitizedContent = format === \"html\" ? sanitizeHtml(content) : content;\n\n if (this.testMode) {\n this.outputHandler(sanitizedContent);\n return;\n }\n\n const msg: Record<string, unknown> = { type: \"write\", content: sanitizedContent };\n if (format !== \"text\") {\n msg.format = format;\n }\n if (options?.component) {\n msg.component = options.component;\n }\n if (options?.componentProps) {\n msg.componentProps = options.componentProps;\n }\n\n const payload = JSON.stringify(msg);\n if (this.clients.size === 0) {\n this.pendingMessages.push(payload);\n } else {\n this.broadcast(payload);\n }\n }\n\n async read(options?: ReadOptions): Promise<string> {\n const timeout = options?.timeout ?? 0;\n if (timeout > 0) {\n return withTimeout(this.inputSource.readLine(), timeout);\n }\n return this.inputSource.readLine();\n }\n\n async prompt(options: PromptOptions): Promise<PromptResult> {\n if (this.testMode) {\n return this.ttyStylePrompt(options);\n }\n\n const msg = JSON.stringify({\n type: \"prompt\",\n message: options.message,\n promptType: options.type,\n options: options.options,\n });\n\n // Store for reconnect replay — if the client disconnects and reconnects\n // while a prompt is pending, the new client will receive it immediately\n this.pendingPromptMessage = msg;\n\n if (this.clients.size > 0) {\n this.broadcast(msg);\n } else {\n // No clients connected — queue so it's sent on first connect\n this.pendingMessages.push(msg);\n }\n\n return new Promise((resolve) => {\n this.promptResolve = resolve;\n });\n }\n\n async notify(message: string): Promise<void> {\n if (this.testMode) {\n this.outputHandler(`${message}\\n`);\n return;\n }\n this.broadcast(JSON.stringify({ type: \"notify\", message }));\n }\n\n async navigate(\n pageId: string,\n content: string,\n format?: string,\n layout?: Record<string, string>,\n ): Promise<void> {\n // Sanitize HTML content\n const sanitizedContent = format === \"html\" ? sanitizeHtml(content) : content;\n\n if (this.testMode) {\n this.outputHandler(sanitizedContent);\n return;\n }\n\n const msg: Record<string, unknown> = {\n type: \"navigate\",\n pageId,\n content: sanitizedContent,\n format: format ?? \"html\",\n };\n if (layout) {\n msg.layout = layout;\n }\n this.broadcast(JSON.stringify(msg));\n }\n\n async clear(): Promise<void> {\n if (this.testMode) {\n this.outputHandler(\"\\x1b[2J\\x1b[H\");\n return;\n }\n this.broadcast(JSON.stringify({ type: \"clear\" }));\n }\n\n hasPendingInput(): boolean {\n return this.inputSource.hasPending();\n }\n\n getViewport(): ViewportInfo {\n return {};\n }\n\n async dispose(): Promise<void> {\n await this.close();\n }\n\n // ─── Private ──────────────────────────────────────────────\n\n private onConnection(ws: WebSocket, req: IncomingMessage): void {\n const origin = req.headers.origin;\n if (typeof origin === \"string\" && origin && !this.isAllowedWsOrigin(origin)) {\n ws.close(1008, \"Invalid origin\");\n return;\n }\n\n this.clients.add(ws);\n\n let initialized = false;\n\n ws.on(\"message\", (data) => {\n try {\n const msg = JSON.parse(data.toString()) as Record<string, unknown>;\n\n // First message is the handshake — route to session or channel\n if (!initialized) {\n initialized = true;\n this.onHandshake(ws, msg);\n return;\n }\n\n this.onMessage(msg, ws);\n } catch {\n // Ignore malformed messages\n }\n });\n\n ws.on(\"close\", () => {\n this.clients.delete(ws);\n // Clean up session tracking\n this.sessionForClient.delete(ws);\n this.sessionTokenForClient.delete(ws);\n // Clean up channel tracking\n const channelId = this.channelForClient.get(ws);\n if (channelId) {\n this.channelForClient.delete(ws);\n const viewers = this.channelSubscribers.get(channelId);\n if (viewers) {\n viewers.delete(ws);\n if (viewers.size === 0) this.channelSubscribers.delete(channelId);\n }\n }\n // Clean up AFS subscriptions\n const subs = this.clientSubscriptions.get(ws);\n if (subs) {\n for (const unsub of subs.values()) unsub();\n this.clientSubscriptions.delete(ws);\n }\n });\n }\n\n /** Handle the first WS message as a typed handshake. */\n private onHandshake(ws: WebSocket, msg: Record<string, unknown>): void {\n if (msg.type === \"join_channel\") {\n // ── Live channel viewer ──\n const channelId = String(msg.channelId ?? \"\");\n if (!channelId) {\n ws.close(4000, \"channelId required\");\n return;\n }\n this.channelForClient.set(ws, channelId);\n if (!this.channelSubscribers.has(channelId)) {\n this.channelSubscribers.set(channelId, new Set());\n }\n this.channelSubscribers.get(channelId)!.add(ws);\n\n ws.send(JSON.stringify({ type: \"channel\", channelId }));\n\n // Deliver current tree snapshot to this viewer\n if (this.channelJoinHandler) {\n this.channelJoinHandler(channelId, (m) => {\n if (ws.readyState === 1) ws.send(JSON.stringify(m));\n });\n }\n } else {\n // ── Private session (join_session or legacy) ──\n if (this.createSessionCallback) {\n const requestedSid = msg.sessionId ? String(msg.sessionId) : undefined;\n const requestedSessionToken = msg.sessionToken ? String(msg.sessionToken) : undefined;\n const caps =\n msg.caps && typeof msg.caps === \"object\" && !Array.isArray(msg.caps)\n ? (msg.caps as Record<string, unknown>)\n : undefined;\n const created = this.createSessionCallback(\n this.type,\n requestedSid,\n requestedSessionToken,\n caps,\n );\n const sessionId = created.sessionId;\n this.sessionForClient.set(ws, sessionId);\n if (created.sessionToken) this.sessionTokenForClient.set(ws, created.sessionToken);\n ws.send(\n JSON.stringify({\n type: \"session\",\n sessionId,\n sessionToken: created.sessionToken ?? null,\n }),\n );\n\n // Replay AUP snapshot if client is stale (or fresh connect)\n if (this.sessionJoinHandler) {\n const clientVersion =\n typeof msg.treeVersion === \"number\" ? (msg.treeVersion as number) : 0;\n this.sessionJoinHandler(sessionId, clientVersion, (m) => {\n if (ws.readyState === 1) ws.send(JSON.stringify(m));\n });\n }\n }\n\n // Flush pending messages\n for (const m of this.pendingMessages) {\n ws.send(m);\n }\n this.pendingMessages = [];\n\n // Re-send pending prompt to reconnecting client\n if (this.pendingPromptMessage && this.promptResolve) {\n ws.send(this.pendingPromptMessage);\n }\n\n // If the handshake message was a regular message (legacy client), process it\n if (msg.type && msg.type !== \"join_session\") {\n this.onMessage(msg, ws);\n }\n }\n }\n\n private onMessage(msg: Record<string, unknown>, ws: WebSocket): void {\n switch (msg.type) {\n case \"input\": {\n const content = String(msg.content ?? \"\");\n this.inputSource.push?.(content);\n break;\n }\n case \"prompt_response\": {\n if (this.promptResolve) {\n const resolve = this.promptResolve;\n this.promptResolve = null;\n this.pendingPromptMessage = null;\n resolve(msg.value as PromptResult);\n }\n break;\n }\n case \"aup_event\": {\n if (this.aupEventHandler) {\n const sessionId = this.sessionForClient.get(ws);\n const channelId = this.channelForClient.get(ws);\n const nodeId = String(msg.nodeId ?? \"\");\n const event = String(msg.event ?? \"\");\n const data = msg.data != null ? (msg.data as Record<string, unknown>) : undefined;\n this.aupEventHandler({ nodeId, event, data }, sessionId, channelId)\n .then((result) => {\n if (ws.readyState === 1) {\n ws.send(\n JSON.stringify({\n type: \"aup_event_result\",\n nodeId,\n event,\n result,\n }),\n );\n }\n })\n .catch((err: Error) => {\n if (ws.readyState === 1) {\n ws.send(\n JSON.stringify({\n type: \"aup_event_result\",\n nodeId,\n event,\n error: err.message,\n }),\n );\n }\n });\n }\n break;\n }\n case \"navigate_request\": {\n // Deep link / popstate: re-serve page content\n const pageId = String(msg.pageId ?? \"\");\n const afsNav = this.afs;\n if (pageId && afsNav) {\n const pagePath = joinURL(\"/ui/web/pages\", pageId);\n afsNav\n .read?.(pagePath)\n ?.then((result) => {\n if (result.data && ws.readyState === 1) {\n const entry = result.data as unknown as Record<string, unknown>;\n const content = String(\n (entry.content as Record<string, unknown>)?.content ?? entry.content ?? \"\",\n );\n ws.send(\n JSON.stringify({\n type: \"navigate\",\n pageId,\n content,\n format: (entry.content as Record<string, unknown>)?.format ?? \"html\",\n }),\n );\n }\n })\n ?.catch(() => {\n // Page not found — ignore silently\n });\n }\n break;\n }\n case \"afs_read\":\n case \"afs_list\":\n case \"afs_write\":\n case \"afs_exec\":\n case \"afs_stat\":\n case \"afs_subscribe\":\n case \"afs_unsubscribe\": {\n this.handleAfsMessage(msg, ws);\n break;\n }\n }\n }\n\n /** Handle AFS proxy messages from browser clients. */\n private handleAfsMessage(msg: Record<string, unknown>, ws: WebSocket): void {\n const reqId = msg.reqId as string | undefined;\n if (!reqId) return; // Ignore messages without reqId\n\n const sendResult = (data: unknown) => {\n if (ws.readyState === 1) {\n ws.send(JSON.stringify({ type: \"afs_result\", reqId, data }));\n }\n };\n const sendError = (error: string) => {\n if (ws.readyState === 1) {\n ws.send(JSON.stringify({ type: \"afs_error\", reqId, error }));\n }\n };\n\n if (!this.afs) {\n sendError(\"AFS not available\");\n return;\n }\n\n const afs = this.afs;\n const path = String(msg.path ?? \"/\");\n\n switch (msg.type) {\n case \"afs_read\": {\n if (!afs.read) {\n sendError(\"read not supported\");\n return;\n }\n afs.read(path).then(\n (r) => sendResult(r.data),\n (e: Error) => sendError(e.message),\n );\n break;\n }\n case \"afs_list\": {\n const listOptions = (msg.options as Record<string, unknown>) || {};\n afs.list(path, listOptions).then(\n (r) => sendResult({ data: r.data, total: r.total }),\n (e: Error) => sendError(e.message),\n );\n break;\n }\n case \"afs_stat\": {\n if (!afs.stat) {\n sendError(\"stat not supported\");\n return;\n }\n afs.stat(path).then(\n (r) => sendResult(r.data),\n (e: Error) => sendError(e.message),\n );\n break;\n }\n case \"afs_write\": {\n if (!afs.write) {\n sendError(\"write not supported\");\n return;\n }\n const payload: Record<string, unknown> = {};\n if (msg.content !== undefined) payload.content = msg.content;\n if (msg.meta !== undefined) payload.meta = msg.meta;\n afs.write(path, payload).then(\n (r) => sendResult(r.data),\n (e: Error) => sendError(e.message),\n );\n break;\n }\n case \"afs_exec\": {\n if (!afs.exec) {\n sendError(\"exec not supported\");\n return;\n }\n const args = (msg.args as Record<string, unknown>) ?? {};\n afs.exec(path, args, {}).then(\n (r) => sendResult(r.data),\n (e: Error) => sendError(e.message),\n );\n break;\n }\n case \"afs_subscribe\": {\n const subId = String(msg.subId ?? \"\");\n const filter = (msg.filter as Record<string, string>) ?? {};\n if (!subId) {\n sendError(\"subId is required for subscribe\");\n return;\n }\n if (!afs.subscribe) {\n sendError(\"subscribe not supported\");\n return;\n }\n try {\n const unsub = afs.subscribe(filter, (event) => {\n if (ws.readyState === 1) {\n ws.send(JSON.stringify({ type: \"afs_event\", subId, event }));\n }\n });\n // Track for cleanup\n if (!this.clientSubscriptions.has(ws)) {\n this.clientSubscriptions.set(ws, new Map());\n }\n this.clientSubscriptions.get(ws)!.set(subId, unsub);\n sendResult(null);\n } catch (e: unknown) {\n sendError(e instanceof Error ? e.message : String(e));\n }\n break;\n }\n case \"afs_unsubscribe\": {\n const subId = String(msg.subId ?? \"\");\n const subs = this.clientSubscriptions.get(ws);\n if (subs) {\n const unsub = subs.get(subId);\n if (unsub) {\n unsub();\n subs.delete(subId);\n }\n }\n sendResult(null);\n break;\n }\n }\n }\n\n /** Serve a page as standalone HTML via /p/:id[?sid=xxx][&st=xxx][&bridge=1] */\n private handlePageRequest(requestUrl: string, res: ServerResponse): void {\n if (!this.pageResolver) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n // Parse /p/:id and query params\n const url = new URL(requestUrl, \"http://localhost\");\n const pathParts = url.pathname.split(\"/\").filter(Boolean); // [\"p\", \"id\"]\n let pageId = \"\";\n try {\n pageId = decodeURIComponent(pathParts[1] ?? \"\");\n } catch {\n res.writeHead(400);\n res.end(\"Bad Request: invalid page ID encoding\");\n return;\n }\n const sessionId = url.searchParams.get(\"sid\") ?? undefined;\n const sessionToken = url.searchParams.get(\"st\") ?? undefined;\n const wantBridge = url.searchParams.get(\"bridge\") === \"1\";\n\n if (!pageId) {\n res.writeHead(400);\n res.end(\"Bad Request: missing page ID\");\n return;\n }\n\n this.pageResolver(pageId, sessionId, sessionToken)\n .then((result) => {\n if (!result) {\n res.writeHead(404);\n res.end(\"Page not found\");\n return;\n }\n\n let html: string;\n if (result.format === \"html\") {\n html = result.content;\n } else {\n // Wrap non-HTML in a minimal shell\n html = `<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"></head><body><pre>${escapeHtmlForPage(result.content)}</pre></body></html>`;\n }\n\n // Inject base styles so pages inherit surface design tokens\n html = injectPageBaseCSS(html);\n\n // Inject bridge script if requested\n if (wantBridge) {\n const serverOrigin = `http://127.0.0.1:${this.port}`;\n html = injectBridgeScript(html, serverOrigin);\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n })\n .catch(() => {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n });\n }\n\n /** Handle HTTP GET /s/:slug — serve snapshot HTML for a sharing entry */\n private handleSnapshotRequest(pathname: string, res: ServerResponse): void {\n if (!this.snapshotResolver) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n // Parse /s/:slug\n const slug = pathname.slice(3); // strip \"/s/\"\n if (!slug) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n const html = this.snapshotResolver(slug);\n if (!html) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"public, max-age=300\",\n });\n res.end(html);\n }\n\n private broadcast(data: string): void {\n for (const ws of this.clients) {\n if (ws.readyState === 1 /* OPEN */) {\n ws.send(data);\n }\n }\n }\n\n private isLoopbackHost(hostname: string): boolean {\n return hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"::1\";\n }\n\n private isAllowedWsOrigin(origin: string): boolean {\n try {\n const u = new URL(origin);\n // Allow any loopback origin (supports cross-port device connections)\n return this.isLoopbackHost(u.hostname);\n } catch {\n return false;\n }\n }\n\n /** TTY-style prompt for test mode (reuses TTYBackend logic) */\n private async ttyStylePrompt(options: PromptOptions): Promise<PromptResult> {\n const { message, type } = options;\n\n switch (type) {\n case \"text\":\n case \"password\": {\n this.outputHandler(`${message} `);\n const input = await this.read();\n return input.trim();\n }\n case \"confirm\": {\n this.outputHandler(`${message} (y/n) `);\n const input = await this.read();\n return input.trim().toLowerCase().startsWith(\"y\");\n }\n case \"select\": {\n if (!options.options || options.options.length === 0) {\n throw new Error(\"select prompt requires options\");\n }\n this.outputHandler(`${message}\\n`);\n for (let i = 0; i < options.options.length; i++) {\n this.outputHandler(` ${i + 1}. ${options.options[i]}\\n`);\n }\n this.outputHandler(\"Choice: \");\n const input = await this.read();\n const idx = Number.parseInt(input.trim(), 10) - 1;\n if (idx >= 0 && idx < options.options.length) {\n return options.options[idx]!;\n }\n return options.options[0]!;\n }\n case \"multiselect\": {\n if (!options.options || options.options.length === 0) {\n throw new Error(\"multiselect prompt requires options\");\n }\n this.outputHandler(`${message}\\n`);\n for (let i = 0; i < options.options.length; i++) {\n this.outputHandler(` ${i + 1}. ${options.options[i]}\\n`);\n }\n this.outputHandler(\"Choices (comma-separated): \");\n const input = await this.read();\n const indices = input\n .split(\",\")\n .map((s) => Number.parseInt(s.trim(), 10) - 1)\n .filter((i) => i >= 0 && i < options.options!.length);\n return indices.map((i) => options.options![i]!);\n }\n default:\n throw new Error(`Unknown prompt type: ${type}`);\n }\n }\n}\n\n/**\n * Strip dangerous HTML: <script> tags, on* event handlers, javascript: URLs.\n */\nfunction sanitizeHtml(html: string): string {\n return (\n html\n // Remove <script>...</script> (including multiline)\n .replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n // Remove standalone <script> tags (unclosed)\n .replace(/<script\\b[^>]*\\/?>/gi, \"\")\n // Remove on* event attributes\n .replace(/\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, \"\")\n // Remove javascript: URLs in href/src/action attributes\n .replace(/(href|src|action)\\s*=\\s*(?:\"javascript:[^\"]*\"|'javascript:[^']*')/gi, '$1=\"\"')\n );\n}\n\n/** Escape HTML for embedding in a minimal page shell. */\nfunction escapeHtmlForPage(str: string): string {\n return str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\n/** AUP Bridge script (~2KB) — injected into iframe pages when bridge=1. */\nfunction buildBridgeScript(serverOrigin: string): string {\n const escaped = serverOrigin.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\");\n return `<script>\n(function(){\n var _msgId = 0;\n var _parentOrigin = '${escaped}';\n\n function _isTrustedParentMessage(e) {\n if (e.source !== parent) return false;\n if (e.origin !== _parentOrigin) return false;\n return true;\n }\n\n function _bridgeRequest(type, params) {\n return new Promise(function(resolve, reject) {\n var id = 'b' + (++_msgId);\n function handler(e) {\n if (!_isTrustedParentMessage(e)) return;\n if (e.data && e.data.type === 'aup_bridge_response' && e.data.id === id) {\n window.removeEventListener('message', handler);\n if (e.data.error) reject(new Error(e.data.error));\n else resolve(e.data.payload);\n }\n }\n window.addEventListener('message', handler);\n parent.postMessage({ type: type, id: id, params: params }, _parentOrigin);\n });\n }\n\n var _subs = {};\n window.addEventListener('message', function(e) {\n if (!_isTrustedParentMessage(e)) return;\n if (e.data && e.data.type === 'aup_subscribe_event' && e.data.subId && _subs[e.data.subId]) {\n _subs[e.data.subId](e.data.payload);\n }\n });\n\n window.aup = {\n emit: function(event, data) {\n parent.postMessage({ type: 'aup_event', event: event, data: data }, _parentOrigin);\n },\n on: function(event, fn) {\n window.addEventListener('message', function(e) {\n if (!_isTrustedParentMessage(e)) return;\n if (e.data && e.data.type === 'aup_data' && e.data.event === event) fn(e.data.payload);\n });\n },\n navigate: function(path) {\n parent.postMessage({ type: 'aup_navigate', path: path }, _parentOrigin);\n },\n toast: function(message, intent) {\n parent.postMessage({ type: 'aup_toast', message: message, intent: intent || 'info' }, _parentOrigin);\n },\n fetch: function(path) {\n return _bridgeRequest('aup_bridge_read', { path: path });\n },\n read: function(path) {\n return _bridgeRequest('aup_bridge_read', { path: path });\n },\n list: function(path, options) {\n return _bridgeRequest('aup_bridge_list', { path: path, options: options });\n },\n write: function(path, content, meta) {\n return _bridgeRequest('aup_bridge_write', { path: path, content: content, meta: meta });\n },\n exec: function(path, args) {\n return _bridgeRequest('aup_bridge_exec', { path: path, args: args || {} });\n },\n subscribe: function(filter, callback) {\n var subId = 'bs' + (++_msgId);\n _subs[subId] = callback;\n parent.postMessage({ type: 'aup_bridge_subscribe', subId: subId, filter: filter }, _parentOrigin);\n return function() {\n delete _subs[subId];\n parent.postMessage({ type: 'aup_bridge_unsubscribe', subId: subId }, _parentOrigin);\n };\n }\n };\n})();\n</script>`;\n}\n\n/** Inject bridge script into HTML — inserts before </head> or at start of <body>. */\nconst PAGE_BASE_CSS = `<style data-aup-base>\n*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }\n:root {\n --bg: #0a0e14; --surface: #131820; --border: #1d2433;\n --text: #b3b1ad; --dim: #626a73;\n --accent: #e6b450; --accent-bg: #2a2000;\n --font: \"Manrope\", -apple-system, \"Segoe UI\", sans-serif;\n --font-mono: \"JetBrains Mono\", \"Fira Code\", monospace;\n --radius: 8px;\n color-scheme: dark;\n}\n@media (prefers-color-scheme: light) {\n :root {\n --bg: #f5f3ef; --surface: #fefdfb; --border: #e0dcd4;\n --text: #2c2418; --dim: #8a7e6e;\n --accent: #b8860b; --accent-bg: #fef7e5;\n color-scheme: light;\n }\n}\nhtml, body { min-height: 100vh; background: var(--bg); color: var(--text); font-family: var(--font); line-height: 1.6; -webkit-font-smoothing: antialiased; }\na { color: var(--accent); }\ncode, pre { font-family: var(--font-mono); }\npre { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1em; overflow-x: auto; }\nimg, video { max-width: 100%; height: auto; }\n</style>`;\n\nfunction injectPageBaseCSS(html: string): string {\n // Try before </head>\n if (html.includes(\"</head>\")) {\n return html.replace(\"</head>\", `${PAGE_BASE_CSS}\\n</head>`);\n }\n // Try after <head...>\n const headMatch = html.match(/<head[^>]*>/i);\n if (headMatch) {\n return html.replace(headMatch[0], `${headMatch[0]}\\n${PAGE_BASE_CSS}`);\n }\n // Fallback: prepend\n return PAGE_BASE_CSS + html;\n}\n\nfunction injectBridgeScript(html: string, serverOrigin: string): string {\n const script = buildBridgeScript(serverOrigin);\n // Try before </head>\n if (html.includes(\"</head>\")) {\n return html.replace(\"</head>\", `${script}\\n</head>`);\n }\n // Try after <body...>\n const bodyMatch = html.match(/<body[^>]*>/i);\n if (bodyMatch) {\n return html.replace(bodyMatch[0], `${bodyMatch[0]}\\n${script}`);\n }\n // Fallback: prepend\n return script + html;\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"Input timeout\")), ms);\n promise.then(\n (val) => {\n clearTimeout(timer);\n resolve(val);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+
2
+ //#region src/wm-state.ts
3
+ /** Built-in panel presets. */
4
+ const PANEL_PRESETS = {
5
+ explorer: [
6
+ {
7
+ id: "header",
8
+ position: "header"
9
+ },
10
+ {
11
+ id: "sidebar",
12
+ position: "sidebar",
13
+ defaultSize: "220px",
14
+ minSize: "120px"
15
+ },
16
+ {
17
+ id: "primary",
18
+ position: "primary",
19
+ defaultSize: "1fr",
20
+ minSize: "200px"
21
+ },
22
+ {
23
+ id: "inspector",
24
+ position: "inspector",
25
+ defaultSize: "280px",
26
+ minSize: "120px"
27
+ },
28
+ {
29
+ id: "statusbar",
30
+ position: "statusbar"
31
+ }
32
+ ],
33
+ settings: [
34
+ {
35
+ id: "header",
36
+ position: "header"
37
+ },
38
+ {
39
+ id: "master",
40
+ position: "sidebar",
41
+ defaultSize: "260px",
42
+ minSize: "160px"
43
+ },
44
+ {
45
+ id: "detail",
46
+ position: "primary",
47
+ defaultSize: "1fr",
48
+ minSize: "200px"
49
+ },
50
+ {
51
+ id: "footer",
52
+ position: "footer"
53
+ }
54
+ ],
55
+ simple: [
56
+ {
57
+ id: "header",
58
+ position: "header"
59
+ },
60
+ {
61
+ id: "primary",
62
+ position: "primary",
63
+ defaultSize: "1fr"
64
+ },
65
+ {
66
+ id: "statusbar",
67
+ position: "statusbar"
68
+ }
69
+ ],
70
+ miller: [
71
+ {
72
+ id: "header",
73
+ position: "header"
74
+ },
75
+ {
76
+ id: "col1",
77
+ position: "col1",
78
+ defaultSize: "1fr",
79
+ minSize: "150px"
80
+ },
81
+ {
82
+ id: "col2",
83
+ position: "col2",
84
+ defaultSize: "1fr",
85
+ minSize: "150px"
86
+ },
87
+ {
88
+ id: "col3",
89
+ position: "col3",
90
+ defaultSize: "1fr",
91
+ minSize: "200px"
92
+ },
93
+ {
94
+ id: "dock",
95
+ position: "dock"
96
+ },
97
+ {
98
+ id: "statusbar",
99
+ position: "statusbar"
100
+ }
101
+ ]
102
+ };
103
+ /**
104
+ * Compute tiled positions for `surfaces` within `viewport`.
105
+ *
106
+ * Grid selection: for N windows in a viewport W×H, we want the grid whose
107
+ * cells are closest to square. We try every valid (cols, rows) pair and
108
+ * pick the one that minimises |cellAspect − 1|.
109
+ */
110
+ function autoArrange(surfaces, viewport, options) {
111
+ const n = surfaces.length;
112
+ if (n === 0) return [];
113
+ const gap = options?.gap ?? 8;
114
+ const pad = options?.padding ?? 8;
115
+ const preserve = options?.preserveAspect ?? false;
116
+ const areaW = viewport.width - pad * 2;
117
+ const areaH = viewport.height - pad * 2;
118
+ if (areaW <= 0 || areaH <= 0) return [];
119
+ let bestCols = 1;
120
+ let bestScore = Number.POSITIVE_INFINITY;
121
+ for (let cols$1 = 1; cols$1 <= n; cols$1++) {
122
+ const rows$1 = Math.ceil(n / cols$1);
123
+ const cellW$1 = (areaW - gap * (cols$1 - 1)) / cols$1;
124
+ const cellH$1 = (areaH - gap * (rows$1 - 1)) / rows$1;
125
+ if (cellW$1 <= 0 || cellH$1 <= 0) continue;
126
+ const aspect = cellW$1 / cellH$1;
127
+ const wastedFraction = (cols$1 * rows$1 - n) / (cols$1 * rows$1);
128
+ const score = Math.abs(Math.log(aspect)) + wastedFraction * .3;
129
+ if (score < bestScore) {
130
+ bestScore = score;
131
+ bestCols = cols$1;
132
+ }
133
+ }
134
+ const cols = bestCols;
135
+ const rows = Math.ceil(n / cols);
136
+ const cellW = (areaW - gap * (cols - 1)) / cols;
137
+ const cellH = (areaH - gap * (rows - 1)) / rows;
138
+ const result = [];
139
+ for (let i = 0; i < n; i++) {
140
+ const col = i % cols;
141
+ const row = Math.floor(i / cols);
142
+ const cx = pad + col * (cellW + gap);
143
+ const cy = pad + row * (cellH + gap);
144
+ let w = cellW;
145
+ let h = cellH;
146
+ if (preserve && surfaces[i].size) {
147
+ const orig = surfaces[i].size;
148
+ const ar = orig.width / orig.height;
149
+ if (ar > cellW / cellH) {
150
+ w = cellW;
151
+ h = cellW / ar;
152
+ } else {
153
+ h = cellH;
154
+ w = cellH * ar;
155
+ }
156
+ }
157
+ const ox = (cellW - w) / 2;
158
+ const oy = (cellH - h) / 2;
159
+ result.push({
160
+ name: surfaces[i].name,
161
+ x: Math.round(cx + ox),
162
+ y: Math.round(cy + oy),
163
+ width: Math.round(w),
164
+ height: Math.round(h)
165
+ });
166
+ }
167
+ return result;
168
+ }
169
+
170
+ //#endregion
171
+ exports.PANEL_PRESETS = PANEL_PRESETS;
172
+ exports.autoArrange = autoArrange;
@@ -0,0 +1,171 @@
1
+ //#region src/wm-state.ts
2
+ /** Built-in panel presets. */
3
+ const PANEL_PRESETS = {
4
+ explorer: [
5
+ {
6
+ id: "header",
7
+ position: "header"
8
+ },
9
+ {
10
+ id: "sidebar",
11
+ position: "sidebar",
12
+ defaultSize: "220px",
13
+ minSize: "120px"
14
+ },
15
+ {
16
+ id: "primary",
17
+ position: "primary",
18
+ defaultSize: "1fr",
19
+ minSize: "200px"
20
+ },
21
+ {
22
+ id: "inspector",
23
+ position: "inspector",
24
+ defaultSize: "280px",
25
+ minSize: "120px"
26
+ },
27
+ {
28
+ id: "statusbar",
29
+ position: "statusbar"
30
+ }
31
+ ],
32
+ settings: [
33
+ {
34
+ id: "header",
35
+ position: "header"
36
+ },
37
+ {
38
+ id: "master",
39
+ position: "sidebar",
40
+ defaultSize: "260px",
41
+ minSize: "160px"
42
+ },
43
+ {
44
+ id: "detail",
45
+ position: "primary",
46
+ defaultSize: "1fr",
47
+ minSize: "200px"
48
+ },
49
+ {
50
+ id: "footer",
51
+ position: "footer"
52
+ }
53
+ ],
54
+ simple: [
55
+ {
56
+ id: "header",
57
+ position: "header"
58
+ },
59
+ {
60
+ id: "primary",
61
+ position: "primary",
62
+ defaultSize: "1fr"
63
+ },
64
+ {
65
+ id: "statusbar",
66
+ position: "statusbar"
67
+ }
68
+ ],
69
+ miller: [
70
+ {
71
+ id: "header",
72
+ position: "header"
73
+ },
74
+ {
75
+ id: "col1",
76
+ position: "col1",
77
+ defaultSize: "1fr",
78
+ minSize: "150px"
79
+ },
80
+ {
81
+ id: "col2",
82
+ position: "col2",
83
+ defaultSize: "1fr",
84
+ minSize: "150px"
85
+ },
86
+ {
87
+ id: "col3",
88
+ position: "col3",
89
+ defaultSize: "1fr",
90
+ minSize: "200px"
91
+ },
92
+ {
93
+ id: "dock",
94
+ position: "dock"
95
+ },
96
+ {
97
+ id: "statusbar",
98
+ position: "statusbar"
99
+ }
100
+ ]
101
+ };
102
+ /**
103
+ * Compute tiled positions for `surfaces` within `viewport`.
104
+ *
105
+ * Grid selection: for N windows in a viewport W×H, we want the grid whose
106
+ * cells are closest to square. We try every valid (cols, rows) pair and
107
+ * pick the one that minimises |cellAspect − 1|.
108
+ */
109
+ function autoArrange(surfaces, viewport, options) {
110
+ const n = surfaces.length;
111
+ if (n === 0) return [];
112
+ const gap = options?.gap ?? 8;
113
+ const pad = options?.padding ?? 8;
114
+ const preserve = options?.preserveAspect ?? false;
115
+ const areaW = viewport.width - pad * 2;
116
+ const areaH = viewport.height - pad * 2;
117
+ if (areaW <= 0 || areaH <= 0) return [];
118
+ let bestCols = 1;
119
+ let bestScore = Number.POSITIVE_INFINITY;
120
+ for (let cols$1 = 1; cols$1 <= n; cols$1++) {
121
+ const rows$1 = Math.ceil(n / cols$1);
122
+ const cellW$1 = (areaW - gap * (cols$1 - 1)) / cols$1;
123
+ const cellH$1 = (areaH - gap * (rows$1 - 1)) / rows$1;
124
+ if (cellW$1 <= 0 || cellH$1 <= 0) continue;
125
+ const aspect = cellW$1 / cellH$1;
126
+ const wastedFraction = (cols$1 * rows$1 - n) / (cols$1 * rows$1);
127
+ const score = Math.abs(Math.log(aspect)) + wastedFraction * .3;
128
+ if (score < bestScore) {
129
+ bestScore = score;
130
+ bestCols = cols$1;
131
+ }
132
+ }
133
+ const cols = bestCols;
134
+ const rows = Math.ceil(n / cols);
135
+ const cellW = (areaW - gap * (cols - 1)) / cols;
136
+ const cellH = (areaH - gap * (rows - 1)) / rows;
137
+ const result = [];
138
+ for (let i = 0; i < n; i++) {
139
+ const col = i % cols;
140
+ const row = Math.floor(i / cols);
141
+ const cx = pad + col * (cellW + gap);
142
+ const cy = pad + row * (cellH + gap);
143
+ let w = cellW;
144
+ let h = cellH;
145
+ if (preserve && surfaces[i].size) {
146
+ const orig = surfaces[i].size;
147
+ const ar = orig.width / orig.height;
148
+ if (ar > cellW / cellH) {
149
+ w = cellW;
150
+ h = cellW / ar;
151
+ } else {
152
+ h = cellH;
153
+ w = cellH * ar;
154
+ }
155
+ }
156
+ const ox = (cellW - w) / 2;
157
+ const oy = (cellH - h) / 2;
158
+ result.push({
159
+ name: surfaces[i].name,
160
+ x: Math.round(cx + ox),
161
+ y: Math.round(cy + oy),
162
+ width: Math.round(w),
163
+ height: Math.round(h)
164
+ });
165
+ }
166
+ return result;
167
+ }
168
+
169
+ //#endregion
170
+ export { PANEL_PRESETS, autoArrange };
171
+ //# sourceMappingURL=wm-state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wm-state.mjs","names":["cols","rows","cellW","cellH"],"sources":["../src/wm-state.ts"],"sourcesContent":["/**\n * WM State Store — Server-side per-session window manager state.\n *\n * Holds: strategy, layout config (active surface, panel arrangement),\n * and surface registry. Each surface maps to an independent AUP session.\n *\n * WM is first-class: even a single fullscreen surface is strategy: \"single\".\n * WM is recursive: a surface can contain another WM internally.\n */\n\nexport type WMStrategy = \"single\" | \"panels\" | \"floating\" | \"virtual\";\n\nconst VALID_STRATEGIES: readonly WMStrategy[] = [\"single\", \"panels\", \"floating\", \"virtual\"];\n\nexport interface WMSurface {\n /** Unique surface name within this WM instance */\n name: string;\n /** Session ID for the AUP session this surface renders */\n session: string;\n /** Panel assignment (for panels strategy) */\n panel?: string;\n /** Display title */\n title?: string;\n /** Position for floating strategy */\n position?: { x: number; y: number };\n /** Size for floating strategy */\n size?: { width: number; height: number };\n /** Z-index for floating strategy */\n zIndex?: number;\n /** Whether this surface is docked (floating strategy) */\n docked?: boolean;\n /** Original panel (for popout/redock tracking) */\n originalPanel?: string;\n}\n\nexport interface WMLayout {\n /** Currently active surface name */\n active: string | null;\n /** Panel configuration (for panels strategy) */\n panels?: WMPanelConfig[];\n /** Panel preset name */\n preset?: string;\n /** Per-panel active surface (for tabbed panels) */\n panelActives?: Record<string, string>;\n}\n\nexport interface WMPanelConfig {\n id: string;\n position: string;\n defaultSize?: string;\n minSize?: string;\n collapsible?: boolean;\n collapsed?: boolean;\n}\n\n/** Built-in panel presets. */\nexport const PANEL_PRESETS: Record<string, WMPanelConfig[]> = {\n explorer: [\n { id: \"header\", position: \"header\" },\n { id: \"sidebar\", position: \"sidebar\", defaultSize: \"220px\", minSize: \"120px\" },\n { id: \"primary\", position: \"primary\", defaultSize: \"1fr\", minSize: \"200px\" },\n { id: \"inspector\", position: \"inspector\", defaultSize: \"280px\", minSize: \"120px\" },\n { id: \"statusbar\", position: \"statusbar\" },\n ],\n settings: [\n { id: \"header\", position: \"header\" },\n { id: \"master\", position: \"sidebar\", defaultSize: \"260px\", minSize: \"160px\" },\n { id: \"detail\", position: \"primary\", defaultSize: \"1fr\", minSize: \"200px\" },\n { id: \"footer\", position: \"footer\" },\n ],\n simple: [\n { id: \"header\", position: \"header\" },\n { id: \"primary\", position: \"primary\", defaultSize: \"1fr\" },\n { id: \"statusbar\", position: \"statusbar\" },\n ],\n miller: [\n { id: \"header\", position: \"header\" },\n { id: \"col1\", position: \"col1\", defaultSize: \"1fr\", minSize: \"150px\" },\n { id: \"col2\", position: \"col2\", defaultSize: \"1fr\", minSize: \"150px\" },\n { id: \"col3\", position: \"col3\", defaultSize: \"1fr\", minSize: \"200px\" },\n { id: \"dock\", position: \"dock\" },\n { id: \"statusbar\", position: \"statusbar\" },\n ],\n};\n\nexport type WMChangeType =\n | \"strategy:change\"\n | \"surface:open\"\n | \"surface:close\"\n | \"surface:dock\"\n | \"surface:undock\"\n | \"surface:move\"\n | \"surface:popout\"\n | \"surface:redock\"\n | \"active:change\"\n | \"layout:change\"\n | \"panel:collapse\"\n | \"panel:expand\";\n\ntype ChangeListener = (type: WMChangeType, data?: unknown) => void;\n\nexport class WMStateStore {\n private strategy: WMStrategy;\n private surfaces = new Map<string, WMSurface>();\n private layout: WMLayout = { active: null };\n private _version = 0;\n private listeners: Set<ChangeListener> = new Set();\n\n constructor(strategy: WMStrategy = \"single\") {\n if (!VALID_STRATEGIES.includes(strategy)) {\n throw new Error(`Invalid WM strategy: ${strategy}`);\n }\n this.strategy = strategy;\n }\n\n /** Current state version (monotonically increasing). */\n get version(): number {\n return this._version;\n }\n\n /** Get current strategy. */\n getStrategy(): WMStrategy {\n return this.strategy;\n }\n\n /** Set strategy. Validates against allowed values. */\n setStrategy(strategy: WMStrategy): void {\n if (!VALID_STRATEGIES.includes(strategy)) {\n throw new Error(\n `Invalid WM strategy: ${String(strategy)}. Must be one of: ${VALID_STRATEGIES.join(\", \")}`,\n );\n }\n this.strategy = strategy;\n this._version++;\n this.notify(\"strategy:change\", { strategy });\n }\n\n /** Open (or replace) a surface. First surface becomes active automatically. */\n openSurface(surface: Omit<WMSurface, \"zIndex\"> & { name: string; session: string }): void {\n if (!surface.name) {\n throw new Error(\"Surface name is required\");\n }\n const entry: WMSurface = { ...surface };\n this.surfaces.set(surface.name, entry);\n this._version++;\n\n // Auto-activate first surface\n if (this.layout.active === null || this.surfaces.size === 1) {\n this.layout.active = surface.name;\n }\n\n this.notify(\"surface:open\", { name: surface.name });\n }\n\n /** Close a surface by name. If it was active, activate next available. */\n closeSurface(name: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) {\n throw new Error(`Surface not found: ${name}`);\n }\n const panelId = surface.panel;\n this.surfaces.delete(name);\n this._version++;\n\n // If we closed the active surface, pick next available\n if (this.layout.active === name) {\n const remaining = Array.from(this.surfaces.keys());\n this.layout.active = remaining.length > 0 ? (remaining[0] ?? null) : null;\n }\n\n // Clean up panelActives: if this surface was the active tab in its panel,\n // pick the next surface in the same panel, or clear it\n if (panelId && this.layout.panelActives && this.layout.panelActives[panelId] === name) {\n const remainingInPanel = Array.from(this.surfaces.values())\n .filter((s) => s.panel === panelId)\n .map((s) => s.name);\n if (remainingInPanel.length > 0) {\n this.layout.panelActives[panelId] = remainingInPanel[0]!;\n } else {\n delete this.layout.panelActives[panelId];\n }\n }\n\n this.notify(\"surface:close\", { name });\n }\n\n /** Get a surface by name (undefined if not found). */\n getSurface(name: string): WMSurface | undefined {\n return this.surfaces.get(name);\n }\n\n /** List all registered surfaces. */\n listSurfaces(): WMSurface[] {\n return Array.from(this.surfaces.values());\n }\n\n /** Set the active surface. Throws if surface doesn't exist. */\n setActive(name: string): void {\n if (!this.surfaces.has(name)) {\n throw new Error(`Surface not found: ${name}`);\n }\n this.layout.active = name;\n this._version++;\n this.notify(\"active:change\", { name });\n }\n\n /** Get the active surface name (null if none). */\n getActive(): string | null {\n return this.layout.active;\n }\n\n /** Get the current layout configuration. */\n getLayout(): WMLayout {\n const result: WMLayout = { ...this.layout };\n if (result.panels) {\n result.panels = result.panels.map((p) => ({ ...p }));\n }\n return result;\n }\n\n /** Set layout configuration (for panels strategy). */\n setLayout(layout: Partial<WMLayout>): void {\n Object.assign(this.layout, layout);\n this._version++;\n this.notify(\"layout:change\", layout);\n }\n\n /** Apply a panel preset by name. Unknown presets fall back to \"simple\". */\n applyPreset(presetName: string): void {\n const preset = PANEL_PRESETS[presetName];\n if (preset) {\n this.layout.preset = presetName;\n this.layout.panels = preset.map((p) => ({ ...p }));\n } else {\n // Fall back to simple\n const simple = PANEL_PRESETS.simple!;\n this.layout.preset = \"simple\";\n this.layout.panels = simple.map((p) => ({ ...p }));\n }\n this._version++;\n this.notify(\"layout:change\", { preset: this.layout.preset });\n }\n\n /** Collapse a panel by ID. */\n collapsePanel(panelId: string): void {\n const panel = this.layout.panels?.find((p) => p.id === panelId);\n if (!panel) throw new Error(`Panel not found: ${panelId}`);\n panel.collapsed = true;\n this._version++;\n this.notify(\"panel:collapse\", { panelId });\n }\n\n /** Expand a panel by ID. */\n expandPanel(panelId: string): void {\n const panel = this.layout.panels?.find((p) => p.id === panelId);\n if (!panel) throw new Error(`Panel not found: ${panelId}`);\n panel.collapsed = false;\n this._version++;\n this.notify(\"panel:expand\", { panelId });\n }\n\n /** Add a surface to a specific panel (tab). Validates panel if panels are configured. */\n addToPanel(surface: { name: string; session: string; panel: string; title?: string }): void {\n if (!surface.name) throw new Error(\"Surface name is required\");\n if (!surface.panel) throw new Error(\"Panel is required\");\n // Validate panel exists if layout has panels configured\n if (this.layout.panels && this.layout.panels.length > 0) {\n const found = this.layout.panels.find((p) => p.id === surface.panel);\n if (!found) throw new Error(`Panel not found: ${surface.panel}`);\n }\n const entry: WMSurface = { ...surface };\n this.surfaces.set(surface.name, entry);\n this._version++;\n\n // Auto-activate first surface\n if (this.layout.active === null || this.surfaces.size === 1) {\n this.layout.active = surface.name;\n }\n\n // Auto-set as panel active if it's the first in this panel\n if (!this.layout.panelActives) this.layout.panelActives = {};\n if (!this.layout.panelActives[surface.panel]) {\n this.layout.panelActives[surface.panel] = surface.name;\n }\n\n this.notify(\"surface:open\", { name: surface.name });\n }\n\n /** Set the active surface within a specific panel (tab switching). */\n setPanelActive(panel: string, surfaceName: string): void {\n const surface = this.surfaces.get(surfaceName);\n if (!surface) throw new Error(`Surface not found: ${surfaceName}`);\n if (surface.panel !== panel) {\n throw new Error(`Surface '${surfaceName}' is not in panel '${panel}'`);\n }\n if (!this.layout.panelActives) this.layout.panelActives = {};\n this.layout.panelActives[panel] = surfaceName;\n this._version++;\n this.notify(\"layout:change\", { panelActives: this.layout.panelActives });\n }\n\n /** Move a surface to a different panel. */\n moveSurface(name: string, panel: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) throw new Error(`Surface not found: ${name}`);\n // Validate panel exists in layout (if panels are configured)\n if (this.layout.panels && this.layout.panels.length > 0) {\n const found = this.layout.panels.find((p) => p.id === panel);\n if (!found) throw new Error(`Panel not found: ${panel}`);\n }\n surface.panel = panel;\n this._version++;\n this.notify(\"surface:move\", { name, panel });\n }\n\n /** Popout a surface from its panel to floating overlay. Stores originalPanel for redock. */\n popout(name: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) throw new Error(`Surface not found: ${name}`);\n if (!surface.panel) throw new Error(`Surface '${name}' is not in a panel (already floating)`);\n surface.originalPanel = surface.panel;\n surface.panel = undefined;\n this._version++;\n\n // Clean up panelActives for the old panel\n if (this.layout.panelActives && this.layout.panelActives[surface.originalPanel] === name) {\n const remainingInPanel = Array.from(this.surfaces.values())\n .filter((s) => s.panel === surface.originalPanel)\n .map((s) => s.name);\n if (remainingInPanel.length > 0) {\n this.layout.panelActives[surface.originalPanel] = remainingInPanel[0]!;\n } else {\n delete this.layout.panelActives[surface.originalPanel];\n }\n }\n\n this.notify(\"surface:popout\", { name, originalPanel: surface.originalPanel });\n }\n\n /** Redock a floating surface back to its original panel. */\n redock(name: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) throw new Error(`Surface not found: ${name}`);\n if (!surface.originalPanel) {\n throw new Error(`Surface '${name}' was not popped out (no originalPanel)`);\n }\n const panel = surface.originalPanel;\n surface.panel = panel;\n surface.originalPanel = undefined;\n this._version++;\n this.notify(\"surface:redock\", { name, panel });\n }\n\n /** Pin a surface to the dock (floating strategy). */\n pinToDock(name: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) throw new Error(`Surface not found: ${name}`);\n surface.docked = true;\n this._version++;\n this.notify(\"surface:dock\", { name });\n }\n\n /** Unpin a surface from the dock (floating strategy). */\n unpinFromDock(name: string): void {\n const surface = this.surfaces.get(name);\n if (!surface) throw new Error(`Surface not found: ${name}`);\n surface.docked = false;\n this._version++;\n this.notify(\"surface:undock\", { name });\n }\n\n /**\n * Auto-arrange all non-docked surfaces to tile evenly within the given\n * viewport. Updates position and size on each surface in-place.\n *\n * Returns the computed layout so the caller can forward it to clients.\n */\n autoArrange(\n viewport: { width: number; height: number },\n options?: AutoArrangeOptions,\n ): AutoArrangeResult[] {\n const result = autoArrange(\n this.listSurfaces().filter((s) => !s.docked),\n viewport,\n options,\n );\n for (const r of result) {\n const s = this.surfaces.get(r.name);\n if (s) {\n s.position = { x: r.x, y: r.y };\n s.size = { width: r.width, height: r.height };\n }\n }\n this._version++;\n this.notify(\"layout:change\", { autoArrange: true });\n return result;\n }\n\n /** Reset all state to defaults. */\n reset(): void {\n this.strategy = \"single\";\n this.surfaces.clear();\n this.layout = { active: null };\n this._version++;\n }\n\n /** Subscribe to state changes. Returns unsubscribe function. */\n onChange(listener: ChangeListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private notify(type: WMChangeType, data?: unknown): void {\n for (const listener of this.listeners) {\n try {\n listener(type, data);\n } catch {\n // listener errors should not break state transitions\n }\n }\n }\n}\n\n// ── Auto-Arrange Algorithm ──────────────────────────────────────────────────\n//\n// Given N windows and a viewport, compute non-overlapping positions that\n// tile evenly. The algorithm picks a grid (cols × rows) whose aspect ratio\n// best matches the viewport, then sizes each window to fill its cell minus\n// the configured gap. Windows keep their original aspect ratio if `preserve`\n// is set; otherwise they fill the cell entirely.\n//\n// Exported standalone so it can also be inlined in the client-side JS.\n\nexport interface AutoArrangeOptions {\n /** Gap between windows in px (default 8) */\n gap?: number;\n /** Padding from viewport edges in px (default 8) */\n padding?: number;\n /** Preserve each window's current aspect ratio instead of filling the cell */\n preserveAspect?: boolean;\n}\n\nexport interface AutoArrangeResult {\n name: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Compute tiled positions for `surfaces` within `viewport`.\n *\n * Grid selection: for N windows in a viewport W×H, we want the grid whose\n * cells are closest to square. We try every valid (cols, rows) pair and\n * pick the one that minimises |cellAspect − 1|.\n */\nexport function autoArrange(\n surfaces: Pick<WMSurface, \"name\" | \"size\">[],\n viewport: { width: number; height: number },\n options?: AutoArrangeOptions,\n): AutoArrangeResult[] {\n const n = surfaces.length;\n if (n === 0) return [];\n\n const gap = options?.gap ?? 8;\n const pad = options?.padding ?? 8;\n const preserve = options?.preserveAspect ?? false;\n\n const areaW = viewport.width - pad * 2;\n const areaH = viewport.height - pad * 2;\n if (areaW <= 0 || areaH <= 0) return [];\n\n // Find optimal grid: minimise cell aspect-ratio deviation from 1,\n // with a penalty for wasted cells (cols×rows − n) to prefer tighter grids.\n let bestCols = 1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let cols = 1; cols <= n; cols++) {\n const rows = Math.ceil(n / cols);\n const cellW = (areaW - gap * (cols - 1)) / cols;\n const cellH = (areaH - gap * (rows - 1)) / rows;\n if (cellW <= 0 || cellH <= 0) continue;\n const aspect = cellW / cellH;\n const wastedFraction = (cols * rows - n) / (cols * rows);\n // Primary: squareness. Secondary: penalise wasted cells (0.3 weight).\n const score = Math.abs(Math.log(aspect)) + wastedFraction * 0.3;\n if (score < bestScore) {\n bestScore = score;\n bestCols = cols;\n }\n }\n\n const cols = bestCols;\n const rows = Math.ceil(n / cols);\n const cellW = (areaW - gap * (cols - 1)) / cols;\n const cellH = (areaH - gap * (rows - 1)) / rows;\n\n const result: AutoArrangeResult[] = [];\n for (let i = 0; i < n; i++) {\n const col = i % cols;\n const row = Math.floor(i / cols);\n const cx = pad + col * (cellW + gap);\n const cy = pad + row * (cellH + gap);\n\n let w = cellW;\n let h = cellH;\n\n if (preserve && surfaces[i]!.size) {\n const orig = surfaces[i]!.size!;\n const ar = orig.width / orig.height;\n if (ar > cellW / cellH) {\n // width-constrained\n w = cellW;\n h = cellW / ar;\n } else {\n // height-constrained\n h = cellH;\n w = cellH * ar;\n }\n }\n\n // Center within cell if aspect-preserved\n const ox = (cellW - w) / 2;\n const oy = (cellH - h) / 2;\n\n result.push({\n name: surfaces[i]!.name,\n x: Math.round(cx + ox),\n y: Math.round(cy + oy),\n width: Math.round(w),\n height: Math.round(h),\n });\n }\n\n return result;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@aigne/afs-ui",
3
+ "version": "1.11.0-beta.12",
4
+ "description": "AIGNE AFS interactive UI device provider — terminal, web, or messaging",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "author": "Arcblock <blocklet@arcblock.io> https://github.com/arcblock",
10
+ "homepage": "https://github.com/arcblock/afs",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/arcblock/afs"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/arcblock/afs/issues"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.cts",
22
+ "exports": {
23
+ ".": {
24
+ "require": "./dist/index.cjs",
25
+ "import": "./dist/index.mjs"
26
+ },
27
+ "./*": "./*"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "LICENSE",
32
+ "README.md",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "dependencies": {
36
+ "ufo": "^1.6.3",
37
+ "ws": "^8.19.0",
38
+ "zod": "^4.0.0",
39
+ "@aigne/afs": "^1.11.0-beta.12"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "^1.3.6",
43
+ "@types/ws": "^8.18.1",
44
+ "npm-run-all": "^4.1.5",
45
+ "rimraf": "^6.1.2",
46
+ "tsdown": "0.20.0-beta.3",
47
+ "typescript": "5.9.2",
48
+ "@aigne/afs-testing": "1.11.0-beta.12",
49
+ "@aigne/scripts": "0.0.0",
50
+ "@aigne/typescript-config": "0.0.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "check-types": "tsc --noEmit",
55
+ "clean": "rimraf dist coverage",
56
+ "test": "bun test",
57
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text"
58
+ }
59
+ }