@kiberon-labs/behave-graph-flow 2.0.0 → 3.0.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 (251) hide show
  1. package/.storybook/manager.ts +6 -0
  2. package/.storybook/preview.ts +49 -1
  3. package/.storybook/styles.css +9 -3
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +368 -0
  6. package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
  7. package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
  8. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
  9. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
  10. package/dist/index.css +36 -33
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.ts +1865 -550
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +14357 -11221
  15. package/dist/index.js.map +1 -1
  16. package/dist/noteImpl-KkrrWgJd.js +242 -0
  17. package/dist/noteImpl-KkrrWgJd.js.map +1 -0
  18. package/dist/styles.module-CvmpDkZj.css +3 -0
  19. package/dist/styles.module-CvmpDkZj.css.map +1 -0
  20. package/dist/styles.module-DZxg8aW9.js +271 -0
  21. package/dist/styles.module-DZxg8aW9.js.map +1 -0
  22. package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
  23. package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
  24. package/docs/protocol.md +43 -20
  25. package/package.json +5 -9
  26. package/src/components/FloatingToolbar/index.module.css +5 -13
  27. package/src/components/FloatingToolbar/index.tsx +9 -9
  28. package/src/components/Flow.tsx +34 -23
  29. package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
  30. package/src/components/contextMenus/NodePicker.module.css +13 -13
  31. package/src/components/contextMenus/edge.tsx +9 -95
  32. package/src/components/contextMenus/node.tsx +9 -149
  33. package/src/components/contextMenus/selection.tsx +5 -71
  34. package/src/components/controls/any/AnyControlImpl.tsx +14 -0
  35. package/src/components/controls/any/index.tsx +13 -2
  36. package/src/components/edges/index.tsx +75 -69
  37. package/src/components/layoutController/index.module.css +3 -0
  38. package/src/components/layoutController/index.tsx +24 -1
  39. package/src/components/layoutController/utils.ts +46 -3
  40. package/src/components/menubar/defaults.tsx +55 -19
  41. package/src/components/menubar/menuItem.module.css +18 -3
  42. package/src/components/menubar/menuItem.tsx +34 -1
  43. package/src/components/nodes/behave/NodeContainer.module.css +26 -25
  44. package/src/components/nodes/group/index.tsx +3 -3
  45. package/src/components/nodes/wrapper/styles.module.css +6 -32
  46. package/src/components/panels/alignment/index.module.css +0 -10
  47. package/src/components/panels/alignment/index.tsx +4 -4
  48. package/src/components/panels/base/styles.module.css +2 -2
  49. package/src/components/panels/common/PanelHeader.module.css +24 -0
  50. package/src/components/panels/common/PanelHeader.tsx +22 -0
  51. package/src/components/panels/common/SectionTitle.module.css +13 -0
  52. package/src/components/panels/common/SectionTitle.tsx +10 -0
  53. package/src/components/panels/events/EditEventPanel.tsx +14 -5
  54. package/src/components/panels/events/ManageEventsPanel.tsx +11 -8
  55. package/src/components/panels/events/styles.module.css +6 -64
  56. package/src/components/panels/graphProperties/index.tsx +125 -0
  57. package/src/components/panels/history/index.tsx +2 -2
  58. package/src/components/panels/history/styles.module.css +0 -9
  59. package/src/components/panels/keymaps/index.module.css +3 -13
  60. package/src/components/panels/keymaps/index.tsx +1 -2
  61. package/src/components/panels/layers/index.tsx +20 -15
  62. package/src/components/panels/layers/styles.module.css +9 -12
  63. package/src/components/panels/legend/index.tsx +1 -1
  64. package/src/components/panels/logs/index.module.css +25 -19
  65. package/src/components/panels/logs/index.tsx +7 -7
  66. package/src/components/panels/nodeInputs/InputsGroup.tsx +1 -0
  67. package/src/components/panels/nodeInputs/NodeSettings.tsx +2 -2
  68. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +1 -1
  69. package/src/components/panels/nodeInputs/OutputsGroup.tsx +2 -12
  70. package/src/components/panels/nodeInputs/index.module.css +99 -75
  71. package/src/components/panels/nodeInputs/index.tsx +21 -11
  72. package/src/components/panels/nodeInputs/useNodeHandlers.ts +2 -2
  73. package/src/components/panels/nodeInputs/useNodeInputsData.ts +23 -43
  74. package/src/components/panels/nodePicker/index.tsx +8 -8
  75. package/src/components/panels/panel/index.module.css +7 -7
  76. package/src/components/panels/search/index.module.css +0 -50
  77. package/src/components/panels/search/index.tsx +2 -2
  78. package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
  79. package/src/components/panels/systemSettings/index.tsx +221 -176
  80. package/src/components/panels/systemSettings/styles.module.css +135 -8
  81. package/src/components/panels/traces/GridLines.tsx +1 -1
  82. package/src/components/panels/traces/TimeGrid.tsx +3 -3
  83. package/src/components/panels/traces/TraceLane.tsx +1 -1
  84. package/src/components/panels/traces/index.module.css +1 -8
  85. package/src/components/panels/traces/index.tsx +8 -4
  86. package/src/components/panels/traces/useDerivedSpans.ts +241 -146
  87. package/src/components/panels/traces/utils.ts +8 -0
  88. package/src/components/panels/variables/CreateVariableScreen.tsx +3 -3
  89. package/src/components/panels/variables/ManageVariablesScreen.tsx +12 -9
  90. package/src/components/panels/variables/index.tsx +2 -2
  91. package/src/components/panels/variables/styles.module.css +4 -91
  92. package/src/components/primitives/icon.module.css +4 -4
  93. package/src/components/sockets/input/index.tsx +9 -2
  94. package/src/components/sockets/input/styles.module.css +2 -3
  95. package/src/components/sockets/output/index.tsx +10 -3
  96. package/src/components/sockets/output/styles.module.css +1 -6
  97. package/src/css/notes.css +135 -0
  98. package/src/css/prosemirror.css +3 -3
  99. package/src/css/rc-dock.css +143 -43
  100. package/src/css/rc-menu.css +56 -55
  101. package/src/css/themes/kiberon.css +127 -0
  102. package/src/css/vars.css +197 -13
  103. package/src/css/vscode-elements.css +124 -0
  104. package/src/generators/CallSubgraphGenerator.tsx +136 -0
  105. package/src/generators/CustomEventOnTriggeredGenerator.tsx +2 -2
  106. package/src/generators/GraphBoundaryGenerator.module.css +32 -0
  107. package/src/generators/GraphBoundaryGenerator.tsx +193 -0
  108. package/src/generators/SequenceGenerator.tsx +2 -2
  109. package/src/generators/SwitchOnIntegerGenerator.tsx +2 -2
  110. package/src/generators/SwitchOnStringGenerator.tsx +2 -2
  111. package/src/generators/callSubgraphSync.ts +126 -0
  112. package/src/generators/registerDefaultGenerators.ts +21 -0
  113. package/src/generators/registerDefaults.ts +26 -0
  114. package/src/hooks/useBehaveGraphFlow.ts +2 -2
  115. package/src/hooks/useFlowHandlers.ts +47 -9
  116. package/src/hooks/useWasdPan.ts +26 -4
  117. package/src/index.css +4 -16
  118. package/src/index.ts +17 -0
  119. package/src/manifest/contributionRegistry.ts +93 -0
  120. package/src/manifest/index.ts +4 -0
  121. package/src/manifest/loadManifest.ts +82 -0
  122. package/src/manifest/manifestPlugin.ts +29 -0
  123. package/src/manifest/passthroughValueType.ts +40 -0
  124. package/src/plugin/alignment/index.ts +22 -12
  125. package/src/plugin/autosave/controller.ts +366 -0
  126. package/src/plugin/autosave/index.tsx +114 -0
  127. package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
  128. package/src/plugin/autosave/panel/index.tsx +1 -0
  129. package/src/plugin/autosave/panel/styles.module.css +56 -0
  130. package/src/plugin/autosave/settings.ts +65 -0
  131. package/src/plugin/autosave/storage.ts +147 -0
  132. package/src/plugin/docs/index.tsx +2 -4
  133. package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
  134. package/src/plugin/docs/panel/index.tsx +15 -194
  135. package/src/plugin/docs/panel/styles.module.css +8 -8
  136. package/src/plugin/graphrunner/actions.ts +258 -185
  137. package/src/plugin/graphrunner/buttons.tsx +34 -26
  138. package/src/plugin/graphrunner/client.ts +4 -1
  139. package/src/plugin/graphrunner/index.tsx +29 -100
  140. package/src/plugin/graphrunner/panel.tsx +2 -2
  141. package/src/plugin/graphrunner/runController.ts +283 -0
  142. package/src/plugin/graphrunner/runner.ts +21 -192
  143. package/src/plugin/graphrunner/store.ts +14 -24
  144. package/src/plugin/graphrunner/styles.module.css +17 -57
  145. package/src/plugin/graphrunner/transport.ts +26 -0
  146. package/src/plugin/graphrunner/types.ts +21 -0
  147. package/src/plugin/graphrunner-local/execution-utils.ts +260 -80
  148. package/src/plugin/graphrunner-local/index.tsx +8 -2
  149. package/src/plugin/graphrunner-local/panel.tsx +131 -175
  150. package/src/plugin/graphrunner-local/styles.module.css +57 -76
  151. package/src/plugin/graphrunner-local/transport.ts +151 -184
  152. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +2 -0
  153. package/src/plugin/graphrunner-webworker/index.tsx +4 -10
  154. package/src/plugin/graphrunner-webworker/store.ts +9 -0
  155. package/src/plugin/kitchen-sink/index.ts +38 -0
  156. package/src/{layout/dagre.tsx → plugin/layout/dagre.ts} +17 -5
  157. package/src/{layout → plugin/layout}/elk.ts +22 -6
  158. package/src/plugin/layout/index.ts +80 -0
  159. package/src/plugin/notes/FormatToolbar.tsx +200 -0
  160. package/src/plugin/notes/index.tsx +191 -0
  161. package/src/plugin/notes/nodeActions.ts +100 -0
  162. package/src/plugin/notes/note.tsx +20 -0
  163. package/src/plugin/notes/noteImpl.tsx +89 -0
  164. package/src/plugin/realtime/realtimeRunner.ts +58 -4
  165. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +2 -2
  166. package/src/specifics/CustomEventTriggerSpecific.tsx +2 -2
  167. package/src/specifics/VariableGetSpecific.tsx +2 -2
  168. package/src/specifics/VariableSetSpecific.tsx +2 -2
  169. package/src/store/actions.tsx +5 -5
  170. package/src/store/commands.ts +278 -0
  171. package/src/store/contextMenu.ts +192 -0
  172. package/src/store/conversions.ts +47 -0
  173. package/src/store/flow.tsx +23 -38
  174. package/src/store/graphMeta.ts +39 -0
  175. package/src/store/hotKeys.tsx +301 -260
  176. package/src/store/layers.ts +3 -3
  177. package/src/store/registry.ts +12 -4
  178. package/src/store/selection.ts +3 -3
  179. package/src/store/settings.ts +82 -82
  180. package/src/store/settingsSchema.ts +210 -0
  181. package/src/store/tabs.ts +5 -1
  182. package/src/store/traces.ts +3 -3
  183. package/src/system/graph.ts +11 -14
  184. package/src/system/graphSession.ts +172 -0
  185. package/src/system/index.ts +3 -0
  186. package/src/system/notifications.ts +13 -0
  187. package/src/system/persistence.ts +82 -0
  188. package/src/system/plugin.ts +28 -0
  189. package/src/system/provider.tsx +64 -0
  190. package/src/system/system.ts +518 -88
  191. package/src/system/tabLoader.tsx +70 -32
  192. package/src/system/undoRedo.ts +1 -1
  193. package/src/transformers/Uigraph.ts +5 -4
  194. package/src/transformers/contract.ts +87 -0
  195. package/src/transformers/flowToBehave.ts +13 -5
  196. package/src/types/nodes.ts +8 -3
  197. package/src/types.ts +2 -0
  198. package/src/util/autoConvert.ts +200 -0
  199. package/src/util/isValidConnection.ts +23 -2
  200. package/stories/defaults/defaultStoryProvider.tsx +17 -14
  201. package/stories/defaults/systemGenerator.ts +6 -1
  202. package/stories/{components/nodes/comment.stories.tsx → plugins/notes.stories.tsx} +24 -30
  203. package/tests/autoConvert.test.ts +329 -0
  204. package/tests/autosavePlugin.test.ts +204 -0
  205. package/tests/callSubgraphSync.test.ts +148 -0
  206. package/tests/commandRegistry.test.ts +137 -0
  207. package/tests/contract.test.ts +51 -0
  208. package/tests/contractSerialize.test.ts +62 -0
  209. package/tests/deriveSpans.test.ts +71 -0
  210. package/tests/flowToBehave.test.ts +2 -1
  211. package/tests/hotkeys.test.ts +79 -0
  212. package/tests/keepAliveLifecycle.test.ts +167 -0
  213. package/tests/loadManifest.test.ts +113 -0
  214. package/tests/noteMarkdown.test.ts +65 -0
  215. package/tests/notesPlugin.test.ts +162 -0
  216. package/tests/persistence.test.ts +51 -0
  217. package/tests/saveLoad.test.ts +7 -6
  218. package/tests/settings.test.ts +178 -0
  219. package/tests/traceStore.test.ts +46 -0
  220. package/tests/visual/README.md +2 -2
  221. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  222. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  223. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  224. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  225. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  226. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  227. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
  228. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  229. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  230. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  231. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  232. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  233. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  234. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  235. package/tests/visual/panels.visual.test.tsx +3 -3
  236. package/tests/wasdPan.test.ts +71 -0
  237. package/vitest.config.ts +1 -1
  238. package/vitest.visual.config.ts +7 -0
  239. package/.storybook/vscode.css +0 -814
  240. package/src/components/nodes/comment/FormatToolbar.tsx +0 -118
  241. package/src/components/nodes/comment/comment.tsx +0 -103
  242. package/src/components/nodes/comment/styles.module.css +0 -150
  243. package/src/components/panels/conversation/index.module.css +0 -151
  244. package/src/components/panels/conversation/index.tsx +0 -162
  245. package/src/components/panels/events/CustomEventsEditor.tsx +0 -384
  246. package/src/css/vscode.css +0 -13
  247. package/src/hooks/useDetachNodes.ts +0 -39
  248. package/src/plugin/graphrunner-webworker/types.ts +0 -17
  249. package/src/specifics/registerDefaultSpecifics.ts +0 -5
  250. package/src/store/chat.ts +0 -73
  251. package/src/store/graphRunnerClient.ts +0 -110
