prompt_objects 0.3.1 → 0.5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/CLAUDE.md +112 -44
  4. data/Gemfile.lock +31 -29
  5. data/README.md +5 -0
  6. data/frontend/index.html +5 -1
  7. data/frontend/package-lock.json +123 -0
  8. data/frontend/package.json +4 -0
  9. data/frontend/src/App.tsx +70 -71
  10. data/frontend/src/canvas/CanvasView.tsx +113 -0
  11. data/frontend/src/canvas/ForceLayout.ts +115 -0
  12. data/frontend/src/canvas/SceneManager.ts +587 -0
  13. data/frontend/src/canvas/canvasStore.ts +47 -0
  14. data/frontend/src/canvas/constants.ts +95 -0
  15. data/frontend/src/canvas/controls/CameraControls.ts +98 -0
  16. data/frontend/src/canvas/edges/MessageArc.ts +149 -0
  17. data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
  18. data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
  19. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
  20. data/frontend/src/canvas/nodes/PONode.ts +249 -0
  21. data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
  22. data/frontend/src/canvas/types.ts +24 -0
  23. data/frontend/src/components/ContextMenu.tsx +5 -4
  24. data/frontend/src/components/Inspector.tsx +232 -0
  25. data/frontend/src/components/MarkdownMessage.tsx +22 -20
  26. data/frontend/src/components/MethodList.tsx +90 -0
  27. data/frontend/src/components/ModelSelector.tsx +13 -14
  28. data/frontend/src/components/NotificationPanel.tsx +29 -33
  29. data/frontend/src/components/ObjectList.tsx +78 -0
  30. data/frontend/src/components/PaneSlot.tsx +30 -0
  31. data/frontend/src/components/SourcePane.tsx +202 -0
  32. data/frontend/src/components/SystemBar.tsx +74 -0
  33. data/frontend/src/components/Transcript.tsx +76 -0
  34. data/frontend/src/components/UsagePanel.tsx +27 -27
  35. data/frontend/src/components/Workspace.tsx +260 -0
  36. data/frontend/src/components/index.ts +10 -9
  37. data/frontend/src/hooks/useResize.ts +55 -0
  38. data/frontend/src/hooks/useWebSocket.ts +274 -189
  39. data/frontend/src/index.css +69 -3
  40. data/frontend/src/store/index.ts +23 -0
  41. data/frontend/src/types/index.ts +5 -0
  42. data/frontend/tailwind.config.js +28 -9
  43. data/lib/prompt_objects/capability.rb +23 -1
  44. data/lib/prompt_objects/connectors/mcp.rb +5 -22
  45. data/lib/prompt_objects/environment.rb +8 -0
  46. data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
  47. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
  48. data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
  49. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
  50. data/lib/prompt_objects/primitives/list_files.rb +1 -2
  51. data/lib/prompt_objects/prompt_object.rb +150 -6
  52. data/lib/prompt_objects/server/api/routes.rb +3 -48
  53. data/lib/prompt_objects/server/app.rb +9 -0
  54. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
  55. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
  56. data/lib/prompt_objects/server/public/index.html +7 -3
  57. data/lib/prompt_objects/server/websocket_handler.rb +23 -100
  58. data/lib/prompt_objects/server.rb +6 -62
  59. data/prompt_objects.gemspec +1 -1
  60. data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
  61. data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
  62. data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
  63. data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
  64. data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
  65. data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
  66. metadata +26 -14
  67. data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
  68. data/frontend/src/components/ChatPanel.tsx +0 -288
  69. data/frontend/src/components/Dashboard.tsx +0 -83
  70. data/frontend/src/components/Header.tsx +0 -141
  71. data/frontend/src/components/MessageBus.tsx +0 -56
  72. data/frontend/src/components/POCard.tsx +0 -56
  73. data/frontend/src/components/PODetail.tsx +0 -124
  74. data/frontend/src/components/PromptPanel.tsx +0 -156
  75. data/frontend/src/components/SessionsPanel.tsx +0 -174
  76. data/frontend/src/components/ThreadsSidebar.tsx +0 -163
  77. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
  78. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
