prompt_objects 0.5.0 → 0.6.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/CLAUDE.md +7 -6
  4. data/README.md +140 -19
  5. data/frontend/src/App.tsx +2 -1
  6. data/frontend/src/components/EnvDataPane.tsx +69 -0
  7. data/frontend/src/components/Inspector.tsx +32 -1
  8. data/frontend/src/hooks/useWebSocket.ts +42 -0
  9. data/frontend/src/index.css +2 -3
  10. data/frontend/src/store/index.ts +32 -0
  11. data/frontend/src/types/index.ts +11 -0
  12. data/lib/prompt_objects/environment.rb +15 -0
  13. data/lib/prompt_objects/prompt_object.rb +113 -7
  14. data/lib/prompt_objects/server/api/routes.rb +13 -0
  15. data/lib/prompt_objects/server/app.rb +14 -0
  16. data/lib/prompt_objects/server/public/assets/{index-D1myxE0l.js → index-DEPawnfZ.js} +209 -209
  17. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +1 -0
  18. data/lib/prompt_objects/server/public/index.html +2 -2
  19. data/lib/prompt_objects/server/websocket_handler.rb +20 -0
  20. data/lib/prompt_objects/session/store.rb +176 -4
  21. data/lib/prompt_objects/universal/delete_env_data.rb +70 -0
  22. data/lib/prompt_objects/universal/get_env_data.rb +64 -0
  23. data/lib/prompt_objects/universal/list_env_data.rb +61 -0
  24. data/lib/prompt_objects/universal/store_env_data.rb +87 -0
  25. data/lib/prompt_objects/universal/update_env_data.rb +88 -0
  26. data/lib/prompt_objects.rb +6 -1
  27. data/prompt_objects.gemspec +1 -1
  28. data/templates/arc-agi-1/objects/observer.md +4 -0
  29. data/templates/arc-agi-1/objects/solver.md +10 -1
  30. data/templates/arc-agi-1/objects/verifier.md +4 -0
  31. data/tools/thread-explorer.html +27 -0
  32. metadata +9 -6
  33. data/Gemfile.lock +0 -233
  34. data/IMPLEMENTATION_PLAN.md +0 -1073
  35. data/design-doc-v2.md +0 -1232
  36. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dd3c14942ef80d1979ded542802ad9d1a24b32cd479aa4ba0a08bb0bf2e349d
4
- data.tar.gz: 3642d5424aa827754dfefde82a9e9e5c770935ebf08e36cfc429d8f86e5e8502
3
+ metadata.gz: 1e9bb13a5f2b407279723343fe43ce570119e64a3fe2a115e4f427d195ec7040
4
+ data.tar.gz: fc1300754c1774ec388928752b84dd8a60286659904b8b72053fde457842ea71
5
5
  SHA512:
6
- metadata.gz: f4e4449d2a8764a762c13ea14b6ccac17b07d1ae06a053a94e5b66ce7ebf38b8ca2903c140f0bb06810e09fa830bdac1dcfb8485d2f6f7fe32f7b0dffdf374bb
7
- data.tar.gz: 1faa31a8c32f067cdfb133c06e6c3d45e55fec3381d86f649e8845b9d2c58bc367e2bed3821cbec4a92a18776b723ccf69c276f9e913347198816005c4608353
6
+ metadata.gz: 56e4a427b43b49fc7520f9268a3e31c4a81d39c50ad0329985ab9d678631a2a88c41d070a7e13d97c01e2328de56e917d59b2b06c078fef9174a5a7fdc5db87b
7
+ data.tar.gz: a6bb7914d1b58c8c445deae870c2a06c004b9e7604b72107574f89c7ea9da2b8172034c358078b920d52b57eefe24acb68668d7057f16e61ed2e62ee2a85ca95
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to PromptObjects are documented in this file.
4
4
 