@@ -1,11 +1,138 @@
1
- .title {
2
- font-size: 0.9em;
3
- font-weight: 600;
4
- color: var(--vscode-foreground);
1
+ /* Compact, VSCode-settings-like layout */
2
+
3
+ .searchRow {
4
+ padding-bottom: var(--space-2, 0.5rem);
5
+ }
6
+
7
+ .list {
8
+ display: flex;
9
+ flex-direction: column;
10
+ }
11
+
12
+ /* Label + reset affordance on one line (non-toggle rows) */
13
+ .labelRow {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: 0.5rem;
18
+ }
19
+
20
+ /* Small dot next to a label whose value differs from its default */
21
+ .modified {
22
+ display: inline-block;
23
+ width: 6px;
24
+ height: 6px;
25
+ margin-left: 0.4rem;
26
+ border-radius: 50%;
27
+ background: var(--ds-focus-border, #0078d4);
28
+ vertical-align: middle;
29
+ }
30
+
31
+ /* Ghost icon button to restore a setting's default */
32
+ .reset {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ flex: none;
37
+ padding: 0.15rem;
38
+ background: transparent;
39
+ border: none;
40
+ border-radius: var(--component-radii-sm, 4px);
41
+ color: var(--ds-fg-muted);
42
+ cursor: pointer;
43
+ }
44
+
45
+ .reset:hover {
46
+ background: var(--ds-toolbar-hover-bg, rgba(90, 93, 94, 0.31));
47
+ color: var(--ds-fg);
48
+ }
49
+
50
+ /* A single setting row */
51
+ .setting {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 0.15rem;
55
+ padding: var(--space-1, 0.25rem) 0;
56
+ }
57
+
58
+ /* Toggle settings put the control inline with the label */
59
+ .toggle {
60
+ flex-direction: row;
61
+ align-items: center;
62
+ gap: 0.5rem;
63
+ }
64
+
65
+ .toggle .body {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.1rem;
69
+ min-width: 0;
70
+ }
71
+
72
+ .label {
73
+ font-size: var(--fs-label, 0.8rem);
74
+ color: var(--ds-fg);
75
+ line-height: 1.3;
5
76
  }
6
77
 
7
78
  .description {
8
- font-size: 0.85em;
9
- color: var(--vscode-descriptionForeground);
10
- opacity: 0.9;
11
- }
79
+ font-size: var(--fs-desc, 0.72rem);
80
+ line-height: 1.35;
81
+ color: var(--ds-fg-muted);
82
+ }
83
+
84
+ .divider {
85
+ margin: 0.25rem 0;
86
+ }
87
+
88
+ /* Conversions editor */
89
+ .conversions {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.4rem;
93
+ margin-top: 0.35rem;
94
+ padding-left: 0.5rem;
95
+ border-left: 1px solid var(--ds-sash-border, var(--ds-border, #2b2b2b));
96
+ }
97
+
98
+ .conversionsLabel {
99
+ font-size: var(--fs-desc, 0.72rem);
100
+ color: var(--ds-fg-muted);
101
+ }
102
+
103
+ .empty {
104
+ font-size: var(--fs-desc, 0.72rem);
105
+ color: var(--ds-fg-muted);
106
+ opacity: 0.8;
107
+ }
108
+
109
+ .rule {
110
+ display: flex;
111
+ gap: 0.4rem;
112
+ align-items: center;
113
+ }
114
+
115
+ .ruleText {
116
+ flex: 1;
117
+ min-width: 0;
118
+ font-size: 0.75rem;
119
+ white-space: nowrap;
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ }
123
+
124
+ .ruleNode {
125
+ opacity: 0.65;
126
+ }
127
+
128
+ /* Stack the add-rule inputs vertically so nothing overflows the panel */
129
+ .addForm {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 0.3rem;
133
+ }
134
+
135
+ .addForm vscode-single-select,
136
+ .addForm vscode-button {
137
+ width: 100%;
138
+ }
@@ -6,7 +6,7 @@ const LINE_STYLE_BASE: React.CSSProperties = {
6
6
  top: 0,
7
7
  bottom: 0,
8
8
  width: 1,
9
- backgroundColor: 'var(--vscode-panel-border)',
9
+ backgroundColor: 'var(--ds-panel-border)',
10
10
  opacity: 0.3
11
11
  };
12
12
 
@@ -12,7 +12,7 @@ const TICK_STYLE: React.CSSProperties = {
12
12
 
13
13
  const LABEL_STYLE: React.CSSProperties = {
14
14
  fontSize: '10px',
15
- color: 'var(--vscode-descriptionForeground)',
15
+ color: 'var(--ds-fg-muted)',
16
16
  padding: '2px 4px',
17
17
  whiteSpace: 'nowrap'
18
18
  };
@@ -32,8 +32,8 @@ export const TimeGrid = memo(function TimeGrid({
32
32
  position: 'sticky',
33
33
  top: 0,
34
34
  height: 24,
35
- background: 'var(--vscode-editor-background)',
36
- borderBottom: '1px solid var(--vscode-panel-border)',
35
+ background: 'var(--ds-editor-bg)',
36
+ borderBottom: '1px solid var(--ds-panel-border)',
37
37
  zIndex: 10,
38
38
  padding: `0 ${padding}px`
39
39
  }}
@@ -13,7 +13,7 @@ type TraceLaneProps = {
13
13
  * Renders a single lane's spans.
14
14
  *
15
15
  * Event handling for hover/click is delegated to the parent scroll
16
- * container via `data-*` attributes no per-span closures needed.
16
+ * container via `data-*` attributes , no per-span closures needed.
17
17
  */
18
18
  export const TraceLane = memo(function TraceLane({
19
19
  laneData,
@@ -1,10 +1,3 @@
1
- .root {
2
- height: 100%;
3
- width: 100%;
4
- display: flex;
5
- flex-direction: column;
6
- }
7
-
8
1
  .header {
9
2
  display: flex;
10
3
  flex-direction: column;
@@ -109,7 +102,7 @@
109
102
  align-items: center;
110
103
  overflow: hidden;
111
104
  font-size: 11px;
112
- font-family: var(--vscode-font-family);
105
+ font-family: var(--ds-font-ui);
113
106
  color: rgba(255, 255, 255, 0.95);
114
107
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
115
108
  white-space: nowrap;
@@ -1,12 +1,12 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { useStore } from 'zustand';
3
- import { useSystem } from '@/system';
3
+ import { useActiveGraph } from '@/system';
4
4
  import type { TraceSpan } from '@/store/traces';
5
5
  import { VscodeButton, VscodeDivider } from '@vscode-elements/react-elements';
6
6
  import styles from './index.module.css';
7
7
  import { BasePanel } from '../base';
8
8
  import type { HoverInfo, ViewState } from './types';
9
- import { clamp } from './utils';
9
+ import { clamp, MAX_ZOOM_OUT_FACTOR } from './utils';
10
10
  import { useDerivedSpans } from './useDerivedSpans';
11
11
  import { TracesHeader } from './TracesHeader';
12
12
  import { TimeGrid } from './TimeGrid';
@@ -17,7 +17,7 @@ import { TraceTooltip } from './TraceTooltip';
17
17
  const PADDING = 8;
18
18
 
19
19
  export function TracesPanel() {
20
- const system = useSystem();
20
+ const system = useActiveGraph()!;
21
21
  const version = useStore(system.traceStore, (s) => s.version);
22
22
  const clear = useStore(system.traceStore, (s) => s.clear);
23
23
  const collector = system.traceStore.getState().collector;
@@ -161,7 +161,11 @@ export function TracesPanel() {
161
161
 
162
162
  const zoomFactor = Math.exp(e.deltaY * 0.0015);
163
163
  setView((v) => {
164
- const nextRange = clamp(v.range * zoomFactor, 1, derived.range);
164
+ const nextRange = clamp(
165
+ v.range * zoomFactor,
166
+ 1,
167
+ derived.range * MAX_ZOOM_OUT_FACTOR
168
+ );
165
169
  const k = x / Math.max(1, rect.width);
166
170
  const nextStart = clampViewStart(t - k * nextRange, nextRange);
167
171
  return {
@@ -7,11 +7,27 @@ import type {
7
7
  ViewState,
8
8
  VisualSpan
9
9
  } from './types';
10
- import { calculateTimeInterval, clamp, hashToHue } from './utils';
10
+ import {
11
+ calculateTimeInterval,
12
+ clamp,
13
+ hashToHue,
14
+ MAX_ZOOM_OUT_FACTOR
15
+ } from './utils';
11
16
 
12
17
  const MIN_WIDTH_PCT = 0.5;
13
18
  const OVERLAP_EPSILON_PCT = 0.5;
14
19
 
20
+ /**
21
+ * Minimum *visual* span length (ms). Instant nodes record start === end (0ms),
22
+ * which otherwise collapses the range (maxEnd <= minStart) and renders nothing.
23
+ * The true duration is still reported to the tooltip.
24
+ */
25
+ const MIN_SPAN_MS = 1;
26
+
27
+ /** Effective end used for layout: clamps instant/zero-length spans to MIN_SPAN_MS. */
28
+ const visualEnd = (start: number, rawEnd: number): number =>
29
+ Math.max(rawEnd, start + MIN_SPAN_MS);
30
+
15
31
  const EMPTY: DerivedData = {
16
32
  now: 0,
17
33
  size: 0,
@@ -30,7 +46,8 @@ const EMPTY: DerivedData = {
30
46
  function computeTicks(
31
47
  viewStart: number,
32
48
  viewEnd: number,
33
- viewRange: number
49
+ viewRange: number,
50
+ origin: number
34
51
  ): Tick[] {
35
52
  const interval = calculateTimeInterval(viewRange);
36
53
  const firstTick = Math.ceil(viewStart / interval) * interval;
@@ -38,7 +55,9 @@ function computeTicks(
38
55
  for (let t = firstTick; t <= viewEnd; t += interval) {
39
56
  const leftPct = ((t - viewStart) / viewRange) * 100;
40
57
  if (leftPct >= 0 && leftPct <= 100) {
41
- ticks.push({ time: t, leftPct });
58
+ // Position uses absolute time; the label is relative to the trace origin
59
+ // (first span) so it reads 0ms… instead of the raw clock value.
60
+ ticks.push({ time: t - origin, leftPct });
42
61
  }
43
62
  }
44
63
  return ticks;
@@ -50,163 +69,239 @@ export function useDerivedSpans(
50
69
  windowMs: number,
51
70
  view: ViewState
52
71
  ): DerivedData {
53
- return useMemo(() => {
54
- const now = performance.now();
55
- const { capacity, size, writeIndex, spans } = collector;
72
+ return useMemo(
73
+ () => computeDerivedSpans(collector, windowMs, view),
74
+ [collector, version, windowMs, view.follow, view.range, view.start]
75
+ );
76
+ }
56
77
 
57
- if (size <= 0) return { ...EMPTY, now };
78
+ /**
79
+ * Pure span → layout derivation used by {@link useDerivedSpans}. Exported so the
80
+ * layout math (lane assignment, min-span visibility, ticks, zoom clamping) can be
81
+ * unit-tested without a React render.
82
+ */
83
+ type TraceSpan = import('@/store/traces').TraceSpan;
58
84
 
59
- let minStart = Number.POSITIVE_INFINITY;
60
- let maxEnd = Number.NEGATIVE_INFINITY;
61
- let maxLane = -1;
85
+ interface SpanScan {
86
+ minStart: number;
87
+ maxEnd: number;
88
+ maxLane: number;
89
+ buckets: TraceSpan[][];
90
+ }
62
91
 
63
- const buckets: Array<import('@/store/traces').TraceSpan[]> = [];
92
+ /** Walk the ring buffer newest-window-first, bucketing spans by lane and
93
+ * tracking the overall time/lane bounds. */
94
+ function scanSpans(collector: SpanCollector, now: number): SpanScan {
95
+ const { capacity, size, writeIndex, spans } = collector;
96
+ let minStart = Number.POSITIVE_INFINITY;
97
+ let maxEnd = Number.NEGATIVE_INFINITY;
98
+ let maxLane = -1;
99
+ const buckets: TraceSpan[][] = [];
64
100
 
65
- const startIndex = (writeIndex - size + capacity) % capacity;
66
- for (let i = 0; i < size; i++) {
67
- const idx = (startIndex + i) % capacity;
68
- const s = spans[idx];
69
- if (!s) continue;
101
+ const startIndex = (writeIndex - size + capacity) % capacity;
102
+ for (let i = 0; i < size; i++) {
103
+ const idx = (startIndex + i) % capacity;
104
+ const s = spans[idx];
105
+ if (!s) continue;
70
106
 
71
- const end = Number.isNaN(s.end) ? now : s.end;
72
- minStart = Math.min(minStart, s.start);
73
- maxEnd = Math.max(maxEnd, end);
74
- maxLane = Math.max(maxLane, s.lane);
107
+ const rawEnd = Number.isNaN(s.end) ? now : s.end;
108
+ const end = visualEnd(s.start, rawEnd);
109
+ minStart = Math.min(minStart, s.start);
110
+ maxEnd = Math.max(maxEnd, end);
111
+ maxLane = Math.max(maxLane, s.lane);
75
112
 
76
- (buckets[s.lane] ??= []).push(s);
77
- }
113
+ (buckets[s.lane] ??= []).push(s);
114
+ }
78
115
 
79
- if (
80
- !Number.isFinite(minStart) ||
81
- !Number.isFinite(maxEnd) ||
82
- maxEnd <= minStart
83
- ) {
84
- return {
85
- ...EMPTY,
86
- now,
87
- size,
88
- lanes: maxLane + 1,
89
- buckets,
90
- laneData: []
91
- };
92
- }
116
+ return { minStart, maxEnd, maxLane, buckets };
117
+ }
93
118
 
94
- const fullRange = Math.max(1, maxEnd - minStart);
95
-
96
- const desiredRange = windowMs <= 0 ? fullRange : Math.max(1, windowMs);
97
- const desiredStart = Math.max(minStart, maxEnd - desiredRange);
98
-
99
- const effectiveRange = clamp(
100
- view.follow ? desiredRange : view.range,
101
- 1,
102
- fullRange
103
- );
104
- const effectiveStart = clamp(
105
- view.follow ? desiredStart : view.start,
106
- minStart,
107
- maxEnd - effectiveRange
108
- );
109
-
110
- const viewStart = effectiveStart;
111
- const viewEnd = viewStart + effectiveRange;
112
- const viewRange = effectiveRange;
113
-
114
- const laneData: LaneData[] = Array.from({ length: maxLane + 1 }, () => ({
115
- stackCount: 1,
116
- visualSpans: [] as VisualSpan[]
117
- }));
118
-
119
- for (let lane = 0; lane <= maxLane; lane++) {
120
- const laneSpans = buckets[lane];
121
- if (!laneSpans || laneSpans.length === 0) {
122
- laneData[lane] = { stackCount: 1, visualSpans: [] };
123
- continue;
124
- }
125
-
126
- const tmp: Array<{
127
- span: import('@/store/traces').TraceSpan;
128
- leftPct: number;
129
- widthPct: number;
130
- rightPct: number;
131
- durationMs: number;
132
- }> = [];
133
-
134
- for (const s of laneSpans) {
135
- const end = Number.isNaN(s.end) ? now : s.end;
136
- if (end < viewStart || s.start > viewEnd) continue;
137
-
138
- const visibleStart = Math.max(s.start, viewStart);
139
- const visibleEnd = Math.min(end, viewEnd);
140
- if (visibleEnd <= visibleStart) continue;
141
-
142
- const rawLeft = ((visibleStart - viewStart) / viewRange) * 100;
143
- const rawWidth = ((visibleEnd - visibleStart) / viewRange) * 100;
144
- const leftPct = clamp(rawLeft, 0, 100);
145
- const widthPct = clamp(
146
- Math.max(MIN_WIDTH_PCT, rawWidth),
147
- 0,
148
- 100 - leftPct
149
- );
150
- const rightPct = leftPct + widthPct;
151
- const durationMs = Math.max(0, end - s.start);
152
-
153
- tmp.push({ span: s, leftPct, widthPct, rightPct, durationMs });
154
- }
155
-
156
- tmp.sort((a, b) => a.leftPct - b.leftPct);
157
-
158
- const stackRight: number[] = [];
159
- const visualSpans: VisualSpan[] = [];
160
-
161
- for (const v of tmp) {
162
- let stack = 0;
163
- for (; stack < stackRight.length; stack++) {
164
- if (v.leftPct >= stackRight[stack]! + OVERLAP_EPSILON_PCT) break;
165
- }
166
- if (stack === stackRight.length) stackRight.push(v.rightPct);
167
- else stackRight[stack] = Math.max(stackRight[stack]!, v.rightPct);
168
-
169
- const hue = hashToHue(v.span.nodeId);
170
- const isOpen = Number.isNaN(v.span.end);
171
- const lightness = clamp(56 - stack * 7, 30, 60);
172
- const bg = isOpen
173
- ? `hsla(${hue}, 80%, ${clamp(lightness + 6, 30, 65)}%, 0.35)`
174
- : `hsla(${hue}, 80%, ${lightness}%, 0.6)`;
175
- const border = `hsla(${hue}, 90%, ${clamp(lightness + 22, 45, 80)}%, 0.95)`;
176
-
177
- visualSpans.push({
178
- span: v.span,
179
- leftPct: v.leftPct,
180
- widthPct: v.widthPct,
181
- rightPct: v.rightPct,
182
- durationMs: v.durationMs,
183
- stack,
184
- bg,
185
- border
186
- });
187
- }
188
-
189
- laneData[lane] = {
190
- stackCount: Math.max(1, stackRight.length),
191
- visualSpans
192
- };
193
- }
119
+ interface ViewWindow {
120
+ viewStart: number;
121
+ viewEnd: number;
122
+ viewRange: number;
123
+ }
124
+
125
+ /** Resolve the visible time window from the data bounds, the follow/zoom mode,
126
+ * and the requested window size. */
127
+ function resolveViewWindow(
128
+ scan: SpanScan,
129
+ fullRange: number,
130
+ windowMs: number,
131
+ view: ViewState
132
+ ): ViewWindow {
133
+ const { minStart, maxEnd } = scan;
134
+ const desiredRange = windowMs <= 0 ? fullRange : Math.max(1, windowMs);
135
+ const desiredStart = Math.max(minStart, maxEnd - desiredRange);
136
+
137
+ // Following snaps to the data range; manual zoom may pull back past the data
138
+ // extent (up to MAX_ZOOM_OUT_FACTOR×) so short traces aren't un-zoom-out-able.
139
+ const viewRange = clamp(
140
+ view.follow ? desiredRange : view.range,
141
+ 1,
142
+ view.follow ? fullRange : fullRange * MAX_ZOOM_OUT_FACTOR
143
+ );
144
+ const viewStart = clamp(
145
+ view.follow ? desiredStart : view.start,
146
+ minStart,
147
+ maxEnd - viewRange
148
+ );
149
+
150
+ return { viewStart, viewEnd: viewStart + viewRange, viewRange };
151
+ }
152
+
153
+ type PlacedSpan = {
154
+ span: TraceSpan;
155
+ leftPct: number;
156
+ widthPct: number;
157
+ rightPct: number;
158
+ durationMs: number;
159
+ };
194
160
 
195
- const ticks = computeTicks(viewStart, viewEnd, viewRange);
161
+ /** Project a single span onto the view window, or null if it falls outside. */
162
+ function placeSpan(
163
+ s: TraceSpan,
164
+ now: number,
165
+ win: ViewWindow
166
+ ): PlacedSpan | null {
167
+ const { viewStart, viewEnd, viewRange } = win;
168
+ const rawEnd = Number.isNaN(s.end) ? now : s.end;
169
+ const end = visualEnd(s.start, rawEnd);
170
+ if (end < viewStart || s.start > viewEnd) return null;
196
171
 
172
+ const visibleStart = Math.max(s.start, viewStart);
173
+ const visibleEnd = Math.min(end, viewEnd);
174
+ if (visibleEnd <= visibleStart) return null;
175
+
176
+ const rawLeft = ((visibleStart - viewStart) / viewRange) * 100;
177
+ const rawWidth = ((visibleEnd - visibleStart) / viewRange) * 100;
178
+ const leftPct = clamp(rawLeft, 0, 100);
179
+ const widthPct = clamp(Math.max(MIN_WIDTH_PCT, rawWidth), 0, 100 - leftPct);
180
+ const rightPct = leftPct + widthPct;
181
+ // Report the true duration (0ms for instant nodes), not the padded one.
182
+ const durationMs = Math.max(0, rawEnd - s.start);
183
+
184
+ return { span: s, leftPct, widthPct, rightPct, durationMs };
185
+ }
186
+
187
+ /** Pack a placed span into the lowest stack row that has cleared its left edge,
188
+ * returning the chosen stack index and updating `stackRight` in place. */
189
+ function assignStack(placed: PlacedSpan, stackRight: number[]): number {
190
+ let stack = 0;
191
+ for (; stack < stackRight.length; stack++) {
192
+ if (placed.leftPct >= stackRight[stack]! + OVERLAP_EPSILON_PCT) break;
193
+ }
194
+ if (stack === stackRight.length) stackRight.push(placed.rightPct);
195
+ else stackRight[stack] = Math.max(stackRight[stack]!, placed.rightPct);
196
+ return stack;
197
+ }
198
+
199
+ /** Build the fill/border colours for a span at a given stack depth. */
200
+ function spanColors(
201
+ span: TraceSpan,
202
+ stack: number
203
+ ): { bg: string; border: string } {
204
+ const hue = hashToHue(span.nodeId);
205
+ const isOpen = Number.isNaN(span.end);
206
+ const lightness = clamp(56 - stack * 7, 30, 60);
207
+ const bg = isOpen
208
+ ? `hsla(${hue}, 80%, ${clamp(lightness + 6, 30, 65)}%, 0.35)`
209
+ : `hsla(${hue}, 80%, ${lightness}%, 0.6)`;
210
+ const border = `hsla(${hue}, 90%, ${clamp(lightness + 22, 45, 80)}%, 0.95)`;
211
+ return { bg, border };
212
+ }
213
+
214
+ /** Lay out one lane's spans into stacked, coloured visual spans. */
215
+ function buildLaneData(
216
+ laneSpans: TraceSpan[] | undefined,
217
+ now: number,
218
+ win: ViewWindow
219
+ ): LaneData {
220
+ if (!laneSpans || laneSpans.length === 0) {
221
+ return { stackCount: 1, visualSpans: [] };
222
+ }
223
+
224
+ const placed: PlacedSpan[] = [];
225
+ for (const s of laneSpans) {
226
+ const p = placeSpan(s, now, win);
227
+ if (p) placed.push(p);
228
+ }
229
+ placed.sort((a, b) => a.leftPct - b.leftPct);
230
+
231
+ const stackRight: number[] = [];
232
+ const visualSpans: VisualSpan[] = [];
233
+ for (const v of placed) {
234
+ const stack = assignStack(v, stackRight);
235
+ const { bg, border } = spanColors(v.span, stack);
236
+ visualSpans.push({
237
+ span: v.span,
238
+ leftPct: v.leftPct,
239
+ widthPct: v.widthPct,
240
+ rightPct: v.rightPct,
241
+ durationMs: v.durationMs,
242
+ stack,
243
+ bg,
244
+ border
245
+ });
246
+ }
247
+
248
+ return { stackCount: Math.max(1, stackRight.length), visualSpans };
249
+ }
250
+
251
+ export function computeDerivedSpans(
252
+ collector: SpanCollector,
253
+ windowMs: number,
254
+ view: ViewState
255
+ ): DerivedData {
256
+ const now = performance.now();
257
+
258
+ if (collector.size <= 0) return { ...EMPTY, now };
259
+
260
+ const scan = scanSpans(collector, now);
261
+ const { minStart, maxEnd, maxLane, buckets } = scan;
262
+ const { size } = collector;
263
+
264
+ if (
265
+ !Number.isFinite(minStart) ||
266
+ !Number.isFinite(maxEnd) ||
267
+ maxEnd <= minStart
268
+ ) {
197
269
  return {
270
+ ...EMPTY,
198
271
  now,
199
272
  size,
200
273
  lanes: maxLane + 1,
201
- minStart,
202
- maxEnd,
203
- range: fullRange,
204
- viewStart,
205
- viewEnd,
206
- viewRange,
207
274
  buckets,
208
- laneData,
209
- ticks
275
+ laneData: []
210
276
  };
211
- }, [collector, version, windowMs, view.follow, view.range, view.start]);
277
+ }
278
+
279
+ const fullRange = Math.max(1, maxEnd - minStart);
280
+ const win = resolveViewWindow(scan, fullRange, windowMs, view);
281
+
282
+ const laneData: LaneData[] = Array.from({ length: maxLane + 1 }, (_, lane) =>
283
+ buildLaneData(buckets[lane], now, win)
284
+ );
285
+
286
+ const ticks = computeTicks(
287
+ win.viewStart,
288
+ win.viewEnd,
289
+ win.viewRange,
290
+ minStart
291
+ );
292
+
293
+ return {
294
+ now,
295
+ size,
296
+ lanes: maxLane + 1,
297
+ minStart,
298
+ maxEnd,
299
+ range: fullRange,
300
+ viewStart: win.viewStart,
301
+ viewEnd: win.viewEnd,
302
+ viewRange: win.viewRange,
303
+ buckets,
304
+ laneData,
305
+ ticks
306
+ };
212
307
  }
@@ -8,6 +8,14 @@ export const hashToHue = (str: string): number => {
8
8
  export const clamp = (v: number, min: number, max: number): number =>
9
9
  Math.max(min, Math.min(max, v));
10
10
 
11
+ /**
12
+ * How far past the captured data extent the user may zoom out. Without headroom
13
+ * the view range is clamped to the data range, so you hit a wall and "can't zoom
14
+ * out" , especially for very short traces. This lets the trace be pulled back
15
+ * into a smaller cluster with surrounding space.
16
+ */
17
+ export const MAX_ZOOM_OUT_FACTOR = 8;
18
+
11
19
  const NICE_INTERVALS = [
12
20
  1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000
13
21
  ] as const;