prompt_objects 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +93 -22
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1337 -20
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +171 -519
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +549 -53
  65. data/lib/prompt_objects/server/app.rb +133 -14
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -227
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +600 -13
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +23 -15
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +8 -14
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +10 -14
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/{store_env_data.rb → set_env_data.rb} +34 -20
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +33 -8
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +2 -2
  92. data/templates/arc-agi-1/objects/solver.md +6 -5
  93. data/templates/arc-agi-1/objects/verifier.md +2 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +1 -1
  230. metadata +172 -65
  231. data/CLAUDE.md +0 -177
  232. data/Gemfile +0 -10
  233. data/Rakefile +0 -27
  234. data/frontend/.gitignore +0 -3
  235. data/frontend/index.html +0 -17
  236. data/frontend/package-lock.json +0 -4540
  237. data/frontend/package.json +0 -36
  238. data/frontend/postcss.config.js +0 -6
  239. data/frontend/src/App.tsx +0 -104
  240. data/frontend/src/canvas/CanvasView.tsx +0 -113
  241. data/frontend/src/canvas/ForceLayout.ts +0 -115
  242. data/frontend/src/canvas/SceneManager.ts +0 -587
  243. data/frontend/src/canvas/canvasStore.ts +0 -47
  244. data/frontend/src/canvas/constants.ts +0 -95
  245. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  246. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  247. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  248. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  249. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  250. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  251. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  252. data/frontend/src/canvas/types.ts +0 -24
  253. data/frontend/src/components/ContextMenu.tsx +0 -68
  254. data/frontend/src/components/EnvDataPane.tsx +0 -69
  255. data/frontend/src/components/Inspector.tsx +0 -263
  256. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  257. data/frontend/src/components/MethodList.tsx +0 -90
  258. data/frontend/src/components/ModelSelector.tsx +0 -115
  259. data/frontend/src/components/NotificationPanel.tsx +0 -130
  260. data/frontend/src/components/ObjectList.tsx +0 -78
  261. data/frontend/src/components/PaneSlot.tsx +0 -30
  262. data/frontend/src/components/SourcePane.tsx +0 -202
  263. data/frontend/src/components/SystemBar.tsx +0 -74
  264. data/frontend/src/components/Transcript.tsx +0 -76
  265. data/frontend/src/components/UsagePanel.tsx +0 -105
  266. data/frontend/src/components/Workspace.tsx +0 -260
  267. data/frontend/src/components/index.ts +0 -12
  268. data/frontend/src/hooks/useResize.ts +0 -55
  269. data/frontend/src/hooks/useWebSocket.ts +0 -559
  270. data/frontend/src/index.css +0 -102
  271. data/frontend/src/main.tsx +0 -10
  272. data/frontend/src/store/index.ts +0 -311
  273. data/frontend/src/types/index.ts +0 -176
  274. data/frontend/tailwind.config.js +0 -44
  275. data/frontend/tsconfig.json +0 -30
  276. data/frontend/vite.config.ts +0 -29
  277. data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
  278. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
  279. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  280. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  281. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  282. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  283. data/lib/prompt_objects/universal/think.rb +0 -41
  284. data/lib/prompt_objects/universal/update_env_data.rb +0 -88
  285. data/prompt_objects.gemspec +0 -50
  286. data/templates/basic/.gitignore +0 -2
  287. data/templates/developer/.gitignore +0 -5
  288. data/templates/empty/.gitignore +0 -5
  289. data/templates/empty/objects/.gitkeep +0 -0
  290. data/templates/minimal/.gitignore +0 -5
  291. data/templates/writer/.gitignore +0 -5
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Geist Mono,IBM Plex Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-0{bottom:0}.bottom-3{bottom:.75rem}.bottom-4{bottom:1rem}.left-0{left:0}.left-2{left:.5rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-3{top:.75rem}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-\[4\.5rem\]{margin-left:4.5rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.line-clamp-4{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-52{max-height:13rem}.max-h-96{max-height:24rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:0px}.min-h-12{min-height:3rem}.min-h-9{min-height:2.25rem}.min-h-\[22rem\]{min-height:22rem}.min-h-\[28rem\]{min-height:28rem}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-96{width:24rem}.w-\[400px\]{width:400px}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-10{min-width:2.5rem}.min-w-20{min-width:5rem}.min-w-\[140px\]{min-width:140px}.min-w-\[68rem\]{min-width:68rem}.min-w-\[72rem\]{min-width:72rem}.min-w-full{min-width:100%}.max-w-36{max-width:9rem}.max-w-3xl{max-width:48rem}.max-w-\[60px\]{max-width:60px}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.\!resize{resize:both!important}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.auto-rows-\[minmax\(22rem\,auto\)\]{grid-auto-rows:minmax(22rem,auto)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[14rem_minmax\(30rem\,1fr\)_22rem\]{grid-template-columns:14rem minmax(30rem,1fr) 22rem}.grid-cols-\[16rem_minmax\(34rem\,1fr\)_22rem\]{grid-template-columns:16rem minmax(34rem,1fr) 22rem}.grid-cols-\[17rem_minmax\(25rem\,1fr\)_minmax\(20rem\,0\.8fr\)\]{grid-template-columns:17rem minmax(25rem,1fr) minmax(20rem,.8fr)}.grid-cols-\[6rem_1fr\]{grid-template-columns:6rem 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-px{gap:1px}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-1\.5{row-gap:.375rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.border-po-accent-muted{--tw-border-opacity: 1;border-color:rgb(154 109 32 / var(--tw-border-opacity, 1))}.border-po-accent\/40{border-color:#d4952a66}.border-po-border{--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1))}.border-po-border-focus{--tw-border-opacity: 1;border-color:rgb(92 87 82 / var(--tw-border-opacity, 1))}.border-po-error{--tw-border-opacity: 1;border-color:rgb(196 92 74 / var(--tw-border-opacity, 1))}.border-po-error\/40{border-color:#c45c4a66}.border-po-error\/60{border-color:#c45c4a99}.border-po-status-active{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.border-po-status-calling,.border-po-success{--tw-border-opacity: 1;border-color:rgb(59 154 110 / var(--tw-border-opacity, 1))}.border-po-success\/40{border-color:#3b9a6e66}.border-po-success\/50{border-color:#3b9a6e80}.border-po-warning{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.border-po-warning\/30{border-color:#d4952a4d}.border-po-warning\/40{border-color:#d4952a66}.border-po-warning\/50{border-color:#d4952a80}.border-transparent{border-color:transparent}.border-violet-400\/50{border-color:#a78bfa80}.bg-\[linear-gradient\(180deg\,rgba\(74\,167\,200\,0\.06\)\,transparent_35\%\)\,\#171513\]{background-color:linear-gradient(180deg,rgba(74,167,200,.06),transparent 35%),#171513}.bg-black\/50{background-color:#00000080}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-po-accent-wash{background-color:#d4952a14}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(26 25 24 / var(--tw-bg-opacity, 1))}.bg-po-bg\/50{background-color:#1a191880}.bg-po-bg\/60{background-color:#1a191899}.bg-po-bg\/70{background-color:#1a1918b3}.bg-po-bg\/80{background-color:#1a1918cc}.bg-po-bg\/95{background-color:#1a1918f2}.bg-po-error{--tw-bg-opacity: 1;background-color:rgb(196 92 74 / var(--tw-bg-opacity, 1))}.bg-po-error\/10{background-color:#c45c4a1a}.bg-po-status-active{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-po-status-calling{--tw-bg-opacity: 1;background-color:rgb(59 154 110 / var(--tw-bg-opacity, 1))}.bg-po-status-idle{--tw-bg-opacity: 1;background-color:rgb(120 114 106 / var(--tw-bg-opacity, 1))}.bg-po-success{--tw-bg-opacity: 1;background-color:rgb(59 154 110 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(34 33 32 / var(--tw-bg-opacity, 1))}.bg-po-surface-2{--tw-bg-opacity: 1;background-color:rgb(44 42 40 / var(--tw-bg-opacity, 1))}.bg-po-surface-2\/90{background-color:#2c2a28e6}.bg-po-surface\/55{background-color:#2221208c}.bg-po-surface\/60{background-color:#22212099}.bg-po-surface\/70{background-color:#222120b3}.bg-po-surface\/80{background-color:#222120cc}.bg-po-surface\/85{background-color:#222120d9}.bg-po-surface\/90{background-color:#222120e6}.bg-po-surface\/95{background-color:#222120f2}.bg-po-text-ghost{--tw-bg-opacity: 1;background-color:rgb(82 78 72 / var(--tw-bg-opacity, 1))}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-po-warning\/5{background-color:#d4952a0d}.bg-transparent{background-color:transparent}.bg-\[linear-gradient\(135deg\,transparent_46\%\,\#5b5752_47\%\,\#5b5752_54\%\,transparent_55\%\,transparent_66\%\,\#5b5752_67\%\,\#5b5752_74\%\,transparent_75\%\)\]{background-image:linear-gradient(135deg,transparent 46%,#5b5752 47%,#5b5752 54%,transparent 55%,transparent 66%,#5b5752 67%,#5b5752 74%,transparent 75%)}.bg-\[radial-gradient\(circle\,\#2c2a28_1px\,transparent_1px\)\]{background-image:radial-gradient(circle,#2c2a28 1px,transparent 1px)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-amber-900{--tw-gradient-from: #78350f var(--tw-gradient-from-position);--tw-gradient-to: rgb(120 53 15 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-slate-900{--tw-gradient-to: #0f172a var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pl-\[4\.5rem\]{padding-left:4.5rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:Geist Mono,IBM Plex Mono,monospace}.font-ui{font-family:Geist,system-ui,sans-serif}.text-2xs{font-size:11px;line-height:15px}.text-\[0\.9em\]{font-size:.9em}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-po-accent{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.text-po-bg{--tw-text-opacity: 1;color:rgb(26 25 24 / var(--tw-text-opacity, 1))}.text-po-error{--tw-text-opacity: 1;color:rgb(196 92 74 / var(--tw-text-opacity, 1))}.text-po-status-active{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.text-po-status-calling{--tw-text-opacity: 1;color:rgb(59 154 110 / var(--tw-text-opacity, 1))}.text-po-status-delegated{--tw-text-opacity: 1;color:rgb(90 143 194 / var(--tw-text-opacity, 1))}.text-po-success{--tw-text-opacity: 1;color:rgb(59 154 110 / var(--tw-text-opacity, 1))}.text-po-text-ghost{--tw-text-opacity: 1;color:rgb(82 78 72 / var(--tw-text-opacity, 1))}.text-po-text-primary{--tw-text-opacity: 1;color:rgb(232 226 218 / var(--tw-text-opacity, 1))}.text-po-text-secondary{--tw-text-opacity: 1;color:rgb(168 162 154 / var(--tw-text-opacity, 1))}.text-po-text-tertiary{--tw-text-opacity: 1;color:rgb(120 114 106 / var(--tw-text-opacity, 1))}.text-po-warning{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.text-violet-300{--tw-text-opacity: 1;color:rgb(196 181 253 / var(--tw-text-opacity, 1))}.placeholder-po-text-ghost::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(82 78 72 / var(--tw-placeholder-opacity, 1))}.placeholder-po-text-ghost::placeholder{--tw-placeholder-opacity: 1;color:rgb(82 78 72 / var(--tw-placeholder-opacity, 1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(212\,149\,42\,0\.6\)\]{--tw-shadow: 0 0 5px rgba(212,149,42,.6);--tw-shadow-colored: 0 0 5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(59\,154\,110\,0\.6\)\]{--tw-shadow: 0 0 5px rgba(59,154,110,.6);--tw-shadow-colored: 0 0 5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(212\,149\,42\,0\.7\)\]{--tw-shadow: 0 0 6px rgba(212,149,42,.7);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(59\,154\,110\,0\.7\)\]{--tw-shadow: 0 0 6px rgba(59,154,110,.7);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-4{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-po-accent{--tw-ring-opacity: 1;--tw-ring-color: rgb(212 149 42 / var(--tw-ring-opacity, 1))}.ring-po-bg{--tw-ring-opacity: 1;--tw-ring-color: rgb(26 25 24 / var(--tw-ring-opacity, 1))}.ring-po-border{--tw-ring-opacity: 1;--tw-ring-color: rgb(61 58 55 / var(--tw-ring-opacity, 1))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.\[background-size\:28px_28px\]{background-size:28px 28px}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(26 25 24 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(61 58 55 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(92 87 82 / var(--tw-bg-opacity, 1))}::-moz-selection{background:#d4952a4d}::selection{background:#d4952a4d}.resize-handle{width:.25rem;cursor:col-resize;border-left-width:1px;--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.resize-handle:hover{background-color:#d4952a4d}.resize-handle:active{background-color:#d4952a80}.resize-handle-h{height:.25rem;cursor:row-resize;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.resize-handle-h:hover{background-color:#d4952a4d}.resize-handle-h:active{background-color:#d4952a80}.floor-surface{background-color:#1a1918;background-image:radial-gradient(circle,#2c2a28 1px,transparent 1px);background-size:28px 28px}@keyframes station-enter{0%{opacity:0;scale:.96}to{opacity:1;scale:1}}.station-enter{animation:station-enter .24s cubic-bezier(.16,1,.3,1)}@keyframes drawer-enter{0%{transform:translate(24px);opacity:0}to{transform:translate(0);opacity:1}}.drawer-enter{animation:drawer-enter .18s cubic-bezier(.16,1,.3,1)}@keyframes delegation-dash{to{stroke-dashoffset:-24}}.delegation-edge{animation:delegation-dash .9s linear infinite}@media(prefers-reduced-motion:reduce){.station-enter,.drawer-enter,.delegation-edge{animation:none}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:bottom-3:before{content:var(--tw-content);bottom:.75rem}.before\:left-\[0\.72rem\]:before{content:var(--tw-content);left:.72rem}.before\:top-3:before{content:var(--tw-content);top:.75rem}.before\:w-px:before{content:var(--tw-content);width:1px}.before\:bg-po-border:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(61 58 55 / var(--tw-bg-opacity, 1))}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.hover\:border-po-border-focus:hover{--tw-border-opacity: 1;border-color:rgb(92 87 82 / var(--tw-border-opacity, 1))}.hover\:bg-po-accent-muted:hover{--tw-bg-opacity: 1;background-color:rgb(154 109 32 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface-2:hover{--tw-bg-opacity: 1;background-color:rgb(44 42 40 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface-3:hover{--tw-bg-opacity: 1;background-color:rgb(54 52 50 / var(--tw-bg-opacity, 1))}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.hover\:text-po-error:hover{--tw-text-opacity: 1;color:rgb(196 92 74 / var(--tw-text-opacity, 1))}.hover\:text-po-text-primary:hover{--tw-text-opacity: 1;color:rgb(232 226 218 / var(--tw-text-opacity, 1))}.hover\:text-po-text-secondary:hover{--tw-text-opacity: 1;color:rgb(168 162 154 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}@media(min-width:640px){.sm\:inline{display:inline}}@media(min-width:1280px){.xl\:col-span-2{grid-column:span 2 / span 2}.xl\:min-h-\[30rem\]{min-height:30rem}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1536px){.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
@@ -9,8 +9,8 @@
9
9
  <link href="https://cdn.jsdelivr.net/npm/geist@1/dist/fonts/geist-sans/style.css" rel="stylesheet" />
10
10
  <link href="https://cdn.jsdelivr.net/npm/geist@1/dist/fonts/geist-mono/style.css" rel="stylesheet" />
11
11
  <title>PromptObjects</title>
12
- <script type="module" crossorigin src="/assets/index-DEPawnfZ.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-oMrRce1m.css">
12
+ <script type="module" crossorigin src="/assets/index-BjKlODDD.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-w3fpIb5c.css">
14
14
  </head>
15
15
  <body class="bg-po-bg text-po-text-primary font-ui">
16
16
  <div id="root"></div>
@@ -8,51 +8,14 @@ module PromptObjects
8
8
  # Handles WebSocket connections for real-time communication with the frontend.
9
9
  # Subscribes to MessageBus for state updates and handles client messages.
10
10
  #
11
- # == Real-time UI Feedback Pattern ==
12
- #
13
- # IMPORTANT: All user actions must provide immediate visual feedback BEFORE
14
- # async operations complete. This makes the UI feel "alive" and responsive.
15
- #
16
- # When implementing new features, follow this pattern:
17
- #
18
- # 1. SWITCH CONTEXT FIRST
19
- # When creating/switching threads, sessions, etc., send the context switch
20
- # message FIRST so the frontend displays the new context immediately.
21
- # Example: thread_created → frontend switches to new thread
22
- #
23
- # 2. UPDATE NAVIGATION
24
- # Send updated lists (sessions, threads) immediately so sidebars reflect
25
- # the change without waiting for async work to complete.
26
- # Example: po_state with sessions list → ThreadsSidebar shows new thread
27
- #
28
- # 3. SHOW USER INPUT
29
- # Send the user's input back immediately so they see it in the UI.
30
- # Don't wait for the AI to respond before showing what the user typed.
31
- # Example: session_updated with user message → chat shows "You: ..."
32
- #
33
- # 4. SHOW PROGRESS
34
- # Update status indicators during work (thinking, calling_tool, etc.).
35
- # Use streaming for incremental content when supported.
36
- # Example: po_state status "thinking" → spinner/animation shown
37
- #
38
- # 5. CONFIRM COMPLETION
39
- # Send final authoritative state after async work completes.
40
- # Example: session_updated with full messages → final chat state
41
- #
42
- # Message flow for "send message with new thread":
43
- # thread_created → switch to new thread
44
- # po_state (sessions) → update sidebar
45
- # session_updated → show user message
46
- # po_state (thinking) → show progress indicator
47
- # [stream chunks] → incremental AI response
48
- # po_response → complete AI response
49
- # session_updated → final messages
50
- # po_state (idle) → clear progress indicator
11
+ # Runtime snapshots establish authoritative state; versioned events then
12
+ # incrementally update runs, messages, artifacts, and environment data.
51
13
  class WebSocketHandler
52
14
  def initialize(runtime:, connection:, app: nil)
53
15
  @runtime = runtime
54
16
  @connection = connection
55
17
  @app = app
18
+ @write_mutex = Mutex.new
56
19
  @subscribed = false
57
20
  @bus_subscription = nil
58
21
  end
@@ -68,8 +31,10 @@ module PromptObjects
68
31
  # Send a message to this client (public for broadcasting).
69
32
  def send_message(data)
70
33
  json = JSON.generate(data)
71
- @connection.write(json)
72
- @connection.flush
34
+ @write_mutex.synchronize do
35
+ @connection.write(json)
36
+ @connection.flush
37
+ end
73
38
  rescue => e
74
39
  puts "WebSocket write error: #{e.message}" if ENV["DEBUG"]
75
40
  end
@@ -141,7 +106,8 @@ module PromptObjects
141
106
  name: @runtime.name,
142
107
  path: @runtime.env_path,
143
108
  po_count: @runtime.registry.prompt_objects.size,
144
- primitive_count: @runtime.registry.primitives.size
109
+ primitive_count: @runtime.registry.primitives.size,
110
+ runtime_model: @runtime.runtime_model
145
111
  }
146
112
  )
147
113
 
@@ -161,6 +127,13 @@ module PromptObjects
161
127
  )
162
128
  end
163
129
 
130
+ # Autonomous scheduler tasks may have started before this client
131
+ # connected. Replay their latest state so countdowns are correct after
132
+ # page load or reconnect.
133
+ @runtime.autonomous_schedule_states.each_value do |schedule|
134
+ send_message(type: "autonomous_schedule", payload: schedule)
135
+ end
136
+
164
137
  # Send pending human requests (notifications)
165
138
  @runtime.human_queue.all_pending.each do |request|
166
139
  send_message(
@@ -182,6 +155,8 @@ module PromptObjects
182
155
  }
183
156
  )
184
157
  end
158
+
159
+ @runtime.runtime_snapshot_envelopes.each { |envelope| send_message(envelope) }
185
160
  end
186
161
 
187
162
  # === Client Message Handling ===
@@ -203,12 +178,20 @@ module PromptObjects
203
178
  message = JSON.parse(data)
204
179
 
205
180
  case message["type"]
181
+ when "enqueue_run"
182
+ handle_enqueue_run(message["payload"])
183
+ when "cancel_run"
184
+ handle_cancel_run(message["payload"])
185
+ when "create_runtime_thread"
186
+ handle_create_runtime_thread(message["payload"])
187
+ when "rename_runtime_thread"
188
+ handle_rename_runtime_thread(message["payload"])
189
+ when "archive_runtime_thread"
190
+ handle_archive_runtime_thread(message["payload"])
206
191
  when "send_message"
207
192
  handle_send_message(message["payload"])
208
193
  when "respond_to_notification"
209
194
  handle_notification_response(message["payload"])
210
- when "update_po"
211
- handle_update_po(message["payload"])
212
195
  when "create_session"
213
196
  handle_create_session(message["payload"])
214
197
  when "switch_session"
@@ -223,12 +206,12 @@ module PromptObjects
223
206
  handle_switch_llm(message["payload"])
224
207
  when "update_prompt"
225
208
  handle_update_prompt(message["payload"])
209
+ when "update_po_source"
210
+ handle_update_po_source(message["payload"])
226
211
  when "get_session_usage"
227
212
  handle_get_session_usage(message["payload"])
228
213
  when "export_thread"
229
214
  handle_export_thread(message["payload"])
230
- when "get_env_data_list"
231
- handle_get_env_data_list(message["payload"])
232
215
  when "ping"
233
216
  send_message(type: "pong", payload: {})
234
217
  else
@@ -242,155 +225,150 @@ module PromptObjects
242
225
  puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
243
226
  end
244
227
 
245
- def handle_send_message(payload)
246
- po_name = payload["target"]
228
+ def handle_enqueue_run(payload)
229
+ target_po = payload["target_po"]
247
230
  content = payload["content"]
248
- new_thread = payload["new_thread"] # If true, create a new thread first
249
-
250
- po = @runtime.registry.get(po_name)
251
-
252
- unless po.is_a?(PromptObject)
253
- send_error("Unknown prompt object: #{po_name}")
254
- return
255
- end
256
-
257
- # If new_thread requested, create one first
258
- if new_thread
259
- thread_id = po.new_thread
260
- # Notify client of new thread immediately so they see it
261
- send_message(
262
- type: "thread_created",
263
- payload: {
264
- target: po_name,
265
- thread_id: thread_id,
266
- thread_type: "root"
267
- }
268
- )
269
- # Also send updated sessions list so ThreadsSidebar shows it immediately
270
- send_message(
271
- type: "po_state",
272
- payload: {
273
- name: po_name,
274
- state: { sessions: po.list_sessions.map { |s| PromptObject.serialize_session(s) } }
275
- }
276
- )
277
- end
278
-
279
- # Capture the session_id at request start so response goes to correct session
280
- request_session_id = po.session_id
281
-
282
- # Update PO state to working AND send the user message for immediate UI feedback
283
- send_message(
284
- type: "po_state",
285
- payload: { name: po_name, state: { status: "thinking" } }
231
+ client_request_id = payload["client_request_id"]
232
+ raise Error, "client_request_id is required" if client_request_id.to_s.empty?
233
+ raise Error, "content is required" if content.to_s.empty?
234
+
235
+ source = payload["source"] || { "type" => "human" }
236
+ from_source = protocol_source(source)
237
+ run = @runtime.enqueue_run(
238
+ target_po: target_po,
239
+ content: content,
240
+ from_source: from_source,
241
+ workspace_session_id: payload["workspace_session_id"],
242
+ thread_id: payload["thread_id"],
243
+ client_request_id: client_request_id,
244
+ deadline: payload["deadline"]
286
245
  )
287
-
288
- # Send immediate session update showing the user's message
289
- # This gives instant feedback before the AI responds
290
- # Include existing messages so we don't clear the chat when continuing a thread
291
- existing_messages = session_messages(po, request_session_id)
292
- new_user_message = { role: "user", content: content, from: "human" }
293
246
  send_message(
294
- type: "session_updated",
295
- payload: {
296
- target: po_name,
297
- session_id: request_session_id,
298
- messages: existing_messages + [new_user_message]
299
- }
247
+ type: "run_acknowledged",
248
+ payload: serialize_runtime_value(run.to_h)
300
249
  )
301
250
 
302
- # Run in async context
303
251
  Async do
304
- # Use tui_mode: true so ask_human uses HumanQueue (blocking) instead of REPL mode
305
- context = @runtime.context(tui_mode: true)
306
- context.current_capability = "human"
307
-
308
- # Set up callback for real-time tool call updates
309
- # This fires after each tool call iteration in the receive loop
310
- po.on_history_updated = ->(po_obj, session_id, history) {
311
- send_message(
312
- type: "session_updated",
313
- payload: {
314
- target: po_obj.name,
315
- session_id: session_id,
316
- messages: history.map { |m| PromptObject.serialize_message(m) }
317
- }
318
- )
319
- }
320
-
321
252
  begin
322
- response = po.receive(content, context: context)
323
-
324
- # Auto-name the thread if it doesn't have a name yet
325
- auto_name_thread_if_needed(po, request_session_id, content)
253
+ response = execute_blocking { @runtime.execute_run(run.id, tui_mode: false) }
254
+ next if response == :queued
326
255
 
327
- # Send the complete response with session_id for correct routing
328
256
  send_message(
329
257
  type: "po_response",
330
- payload: {
331
- target: po_name,
332
- session_id: request_session_id,
333
- content: response
334
- }
335
- )
336
-
337
- # Send session update for the session where messages were added
338
- # This ensures the correct session is updated even if user switched
339
- if request_session_id
340
- send_message(
341
- type: "session_updated",
342
- payload: {
343
- target: po_name,
344
- session_id: request_session_id,
345
- messages: session_messages(po, request_session_id)
346
- }
347
- )
348
- end
349
-
350
- # Also send updated sessions list (for thread panel)
351
- send_message(
352
- type: "po_state",
353
- payload: {
354
- name: po_name,
355
- state: { sessions: po.list_sessions.map { |s| PromptObject.serialize_session(s) } }
356
- }
258
+ payload: { target: target_po, thread_id: run.thread_id, content: response }
357
259
  )
358
260
  rescue => e
359
- send_error("Error from #{po_name}: #{e.message}")
360
-
361
- # Broadcast rich error context for diagnostics
362
- llm_config = @runtime.llm_config
261
+ send_error("Error from #{target_po}: #{e.message}")
363
262
  send_message(
364
263
  type: "llm_error",
365
264
  payload: {
366
- po_name: po_name,
367
- provider: llm_config[:provider],
368
- model: llm_config[:model],
265
+ po_name: target_po,
266
+ provider: run.provider,
267
+ model: run.model,
369
268
  error: e.message,
370
269
  error_class: e.class.name
371
270
  }
372
271
  )
373
- ensure
374
- # Clean up the callback
375
- po.on_history_updated = nil
376
-
377
- # Update PO state back to idle (status only, not session data)
378
- send_message(
379
- type: "po_state",
380
- payload: { name: po_name, state: { status: "idle" } }
381
- )
382
272
  end
383
273
  end
384
274
  end
385
275
 
386
- # Auto-name a thread based on the user's first message if it doesn't have a name
387
- def auto_name_thread_if_needed(po, session_id, user_message)
388
- return unless @runtime.session_store && session_id
276
+ def handle_cancel_run(payload)
277
+ run_id = payload["run_id"]
278
+ raise Error, "run_id is required" if run_id.to_s.empty?
279
+
280
+ cancelled = @runtime.cancel_run(run_id, reason: payload["reason"] || "client request")
281
+ send_message(
282
+ type: "run_cancellation_acknowledged",
283
+ payload: { run_id: run_id, cancelled: cancelled }
284
+ )
285
+ end
286
+
287
+ def handle_create_runtime_thread(payload)
288
+ thread = @runtime.create_runtime_thread(
289
+ workspace_session_id: payload["workspace_session_id"],
290
+ po_name: payload["po_name"],
291
+ thread_type: payload["thread_type"] || "human",
292
+ name: payload["name"]
293
+ )
294
+ send_message(
295
+ type: "runtime_thread_created",
296
+ payload: serialize_runtime_value(thread.to_h)
297
+ )
298
+ end
299
+
300
+ def handle_rename_runtime_thread(payload)
301
+ thread = @runtime.rename_runtime_thread(payload["thread_id"], name: payload["name"])
302
+ send_message(
303
+ type: "runtime_thread_renamed",
304
+ payload: serialize_runtime_value(thread.to_h)
305
+ )
306
+ end
307
+
308
+ def handle_archive_runtime_thread(payload)
309
+ thread = @runtime.archive_runtime_thread(payload["thread_id"])
310
+ send_message(
311
+ type: "runtime_thread_archived",
312
+ payload: serialize_runtime_value(thread.to_h)
313
+ )
314
+ end
315
+
316
+ def handle_send_message(payload)
317
+ po_name = payload["target"]
318
+ content = payload["content"]
319
+ new_thread = payload["new_thread"] # If true, create a new thread first
320
+
321
+ po = @runtime.registry.get(po_name)
322
+
323
+ unless po.is_a?(PromptObject)
324
+ send_error("Unknown prompt object: #{po_name}")
325
+ return
326
+ end
327
+
328
+ workspace_id = payload["workspace_session_id"] || @runtime.active_session(initiated_by: "human")[:id]
329
+ thread_id = payload["thread_id"] || payload["session_id"]
330
+ if new_thread
331
+ thread = @runtime.create_runtime_thread(
332
+ workspace_session_id: workspace_id,
333
+ po_name: po_name,
334
+ thread_type: "human",
335
+ name: payload["name"]
336
+ )
337
+ thread_id = thread.id
338
+ send_message(type: "runtime_thread_created", payload: serialize_runtime_value(thread.to_h))
339
+ end
340
+ handle_enqueue_run(
341
+ "target_po" => po_name,
342
+ "content" => content,
343
+ "client_request_id" => payload["client_request_id"] || SecureRandom.uuid,
344
+ "workspace_session_id" => workspace_id,
345
+ "thread_id" => thread_id,
346
+ "source" => { "type" => "human" }
347
+ )
348
+ end
349
+
350
+ def execute_blocking(&block)
351
+ return @app.blocking_executor.call(&block) if @app&.respond_to?(:blocking_executor)
352
+
353
+ block.call
354
+ end
355
+
356
+ def protocol_source(source)
357
+ type = source.fetch("type", "human")
358
+ name = source["name"]
359
+ return type if %w[human api].include?(type)
360
+ return "#{type}:#{name}" if %w[timer artifact service].include?(type) && !name.to_s.empty?
389
361
 
390
- session = @runtime.session_store.get_session(session_id)
391
- return if session.nil? || session[:name] # Already has a name
362
+ raise Error, "Unsupported run source: #{type}"
363
+ end
392
364
 
393
- @runtime.session_store.auto_name_thread(session_id, user_message)
365
+ def serialize_runtime_value(value)
366
+ case value
367
+ when Time then value.iso8601
368
+ when Hash then value.to_h { |key, nested| [key, serialize_runtime_value(nested)] }
369
+ when Array then value.map { |nested| serialize_runtime_value(nested) }
370
+ else value
371
+ end
394
372
  end
395
373
 
396
374
  def handle_notification_response(payload)
@@ -405,18 +383,39 @@ module PromptObjects
405
383
  )
406
384
  end
407
385
 
408
- def handle_update_po(payload)
409
- po_name = payload["name"]
410
- updates = payload["updates"]
386
+ # Replace a PO's entire .md source (frontmatter + body) from the editor.
387
+ # Re-parses in place so autonomous/delegates_to/tools changes take effect,
388
+ # and re-keys the registry if the frontmatter name changed.
389
+ def handle_update_po_source(payload)
390
+ po_name = payload["target"]
391
+ source = payload["source"]
411
392
 
412
393
  po = @runtime.registry.get(po_name)
413
394
  return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
395
+ return send_error("#{po_name} has no source file to edit") unless po.path
396
+
397
+ old_name = po.name
398
+ if po.update_source(source)
399
+ if po.name != old_name
400
+ @runtime.registry.unregister(old_name)
401
+ @runtime.registry.register(po)
402
+
403
+ # Keep the filename in sync with the new name so the file and the
404
+ # registered PO don't diverge (update_source wrote to the old path).
405
+ new_path = File.join(File.dirname(po.path), "#{po.name}.md")
406
+ po.relocate(new_path)
407
+ end
414
408
 
415
- # TODO: Implement PO updates (capabilities, etc.)
416
- send_message(
417
- type: "po_updated",
418
- payload: { name: po_name }
419
- )
409
+ # Broadcasts full PO state (incl. the re-read source) to all clients.
410
+ @runtime.notify_po_modified(po)
411
+
412
+ send_message(
413
+ type: "po_source_updated",
414
+ payload: { target: po.name, previous_name: old_name, success: true }
415
+ )
416
+ else
417
+ send_error("Failed to update #{po_name} — check the YAML frontmatter")
418
+ end
420
419
  end
421
420
 
422
421
  def handle_create_session(payload)
@@ -426,22 +425,25 @@ module PromptObjects
426
425
  po = @runtime.registry.get(po_name)
427
426
  return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
428
427
 
429
- session_id = po.new_session(name: session_name)
428
+ workspace_id = payload["workspace_session_id"] || @runtime.active_session(initiated_by: "human")[:id]
429
+ thread = @runtime.create_runtime_thread(
430
+ workspace_session_id: workspace_id,
431
+ po_name: po_name,
432
+ thread_type: "human",
433
+ name: session_name
434
+ )
430
435
 
431
436
  send_message(
432
437
  type: "session_created",
433
438
  payload: {
434
439
  target: po_name,
435
- session_id: session_id,
440
+ session_id: thread.id,
441
+ thread_id: thread.id,
442
+ workspace_session_id: thread.workspace_session_id,
436
443
  name: session_name
437
444
  }
438
445
  )
439
-
440
- # Also send updated PO state
441
- send_message(
442
- type: "po_state",
443
- payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
444
- )
446
+ send_message(type: "runtime_thread_created", payload: serialize_runtime_value(thread.to_h))
445
447
  end
446
448
 
447
449
  def handle_switch_session(payload)
@@ -451,17 +453,13 @@ module PromptObjects
451
453
  po = @runtime.registry.get(po_name)
452
454
  return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
453
455
 
454
- if po.switch_session(session_id)
456
+ thread = Repositories::ThreadRepository.new(@runtime.session_store).get(session_id)
457
+ if thread && thread.po_name == po_name
455
458
  send_message(
456
459
  type: "session_switched",
457
460
  payload: { target: po_name, session_id: session_id }
458
461
  )
459
462
 
460
- # Send updated PO state with new session's messages
461
- send_message(
462
- type: "po_state",
463
- payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
464
- )
465
463
  else
466
464
  send_error("Could not switch to session: #{session_id}")
467
465
  end
@@ -475,22 +473,17 @@ module PromptObjects
475
473
  po = @runtime.registry.get(po_name)
476
474
  return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
477
475
 
478
- thread_id = po.new_thread(name: thread_name)
479
-
480
- send_message(
481
- type: "thread_created",
482
- payload: {
483
- target: po_name,
484
- thread_id: thread_id,
485
- name: thread_name,
486
- thread_type: thread_type
487
- }
476
+ workspace_id = payload["workspace_session_id"] || @runtime.active_session(initiated_by: "human")[:id]
477
+ thread = @runtime.create_runtime_thread(
478
+ workspace_session_id: workspace_id,
479
+ po_name: po_name,
480
+ thread_type: thread_type == "root" ? "human" : thread_type,
481
+ name: thread_name
488
482
  )
489
483
 
490
- # Also send updated PO state
491
484
  send_message(
492
- type: "po_state",
493
- payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
485
+ type: "runtime_thread_created",
486
+ payload: serialize_runtime_value(thread.to_h)
494
487
  )
495
488
  end
496
489
 
@@ -641,24 +634,6 @@ module PromptObjects
641
634
  )
642
635
  end
643
636
 
644
- def handle_get_env_data_list(payload)
645
- session_id = payload["session_id"]
646
- return send_error("Session ID required") unless session_id
647
- return send_error("No session store available") unless @runtime.session_store
648
-
649
- root_thread_id = @runtime.session_store.resolve_root_thread(session_id)
650
- entries = @runtime.session_store.list_env_data_full(root_thread_id: root_thread_id)
651
-
652
- send_message(
653
- type: "env_data_list",
654
- payload: {
655
- session_id: session_id,
656
- root_thread_id: root_thread_id,
657
- entries: entries
658
- }
659
- )
660
- end
661
-
662
637
  # === Helpers ===
663
638
 
664
639
  def send_error(message)
@@ -686,9 +661,9 @@ module PromptObjects
686
661
  def request_to_hash(request)
687
662
  {
688
663
  id: request.id,
689
- po_name: request.capability, # capability is the PO name
690
- type: "ask_human", # hardcode type since it's always ask_human
691
- message: request.question, # question is the message
664
+ po_name: request.capability,
665
+ type: request.capability,
666
+ message: request.question,
692
667
  options: request.options || []
693
668
  }
694
669
  end