@@ -10,16 +10,20 @@
10
10
  "dependencies": {
11
11
  "@types/react-syntax-highlighter": "^15.5.13",
12
12
  "clsx": "^2.1.1",
13
+ "d3-force": "^3.0.0",
13
14
  "react": "^18.3.1",
14
15
  "react-dom": "^18.3.1",
15
16
  "react-markdown": "^10.1.0",
16
17
  "react-syntax-highlighter": "^16.1.0",
17
18
  "remark-gfm": "^4.0.1",
19
+ "three": "^0.182.0",
18
20
  "zustand": "^5.0.0"
19
21
  },
20
22
  "devDependencies": {
23
+ "@types/d3-force": "^3.0.10",
21
24
  "@types/react": "^18.3.12",
22
25
  "@types/react-dom": "^18.3.1",
26
+ "@types/three": "^0.182.0",
23
27
  "@vitejs/plugin-react": "^4.3.4",
24
28
  "autoprefixer": "^10.4.20",
25
29
  "postcss": "^8.4.49",
@@ -332,6 +336,13 @@
332
336
  "node": ">=6.9.0"
333
337
  }
334
338
  },
339
+ "node_modules/@dimforge/rapier3d-compat": {
340
+ "version": "0.12.0",
341
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
342
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
343
+ "dev": true,
344
+ "license": "Apache-2.0"
345
+ },
335
346
  "node_modules/@esbuild/aix-ppc64": {
336
347
  "version": "0.25.12",
337
348
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1219,6 +1230,13 @@
1219
1230
  "win32"
1220
1231
  ]
1221
1232
  },
1233
+ "node_modules/@tweenjs/tween.js": {
1234
+ "version": "23.1.3",
1235
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
1236
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
1237
+ "dev": true,
1238
+ "license": "MIT"
1239
+ },
1222
1240
  "node_modules/@types/babel__core": {
1223
1241
  "version": "7.20.5",
1224
1242
  "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1264,6 +1282,13 @@
1264
1282
  "@babel/types": "^7.28.2"
1265
1283
  }
1266
1284
  },
1285
+ "node_modules/@types/d3-force": {
1286
+ "version": "3.0.10",
1287
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
1288
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
1289
+ "dev": true,
1290
+ "license": "MIT"
1291
+ },
1267
1292
  "node_modules/@types/debug": {
1268
1293
  "version": "4.1.12",
1269
1294
  "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -1353,12 +1378,42 @@
1353
1378
  "@types/react": "*"
1354
1379
  }
1355
1380
  },
1381
+ "node_modules/@types/stats.js": {
1382
+ "version": "0.17.4",
1383
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
1384
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
1385
+ "dev": true,
1386
+ "license": "MIT"
1387
+ },
1388
+ "node_modules/@types/three": {
1389
+ "version": "0.182.0",
1390
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
1391
+ "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
1392
+ "dev": true,
1393
+ "license": "MIT",
1394
+ "dependencies": {
1395
+ "@dimforge/rapier3d-compat": "~0.12.0",
1396
+ "@tweenjs/tween.js": "~23.1.3",
1397
+ "@types/stats.js": "*",
1398
+ "@types/webxr": ">=0.5.17",
1399
+ "@webgpu/types": "*",
1400
+ "fflate": "~0.8.2",
1401
+ "meshoptimizer": "~0.22.0"
1402
+ }
1403
+ },
1356
1404
  "node_modules/@types/unist": {
1357
1405
  "version": "3.0.3",
1358
1406
  "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1359
1407
  "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1360
1408
  "license": "MIT"
1361
1409
  },
1410
+ "node_modules/@types/webxr": {
1411
+ "version": "0.5.24",
1412
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
1413
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
1414
+ "dev": true,
1415
+ "license": "MIT"
1416
+ },
1362
1417
  "node_modules/@ungap/structured-clone": {
1363
1418
  "version": "1.3.0",
1364
1419
  "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -1386,6 +1441,13 @@
1386
1441
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1387
1442
  }