5
+ ## [0.6.0] - 2026-02-17
6
+
7
+ ### Added
8
+
9
+ - **Shared environment data** — 5 new universal capabilities (`store_env_data`, `get_env_data`, `list_env_data`, `update_env_data`, `delete_env_data`) provide a thread-scoped key-value store for delegation chains. Data is scoped to the root thread so separate conversations stay isolated. Entries include a `short_description` for lightweight LLM discovery without fetching full values.
10
+ - **Live environment data pane** — New collapsible pane in the Inspector shows shared env data updating in real-time as POs store and modify entries during delegation chains. WebSocket broadcasting (`env_data_changed`, `env_data_list`), a REST endpoint (`GET /api/sessions/:id/env_data`), and env data rendering in the Thread Explorer.
11
+ - **Delegation context** — POs now receive context about their delegation chain. An expanded system prompt teaches POs about their nature, a delegation preamble prepends caller context to delegated messages, and the full delegation chain is built from thread lineage.
12
+ - **Capability guard** — `execute_tool_calls` now rejects tools not in a PO's allowed set (declared capabilities + universals). Previously the LLM could hallucinate calls to any registered tool and they would execute. Now it receives an error directing it to use `add_capability` first.
13
+ - **Env data in thread exports** — `serialize_tree_for_export` includes env data entries at the root level of exported thread trees. Thread Explorer renders these in an amber-colored section.
14
+
15
+ ### Fixed
16
+
17
+ - **Root font-size causing undersized text** — Removed a `font-size: 14px` on the root `html` element that made all rem-based Tailwind sizes ~12% smaller than intended (e.g. `text-xs` computed to 10.5px instead of 12px).
18
+ - **Invisible resize handle boundaries** — Added border styling to horizontal and vertical resize handles so pane boundaries are visible without hovering.
19
+ - **Stale MCP tools tests** — Fixed test expectations broken by PO serialization centralization in 0.5.0.
20
+
5
21
  ## [0.5.0] - 2026-02-13
6
22
 
7
23
  ### Added
data/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  **PromptObjects** is a Ruby framework where markdown files with LLM-backed behavior act as first-class autonomous entities. The core insight: **everything is a capability**—primitives (Ruby code) and Prompt-Objects (markdown files) share the same interface, differing only in interpretation complexity.
8
8
 
9
- **Current Status**: v0.5.0 — The core framework is fully implemented and functional. The original 6-phase implementation plan is complete. Active development is focused on visualization, developer experience, and exploring new primitives. See `CHANGELOG.md` for release history and `design-doc-v2.md` / `IMPLEMENTATION_PLAN.md` for original design context.
9
+ **Current Status**: v0.5.0 — The core framework is fully implemented and functional. The original 6-phase implementation plan is complete. Active development is focused on visualization, developer experience, and exploring new primitives. See `CHANGELOG.md` for release history and `docs/archive/` for original design context.
10
10
 
11
11
  ## Architecture
12
12
 
@@ -19,7 +19,7 @@ RUNTIME (Environment)
19
19
  ├── MESSAGE BUS - routes messages, logs to SQLite for replay
20
20
  ├── SESSION STORE (SQLite) - persistent conversation threads, delegation tracking
21
21
  ├── HUMAN QUEUE - non-blocking ask_human requests
22
- ├── WEB SERVER (Sinatra + WebSocket) - serves React frontend
22
+ ├── WEB SERVER (Falcon + async-websocket) - serves React frontend
23
23
  └── MCP SERVER - exposes POs as tools via Model Context Protocol
