prompt_objects 0.1.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.
- checksums.yaml +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -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:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,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}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-4{bottom:1rem}.left-2{left:.5rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-\[80\%\]{max-width:80%}.max-w-none{max-width:none}.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))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.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-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-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-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;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-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.border-po-border{--tw-border-opacity: 1;border-color:rgb(45 45 68 / var(--tw-border-opacity, 1))}.border-po-border\/50{border-color:#2d2d4480}.border-po-warning\/30{border-color:#f59e0b4d}.border-transparent{border-color:transparent}.bg-blue-600\/30{background-color:#2563eb4d}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600\/30{background-color:#4b55634d}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.bg-po-accent\/20{background-color:#7c3aed33}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.bg-po-bg\/50{background-color:#0f0f1a80}.bg-po-bg\/80{background-color:#0f0f1acc}.bg-po-border{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-po-warning\/10{background-color:#f59e0b1a}.bg-purple-600\/30{background-color:#9333ea4d}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.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-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.text-po-warning{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.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)}.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-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.hover\:border-po-accent\/50:hover{border-color:#7c3aed80}.hover\:bg-po-accent\/50:hover{background-color:#7c3aed80}.hover\:bg-po-accent\/80:hover{background-color:#7c3aedcc}.hover\:bg-po-bg:hover{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.hover\:bg-po-bg\/70:hover{background-color:#0f0f1ab3}.hover\:bg-po-border:hover{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface:hover{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.hover\:bg-po-warning\/20:hover{background-color:#f59e0b33}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PromptObjects</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-2acS2FYZ.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DXU5uRXQ.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-po-bg text-gray-100">
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "async"
|
|
5
|
+
|
|
6
|
+
module PromptObjects
|
|
7
|
+
module Server
|
|
8
|
+
# Handles WebSocket connections for real-time communication with the frontend.
|
|
9
|
+
# Subscribes to MessageBus for state updates and handles client messages.
|
|
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
|
|
51
|
+
class WebSocketHandler
|
|
52
|
+
def initialize(runtime:, connection:, app: nil)
|
|
53
|
+
@runtime = runtime
|
|
54
|
+
@connection = connection
|
|
55
|
+
@app = app
|
|
56
|
+
@subscribed = false
|
|
57
|
+
@bus_subscription = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run
|
|
61
|
+
subscribe_to_bus
|
|
62
|
+
send_initial_state
|
|
63
|
+
read_loop
|
|
64
|
+
ensure
|
|
65
|
+
unsubscribe_from_bus
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Send a message to this client (public for broadcasting).
|
|
69
|
+
def send_message(data)
|
|
70
|
+
json = JSON.generate(data)
|
|
71
|
+
@connection.write(json)
|
|
72
|
+
@connection.flush
|
|
73
|
+
rescue => e
|
|
74
|
+
puts "WebSocket write error: #{e.message}" if ENV["DEBUG"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# === MessageBus Integration ===
|
|
80
|
+
|
|
81
|
+
def subscribe_to_bus
|
|
82
|
+
@bus_subscription = ->(entry) { on_bus_message(entry) }
|
|
83
|
+
@runtime.bus.subscribe(&@bus_subscription)
|
|
84
|
+
|
|
85
|
+
# Subscribe to HumanQueue for notification events
|
|
86
|
+
@human_queue_subscription = ->(event, request) { on_human_queue_event(event, request) }
|
|
87
|
+
@runtime.human_queue.subscribe(&@human_queue_subscription)
|
|
88
|
+
|
|
89
|
+
@subscribed = true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def unsubscribe_from_bus
|
|
93
|
+
return unless @subscribed
|
|
94
|
+
|
|
95
|
+
@runtime.bus.unsubscribe(@bus_subscription) if @bus_subscription
|
|
96
|
+
@runtime.human_queue.unsubscribe(@human_queue_subscription) if @human_queue_subscription
|
|
97
|
+
|
|
98
|
+
@subscribed = false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def on_human_queue_event(event, request)
|
|
102
|
+
case event
|
|
103
|
+
when :added
|
|
104
|
+
send_message(
|
|
105
|
+
type: "notification",
|
|
106
|
+
payload: request_to_hash(request)
|
|
107
|
+
)
|
|
108
|
+
when :resolved
|
|
109
|
+
send_message(
|
|
110
|
+
type: "notification_resolved",
|
|
111
|
+
payload: { id: request.id }
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
rescue => e
|
|
115
|
+
puts "WebSocket notification error: #{e.message}" if ENV["DEBUG"]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def on_bus_message(entry)
|
|
119
|
+
send_message(
|
|
120
|
+
type: "bus_message",
|
|
121
|
+
payload: {
|
|
122
|
+
from: entry[:from],
|
|
123
|
+
to: entry[:to],
|
|
124
|
+
content: entry[:message],
|
|
125
|
+
timestamp: entry[:timestamp].iso8601
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
rescue => e
|
|
129
|
+
# Connection may be closed, ignore errors
|
|
130
|
+
puts "WebSocket send error: #{e.message}" if ENV["DEBUG"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# === Initial State ===
|
|
134
|
+
|
|
135
|
+
def send_initial_state
|
|
136
|
+
# Send environment info
|
|
137
|
+
send_message(
|
|
138
|
+
type: "environment",
|
|
139
|
+
payload: {
|
|
140
|
+
name: @runtime.name,
|
|
141
|
+
path: @runtime.env_path,
|
|
142
|
+
po_count: @runtime.registry.prompt_objects.size,
|
|
143
|
+
primitive_count: @runtime.registry.primitives.size
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Send LLM config
|
|
148
|
+
handle_get_llm_config
|
|
149
|
+
|
|
150
|
+
# Send state of all POs
|
|
151
|
+
# Always send "idle" status for initial state - from this connection's
|
|
152
|
+
# perspective, no work is pending (even if another connection has a request)
|
|
153
|
+
@runtime.registry.prompt_objects.each do |po|
|
|
154
|
+
send_message(
|
|
155
|
+
type: "po_state",
|
|
156
|
+
payload: {
|
|
157
|
+
name: po.name,
|
|
158
|
+
state: po_state_hash(po).merge(status: "idle")
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Send pending human requests (notifications)
|
|
164
|
+
@runtime.human_queue.all_pending.each do |request|
|
|
165
|
+
send_message(
|
|
166
|
+
type: "notification",
|
|
167
|
+
payload: request_to_hash(request)
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Send recent bus messages (last 50)
|
|
172
|
+
@runtime.bus.log.last(50).each do |entry|
|
|
173
|
+
send_message(
|
|
174
|
+
type: "bus_message",
|
|
175
|
+
payload: {
|
|
176
|
+
from: entry[:from],
|
|
177
|
+
to: entry[:to],
|
|
178
|
+
content: entry[:message],
|
|
179
|
+
timestamp: entry[:timestamp].iso8601
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# === Client Message Handling ===
|
|
186
|
+
|
|
187
|
+
def read_loop
|
|
188
|
+
while (message = @connection.read)
|
|
189
|
+
handle_client_message(message)
|
|
190
|
+
end
|
|
191
|
+
rescue Async::Stop, EOFError, IOError
|
|
192
|
+
# Connection closed, exit gracefully
|
|
193
|
+
rescue => e
|
|
194
|
+
puts "WebSocket error: #{e.message}"
|
|
195
|
+
puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def handle_client_message(raw_message)
|
|
199
|
+
# raw_message is a Protocol::WebSocket::TextMessage
|
|
200
|
+
data = raw_message.respond_to?(:buffer) ? raw_message.buffer : raw_message.to_s
|
|
201
|
+
message = JSON.parse(data)
|
|
202
|
+
|
|
203
|
+
case message["type"]
|
|
204
|
+
when "send_message"
|
|
205
|
+
handle_send_message(message["payload"])
|
|
206
|
+
when "respond_to_notification"
|
|
207
|
+
handle_notification_response(message["payload"])
|
|
208
|
+
when "update_po"
|
|
209
|
+
handle_update_po(message["payload"])
|
|
210
|
+
when "create_session"
|
|
211
|
+
handle_create_session(message["payload"])
|
|
212
|
+
when "switch_session"
|
|
213
|
+
handle_switch_session(message["payload"])
|
|
214
|
+
when "create_thread"
|
|
215
|
+
handle_create_thread(message["payload"])
|
|
216
|
+
when "get_thread_tree"
|
|
217
|
+
handle_get_thread_tree(message["payload"])
|
|
218
|
+
when "get_llm_config"
|
|
219
|
+
handle_get_llm_config
|
|
220
|
+
when "switch_llm"
|
|
221
|
+
handle_switch_llm(message["payload"])
|
|
222
|
+
when "ping"
|
|
223
|
+
send_message(type: "pong", payload: {})
|
|
224
|
+
else
|
|
225
|
+
send_error("Unknown message type: #{message['type']}")
|
|
226
|
+
end
|
|
227
|
+
rescue JSON::ParserError => e
|
|
228
|
+
send_error("Invalid JSON: #{e.message}")
|
|
229
|
+
rescue => e
|
|
230
|
+
send_error("Error: #{e.message}")
|
|
231
|
+
puts "Handler error: #{e.message}"
|
|
232
|
+
puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_send_message(payload)
|
|
236
|
+
po_name = payload["target"]
|
|
237
|
+
content = payload["content"]
|
|
238
|
+
new_thread = payload["new_thread"] # If true, create a new thread first
|
|
239
|
+
|
|
240
|
+
po = @runtime.registry.get(po_name)
|
|
241
|
+
|
|
242
|
+
unless po.is_a?(PromptObject)
|
|
243
|
+
send_error("Unknown prompt object: #{po_name}")
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# If new_thread requested, create one first
|
|
248
|
+
if new_thread
|
|
249
|
+
thread_id = po.new_thread
|
|
250
|
+
# Notify client of new thread immediately so they see it
|
|
251
|
+
send_message(
|
|
252
|
+
type: "thread_created",
|
|
253
|
+
payload: {
|
|
254
|
+
target: po_name,
|
|
255
|
+
thread_id: thread_id,
|
|
256
|
+
thread_type: "root"
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
# Also send updated sessions list so ThreadsSidebar shows it immediately
|
|
260
|
+
send_message(
|
|
261
|
+
type: "po_state",
|
|
262
|
+
payload: {
|
|
263
|
+
name: po_name,
|
|
264
|
+
state: { sessions: po.list_sessions.map { |s| session_summary(s) } }
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Capture the session_id at request start so response goes to correct session
|
|
270
|
+
request_session_id = po.session_id
|
|
271
|
+
|
|
272
|
+
# Update PO state to working AND send the user message for immediate UI feedback
|
|
273
|
+
send_message(
|
|
274
|
+
type: "po_state",
|
|
275
|
+
payload: { name: po_name, state: { status: "thinking" } }
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Send immediate session update showing the user's message
|
|
279
|
+
# This gives instant feedback before the AI responds
|
|
280
|
+
# Include existing messages so we don't clear the chat when continuing a thread
|
|
281
|
+
existing_messages = session_messages(po, request_session_id)
|
|
282
|
+
new_user_message = { role: "user", content: content, from: "human" }
|
|
283
|
+
send_message(
|
|
284
|
+
type: "session_updated",
|
|
285
|
+
payload: {
|
|
286
|
+
target: po_name,
|
|
287
|
+
session_id: request_session_id,
|
|
288
|
+
messages: existing_messages + [new_user_message]
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Run in async context
|
|
293
|
+
Async do
|
|
294
|
+
# Use tui_mode: true so ask_human uses HumanQueue (blocking) instead of REPL mode
|
|
295
|
+
context = @runtime.context(tui_mode: true)
|
|
296
|
+
context.current_capability = "human"
|
|
297
|
+
|
|
298
|
+
begin
|
|
299
|
+
# TODO: Add streaming support to receive()
|
|
300
|
+
# For now, just get the full response
|
|
301
|
+
response = po.receive(content, context: context)
|
|
302
|
+
|
|
303
|
+
# Auto-name the thread if it doesn't have a name yet
|
|
304
|
+
auto_name_thread_if_needed(po, request_session_id, content)
|
|
305
|
+
|
|
306
|
+
# Send the complete response with session_id for correct routing
|
|
307
|
+
send_message(
|
|
308
|
+
type: "po_response",
|
|
309
|
+
payload: {
|
|
310
|
+
target: po_name,
|
|
311
|
+
session_id: request_session_id,
|
|
312
|
+
content: response
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Send session update for the session where messages were added
|
|
317
|
+
# This ensures the correct session is updated even if user switched
|
|
318
|
+
if request_session_id
|
|
319
|
+
send_message(
|
|
320
|
+
type: "session_updated",
|
|
321
|
+
payload: {
|
|
322
|
+
target: po_name,
|
|
323
|
+
session_id: request_session_id,
|
|
324
|
+
messages: session_messages(po, request_session_id)
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Also send updated sessions list (for thread panel)
|
|
330
|
+
send_message(
|
|
331
|
+
type: "po_state",
|
|
332
|
+
payload: {
|
|
333
|
+
name: po_name,
|
|
334
|
+
state: { sessions: po.list_sessions.map { |s| session_summary(s) } }
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
rescue => e
|
|
338
|
+
send_error("Error from #{po_name}: #{e.message}")
|
|
339
|
+
ensure
|
|
340
|
+
# Update PO state back to idle (status only, not session data)
|
|
341
|
+
send_message(
|
|
342
|
+
type: "po_state",
|
|
343
|
+
payload: { name: po_name, state: { status: "idle" } }
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Auto-name a thread based on the user's first message if it doesn't have a name
|
|
350
|
+
def auto_name_thread_if_needed(po, session_id, user_message)
|
|
351
|
+
return unless @runtime.session_store && session_id
|
|
352
|
+
|
|
353
|
+
session = @runtime.session_store.get_session(session_id)
|
|
354
|
+
return if session.nil? || session[:name] # Already has a name
|
|
355
|
+
|
|
356
|
+
@runtime.session_store.auto_name_thread(session_id, user_message)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def handle_notification_response(payload)
|
|
360
|
+
request_id = payload["id"]
|
|
361
|
+
response = payload["response"]
|
|
362
|
+
|
|
363
|
+
@runtime.human_queue.respond(request_id, response)
|
|
364
|
+
|
|
365
|
+
send_message(
|
|
366
|
+
type: "notification_resolved",
|
|
367
|
+
payload: { id: request_id }
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def handle_update_po(payload)
|
|
372
|
+
po_name = payload["name"]
|
|
373
|
+
updates = payload["updates"]
|
|
374
|
+
|
|
375
|
+
po = @runtime.registry.get(po_name)
|
|
376
|
+
return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
|
|
377
|
+
|
|
378
|
+
# TODO: Implement PO updates (capabilities, etc.)
|
|
379
|
+
send_message(
|
|
380
|
+
type: "po_updated",
|
|
381
|
+
payload: { name: po_name }
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def handle_create_session(payload)
|
|
386
|
+
po_name = payload["target"]
|
|
387
|
+
session_name = payload["name"]
|
|
388
|
+
|
|
389
|
+
po = @runtime.registry.get(po_name)
|
|
390
|
+
return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
|
|
391
|
+
|
|
392
|
+
session_id = po.new_session(name: session_name)
|
|
393
|
+
|
|
394
|
+
send_message(
|
|
395
|
+
type: "session_created",
|
|
396
|
+
payload: {
|
|
397
|
+
target: po_name,
|
|
398
|
+
session_id: session_id,
|
|
399
|
+
name: session_name
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Also send updated PO state
|
|
404
|
+
send_message(
|
|
405
|
+
type: "po_state",
|
|
406
|
+
payload: { name: po_name, state: po_state_hash(po) }
|
|
407
|
+
)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def handle_switch_session(payload)
|
|
411
|
+
po_name = payload["target"]
|
|
412
|
+
session_id = payload["session_id"]
|
|
413
|
+
|
|
414
|
+
po = @runtime.registry.get(po_name)
|
|
415
|
+
return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
|
|
416
|
+
|
|
417
|
+
if po.switch_session(session_id)
|
|
418
|
+
send_message(
|
|
419
|
+
type: "session_switched",
|
|
420
|
+
payload: { target: po_name, session_id: session_id }
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Send updated PO state with new session's messages
|
|
424
|
+
send_message(
|
|
425
|
+
type: "po_state",
|
|
426
|
+
payload: { name: po_name, state: po_state_hash(po) }
|
|
427
|
+
)
|
|
428
|
+
else
|
|
429
|
+
send_error("Could not switch to session: #{session_id}")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def handle_create_thread(payload)
|
|
434
|
+
po_name = payload["target"]
|
|
435
|
+
thread_name = payload["name"]
|
|
436
|
+
thread_type = payload["thread_type"] || "root"
|
|
437
|
+
|
|
438
|
+
po = @runtime.registry.get(po_name)
|
|
439
|
+
return send_error("Unknown prompt object: #{po_name}") unless po.is_a?(PromptObject)
|
|
440
|
+
|
|
441
|
+
thread_id = po.new_thread(name: thread_name)
|
|
442
|
+
|
|
443
|
+
send_message(
|
|
444
|
+
type: "thread_created",
|
|
445
|
+
payload: {
|
|
446
|
+
target: po_name,
|
|
447
|
+
thread_id: thread_id,
|
|
448
|
+
name: thread_name,
|
|
449
|
+
thread_type: thread_type
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Also send updated PO state
|
|
454
|
+
send_message(
|
|
455
|
+
type: "po_state",
|
|
456
|
+
payload: { name: po_name, state: po_state_hash(po) }
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def handle_get_thread_tree(payload)
|
|
461
|
+
session_id = payload["session_id"]
|
|
462
|
+
return send_error("Session ID required") unless session_id
|
|
463
|
+
return send_error("No session store available") unless @runtime.session_store
|
|
464
|
+
|
|
465
|
+
tree = @runtime.session_store.get_thread_tree(session_id)
|
|
466
|
+
return send_error("Session not found: #{session_id}") unless tree
|
|
467
|
+
|
|
468
|
+
send_message(
|
|
469
|
+
type: "thread_tree",
|
|
470
|
+
payload: { tree: serialize_thread_tree(tree) }
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def handle_get_llm_config
|
|
475
|
+
config = @runtime.llm_config
|
|
476
|
+
|
|
477
|
+
# Get models for each provider
|
|
478
|
+
providers_info = LLM::Factory.providers.map do |provider|
|
|
479
|
+
info = LLM::Factory.provider_info(provider)
|
|
480
|
+
{
|
|
481
|
+
name: provider,
|
|
482
|
+
models: info[:models],
|
|
483
|
+
default_model: info[:default_model],
|
|
484
|
+
available: LLM::Factory.available_providers[provider]
|
|
485
|
+
}
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
send_message(
|
|
489
|
+
type: "llm_config",
|
|
490
|
+
payload: {
|
|
491
|
+
current_provider: config[:provider],
|
|
492
|
+
current_model: config[:model],
|
|
493
|
+
providers: providers_info
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def handle_switch_llm(payload)
|
|
499
|
+
provider = payload["provider"]
|
|
500
|
+
model = payload["model"]
|
|
501
|
+
|
|
502
|
+
begin
|
|
503
|
+
result = @runtime.switch_llm(provider: provider, model: model)
|
|
504
|
+
|
|
505
|
+
send_message(
|
|
506
|
+
type: "llm_switched",
|
|
507
|
+
payload: {
|
|
508
|
+
provider: result[:provider],
|
|
509
|
+
model: result[:model]
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Broadcast to all connected clients via app
|
|
514
|
+
@app&.broadcast(
|
|
515
|
+
type: "llm_switched",
|
|
516
|
+
payload: {
|
|
517
|
+
provider: result[:provider],
|
|
518
|
+
model: result[:model]
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
rescue PromptObjects::Error => e
|
|
522
|
+
send_error("Failed to switch LLM: #{e.message}")
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# === Helpers ===
|
|
527
|
+
|
|
528
|
+
def send_error(message)
|
|
529
|
+
send_message(type: "error", payload: { message: message })
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def po_state_hash(po)
|
|
533
|
+
{
|
|
534
|
+
status: po.instance_variable_get(:@state) || "idle",
|
|
535
|
+
description: po.description,
|
|
536
|
+
capabilities: po.config["capabilities"] || [],
|
|
537
|
+
current_session: current_session_hash(po),
|
|
538
|
+
sessions: po.list_sessions.map { |s| session_summary(s) },
|
|
539
|
+
# Include full prompt for inspection
|
|
540
|
+
prompt: po.body,
|
|
541
|
+
config: po.config
|
|
542
|
+
}
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def current_session_hash(po)
|
|
546
|
+
return nil unless po.session_id
|
|
547
|
+
|
|
548
|
+
{
|
|
549
|
+
id: po.session_id,
|
|
550
|
+
messages: po.history.map { |m| message_to_hash(m) }
|
|
551
|
+
}
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Get messages for a specific session (may not be current session)
|
|
555
|
+
def session_messages(po, session_id)
|
|
556
|
+
return [] unless @runtime.session_store
|
|
557
|
+
|
|
558
|
+
messages = @runtime.session_store.get_messages(session_id)
|
|
559
|
+
messages.map { |m| message_to_hash(m) }
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def session_summary(session)
|
|
563
|
+
{
|
|
564
|
+
id: session[:id],
|
|
565
|
+
name: session[:name],
|
|
566
|
+
message_count: session[:message_count] || 0,
|
|
567
|
+
updated_at: session[:updated_at]&.iso8601,
|
|
568
|
+
# Thread fields
|
|
569
|
+
parent_session_id: session[:parent_session_id],
|
|
570
|
+
parent_po: session[:parent_po],
|
|
571
|
+
thread_type: session[:thread_type] || "root"
|
|
572
|
+
}
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Recursively serialize a thread tree for JSON
|
|
576
|
+
def serialize_thread_tree(tree)
|
|
577
|
+
return nil unless tree
|
|
578
|
+
|
|
579
|
+
{
|
|
580
|
+
session: session_summary(tree[:session]),
|
|
581
|
+
children: (tree[:children] || []).map { |child| serialize_thread_tree(child) }
|
|
582
|
+
}
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def message_to_hash(msg)
|
|
586
|
+
case msg[:role]
|
|
587
|
+
when :user
|
|
588
|
+
{ role: "user", content: msg[:content], from: msg[:from] }
|
|
589
|
+
when :assistant
|
|
590
|
+
hash = { role: "assistant", content: msg[:content] }
|
|
591
|
+
if msg[:tool_calls]
|
|
592
|
+
hash[:tool_calls] = msg[:tool_calls].map do |tc|
|
|
593
|
+
# Handle both ToolCall objects and Hashes
|
|
594
|
+
tc_id = tc.respond_to?(:id) ? tc.id : (tc[:id] || tc["id"])
|
|
595
|
+
tc_name = tc.respond_to?(:name) ? tc.name : (tc[:name] || tc["name"])
|
|
596
|
+
tc_args = tc.respond_to?(:arguments) ? tc.arguments : (tc[:arguments] || tc["arguments"] || {})
|
|
597
|
+
{ id: tc_id, name: tc_name, arguments: tc_args }
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
hash
|
|
601
|
+
when :tool
|
|
602
|
+
{ role: "tool", results: msg[:results] }
|
|
603
|
+
else
|
|
604
|
+
{ role: msg[:role].to_s, content: msg[:content] }
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def request_to_hash(request)
|
|
609
|
+
{
|
|
610
|
+
id: request.id,
|
|
611
|
+
po_name: request.capability, # capability is the PO name
|
|
612
|
+
type: "ask_human", # hardcode type since it's always ask_human
|
|
613
|
+
message: request.question, # question is the message
|
|
614
|
+
options: request.options || []
|
|
615
|
+
}
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|