1388
1443
  },
1444
+ "node_modules/@webgpu/types": {
1445
+ "version": "0.1.69",
1446
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
1447
+ "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
1448
+ "dev": true,
1449
+ "license": "BSD-3-Clause"
1450
+ },
1389
1451
  "node_modules/any-promise": {
1390
1452
  "version": "1.3.0",
1391
1453
  "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -1705,6 +1767,47 @@
1705
1767
  "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1706
1768
  "license": "MIT"
1707
1769
  },
1770
+ "node_modules/d3-dispatch": {
1771
+ "version": "3.0.1",
1772
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
1773
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
1774
+ "license": "ISC",
1775
+ "engines": {
1776
+ "node": ">=12"
1777
+ }
1778
+ },
1779
+ "node_modules/d3-force": {
1780
+ "version": "3.0.0",
1781
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
1782
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
1783
+ "license": "ISC",
1784
+ "dependencies": {
1785
+ "d3-dispatch": "1 - 3",
1786
+ "d3-quadtree": "1 - 3",
1787
+ "d3-timer": "1 - 3"
1788
+ },
1789
+ "engines": {
1790
+ "node": ">=12"
1791
+ }
1792
+ },
1793
+ "node_modules/d3-quadtree": {
1794
+ "version": "3.0.1",
1795
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
1796
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
1797
+ "license": "ISC",
1798
+ "engines": {
1799
+ "node": ">=12"
1800
+ }
1801
+ },
1802
+ "node_modules/d3-timer": {
1803
+ "version": "3.0.1",
1804
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
1805
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
1806
+ "license": "ISC",
1807
+ "engines": {
1808
+ "node": ">=12"
1809
+ }
1810
+ },
1708
1811
  "node_modules/debug": {
1709
1812
  "version": "4.4.3",
1710
1813
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1911,6 +2014,13 @@
1911
2014
  "url": "https://github.com/sponsors/wooorm"
1912
2015
  }
1913
2016
  },
2017
+ "node_modules/fflate": {
2018
+ "version": "0.8.2",
2019
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
2020
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
2021
+ "dev": true,
2022
+ "license": "MIT"
2023
+ },
1914
2024
  "node_modules/fill-range": {
1915
2025
  "version": "7.1.1",
1916
2026
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -2624,6 +2734,13 @@
2624
2734
  "node": ">= 8"
2625
2735
  }
2626
2736
  },
2737
+ "node_modules/meshoptimizer": {
2738
+ "version": "0.22.0",
2739
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
2740
+ "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
2741
+ "dev": true,
2742
+ "license": "MIT"
2743
+ },
2627
2744
  "node_modules/micromark": {
2628
2745
  "version": "4.0.2",
2629
2746
  "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -4006,6 +4123,12 @@
4006
4123
  "node": ">=0.8"
4007
4124
  }
4008
4125
  },