24
24
  ```
25
25
 
@@ -61,6 +61,7 @@ Available to all Prompt-Objects automatically (no frontmatter declaration needed
61
61
  - `list_capabilities` / `list_primitives` - introspection
62
62
  - `create_primitive` / `add_primitive` / `delete_primitive` / `verify_primitive` / `modify_primitive` / `request_primitive` - primitive management
63
63
  - `modify_prompt` - rewrite own system prompt at runtime
64
+ - `store_env_data` / `get_env_data` / `list_env_data` / `update_env_data` / `delete_env_data` - thread-scoped shared key-value store for delegation chains
64
65
 
65
66
  ### PO-to-PO Delegation
66
67
  When a PO calls another PO, the system creates an isolated delegation thread in the target PO. The caller's context is tracked so messages show correct provenance. Delegation start/complete events are broadcast via WebSocket for real-time UI updates.
@@ -69,9 +70,9 @@ When a PO calls another PO, the system creates an isolated delegation thread in
69
70
 
70
71
  - **Ruby** (>= 3.2, tested through Ruby 4) - core implementation
71
72
  - **LLM APIs** - OpenAI, Anthropic, Gemini, Ollama, OpenRouter (adapter pattern via `LLM::Factory`)
72
- - **Sinatra** - web server for REST API and static file serving
73
- - **faye-websocket** - real-time WebSocket communication
74
- - **React + TypeScript** - web frontend (dashboard, chat, capabilities panel)
73
+ - **Falcon** - async HTTP server for REST API and static file serving
74
+ - **async-websocket** - real-time WebSocket communication
75
+ - **React + TypeScript** - web frontend (Smalltalk System Browser-inspired multi-pane UI)
75
76
  - **Three.js** - spatial canvas visualization (force-directed PO graph)
76
77
  - **SQLite** - session persistence and event log storage
77
78
  - **MCP** - Model Context Protocol server mode
@@ -121,7 +122,7 @@ prompt_objects/
121
122
  ├── frontend/ # React + TypeScript web UI
122
123
  │ └── src/
123
124
  │ ├── App.tsx
124
- │ ├── components/ # Dashboard, chat, capabilities panel
125
+ │ ├── components/ # SystemBar, ObjectList, Inspector, Workspace, Transcript
125
126
  │ ├── canvas/ # Three.js spatial visualization
126
127
  │ ├── hooks/ # WebSocket, state management hooks
127
128
  │ ├── store/ # Frontend state
data/README.md CHANGED
@@ -12,11 +12,6 @@ Prompt Objects applies this to AI. Instead of treating LLMs as external APIs you
12
12
 
13
13
  This is a new computing primitive: semantic late binding at runtime, where natural language becomes the interface between intelligent components.
14
14
 
15
- Blog Posts
16
- ==========
17
-
18
- - [What if we took message passing seriously?](https://worksonmymachine.ai/p/what-if-we-took-message-passing-seriously) — The origin story and motivation behind Prompt Objects.
19
-
20
15
  Who
21
16
  ===
22
17
 
@@ -32,8 +27,86 @@ A Ruby framework where:
32
27
  - **Markdown files** define autonomous entities (Prompt Objects)
33
28
  - **YAML frontmatter** declares capabilities and configuration
34
29
  - **Markdown body** becomes identity and behavior (the system prompt)
35
- - **Capabilities** are shared between primitives (Ruby) and Prompt Objects (markdown)
36
- - **Environments** isolate collections of objects with their own memory
30
+ - **Capabilities** are shared between primitives (Ruby) and Prompt Objects (markdown) -- everything is a capability
31
+ - **Environments** isolate collections of objects with their own memory, git history, and configuration
32
+ - **PO-to-PO delegation** lets objects call each other through isolated threads with full provenance tracking
33
+
34
+ ### Prompt Object structure
35
+
36
+ ```markdown
37
+ ---
38
+ name: reader
39
+ description: Helps people understand files
40
+ capabilities:
41
+ - read_file
42
+ - list_files
43
+ ---
44
+
45
+ # Reader
46
+ ## Identity
47
+ You are a careful, thoughtful file reader...
48
+ ```
49
+
50
+ ### Web UI
51
+
52
+ The web interface is modeled after the Smalltalk System Browser -- a multi-pane environment for inspecting and interacting with live objects:
53
+
54
+ - **ObjectList** -- permanent left pane listing all POs in the environment
55
+ - **Inspector** -- split into a MethodList (capabilities) and SourcePane (the prompt markdown or capability source)
56
+ - **Workspace** -- a REPL-style chat pane for sending messages to the selected PO
57
+ - **Transcript** -- bottom pane showing message bus events in real time
58
+
59
+ All panels are resizable. The inspector's top pane collapses so the Workspace can fill the full height when you just want to talk.
60
+
61
+ ### Spatial Canvas
62
+
63
+ Navigate to `/canvas` for a Three.js 2D visualization of your environment. POs appear as glowing hexagonal nodes in a force-directed layout. Tool calls show as diamonds. Message arcs animate with traveling particles. Delegation lights up the target PO with a cyan glow. Click any node to inspect it. Keyboard shortcuts: F to fit, Escape to deselect.
64
+
65
+ ### MCP Server mode
66
+
67
+ Run any environment as an MCP (Model Context Protocol) server for integration with Claude Desktop, Cursor, or any MCP-compatible client:
68
+
69
+ ```bash
70
+ prompt_objects serve my-assistant --mcp
71
+ ```
72
+
73
+ Add to your `claude_desktop_config.json`:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "my-assistant": {
79
+ "command": "prompt_objects",
80
+ "args": ["serve", "--mcp", "my-assistant"]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Exposes tools for sending messages, listing POs, inspecting state, getting conversations, and responding to pending `ask_human` requests.
87
+
88
+ ### Multi-provider LLM support
89
+
90
+ Swap providers with an environment variable. Adapters for:
91
+
92
+ - **OpenAI** -- GPT-5.2, GPT-4.1, o3-mini, o1
93
+ - **Anthropic** -- Claude Haiku 4.5, Claude Sonnet 4.5, Claude Opus 4
94
+ - **Gemini** -- Gemini 3 Flash, Gemini 2.5 Pro, Gemini 2.5 Flash
95
+ - **Ollama** -- any locally installed model, discovered automatically
96
+ - **OpenRouter** -- access any model through a single API key
97
+
98
+ ### Thread Explorer
99
+
100
+ A standalone HTML visualizer for exported conversation threads. Open it from the CLI to browse delegation chains, message flow, and tool call sequences:
101
+
102
+ ```bash
103
+ prompt_objects explore my-env
104
+ prompt_objects explore my-env --session abc123
105
+ ```
106
+
107
+ ### Note on the TUI
108
+
109
+ A terminal UI (built with Charm libraries) exists in the codebase but is deprioritized. The web UI is the primary interface for day-to-day use.
37
110
 
38
111
  How
39
112
  ===
@@ -54,25 +127,73 @@ prompt_objects env create my-project --template basic
54
127
  prompt_objects serve my-project --open
55
128
  ```