4126
+ "node_modules/three": {
4127
+ "version": "0.182.0",
4128
+ "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
4129
+ "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
4130
+ "license": "MIT"
4131
+ },
4009
4132
  "node_modules/tinyglobby": {
4010
4133
  "version": "0.2.15",
4011
4134
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -12,16 +12,20 @@
12
12
  "dependencies": {
13
13
  "@types/react-syntax-highlighter": "^15.5.13",
14
14
  "clsx": "^2.1.1",
15
+ "d3-force": "^3.0.0",
15
16
  "react": "^18.3.1",
16
17
  "react-dom": "^18.3.1",
17
18
  "react-markdown": "^10.1.0",
18
19
  "react-syntax-highlighter": "^16.1.0",
19
20
  "remark-gfm": "^4.0.1",
21
+ "three": "^0.182.0",
20
22
  "zustand": "^5.0.0"
21
23
  },
22
24
  "devDependencies": {
25
+ "@types/d3-force": "^3.0.10",
23
26
  "@types/react": "^18.3.12",
24
27
  "@types/react-dom": "^18.3.1",
28
+ "@types/three": "^0.182.0",
25
29
  "@vitejs/plugin-react": "^4.3.4",
26
30
  "autoprefixer": "^10.4.20",
27
31
  "postcss": "^8.4.49",
data/frontend/src/App.tsx CHANGED
@@ -1,96 +1,95 @@
1
- import { useState } from 'react'
2
1
  import { useWebSocket } from './hooks/useWebSocket'
3
2
  import { useStore, useSelectedPO } from './store'
4
- import { Header } from './components/Header'
5
- import { Dashboard } from './components/Dashboard'
6
- import { PODetail } from './components/PODetail'
7
- import { MessageBus } from './components/MessageBus'
3
+ import { useResize } from './hooks/useResize'
4
+ import { SystemBar } from './components/SystemBar'
5
+ import { ObjectList } from './components/ObjectList'
6
+ import { Inspector } from './components/Inspector'
7
+ import { Transcript } from './components/Transcript'
8
8
  import { NotificationPanel } from './components/NotificationPanel'
9
- import { ThreadsSidebar } from './components/ThreadsSidebar'
10
9
  import { UsagePanel } from './components/UsagePanel'
10
+ import { CanvasView } from './canvas/CanvasView'
11
11
 
12
12
  export default function App() {
13
13
  const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt, requestUsage, exportThread } =
14
14
  useWebSocket()
15
- const { selectedPO, busOpen, notifications, usageData, clearUsageData } = useStore()
15
+ const { selectedPO, busOpen, notifications, usageData, clearUsageData, currentView } = useStore()
16
16
  const selectedPOData = useSelectedPO()
17
- const [splitView, setSplitView] = useState(true) // Default to split view
17
+
18
+ const objectListResize = useResize({
19
+ direction: 'horizontal',
20
+ initialSize: 192,
21
+ minSize: 120,
22
+ maxSize: 320,
23
+ })
24
+
25
+ const transcriptResize = useResize({
26
+ direction: 'vertical',
27
+ initialSize: 180,
28
+ minSize: 80,
29
+ maxSize: 400,
30
+ inverted: true,
31
+ })
18
32
 
19
33
  return (
20
34
  <div className="h-screen flex flex-col bg-po-bg">
21
- <Header switchLLM={switchLLM} />
35
+ <SystemBar switchLLM={switchLLM} />
22
36
 
23
- <div className="flex-1 flex overflow-hidden">
24
- {/* Split view: Dashboard sidebar on left when PO selected */}
25
- {splitView && selectedPO && (
26
- <>
27
- {/* PO List */}
28
- <aside className="w-56 border-r border-po-border bg-po-surface overflow-hidden flex flex-col">
29
- <div className="p-3 border-b border-po-border flex items-center justify-between">
30
- <h2 className="text-sm font-medium text-gray-400">Prompt Objects</h2>
31
- <button
32
- onClick={() => setSplitView(false)}
33
- className="text-xs text-gray-500 hover:text-white"
34
- title="Hide sidebar"
35
- >
36
-
37
- </button>
38
- </div>
39
- <div className="flex-1 overflow-auto">
40
- <Dashboard compact />
37
+ <div className="flex-1 flex flex-col overflow-hidden">
38
+ {/* Main view area */}
39
+ <div className="flex-1 flex overflow-hidden">
40
+ {currentView === 'canvas' ? (
41
+ <CanvasView />
42
+ ) : (
43
+ <>
44
+ {/* Object List - resizable */}
45
+ <div style={{ width: objectListResize.size }} className="flex-shrink-0">
46
+ <ObjectList />
41
47
  </div>
42
- </aside>
43
-
44
- {/* Threads List for selected PO */}
45
- {selectedPOData && (
46
- <aside className="w-56 border-r border-po-border bg-po-bg overflow-hidden">
47
- <ThreadsSidebar
48
- po={selectedPOData}
49
- switchSession={switchSession}
50
- createThread={createThread}
51
- requestUsage={requestUsage}
52
- exportThread={exportThread}
53
- />
54
- </aside>
55
- )}
56
- </>
57
- )}
58
48
 
59
- {/* Main content */}
60
- <main className="flex-1 overflow-hidden flex flex-col">
61
- {/* Show expand button when sidebar is hidden */}
62
- {!splitView && selectedPO && (
63
- <button
64
- onClick={() => setSplitView(true)}
65
- className="absolute left-2 top-16 z-10 bg-po-surface border border-po-border rounded px-2 py-1 text-xs text-gray-400 hover:text-white hover:border-po-accent transition-colors"
66
- title="Show dashboard sidebar"
67
- >
68
- ☰ POs
69
- </button>
70
- )}
49
+ {/* Resize handle */}
50
+ <div
51
+ className="resize-handle"
52
+ onMouseDown={objectListResize.onMouseDown}
53
+ />
71
54
 
72
- {selectedPO ? (
73
- <PODetail
74
- sendMessage={sendMessage}
75
- createSession={createSession}
76
- switchSession={switchSession}
77
- createThread={createThread}
78
- updatePrompt={updatePrompt}
79
- />
80
- ) : (
81
- <Dashboard />
55
+ {/* Main content */}
56
+ <main className="flex-1 overflow-hidden flex flex-col">
57
+ {selectedPO && selectedPOData ? (
58
+ <Inspector
59
+ po={selectedPOData}
60
+ sendMessage={sendMessage}
61
+ createSession={createSession}
62
+ switchSession={switchSession}
63
+ createThread={createThread}
64
+ updatePrompt={updatePrompt}
65
+ requestUsage={requestUsage}
66
+ exportThread={exportThread}
67
+ />
68
+ ) : (
69
+ <div className="h-full flex items-center justify-center text-po-text-ghost">
70
+ <span className="font-mono text-xs">Select an object</span>
71
+ </div>
72
+ )}
73
+ </main>
74
+ </>
82
75
  )}
83
- </main>
76
+ </div>
84
77
 
85
- {/* Message Bus sidebar */}
78
+ {/* Transcript - resizable bottom pane, visible in both views */}
86
79
  {busOpen && (
87
- <aside className="w-80 flex-shrink-0 border-l border-po-border bg-po-surface overflow-hidden">
88
- <MessageBus />
89
- </aside>
80
+ <>
81
+ <div
82
+ className="resize-handle-h"
83
+ onMouseDown={transcriptResize.onMouseDown}
84
+ />
85
+ <div style={{ height: transcriptResize.size }} className="flex-shrink-0">
86
+ <Transcript />
87
+ </div>
88
+ </>
90
89
  )}
91
90
  </div>
92
91
 
93
- {/* Notification panel */}
92
+ {/* Notification panel - floating */}
94
93
  {notifications.length > 0 && (
95
94
  <NotificationPanel respondToNotification={respondToNotification} />
96
95
  )}
@@ -0,0 +1,113 @@
1
+ import { useEffect, useRef, useCallback } from 'react'
2
+ import { useStore } from '../store'
3
+ import { useCanvasStore } from './canvasStore'
4
+ import { SceneManager } from './SceneManager'
5
+ import { InspectorPanel } from './inspector/InspectorPanel'
6
+
7
+ export function CanvasView() {
8
+ const containerRef = useRef<HTMLDivElement>(null)
9
+ const sceneRef = useRef<SceneManager | null>(null)
10
+ const syncScheduled = useRef(false)
11
+
12
+ // Schedule a throttled sync via requestAnimationFrame
13
+ const scheduleSync = useCallback(() => {
14
+ if (syncScheduled.current) return
15
+ syncScheduled.current = true
16
+ requestAnimationFrame(() => {
17
+ syncScheduled.current = false
18
+ const scene = sceneRef.current
19
+ if (!scene) return
20
+ const state = useStore.getState()
21
+ scene.syncPromptObjects(state.promptObjects)
22
+ scene.syncBusMessages(state.busMessages)
23
+ scene.syncNotifications(state.notifications)
24
+ })
25
+ }, [])
26
+
27
+ // Mount/unmount SceneManager
28
+ useEffect(() => {
29
+ const container = containerRef.current
30
+ if (!container) return
31
+
32
+ const scene = new SceneManager(container)
33
+ sceneRef.current = scene
34
+
35
+ // Initial sync
36
+ const state = useStore.getState()
37
+ scene.syncPromptObjects(state.promptObjects)
38
+ scene.syncBusMessages(state.busMessages)
39
+ scene.syncNotifications(state.notifications)
40
+ scene.start()
41
+
42
+ // Fit all after a short delay to let force layout settle
43
+ const fitTimer = setTimeout(() => scene.fitAll(), 500)
44
+
45
+ return () => {
46
+ clearTimeout(fitTimer)
47
+ scene.dispose()
48
+ sceneRef.current = null
49
+ }
50
+ }, [])
51
+
52
+ // Subscribe to store changes (non-React API to avoid re-renders)
53
+ useEffect(() => {
54
+ const unsub = useStore.subscribe(scheduleSync)
55
+ return unsub
56
+ }, [scheduleSync])
57
+
58
+ // Keyboard shortcuts
59
+ useEffect(() => {
60
+ const handler = (e: KeyboardEvent) => {
61
+ if (e.key === 'f' && !e.ctrlKey && !e.metaKey) {
62
+ // Don't intercept when typing in an input
63
+ if ((e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA') return
64
+ sceneRef.current?.fitAll()
65
+ }
66
+ if (e.key === 'Escape') {
67
+ useCanvasStore.getState().selectNode(null)
68
+ }
69
+ }
70
+ window.addEventListener('keydown', handler)
71
+ return () => window.removeEventListener('keydown', handler)
72
+ }, [])
73
+
74
+ const showLabels = useCanvasStore((s) => s.showLabels)
75
+ const toggleLabels = useCanvasStore((s) => s.toggleLabels)
76
+
77
+ return (
78
+ <div className="flex-1 flex overflow-hidden relative">
79
+ {/* Three.js container */}
80
+ <div ref={containerRef} className="flex-1 relative" />
81
+
82
+ {/* Toolbar overlay */}
83
+ <div className="absolute top-3 left-3 flex gap-2 z-10">
84
+ <button
85
+ onClick={() => sceneRef.current?.fitAll()}
86
+ className="px-2.5 py-1 text-xs bg-po-surface-2/80 backdrop-blur border border-po-border rounded hover:border-po-accent transition-colors duration-150 text-po-text-secondary hover:text-po-text-primary"
87
+ title="Fit all nodes (F)"
88
+ >
89
+ Fit All
90
+ </button>
91
+ <button
92
+ onClick={toggleLabels}
93
+ className={`px-2.5 py-1 text-xs backdrop-blur border rounded transition-colors duration-150 ${
94
+ showLabels
95
+ ? 'bg-po-accent-wash border-po-accent text-po-accent'
96
+ : 'bg-po-surface-2/80 border-po-border text-po-text-secondary hover:text-po-text-primary'
97
+ }`}
98
+ title="Toggle labels"
99
+ >
100
+ Labels
101
+ </button>
102
+ </div>
103
+
104
+ {/* Help hint */}
105
+ <div className="absolute bottom-3 left-3 text-2xs text-po-text-ghost z-10 font-mono">
106
+ Scroll to zoom · Shift+drag to pan · F to fit · Click node to inspect
107
+ </div>
108
+
109
+ {/* Inspector panel */}
110
+ <InspectorPanel />
111
+ </div>
112
+ )
113
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ forceSimulation,
3
+ forceManyBody,
4
+ forceCenter,
5
+ forceCollide,
6
+ forceLink,
7
+ type Simulation,
8
+ type SimulationNodeDatum,
9
+ type SimulationLinkDatum,
10
+ } from 'd3-force'
11
+ import { FORCE } from './constants'
12
+
13
+ export interface ForceNode extends SimulationNodeDatum {
14
+ id: string
15
+ type: 'po' | 'toolcall'
16
+ }
17
+
18
+ export interface ForceLink extends SimulationLinkDatum<ForceNode> {
19
+ id: string
20
+ }
21
+
22
+ export class ForceLayout {
23
+ private simulation: Simulation<ForceNode, ForceLink>
24
+ private nodes: ForceNode[] = []
25
+ private links: ForceLink[] = []
26
+ private dirty = false
27
+
28
+ constructor() {
29
+ this.simulation = forceSimulation<ForceNode, ForceLink>()
30
+ .alphaDecay(FORCE.alphaDecay)
31
+ .velocityDecay(FORCE.velocityDecay)
32
+ .force('charge', forceManyBody<ForceNode>().strength(FORCE.chargeStrength))
33
+ .force('center', forceCenter<ForceNode>(0, 0).strength(FORCE.centerStrength))
34
+ .force(
35
+ 'collision',
36
+ forceCollide<ForceNode>().radius(FORCE.collisionRadius)
37
+ )
38
+ .force(
39
+ 'link',
40
+ forceLink<ForceNode, ForceLink>()
41
+ .id((d) => d.id)
42
+ .distance(FORCE.linkDistance)
43
+ )
44
+ .stop() // Manual tick mode — we call tick() from animation loop
45
+ }
46
+
47
+ addNode(id: string, type: 'po' | 'toolcall'): void {
48
+ if (this.nodes.find((n) => n.id === id)) return
49
+ this.nodes.push({ id, type })
50
+ this.dirty = true
51
+ }
52
+
53
+ removeNode(id: string): void {
54
+ const idx = this.nodes.findIndex((n) => n.id === id)
55
+ if (idx === -1) return
56
+ this.nodes.splice(idx, 1)
57
+ // Also remove any links referencing this node
58
+ this.links = this.links.filter((l) => {
59
+ const src = typeof l.source === 'object' ? (l.source as ForceNode).id : l.source
60
+ const tgt = typeof l.target === 'object' ? (l.target as ForceNode).id : l.target
61
+ return src !== id && tgt !== id
62
+ })
63
+ this.dirty = true
64
+ }
65
+
66
+ addLink(id: string, sourceId: string, targetId: string): void {
67
+ if (this.links.find((l) => l.id === id)) return
68
+ this.links.push({ id, source: sourceId, target: targetId })
69
+ this.dirty = true
70
+ }
71
+
72
+ removeLink(id: string): void {
73
+ const idx = this.links.findIndex((l) => l.id === id)
74
+ if (idx === -1) return
75
+ this.links.splice(idx, 1)
76
+ this.dirty = true
77
+ }
78
+
79
+ tick(): void {
80
+ if (this.dirty) {
81
+ this.rebuild()
82
+ this.dirty = false
83
+ }
84
+ this.simulation.tick()
85
+ }
86
+
87
+ getPositions(): Map<string, { x: number; y: number }> {
88
+ const positions = new Map<string, { x: number; y: number }>()
89
+ for (const node of this.nodes) {
90
+ positions.set(node.id, { x: node.x ?? 0, y: node.y ?? 0 })
91
+ }
92
+ return positions
93
+ }
94
+
95
+ reheat(): void {
96
+ this.simulation.alpha(0.8).restart().stop()
97
+ }
98
+
99
+ private rebuild(): void {
100
+ this.simulation.nodes(this.nodes)
101
+ const linkForce = this.simulation.force('link') as ReturnType<
102
+ typeof forceLink<ForceNode, ForceLink>
103
+ >
104
+ if (linkForce) {
105
+ linkForce.links(this.links)
106
+ }
107
+ this.reheat()
108
+ }
109
+
110
+ dispose(): void {
111
+ this.simulation.stop()
112
+ this.nodes = []
113
+ this.links = []
114
+ }
115
+ }