56
129
 
57
- ### Environment Commands
130
+ ### Commands
58
131
 
59
- ```bash
60
- prompt_objects env list # List all environments
61
- prompt_objects env create <name> # Create new environment
62
- prompt_objects env info <name> # Show environment details
63
- prompt_objects env clone <src> <dst> # Clone an environment
132
+ ```
133
+ prompt_objects serve <env> Start web server (default)
134
+ prompt_objects serve <env> --open Start and open browser
135
+ prompt_objects serve <env> --mcp Start as MCP server
136
+ prompt_objects serve <env> --port 4000 Custom port (default: 3000)
137
+
138
+ prompt_objects repl [name] [objects_dir] Interactive REPL with a prompt object
139
+ prompt_objects repl --sandbox REPL in sandbox mode (isolates changes)
140
+
141
+ prompt_objects message <env> <po> "text" Send a message and print the response
142
+ prompt_objects message <env> <po> "text" --json JSON output
143
+ prompt_objects message <env> <po> "text" --events Include event log
144
+
145
+ prompt_objects events <env> Show recent message bus events
146
+ prompt_objects events <env> --session ID Events for a specific session
147
+ prompt_objects events <env> --json JSON output
148
+
149
+ prompt_objects explore [env] Open Thread Explorer in browser
150
+ prompt_objects explore <env> --session ID Visualize a specific thread
151
+ ```
152
+
153
+ ### Environment management
154
+
155
+ ```
156
+ prompt_objects env list List all environments
157
+ prompt_objects env create <name> Create new environment
158
+ prompt_objects env create <name> -t <tmpl> Create from template
159
+ prompt_objects env info <name> Show environment details
160
+ prompt_objects env clone <src> <dst> Clone an environment
161
+ prompt_objects env export <name> Export as .poenv bundle
162
+ prompt_objects env import <file.poenv> Import from bundle
163
+ prompt_objects env archive <name> Soft-delete (archive)
164
+ prompt_objects env restore <name> Restore archived environment
165
+ prompt_objects env delete <name> --permanent Permanently delete archived env
166
+ prompt_objects env default <name> Set the default environment
64
167
  ```
65
168
 
66
169
  ### Templates
67
170
 
68
- - `basic` - No capabilities, learns as needed (great for demos)
69
- - `minimal` - Basic assistant with file reading
70
- - `developer` - Code review, debugging, testing specialists
71
- - `writer` - Editor, researcher for content creation
171
+ Create an environment from a template with `prompt_objects env create <name> --template <template>`:
172
+
173
+ | Template | Description |
174
+ |---|---|
175
+ | `basic` | No capabilities, learns as needed -- great for demos |
176
+ | `minimal` | Basic assistant with file reading |
177
+ | `developer` | Code review, debugging, testing specialists |
178
+ | `writer` | Editor, researcher for content creation |
179
+ | `empty` | Start from scratch with just a bootstrap assistant |
180
+ | `arc-agi-1` | ARC-AGI-1 challenge solver with grid primitives |
72
181
 
73
182
  Extras
74
183
  ======
75
184
 
76
- - **License**: MIT
77
- - **Ruby**: >= 3.2.0
185
+ ### Community
186
+
187
+ Join the [Discord](https://discord.gg/fcMvcwdrZS) for support, discussion, and updates.
188
+
189
+ ### Blog Posts
190
+
191
+ - [What if we took message passing seriously?](https://worksonmymachine.ai/p/what-if-we-took-message-passing-seriously) -- the origin story and motivation behind Prompt Objects
192
+ - [As complexity grows, architecture](https://worksonmymachine.ai/p/as-complexity-grows-architecture) -- on why the hard problems are structural, not generative
193
+
194
+ ### Links
195
+
78
196
  - **Repository**: https://github.com/works-on-your-machine/prompt_objects
197
+ - **Changelog**: https://github.com/works-on-your-machine/prompt_objects/blob/main/CHANGELOG.md
198
+ - **License**: MIT
199
+ - **Ruby**: >= 3.2.0 (tested through Ruby 4)
data/frontend/src/App.tsx CHANGED
@@ -10,7 +10,7 @@ import { UsagePanel } from './components/UsagePanel'
10
10
  import { CanvasView } from './canvas/CanvasView'
11
11
 
12
12
  export default function App() {
13
- const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt, requestUsage, exportThread } =
13
+ const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt, requestUsage, exportThread, requestEnvData } =
14
14
  useWebSocket()
15
15
  const { selectedPO, busOpen, notifications, usageData, clearUsageData, currentView } = useStore()
16
16
  const selectedPOData = useSelectedPO()
@@ -64,6 +64,7 @@ export default function App() {
64
64
  updatePrompt={updatePrompt}
65
65
  requestUsage={requestUsage}
66
66
  exportThread={exportThread}
67
+ requestEnvData={requestEnvData}
67
68
  />
68
69
  ) : (
69
70
  <div className="h-full flex items-center justify-center text-po-text-ghost">
@@ -0,0 +1,69 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useStore, useEnvData } from '../store'
3
+ import type { EnvDataEntry } from '../types'
4
+
5
+ interface EnvDataPaneProps {
6
+ sessionId: string | undefined
7
+ requestEnvData: (sessionId: string) => void
8
+ }
9
+
10
+ export function EnvDataPane({ sessionId, requestEnvData }: EnvDataPaneProps) {
11
+ const sessionRootMap = useStore((s) => s.sessionRootMap)
12
+ const rootThreadId = sessionId ? sessionRootMap[sessionId] : undefined
13
+ const entries = useEnvData(rootThreadId)
14
+ const [expandedKey, setExpandedKey] = useState<string | null>(null)
15
+
16
+ useEffect(() => {
17
+ if (sessionId) {
18
+ requestEnvData(sessionId)
19
+ }
20
+ }, [sessionId, requestEnvData])
21
+
22
+ if (entries.length === 0) {
23
+ return (
24
+ <div className="h-full flex items-center justify-center">
25
+ <span className="font-mono text-xs text-po-text-ghost">No shared data</span>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <div className="h-full overflow-auto px-2 py-1">
32
+ {entries.map((entry) => (
33
+ <EnvDataRow
34
+ key={entry.key}
35
+ entry={entry}
36
+ expanded={expandedKey === entry.key}
37
+ onToggle={() => setExpandedKey(expandedKey === entry.key ? null : entry.key)}
38
+ />
39
+ ))}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ function EnvDataRow({ entry, expanded, onToggle }: { entry: EnvDataEntry; expanded: boolean; onToggle: () => void }) {
45
+ return (
46
+ <div className="border-b border-po-border last:border-b-0">
47
+ <button
48
+ onClick={onToggle}
49
+ className="w-full text-left px-1.5 py-1.5 hover:bg-po-surface-3 transition-colors duration-150 flex items-center gap-2"
50
+ >
51
+ <span className="text-2xs text-po-text-ghost">{expanded ? '▼' : '▶'}</span>
52
+ <span className="font-mono text-sm text-po-accent truncate">{entry.key}</span>
53
+ <span className="text-xs text-po-text-ghost truncate flex-1">{entry.short_description}</span>
54
+ <span className="text-xs text-po-text-ghost flex-shrink-0">{entry.stored_by}</span>
55
+ </button>
56
+ {expanded && (
57
+ <div className="px-2 pb-2">
58
+ <div className="text-xs text-po-text-ghost mb-1">
59
+ stored by <span className="text-po-text-secondary">{entry.stored_by}</span>
60
+ {entry.updated_at && <> &middot; {new Date(entry.updated_at).toLocaleTimeString()}</>}
61
+ </div>
62
+ <pre className="font-mono text-xs text-po-text-primary bg-po-surface-1 rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all">
63
+ {typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value, null, 2)}
64
+ </pre>
65
+ </div>
66
+ )}
67
+ </div>
68
+ )
69
+ }
@@ -1,8 +1,9 @@
1
1
  import { useState, useMemo } from 'react'
2
- import { useStore, usePONotifications } from '../store'
2
+ import { useStore, usePONotifications, useEnvData } from '../store'
3
3
  import { useResize } from '../hooks/useResize'
4
4
  import { MethodList } from './MethodList'
5
5
  import { SourcePane } from './SourcePane'
6
+ import { EnvDataPane } from './EnvDataPane'
6
7
  import { Workspace } from './Workspace'
7
8
  import { ContextMenu } from './ContextMenu'
8
9
  import { PaneSlot } from './PaneSlot'
@@ -17,6 +18,7 @@ interface InspectorProps {
17
18
  updatePrompt: (target: string, prompt: string) => void
18
19
  requestUsage?: (sessionId: string, includeTree?: boolean) => void
19
20
  exportThread?: (sessionId: string, format?: string) => void
21
+ requestEnvData: (sessionId: string) => void
20
22
  }
21
23
 
22
24
  export function Inspector({
@@ -27,6 +29,7 @@ export function Inspector({
27
29
  updatePrompt,
28
30
  requestUsage,
29
31
  exportThread,
32
+ requestEnvData,
30
33
  }: InspectorProps) {
31
34
  const [selectedCapability, setSelectedCapability] = useState<CapabilityInfo | null>(null)
32
35
  const [threadMenuOpen, setThreadMenuOpen] = useState(false)
@@ -34,6 +37,8 @@ export function Inspector({
34
37
  const notifications = usePONotifications(po.name)
35
38
  const topPaneCollapsed = useStore((s) => s.topPaneCollapsed)
36
39
  const toggleTopPane = useStore((s) => s.toggleTopPane)
40
+ const envDataPaneCollapsed = useStore((s) => s.envDataPaneCollapsed)
41
+ const toggleEnvDataPane = useStore((s) => s.toggleEnvDataPane)
37
42
 
38
43
  const topPaneResize = useResize({
39
44
  direction: 'vertical',
@@ -42,6 +47,13 @@ export function Inspector({
42
47
  maxSize: 600,
43
48
  })
44
49
 
50
+ const envDataResize = useResize({
51
+ direction: 'vertical',
52
+ initialSize: 160,
53
+ minSize: 80,
54
+ maxSize: 400,
55
+ })
56
+
45
57
  const methodListResize = useResize({
46
58
  direction: 'horizontal',
47
59
  initialSize: 192,
@@ -51,6 +63,9 @@ export function Inspector({
51
63
 
52
64
  const sessions = po.sessions || []
53
65
  const currentSessionId = po.current_session?.id
66
+ const sessionRootMap = useStore((s) => s.sessionRootMap)
67
+ const rootThreadId = currentSessionId ? sessionRootMap[currentSessionId] : undefined
68
+ const envDataEntries = useEnvData(rootThreadId)
54
69
 
55
70
  // Sort sessions: current first, then by updated_at desc
56
71
  const sortedSessions = useMemo(() => {
@@ -204,6 +219,22 @@ export function Inspector({
204
219
  </div>
205
220
  </PaneSlot>
206
221
 
222
+ {/* Middle: Env Data (collapsible, resizable height) */}
223
+ <PaneSlot
224
+ label={`Env Data${envDataEntries.length > 0 ? ` (${envDataEntries.length})` : ''}`}
225
+ collapsed={envDataPaneCollapsed}
226
+ onToggle={toggleEnvDataPane}
227
+ height={envDataResize.size}
228
+ resizeHandle={
229
+ <div
230
+ className="resize-handle-h"
231
+ onMouseDown={envDataResize.onMouseDown}
232
+ />
233
+ }
234
+ >
235
+ <EnvDataPane sessionId={currentSessionId} requestEnvData={requestEnvData} />
236
+ </PaneSlot>
237
+
207
238
  {/* Bottom: Workspace */}
208
239
  <div className="flex-1 overflow-hidden">
209
240
  <Workspace po={po} sendMessage={sendMessage} />
@@ -8,6 +8,7 @@ import type {
8
8
  Environment,
9
9
  Message,
10
10
  LLMConfig,
11
+ EnvDataEntry,
11
12
  SendMessagePayload,
12
13
  RespondToNotificationPayload,
13
14
  CreateSessionPayload,
@@ -43,6 +44,8 @@ export function useWebSocket() {
43
44
  setLLMConfig,
44
45
  updateCurrentLLM,
45
46
  setUsageData,
47
+ setEnvData,
48
+ setSessionRoot,
46
49
  } = useStore()
47
50
 
48
51
  // Keep the handler ref up to date every render
@@ -243,6 +246,29 @@ export function useWebSocket() {
243
246
  // Session switch confirmed — po_state update follows with full state
244
247
  break
245
248
 
249
+ case 'env_data_changed': {
250
+ const { root_thread_id, entries } = message.payload as {
251
+ action: string
252
+ root_thread_id: string
253
+ key: string
254
+ stored_by: string
255
+ entries: EnvDataEntry[]
256
+ }
257
+ setEnvData(root_thread_id, entries)
258
+ break
259
+ }
260
+
261
+ case 'env_data_list': {
262
+ const { session_id, root_thread_id, entries } = message.payload as {
263
+ session_id: string
264
+ root_thread_id: string
265
+ entries: EnvDataEntry[]
266
+ }
267
+ setEnvData(root_thread_id, entries)
268
+ setSessionRoot(session_id, root_thread_id)
269
+ break
270
+ }
271
+
246
272
  case 'error': {
247
273
  const { message: errorMsg } = message.payload as { message: string }
248
274
  console.error('Server error:', errorMsg)
@@ -488,6 +514,21 @@ export function useWebSocket() {
488
514
  )
489
515
  }, [])
490
516
 
517
+ // Request env data for a session
518
+ const requestEnvData = useCallback((sessionId: string) => {
519
+ if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
520
+ console.error('WebSocket not connected')
521
+ return
522
+ }
523
+
524
+ ws.current.send(
525
+ JSON.stringify({
526
+ type: 'get_env_data_list',
527
+ payload: { session_id: sessionId },
528
+ })
529
+ )
530
+ }, [])
531
+
491
532
  // Update a PO's prompt (markdown body)
492
533
  const updatePrompt = useCallback((target: string, prompt: string) => {
493
534
  if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
@@ -513,5 +554,6 @@ export function useWebSocket() {
513
554
  updatePrompt,
514
555
  requestUsage,
515
556
  exportThread,
557
+ requestEnvData,
516
558
  }
517
559
  }
@@ -5,7 +5,6 @@
5
5
  /* Base styles */
6
6
  html {
7
7
  color-scheme: dark;
8
- font-size: 14px;
9
8
  }
10
9
 
11
10
  body {
@@ -45,11 +44,11 @@ body {
45
44
 
46
45
  /* Resize handles */
47
46
  .resize-handle {
48
- @apply w-1 cursor-col-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors;
47
+ @apply w-1 cursor-col-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors border-l border-po-border;
49
48
  }
50
49
 
51
50
  .resize-handle-h {
52
- @apply h-1 cursor-row-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors;
51
+ @apply h-1 cursor-row-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors border-t border-po-border;
53
52
  }
54
53
 
55
54
  /* Canvas CSS2DRenderer labels */
@@ -7,6 +7,7 @@ import type {
7
7
  Environment,
8
8
  Message,
9
9
  LLMConfig,
10
+ EnvDataEntry,
10
11
  } from '../types'
11
12
 
12
13
  interface Store {
@@ -68,6 +69,15 @@ interface Store {
68
69
  usageData: Record<string, unknown> | null
69
70
  setUsageData: (data: Record<string, unknown>) => void
70
71
  clearUsageData: () => void
72
+
73
+ // Env Data
74
+ envData: Record<string, EnvDataEntry[]>
75
+ setEnvData: (rootThreadId: string, entries: EnvDataEntry[]) => void
76
+ clearEnvData: (rootThreadId: string) => void
77
+ sessionRootMap: Record<string, string>
78
+ setSessionRoot: (sessionId: string, rootThreadId: string) => void
79
+ envDataPaneCollapsed: boolean
80
+ toggleEnvDataPane: () => void
71
81
  }
72
82
 
73
83
  export const useStore = create<Store>((set) => ({
@@ -263,6 +273,25 @@ export const useStore = create<Store>((set) => ({
263
273
  usageData: null,
264
274
  setUsageData: (data) => set({ usageData: data }),
265
275
  clearUsageData: () => set({ usageData: null }),
276
+
277
+ // Env Data
278
+ envData: {},
279
+ setEnvData: (rootThreadId, entries) =>
280
+ set((s) => ({
281
+ envData: { ...s.envData, [rootThreadId]: entries },
282
+ })),
283
+ clearEnvData: (rootThreadId) =>
284
+ set((s) => {
285
+ const { [rootThreadId]: _, ...rest } = s.envData
286
+ return { envData: rest }
287
+ }),
288
+ sessionRootMap: {},
289
+ setSessionRoot: (sessionId, rootThreadId) =>
290
+ set((s) => ({
291
+ sessionRootMap: { ...s.sessionRootMap, [sessionId]: rootThreadId },
292
+ })),
293
+ envDataPaneCollapsed: true,
294
+ toggleEnvDataPane: () => set((s) => ({ envDataPaneCollapsed: !s.envDataPaneCollapsed })),
266
295
  }))
267
296
 
268
297
  // Selectors - use useShallow to prevent infinite re-renders with derived arrays
@@ -277,3 +306,6 @@ export const useNotificationCount = () =>
277
306
 
278
307
  export const usePONotifications = (poName: string) =>
279
308
  useStore(useShallow((s) => s.notifications.filter((n) => n.po_name === poName)))
309
+
310
+ export const useEnvData = (rootThreadId: string | undefined) =>
311
+ useStore(useShallow((s) => (rootThreadId ? s.envData[rootThreadId] || [] : [])))
@@ -102,6 +102,15 @@ export interface LLMConfig {
102
102
  providers: LLMProvider[]
103
103
  }
104
104
 
105
+ export interface EnvDataEntry {
106
+ key: string
107
+ short_description: string
108
+ value: unknown
109
+ stored_by: string
110
+ created_at: string
111
+ updated_at: string
112
+ }
113
+
105
114
  // WebSocket message types
106
115
  export type WSMessageType =
107
116
  | 'environment'
@@ -128,6 +137,8 @@ export type WSMessageType =
128
137
  | 'thread_export'
129
138
  | 'prompt_updated'
130
139
  | 'llm_error'
140
+ | 'env_data_changed'
141
+ | 'env_data_list'
131
142
  | 'error'
132
143
  | 'pong'
133
144
 
@@ -42,6 +42,7 @@ module PromptObjects
42
42
  attr_accessor :on_po_registered # Callback for when a PO is registered
43
43
  attr_accessor :on_po_modified # Callback for when a PO is modified (capabilities changed, etc.)
44
44
  attr_accessor :on_delegation_event # Callback for PO-to-PO delegation start/complete
45
+ attr_accessor :on_env_data_changed # Callback for env data store/update/delete
45
46
 
46
47
  # Initialize from an environment path (with manifest) or objects directory.
47
48
  # @param env_path [String, nil] Path to environment directory (preferred)
@@ -223,6 +224,15 @@ module PromptObjects
223
224
  @on_delegation_event&.call(event_type, payload)
224
225
  end
225
226
 
227
+ # Notify that environment data has changed (stored, updated, or deleted).
228
+ # @param action [String] "store", "update", or "delete"
229
+ # @param root_thread_id [String] Root thread scope
230
+ # @param key [String] The data key that changed
231
+ # @param stored_by [String] PO name that made the change
232
+ def notify_env_data_changed(action:, root_thread_id:, key:, stored_by:)
233
+ @on_env_data_changed&.call(action: action, root_thread_id: root_thread_id, key: key, stored_by: stored_by)
234
+ end
235
+
226
236
  # Load a prompt object by name from the objects directory.
227
237
  # @param name [String] Name of the prompt object (without .md extension)
228
238
  # @return [PromptObject]
@@ -316,6 +326,11 @@ module PromptObjects
316
326
  @registry.register(Universal::ModifyPrimitive.new)
317
327
  @registry.register(Universal::RequestPrimitive.new)
318
328
  @registry.register(Universal::ModifyPrompt.new)
329
+ @registry.register(Universal::StoreEnvData.new)
330
+ @registry.register(Universal::GetEnvData.new)
331
+ @registry.register(Universal::ListEnvData.new)
332
+ @registry.register(Universal::UpdateEnvData.new)
333
+ @registry.register(Universal::DeleteEnvData.new)
319
334
  end
320
335
  end
321
336