@grackle-ai/web-components 0.107.2

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 (279) hide show
  1. package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
  2. package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
  3. package/.rush/temp/operation/_phase_build/all.log +18 -0
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
  5. package/.rush/temp/operation/_phase_build/state.json +3 -0
  6. package/.rush/temp/operation/_phase_test/all.log +121 -0
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
  8. package/.rush/temp/operation/_phase_test/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +938 -0
  10. package/.storybook/main.ts +22 -0
  11. package/.storybook/preview.tsx +30 -0
  12. package/config/rig.json +4 -0
  13. package/config/rush-project.json +12 -0
  14. package/dist/index.css +1 -0
  15. package/dist/index.js +39221 -0
  16. package/eslint.config.cjs +5 -0
  17. package/package.json +83 -0
  18. package/rush-logs/web-components._phase_build.cache.log +4 -0
  19. package/rush-logs/web-components._phase_test.cache.log +4 -0
  20. package/src/components/chat/ChatInput.module.scss +81 -0
  21. package/src/components/chat/ChatInput.stories.tsx +91 -0
  22. package/src/components/chat/ChatInput.tsx +168 -0
  23. package/src/components/chat/index.ts +6 -0
  24. package/src/components/dag/DagView.module.scss +149 -0
  25. package/src/components/dag/DagView.stories.tsx +125 -0
  26. package/src/components/dag/DagView.tsx +109 -0
  27. package/src/components/dag/TaskNode.stories.tsx +133 -0
  28. package/src/components/dag/TaskNode.tsx +40 -0
  29. package/src/components/dag/useDagLayout.ts +139 -0
  30. package/src/components/display/Breadcrumbs.module.scss +71 -0
  31. package/src/components/display/Breadcrumbs.stories.tsx +80 -0
  32. package/src/components/display/Breadcrumbs.tsx +46 -0
  33. package/src/components/display/Button.module.scss +110 -0
  34. package/src/components/display/Button.stories.tsx +88 -0
  35. package/src/components/display/Button.tsx +40 -0
  36. package/src/components/display/ConfirmDialog.module.scss +67 -0
  37. package/src/components/display/ConfirmDialog.stories.tsx +81 -0
  38. package/src/components/display/ConfirmDialog.tsx +88 -0
  39. package/src/components/display/CopyButton.module.scss +41 -0
  40. package/src/components/display/CopyButton.stories.tsx +78 -0
  41. package/src/components/display/CopyButton.tsx +64 -0
  42. package/src/components/display/DemoBanner.module.scss +37 -0
  43. package/src/components/display/DemoBanner.stories.tsx +40 -0
  44. package/src/components/display/DemoBanner.tsx +23 -0
  45. package/src/components/display/EventHoverRow.module.scss +102 -0
  46. package/src/components/display/EventHoverRow.stories.tsx +99 -0
  47. package/src/components/display/EventHoverRow.tsx +154 -0
  48. package/src/components/display/EventRenderer.module.scss +272 -0
  49. package/src/components/display/EventRenderer.stories.tsx +186 -0
  50. package/src/components/display/EventRenderer.tsx +271 -0
  51. package/src/components/display/EventStream.module.scss +93 -0
  52. package/src/components/display/EventStream.stories.tsx +249 -0
  53. package/src/components/display/EventStream.tsx +369 -0
  54. package/src/components/display/FloatingActionBar.module.scss +107 -0
  55. package/src/components/display/FloatingActionBar.stories.tsx +122 -0
  56. package/src/components/display/FloatingActionBar.tsx +119 -0
  57. package/src/components/display/SessionAttemptSelector.module.scss +50 -0
  58. package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
  59. package/src/components/display/SessionAttemptSelector.tsx +49 -0
  60. package/src/components/display/SessionPicker.module.scss +200 -0
  61. package/src/components/display/SessionPicker.stories.tsx +169 -0
  62. package/src/components/display/SessionPicker.tsx +214 -0
  63. package/src/components/display/Skeleton.module.scss +58 -0
  64. package/src/components/display/Skeleton.stories.tsx +94 -0
  65. package/src/components/display/Skeleton.tsx +127 -0
  66. package/src/components/display/Spinner.module.scss +41 -0
  67. package/src/components/display/Spinner.stories.tsx +66 -0
  68. package/src/components/display/Spinner.tsx +32 -0
  69. package/src/components/display/SplashScreen.module.scss +20 -0
  70. package/src/components/display/SplashScreen.stories.tsx +26 -0
  71. package/src/components/display/SplashScreen.tsx +16 -0
  72. package/src/components/display/SplitButton.module.scss +166 -0
  73. package/src/components/display/SplitButton.stories.tsx +95 -0
  74. package/src/components/display/SplitButton.tsx +128 -0
  75. package/src/components/display/Tooltip.module.scss +84 -0
  76. package/src/components/display/Tooltip.stories.tsx +240 -0
  77. package/src/components/display/Tooltip.tsx +184 -0
  78. package/src/components/display/extractText.test.tsx +48 -0
  79. package/src/components/display/index.ts +20 -0
  80. package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
  81. package/src/components/editable/EditableCheckbox.tsx +39 -0
  82. package/src/components/editable/EditableField.module.scss +135 -0
  83. package/src/components/editable/EditableSelect.tsx +164 -0
  84. package/src/components/editable/EditableTextArea.stories.tsx +50 -0
  85. package/src/components/editable/EditableTextArea.tsx +148 -0
  86. package/src/components/editable/EditableTextField.stories.tsx +62 -0
  87. package/src/components/editable/EditableTextField.tsx +153 -0
  88. package/src/components/editable/EnvironmentSelect.module.scss +17 -0
  89. package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
  90. package/src/components/editable/EnvironmentSelect.tsx +87 -0
  91. package/src/components/editable/index.ts +13 -0
  92. package/src/components/editable/useEditableField.test.tsx +233 -0
  93. package/src/components/editable/useEditableField.ts +173 -0
  94. package/src/components/index.ts +20 -0
  95. package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
  96. package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
  97. package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
  98. package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
  99. package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
  100. package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
  101. package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
  102. package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
  103. package/src/components/knowledge/KnowledgeNav.tsx +138 -0
  104. package/src/components/knowledge/index.ts +3 -0
  105. package/src/components/layout/AppNav.module.scss +82 -0
  106. package/src/components/layout/AppNav.stories.tsx +115 -0
  107. package/src/components/layout/AppNav.tsx +133 -0
  108. package/src/components/layout/BottomStatusBar.module.scss +58 -0
  109. package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
  110. package/src/components/layout/BottomStatusBar.tsx +206 -0
  111. package/src/components/layout/Sidebar.module.scss +60 -0
  112. package/src/components/layout/Sidebar.stories.tsx +46 -0
  113. package/src/components/layout/Sidebar.tsx +84 -0
  114. package/src/components/layout/StatusBar.module.scss +108 -0
  115. package/src/components/layout/StatusBar.stories.tsx +119 -0
  116. package/src/components/layout/StatusBar.tsx +70 -0
  117. package/src/components/layout/index.ts +9 -0
  118. package/src/components/lists/EnvironmentNav.module.scss +118 -0
  119. package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
  120. package/src/components/lists/EnvironmentNav.tsx +133 -0
  121. package/src/components/lists/FindingsNav.module.scss +126 -0
  122. package/src/components/lists/FindingsNav.tsx +146 -0
  123. package/src/components/lists/TaskList.module.scss +206 -0
  124. package/src/components/lists/TaskList.stories.tsx +401 -0
  125. package/src/components/lists/TaskList.tsx +509 -0
  126. package/src/components/lists/index.ts +6 -0
  127. package/src/components/lists/listHelpers.tsx +130 -0
  128. package/src/components/notifications/Callout.module.scss +83 -0
  129. package/src/components/notifications/Callout.stories.tsx +81 -0
  130. package/src/components/notifications/Callout.tsx +64 -0
  131. package/src/components/notifications/Toast.module.scss +86 -0
  132. package/src/components/notifications/Toast.stories.tsx +71 -0
  133. package/src/components/notifications/Toast.tsx +51 -0
  134. package/src/components/notifications/ToastContainer.module.scss +23 -0
  135. package/src/components/notifications/ToastContainer.stories.tsx +66 -0
  136. package/src/components/notifications/ToastContainer.tsx +29 -0
  137. package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
  138. package/src/components/notifications/UpdateBanner.test.tsx +64 -0
  139. package/src/components/notifications/UpdateBanner.tsx +44 -0
  140. package/src/components/notifications/index.ts +8 -0
  141. package/src/components/panels/AboutPanel.stories.tsx +70 -0
  142. package/src/components/panels/AboutPanel.tsx +66 -0
  143. package/src/components/panels/AppearancePanel.stories.tsx +45 -0
  144. package/src/components/panels/AppearancePanel.tsx +97 -0
  145. package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
  146. package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
  147. package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
  148. package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
  149. package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
  150. package/src/components/panels/FindingsPanel.module.scss +94 -0
  151. package/src/components/panels/FindingsPanel.stories.tsx +109 -0
  152. package/src/components/panels/FindingsPanel.tsx +76 -0
  153. package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
  154. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
  155. package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
  156. package/src/components/panels/PluginsPanel.tsx +77 -0
  157. package/src/components/panels/SettingsPanel.module.scss +336 -0
  158. package/src/components/panels/TaskActionButtons.module.scss +22 -0
  159. package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
  160. package/src/components/panels/TaskActionButtons.tsx +87 -0
  161. package/src/components/panels/TaskEditPanel.module.scss +202 -0
  162. package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
  163. package/src/components/panels/TaskEditPanel.tsx +328 -0
  164. package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
  165. package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
  166. package/src/components/panels/TaskOverviewPanel.tsx +270 -0
  167. package/src/components/panels/TokensPanel.stories.tsx +131 -0
  168. package/src/components/panels/TokensPanel.tsx +143 -0
  169. package/src/components/panels/WorkpadPanel.module.scss +39 -0
  170. package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
  171. package/src/components/panels/WorkpadPanel.tsx +63 -0
  172. package/src/components/panels/index.ts +13 -0
  173. package/src/components/personas/McpToolSelector.module.scss +109 -0
  174. package/src/components/personas/McpToolSelector.stories.tsx +129 -0
  175. package/src/components/personas/McpToolSelector.tsx +180 -0
  176. package/src/components/personas/PersonaManager.module.scss +233 -0
  177. package/src/components/personas/PersonaManager.stories.tsx +139 -0
  178. package/src/components/personas/PersonaManager.tsx +122 -0
  179. package/src/components/schedules/ScheduleManager.module.scss +98 -0
  180. package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
  181. package/src/components/schedules/ScheduleManager.tsx +160 -0
  182. package/src/components/settings/SettingsNav.module.scss +82 -0
  183. package/src/components/settings/SettingsNav.stories.tsx +83 -0
  184. package/src/components/settings/SettingsNav.tsx +104 -0
  185. package/src/components/streams/StreamDetailPanel.module.scss +206 -0
  186. package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
  187. package/src/components/streams/StreamDetailPanel.tsx +119 -0
  188. package/src/components/streams/StreamList.module.scss +92 -0
  189. package/src/components/streams/StreamList.stories.tsx +99 -0
  190. package/src/components/streams/StreamList.tsx +114 -0
  191. package/src/components/streams/index.ts +10 -0
  192. package/src/components/tools/AgentToolCard.module.scss +118 -0
  193. package/src/components/tools/AgentToolCard.stories.tsx +304 -0
  194. package/src/components/tools/AgentToolCard.tsx +247 -0
  195. package/src/components/tools/FileEditCard.stories.tsx +138 -0
  196. package/src/components/tools/FileEditCard.tsx +160 -0
  197. package/src/components/tools/FileReadCard.stories.tsx +120 -0
  198. package/src/components/tools/FileReadCard.tsx +106 -0
  199. package/src/components/tools/FindingCard.stories.tsx +124 -0
  200. package/src/components/tools/FindingCard.tsx +178 -0
  201. package/src/components/tools/GenericToolCard.stories.tsx +80 -0
  202. package/src/components/tools/GenericToolCard.tsx +111 -0
  203. package/src/components/tools/IpcCard.stories.tsx +129 -0
  204. package/src/components/tools/IpcCard.tsx +178 -0
  205. package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
  206. package/src/components/tools/KnowledgeCard.tsx +165 -0
  207. package/src/components/tools/MetadataCard.stories.tsx +32 -0
  208. package/src/components/tools/MetadataCard.tsx +39 -0
  209. package/src/components/tools/SearchCard.stories.tsx +74 -0
  210. package/src/components/tools/SearchCard.tsx +86 -0
  211. package/src/components/tools/ShellCard.stories.tsx +112 -0
  212. package/src/components/tools/ShellCard.tsx +106 -0
  213. package/src/components/tools/TaskCard.stories.tsx +123 -0
  214. package/src/components/tools/TaskCard.tsx +203 -0
  215. package/src/components/tools/TodoCard.module.scss +131 -0
  216. package/src/components/tools/TodoCard.stories.tsx +202 -0
  217. package/src/components/tools/TodoCard.tsx +200 -0
  218. package/src/components/tools/ToolCard.stories.tsx +177 -0
  219. package/src/components/tools/ToolCard.tsx +60 -0
  220. package/src/components/tools/ToolCardProps.ts +20 -0
  221. package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
  222. package/src/components/tools/ToolSearchCard.tsx +86 -0
  223. package/src/components/tools/WorkpadCard.stories.tsx +106 -0
  224. package/src/components/tools/WorkpadCard.tsx +125 -0
  225. package/src/components/tools/classifyTool.test.ts +44 -0
  226. package/src/components/tools/classifyTool.ts +134 -0
  227. package/src/components/tools/parseDiff.ts +95 -0
  228. package/src/components/tools/parseShellOutput.ts +28 -0
  229. package/src/components/tools/toolCardHelpers.test.ts +53 -0
  230. package/src/components/tools/toolCards.module.scss +234 -0
  231. package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
  232. package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
  233. package/src/components/workspace/WorkspaceBoard.tsx +232 -0
  234. package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
  235. package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
  236. package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
  237. package/src/context/GrackleContext.ts +28 -0
  238. package/src/context/GrackleContextTypes.ts +64 -0
  239. package/src/context/SidebarContext.tsx +53 -0
  240. package/src/context/ThemeContext.tsx +21 -0
  241. package/src/context/ToastContext.tsx +56 -0
  242. package/src/hooks/types.ts +864 -0
  243. package/src/hooks/useEventSelection.test.ts +204 -0
  244. package/src/hooks/useEventSelection.ts +158 -0
  245. package/src/hooks/useSmartScroll.ts +151 -0
  246. package/src/hooks/useTheme.ts +228 -0
  247. package/src/index.ts +210 -0
  248. package/src/mocks/MockGrackleProvider.tsx +1397 -0
  249. package/src/mocks/mockData.ts +1966 -0
  250. package/src/mocks/mockKnowledgeData.ts +294 -0
  251. package/src/scss.d.ts +12 -0
  252. package/src/styles/global.scss +244 -0
  253. package/src/styles/mixins.scss +278 -0
  254. package/src/styles/prism-theme.scss +148 -0
  255. package/src/styles/theme.scss +1102 -0
  256. package/src/test-utils/storybook-decorators.tsx +50 -0
  257. package/src/test-utils/storybook-helpers.ts +262 -0
  258. package/src/themes.ts +142 -0
  259. package/src/utils/boardColumns.ts +141 -0
  260. package/src/utils/breadcrumbs.test.ts +285 -0
  261. package/src/utils/breadcrumbs.ts +222 -0
  262. package/src/utils/dashboard.test.ts +156 -0
  263. package/src/utils/dashboard.ts +195 -0
  264. package/src/utils/eventContent.test.ts +353 -0
  265. package/src/utils/eventContent.ts +209 -0
  266. package/src/utils/findingCategory.ts +33 -0
  267. package/src/utils/format.ts +27 -0
  268. package/src/utils/iconSize.ts +18 -0
  269. package/src/utils/navigation.ts +205 -0
  270. package/src/utils/route-config.test.ts +128 -0
  271. package/src/utils/scrollUtils.test.ts +65 -0
  272. package/src/utils/scrollUtils.ts +49 -0
  273. package/src/utils/sessionEvents.test.ts +302 -0
  274. package/src/utils/sessionEvents.ts +233 -0
  275. package/src/utils/taskStatus.tsx +137 -0
  276. package/src/utils/time.ts +92 -0
  277. package/tsconfig.json +8 -0
  278. package/vite.config.ts +20 -0
  279. package/vitest.config.ts +10 -0
@@ -0,0 +1,1397 @@
1
+ /**
2
+ * Mock provider for visual testing without a running Grackle server.
3
+ *
4
+ * Wraps children with the same GrackleContext used by the real provider,
5
+ * but supplies fully interactive mock state. Actions like spawn, kill,
6
+ * sendInput, and task lifecycle methods all produce realistic state
7
+ * transitions and timed event streams.
8
+ *
9
+ * Activate by adding `?mock` to the URL (e.g. `http://localhost:3000?mock`).
10
+ */
11
+
12
+ import {
13
+ useMemo,
14
+ useState,
15
+ useCallback,
16
+ useRef,
17
+ useEffect,
18
+ type ReactNode,
19
+ type JSX,
20
+ } from "react";
21
+ import { GrackleContext } from "../context/GrackleContext.js";
22
+ import type { UseGrackleSocketResult } from "../context/GrackleContextTypes.js";
23
+ import type {
24
+ Environment,
25
+ Session,
26
+ SessionEvent,
27
+ FindingData,
28
+ TaskData,
29
+ Workspace,
30
+ TokenInfo,
31
+ PersonaData,
32
+ ScheduleData,
33
+ CredentialProviderConfig,
34
+ DomainHook,
35
+ } from "../hooks/types.js";
36
+ import { mapSessionStatus, mapEndReason } from "../hooks/types.js";
37
+
38
+ /** No-op domain hook for mock providers. */
39
+ const NOOP_DOMAIN_HOOK: DomainHook = {
40
+ onConnect: async () => {},
41
+ onDisconnect: () => {},
42
+ handleEvent: () => false,
43
+ };
44
+ import {
45
+ MOCK_ENVIRONMENTS,
46
+ MOCK_SESSIONS,
47
+ MOCK_EVENTS,
48
+ MOCK_WORKSPACES,
49
+ MOCK_TASKS,
50
+ MOCK_FINDINGS,
51
+ MOCK_TOKENS,
52
+ MOCK_PERSONAS,
53
+ MOCK_TASK_SESSIONS,
54
+ MOCK_STREAM_SCENARIOS,
55
+ MOCK_KNOWLEDGE_NODES,
56
+ MOCK_KNOWLEDGE_LINKS,
57
+ MOCK_KNOWLEDGE_DETAILS,
58
+ type MockStreamStep,
59
+ } from "./mockData.js";
60
+ import type { GraphNode, GraphLink, NodeDetail } from "../hooks/types.js";
61
+
62
+ // ─── Constants ──────────────────────────────────────
63
+
64
+ /** Delay before the "idle" status is set after the last pre-pause step. */
65
+ const IDLE_DELAY_MS: number = 400;
66
+
67
+ // ─── Props ──────────────────────────────────────────
68
+
69
+ /** Props for the MockGrackleProvider component. */
70
+ interface MockGrackleProviderProps {
71
+ children: ReactNode;
72
+ }
73
+
74
+ // ─── Provider ───────────────────────────────────────
75
+
76
+ /**
77
+ * Provides interactive mock data matching the shape of UseGrackleSocketResult.
78
+ * All actions produce real state changes so every UI path is exercisable.
79
+ */
80
+ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX.Element {
81
+ // ── State ─────────────────────────────────────────
82
+ const [environments, setEnvironments] = useState<Environment[]>(MOCK_ENVIRONMENTS);
83
+ const [sessions, setSessions] = useState<Session[]>(MOCK_SESSIONS);
84
+ const [events, setEvents] = useState<SessionEvent[]>(MOCK_EVENTS);
85
+ const [lastSpawnedId, setLastSpawnedId] = useState<string | undefined>(undefined);
86
+ const [workspaces, setWorkspaces] = useState<Workspace[]>(MOCK_WORKSPACES);
87
+ const [workspaceLinkError, setWorkspaceLinkError] = useState("");
88
+ const [tasks, setTasks] = useState<TaskData[]>(MOCK_TASKS);
89
+ const [findings, setFindings] = useState<FindingData[]>(MOCK_FINDINGS);
90
+ const [selectedFinding, setSelectedFinding] = useState<FindingData | undefined>(undefined);
91
+ const findingLoading = false;
92
+ const [tokens, setTokens] = useState<TokenInfo[]>(MOCK_TOKENS);
93
+ const [credentialProviders, setCredentialProviders] = useState<CredentialProviderConfig>({
94
+ claude: "off",
95
+ github: "off",
96
+ copilot: "off",
97
+ codex: "off",
98
+ goose: "off",
99
+ });
100
+ const [personas, setPersonas] = useState<PersonaData[]>(MOCK_PERSONAS);
101
+ const [schedules, setSchedules] = useState<ScheduleData[]>([]);
102
+ const [taskSessions] = useState<Record<string, Session[]>>(MOCK_TASK_SESSIONS);
103
+ const [appDefaultPersonaId, setAppDefaultPersonaIdState] = useState<string>("");
104
+
105
+ // ── Knowledge state ────────────────────────────────
106
+ const [knowledgeNodes, setKnowledgeNodes] = useState<GraphNode[]>(MOCK_KNOWLEDGE_NODES);
107
+ const [knowledgeLinks, setKnowledgeLinks] = useState<GraphLink[]>(MOCK_KNOWLEDGE_LINKS);
108
+ const [knowledgeSelectedNode, setKnowledgeSelectedNode] = useState<NodeDetail | undefined>(undefined);
109
+ const [knowledgeSelectedId, setKnowledgeSelectedId] = useState<string | undefined>(undefined);
110
+ const [knowledgeSearchQuery, setKnowledgeSearchQuery] = useState<string>("");
111
+ /** Active workspace filter for knowledge graph. */
112
+ const knowledgeWorkspaceRef = useRef<string | undefined>(undefined);
113
+
114
+ // ── Refs ──────────────────────────────────────────
115
+ /** Auto-incrementing counter for generating unique mock IDs. */
116
+ const counterRef = useRef<number>(0);
117
+ /** Index into MOCK_STREAM_SCENARIOS, cycling through scenarios. */
118
+ const scenarioIndexRef = useRef<number>(0);
119
+ /** All active timeouts for cleanup on unmount. */
120
+ const timersRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
121
+ /** Per-session timeout tracking for selective cancellation (kill). */
122
+ const sessionTimersRef = useRef<Map<string, Set<ReturnType<typeof setTimeout>>>>(new Map());
123
+ /** Ref mirror of tasks state for reading current values without triggering re-renders. */
124
+ const tasksRef = useRef<TaskData[]>(tasks);
125
+ tasksRef.current = tasks;
126
+ /** Resume steps keyed by sessionId, stored when a scenario pauses for input. */
127
+ const pendingResumeRef = useRef<Map<string, MockStreamStep[]>>(new Map());
128
+ /**
129
+ * Tracks task review transitions after a paused scenario resumes.
130
+ * Maps sessionId to { taskId, delayAfterResume }.
131
+ */
132
+ const pendingTaskReviewRef = useRef<
133
+ Map<string, { taskId: string; delayAfterResume: number }>
134
+ >(new Map());
135
+
136
+ // ── Helpers ───────────────────────────────────────
137
+
138
+ /** Generates a unique mock ID with the given prefix (e.g. "mock-sess-001"). */
139
+ const nextId = useCallback((prefix: string): string => {
140
+ counterRef.current += 1;
141
+ return `mock-${prefix}-${String(counterRef.current).padStart(3, "0")}`;
142
+ }, []);
143
+
144
+ /** Returns the next scenario from the rotating list. */
145
+ const nextScenario = useCallback(() => {
146
+ const scenario = MOCK_STREAM_SCENARIOS[scenarioIndexRef.current % MOCK_STREAM_SCENARIOS.length];
147
+ scenarioIndexRef.current += 1;
148
+ return scenario;
149
+ }, []);
150
+
151
+ /** Schedules a callback, tracking it globally and optionally per-session. */
152
+ const schedule = useCallback(
153
+ (fn: () => void, delayMs: number, sessionId?: string): void => {
154
+ const handle = setTimeout(() => {
155
+ timersRef.current.delete(handle);
156
+ if (sessionId) {
157
+ sessionTimersRef.current.get(sessionId)?.delete(handle);
158
+ }
159
+ fn();
160
+ }, delayMs);
161
+ timersRef.current.add(handle);
162
+ if (sessionId) {
163
+ if (!sessionTimersRef.current.has(sessionId)) {
164
+ sessionTimersRef.current.set(sessionId, new Set());
165
+ }
166
+ sessionTimersRef.current.get(sessionId)!.add(handle);
167
+ }
168
+ },
169
+ [],
170
+ );
171
+
172
+ /** Cancels all pending timers for a given session. */
173
+ const cancelSessionTimers = useCallback((sessionId: string): void => {
174
+ const sessionHandles = sessionTimersRef.current.get(sessionId);
175
+ if (sessionHandles) {
176
+ sessionHandles.forEach((handle) => {
177
+ clearTimeout(handle);
178
+ timersRef.current.delete(handle);
179
+ });
180
+ sessionTimersRef.current.delete(sessionId);
181
+ }
182
+ }, []);
183
+
184
+ /** Updates a single session's status (and optionally endReason) in state. */
185
+ const updateSessionStatus = useCallback((sessionId: string, status: string, endReason?: string): void => {
186
+ setSessions((prev) =>
187
+ prev.map((s) => (s.id === sessionId ? { ...s, status, ...(endReason !== undefined ? { endReason } : {}) } : s)),
188
+ );
189
+ }, []);
190
+
191
+ /** Appends a single event to the events array. */
192
+ const appendEvent = useCallback((event: SessionEvent): void => {
193
+ setEvents((prev) => [...prev, event]);
194
+ }, []);
195
+
196
+ /**
197
+ * Plays a sequence of stream steps, appending events and updating session
198
+ * status as each step fires. Calls onComplete after the last step.
199
+ */
200
+ const playScenario = useCallback(
201
+ (
202
+ sessionId: string,
203
+ steps: MockStreamStep[],
204
+ onComplete?: () => void,
205
+ ): void => {
206
+ steps.forEach((step, index) => {
207
+ schedule(
208
+ () => {
209
+ const event: SessionEvent = {
210
+ sessionId,
211
+ ...step.event,
212
+ timestamp: new Date().toISOString(),
213
+ };
214
+ appendEvent(event);
215
+
216
+ // Status events also update the session record (apply mapping
217
+ // so raw PowerLine values like "completed"/"failed"/"killed" are
218
+ // stored as "stopped" with the appropriate endReason).
219
+ if (step.event.eventType === "status") {
220
+ const mappedStatus = mapSessionStatus(step.event.content);
221
+ const endReason = mapEndReason(step.event.content);
222
+ updateSessionStatus(sessionId, mappedStatus, endReason);
223
+ }
224
+
225
+ // Fire onComplete after the last step
226
+ if (index === steps.length - 1 && onComplete) {
227
+ onComplete();
228
+ }
229
+ },
230
+ step.delayMs,
231
+ sessionId,
232
+ );
233
+ });
234
+ },
235
+ [schedule, appendEvent, updateSessionStatus],
236
+ );
237
+
238
+ // ── Actions ───────────────────────────────────────
239
+
240
+ /** Spawns a new session, appends it to state, and plays a stream scenario. */
241
+ const spawn: UseGrackleSocketResult["sessions"]["spawn"] = useCallback(
242
+ async (environmentId: string, prompt: string, _model?: string, runtime?: string) => {
243
+ console.log("[MockGrackle] spawn", { environmentId, prompt, runtime });
244
+
245
+ const sessionId = nextId("sess");
246
+ const newSession: Session = {
247
+ id: sessionId,
248
+ environmentId,
249
+ runtime: runtime || "claude-code",
250
+ status: "running",
251
+ prompt,
252
+ startedAt: new Date().toISOString(),
253
+ };
254
+
255
+ setSessions((prev) => [...prev, newSession]);
256
+ setLastSpawnedId(sessionId);
257
+
258
+ const scenario = nextScenario();
259
+ console.log(`[MockGrackle] Playing scenario: ${scenario.label}`);
260
+
261
+ if (scenario.pauseForInput) {
262
+ // Play steps up to and including pauseAfterStep, then pause
263
+ const pauseIndex = scenario.pauseAfterStep ?? scenario.steps.length - 1;
264
+ const preSteps = scenario.steps.slice(0, pauseIndex + 1);
265
+ const lastStepDelay = preSteps.length > 0 ? preSteps[preSteps.length - 1].delayMs : 0;
266
+
267
+ playScenario(sessionId, preSteps);
268
+
269
+ // After the last pre-pause step, transition to idle
270
+ schedule(
271
+ () => {
272
+ updateSessionStatus(sessionId, "idle");
273
+ appendEvent({
274
+ sessionId,
275
+ eventType: "status",
276
+ timestamp: new Date().toISOString(),
277
+ content: "idle",
278
+ });
279
+ },
280
+ lastStepDelay + IDLE_DELAY_MS,
281
+ sessionId,
282
+ );
283
+
284
+ // Store resume steps for sendInput
285
+ if (scenario.resumeSteps) {
286
+ pendingResumeRef.current.set(sessionId, scenario.resumeSteps);
287
+ }
288
+ } else {
289
+ // Play all steps straight through
290
+ playScenario(sessionId, scenario.steps);
291
+ }
292
+ },
293
+ [nextId, nextScenario, playScenario, schedule, updateSessionStatus, appendEvent],
294
+ );
295
+
296
+ /** Kills a session: cancels timers, sets status to stopped with endReason killed, resets associated tasks. */
297
+ const kill: UseGrackleSocketResult["sessions"]["kill"] = useCallback(
298
+ async (sessionId: string) => {
299
+ console.log("[MockGrackle] kill", sessionId);
300
+
301
+ // 1. Cancel pending timers for this session
302
+ cancelSessionTimers(sessionId);
303
+
304
+ // 2. Remove any pending resume steps
305
+ pendingResumeRef.current.delete(sessionId);
306
+
307
+ // 3. Update session status to "stopped" with endReason "killed"
308
+ updateSessionStatus(sessionId, "stopped", "killed");
309
+
310
+ // 4. Append a status event
311
+ appendEvent({
312
+ sessionId,
313
+ eventType: "status",
314
+ timestamp: new Date().toISOString(),
315
+ content: "killed",
316
+ });
317
+
318
+ // 5. With computed status, killing a session makes the task retryable
319
+ // (computed back to "not_started"), so reset in-progress tasks to "not_started".
320
+ setTasks((prev) =>
321
+ prev.map((t) =>
322
+ t.latestSessionId === sessionId && t.status === "working"
323
+ ? { ...t, status: "not_started" }
324
+ : t,
325
+ ),
326
+ );
327
+ },
328
+ [cancelSessionTimers, updateSessionStatus, appendEvent],
329
+ );
330
+
331
+ /** Graceful stop — mirrors kill but with "terminated" end reason. */
332
+ const stopGraceful: UseGrackleSocketResult["sessions"]["stopGraceful"] = useCallback(
333
+ async (sessionId: string) => {
334
+ console.log("[MockGrackle] stopGraceful", sessionId);
335
+ cancelSessionTimers(sessionId);
336
+ pendingResumeRef.current.delete(sessionId);
337
+ updateSessionStatus(sessionId, "stopped", "terminated");
338
+ appendEvent({
339
+ sessionId,
340
+ eventType: "status",
341
+ timestamp: new Date().toISOString(),
342
+ content: "terminated",
343
+ });
344
+ // Reset tasks just like kill() — stopped sessions make tasks retryable
345
+ setTasks((prev) =>
346
+ prev.map((t) =>
347
+ t.latestSessionId === sessionId && t.status === "working"
348
+ ? { ...t, status: "not_started" }
349
+ : t,
350
+ ),
351
+ );
352
+ },
353
+ [cancelSessionTimers, updateSessionStatus, appendEvent],
354
+ );
355
+
356
+ /** No-op refresh — the mock has no server to re-fetch from. */
357
+ const refresh: UseGrackleSocketResult["refresh"] = useCallback((): void => {
358
+ console.log("[MockGrackle] refresh");
359
+ }, []);
360
+
361
+ /** No-op — events are already accumulated in state. */
362
+ const loadSessionEvents: UseGrackleSocketResult["sessions"]["loadSessionEvents"] = useCallback(
363
+ async (sessionId: string) => {
364
+ console.log("[MockGrackle] loadSessionEvents", sessionId);
365
+ },
366
+ [],
367
+ );
368
+
369
+ /** Clears all events from state. */
370
+ const clearEvents: UseGrackleSocketResult["sessions"]["clearEvents"] = useCallback(() => {
371
+ console.log("[MockGrackle] clearEvents");
372
+ setEvents([]);
373
+ }, []);
374
+
375
+ /** Creates a new workspace and adds it to state. */
376
+ const createWorkspace: UseGrackleSocketResult["workspaces"]["createWorkspace"] = useCallback(
377
+ async (
378
+ name: string,
379
+ description?: string,
380
+ repoUrl?: string,
381
+ environmentId?: string,
382
+ defaultPersonaId?: string,
383
+ useWorktrees?: boolean,
384
+ workingDirectory?: string,
385
+ onSuccess?: () => void,
386
+ _onError?: (message: string) => void,
387
+ ) => {
388
+ console.log("[MockGrackle] createWorkspace", { name, description });
389
+
390
+ const newWorkspace: Workspace = {
391
+ id: nextId("proj"),
392
+ name,
393
+ description: description || "",
394
+ repoUrl: repoUrl || "",
395
+ linkedEnvironmentIds: environmentId ? [environmentId] : [],
396
+ status: "active",
397
+ workingDirectory: workingDirectory || "",
398
+ useWorktrees: useWorktrees ?? true,
399
+ defaultPersonaId: defaultPersonaId || "",
400
+ tokenBudget: 0,
401
+ costBudgetMillicents: 0,
402
+ createdAt: new Date().toISOString(),
403
+ updatedAt: new Date().toISOString(),
404
+ };
405
+
406
+ setWorkspaces((prev) => [...prev, newWorkspace]);
407
+ if (onSuccess) {
408
+ onSuccess();
409
+ }
410
+ },
411
+ [nextId],
412
+ );
413
+
414
+ /** Sets a workspace's status to "archived". */
415
+ const archiveWorkspace: UseGrackleSocketResult["workspaces"]["archiveWorkspace"] = useCallback(
416
+ async (workspaceId: string) => {
417
+ console.log("[MockGrackle] archiveWorkspace", workspaceId);
418
+ setWorkspaces((prev) =>
419
+ prev.map((p) => (p.id === workspaceId ? { ...p, status: "archived" } : p)),
420
+ );
421
+ },
422
+ [],
423
+ );
424
+
425
+ /** No-op — tasks are already in state from initial load. */
426
+ const loadTasks: UseGrackleSocketResult["tasks"]["loadTasks"] = useCallback(
427
+ async (workspaceId: string) => {
428
+ console.log("[MockGrackle] loadTasks", workspaceId);
429
+ },
430
+ [],
431
+ );
432
+
433
+ /** Creates a new task and adds it to state. */
434
+ const createTask: UseGrackleSocketResult["tasks"]["createTask"] = useCallback(
435
+ async (
436
+ workspaceId: string,
437
+ title: string,
438
+ description?: string,
439
+ dependsOn?: string[],
440
+ parentTaskId?: string,
441
+ defaultPersonaId?: string,
442
+ canDecompose?: boolean,
443
+ onSuccess?: () => void,
444
+ _onError?: (message: string) => void,
445
+ ) => {
446
+ console.log("[MockGrackle] createTask", { workspaceId, title, parentTaskId });
447
+
448
+ setTasks((prev) => {
449
+ const wsTasks = prev.filter((t) => t.workspaceId === workspaceId);
450
+ const maxSort = wsTasks.reduce((max, t) => Math.max(max, t.sortOrder), 0);
451
+ const parent = parentTaskId ? prev.find((t) => t.id === parentTaskId) : undefined;
452
+ if (parentTaskId && !parent) {
453
+ console.warn("[MockGrackle] Parent task not found:", parentTaskId);
454
+ return prev;
455
+ }
456
+ if (parent && !parent.canDecompose) {
457
+ console.warn("[MockGrackle] Parent task does not have decomposition rights:", parentTaskId);
458
+ return prev;
459
+ }
460
+ const depth = parent ? parent.depth + 1 : 0;
461
+
462
+ const newTask: TaskData = {
463
+ id: nextId("task"),
464
+ workspaceId,
465
+ title,
466
+ description: description || "",
467
+ status: "not_started",
468
+ branch: "",
469
+ latestSessionId: "",
470
+ dependsOn: dependsOn || [],
471
+ reviewNotes: undefined,
472
+ sortOrder: maxSort + 1,
473
+ createdAt: new Date().toISOString(),
474
+ parentTaskId: parentTaskId || "",
475
+ depth,
476
+ childTaskIds: [],
477
+ canDecompose: canDecompose ?? !parentTaskId,
478
+ defaultPersonaId: defaultPersonaId || "",
479
+ workpad: "",
480
+ tokenBudget: 0,
481
+ costBudgetMillicents: 0,
482
+ };
483
+
484
+ return [...prev, newTask];
485
+ });
486
+ if (onSuccess) {
487
+ onSuccess();
488
+ }
489
+ },
490
+ [nextId],
491
+ );
492
+
493
+ /**
494
+ * Starts a task: creates a new session, links it to the task, sets the
495
+ * task to "working", and plays a stream scenario. On scenario
496
+ * completion, transitions the task to "paused".
497
+ *
498
+ * Also handles retry from "failed" — the task gets a fresh session.
499
+ */
500
+ const startTask: UseGrackleSocketResult["tasks"]["startTask"] = useCallback(
501
+ async (taskId: string, _personaId?: string, _environmentId?: string, _notes?: string) => {
502
+ console.log("[MockGrackle] startTask", { taskId });
503
+
504
+ // Find the task to get its metadata
505
+ const target = tasksRef.current.find((t) => t.id === taskId);
506
+ const taskTitle = target?.title ?? "";
507
+
508
+ const sessionId = nextId("sess");
509
+ const newSession: Session = {
510
+ id: sessionId,
511
+ environmentId: "env-local-01",
512
+ runtime: "claude-code",
513
+ status: "running",
514
+ prompt: taskTitle || taskId,
515
+ startedAt: new Date().toISOString(),
516
+ };
517
+
518
+ setSessions((prev) => [...prev, newSession]);
519
+
520
+ // Update task: status → "working", latestSessionId → new session, branch → mock branch
521
+ setTasks((prev) =>
522
+ prev.map((t) =>
523
+ t.id === taskId
524
+ ? {
525
+ ...t,
526
+ status: "working",
527
+ latestSessionId: sessionId,
528
+ branch: `mock/${taskId.slice(0, 8)}`,
529
+ }
530
+ : t,
531
+ ),
532
+ );
533
+
534
+ // Pick and play a scenario
535
+ const scenario = nextScenario();
536
+ console.log(`[MockGrackle] Playing task scenario: ${scenario.label}`);
537
+
538
+ if (scenario.pauseForInput) {
539
+ const pauseIndex = scenario.pauseAfterStep ?? scenario.steps.length - 1;
540
+ const preSteps = scenario.steps.slice(0, pauseIndex + 1);
541
+ const lastStepDelay = preSteps.length > 0 ? preSteps[preSteps.length - 1].delayMs : 0;
542
+
543
+ playScenario(sessionId, preSteps);
544
+
545
+ schedule(
546
+ () => {
547
+ updateSessionStatus(sessionId, "idle");
548
+ appendEvent({
549
+ sessionId,
550
+ eventType: "status",
551
+ timestamp: new Date().toISOString(),
552
+ content: "idle",
553
+ });
554
+ },
555
+ lastStepDelay + IDLE_DELAY_MS,
556
+ sessionId,
557
+ );
558
+
559
+ // Store resume steps; on resume completion, transition task to "paused"
560
+ if (scenario.resumeSteps) {
561
+ // Append a synthetic completion callback step
562
+ const resumeWithReview: MockStreamStep[] = [
563
+ ...scenario.resumeSteps,
564
+ ];
565
+ pendingResumeRef.current.set(sessionId, resumeWithReview);
566
+
567
+ // We need to handle the task → review transition after resume.
568
+ // playScenario's last step will set status to "completed"; we
569
+ // listen for that by scheduling the review transition after
570
+ // the resume steps' total delay.
571
+ // This is handled by overriding sendInput's onComplete below —
572
+ // but since we can't easily pass a callback through pendingResumeRef,
573
+ // we use a separate approach: schedule a check after the resume
574
+ // steps would complete. The longest delay in resumeSteps:
575
+ const maxResumeDelay = resumeWithReview.reduce(
576
+ (max, s) => Math.max(max, s.delayMs),
577
+ 0,
578
+ );
579
+ // We can't schedule this now because the user hasn't sent input yet.
580
+ // Instead, we store metadata so sendInput can schedule the review
581
+ // transition. We'll handle this by checking if the session belongs
582
+ // to a task after resume completes.
583
+ // For simplicity, we store the task ID alongside the resume steps
584
+ // and handle it in sendInput via a post-resume schedule.
585
+ pendingTaskReviewRef.current.set(sessionId, {
586
+ taskId,
587
+ delayAfterResume: maxResumeDelay + 200,
588
+ });
589
+ }
590
+ } else {
591
+ // Straight-through scenario: on last step, transition task to "paused"
592
+ const lastStepDelay =
593
+ scenario.steps.length > 0
594
+ ? scenario.steps[scenario.steps.length - 1].delayMs
595
+ : 0;
596
+
597
+ playScenario(sessionId, scenario.steps);
598
+
599
+ // After scenario completes, if the session ended in "completed",
600
+ // set task to "paused". If it ended in "failed", set task to "failed".
601
+ const finalStatus = scenario.steps[scenario.steps.length - 1]?.event.content;
602
+ schedule(
603
+ () => {
604
+ if (finalStatus === "completed") {
605
+ setTasks((prev) =>
606
+ prev.map((t) =>
607
+ t.id === taskId && t.status === "working"
608
+ ? { ...t, status: "paused" }
609
+ : t,
610
+ ),
611
+ );
612
+ } else if (finalStatus === "failed") {
613
+ setTasks((prev) =>
614
+ prev.map((t) =>
615
+ t.id === taskId && t.status === "working"
616
+ ? { ...t, status: "failed" }
617
+ : t,
618
+ ),
619
+ );
620
+ }
621
+ },
622
+ lastStepDelay + 100,
623
+ sessionId,
624
+ );
625
+ }
626
+ },
627
+ [nextId, nextScenario, playScenario, schedule, updateSessionStatus, appendEvent],
628
+ );
629
+
630
+ /**
631
+ * Sends input to a waiting session: echoes the user's text, transitions
632
+ * back to "running", plays any pending resume steps, and handles
633
+ * post-resume task → review transitions.
634
+ */
635
+ const sendInput: UseGrackleSocketResult["sessions"]["sendInput"] = useCallback(
636
+ async (sessionId: string, text: string) => {
637
+ console.log("[MockGrackle] sendInput", { sessionId, text });
638
+
639
+ // 1. Append echo event
640
+ appendEvent({
641
+ sessionId,
642
+ eventType: "output",
643
+ timestamp: new Date().toISOString(),
644
+ content: `User input: ${text}`,
645
+ });
646
+
647
+ // 2. Transition to running
648
+ updateSessionStatus(sessionId, "running");
649
+ appendEvent({
650
+ sessionId,
651
+ eventType: "status",
652
+ timestamp: new Date().toISOString(),
653
+ content: "running",
654
+ });
655
+
656
+ // 3. Play resume steps
657
+ const resumeSteps = pendingResumeRef.current.get(sessionId);
658
+ if (resumeSteps) {
659
+ pendingResumeRef.current.delete(sessionId);
660
+ playScenario(sessionId, resumeSteps);
661
+ } else {
662
+ const fallbackSteps: MockStreamStep[] = [
663
+ {
664
+ delayMs: 500,
665
+ event: {
666
+ eventType: "output",
667
+ timestamp: new Date().toISOString(),
668
+ content: "I received your input, continuing...",
669
+ },
670
+ },
671
+ {
672
+ delayMs: 1500,
673
+ event: {
674
+ eventType: "status",
675
+ timestamp: new Date().toISOString(),
676
+ content: "completed",
677
+ },
678
+ },
679
+ ];
680
+ playScenario(sessionId, fallbackSteps);
681
+ }
682
+
683
+ // 4. Check for pending task review transition
684
+ const pendingReview = pendingTaskReviewRef.current.get(sessionId);
685
+ if (pendingReview) {
686
+ pendingTaskReviewRef.current.delete(sessionId);
687
+ schedule(
688
+ () => {
689
+ setTasks((prev) =>
690
+ prev.map((t) =>
691
+ t.id === pendingReview.taskId && t.status === "working"
692
+ ? { ...t, status: "paused" }
693
+ : t,
694
+ ),
695
+ );
696
+ },
697
+ pendingReview.delayAfterResume,
698
+ sessionId,
699
+ );
700
+ }
701
+ },
702
+ [appendEvent, updateSessionStatus, playScenario, schedule],
703
+ );
704
+
705
+ /** Completes a task: sets status to "complete" (human-authoritative). */
706
+ const completeTask: UseGrackleSocketResult["tasks"]["completeTask"] = useCallback(
707
+ async (taskId: string) => {
708
+ console.log("[MockGrackle] completeTask", taskId);
709
+ setTasks((prev) =>
710
+ prev.map((t) => (t.id === taskId ? { ...t, status: "complete" } : t)),
711
+ );
712
+ },
713
+ [],
714
+ );
715
+
716
+ /** Resumes the latest session for a task (mock: no-op, just logs). */
717
+ const resumeTask: UseGrackleSocketResult["tasks"]["resumeTask"] = useCallback(
718
+ async (taskId: string) => {
719
+ console.log("[MockGrackle] resumeTask", taskId);
720
+ },
721
+ [],
722
+ );
723
+
724
+ /** Updates title, description, dependencies, and default persona of a pending/assigned task. */
725
+ const updateTask: UseGrackleSocketResult["tasks"]["updateTask"] = useCallback(
726
+ async (
727
+ taskId: string,
728
+ title: string,
729
+ description: string,
730
+ dependsOn: string[],
731
+ defaultPersonaId?: string,
732
+ ) => {
733
+ console.log("[MockGrackle] updateTask", { taskId, title });
734
+ setTasks((prev) =>
735
+ prev.map((t) =>
736
+ t.id === taskId
737
+ ? {
738
+ ...t,
739
+ title: title.trim() || t.title,
740
+ description,
741
+ dependsOn,
742
+ ...(defaultPersonaId !== undefined ? { defaultPersonaId } : {}),
743
+ }
744
+ : t,
745
+ ),
746
+ );
747
+ },
748
+ [],
749
+ );
750
+
751
+ /** Removes a task from state. */
752
+ const deleteTask: UseGrackleSocketResult["tasks"]["deleteTask"] = useCallback(
753
+ async (taskId: string) => {
754
+ console.log("[MockGrackle] deleteTask", taskId);
755
+ setTasks((prev) => prev.filter((t) => t.id !== taskId));
756
+ },
757
+ [],
758
+ );
759
+
760
+ /** Filters findings by workspaceId. */
761
+ const loadFindings: UseGrackleSocketResult["findings"]["loadFindings"] = useCallback(
762
+ async (workspaceId: string) => {
763
+ console.log("[MockGrackle] loadFindings", workspaceId);
764
+ setFindings(MOCK_FINDINGS.filter((f) => f.workspaceId === workspaceId));
765
+ },
766
+ [],
767
+ );
768
+
769
+ /** Load all findings across all workspaces. */
770
+ const loadAllFindings: UseGrackleSocketResult["findings"]["loadAllFindings"] = useCallback(async () => {
771
+ console.log("[MockGrackle] loadAllFindings");
772
+ setFindings([...MOCK_FINDINGS]);
773
+ }, []);
774
+
775
+ /** Load a single finding by ID. */
776
+ const loadFinding: UseGrackleSocketResult["findings"]["loadFinding"] = useCallback(
777
+ async (findingId: string) => {
778
+ console.log("[MockGrackle] loadFinding", findingId);
779
+ const found = MOCK_FINDINGS.find((f) => f.id === findingId);
780
+ setSelectedFinding(found);
781
+ },
782
+ [],
783
+ );
784
+
785
+ /** Adds a new finding to state. */
786
+ const postFinding: UseGrackleSocketResult["findings"]["postFinding"] = useCallback(
787
+ async (
788
+ workspaceId: string,
789
+ title: string,
790
+ content: string,
791
+ category?: string,
792
+ tags?: string[],
793
+ ) => {
794
+ console.log("[MockGrackle] postFinding", { workspaceId, title });
795
+
796
+ const newFinding: FindingData = {
797
+ id: nextId("find"),
798
+ workspaceId,
799
+ taskId: "",
800
+ sessionId: "",
801
+ category: category || "general",
802
+ title,
803
+ content,
804
+ tags: tags || [],
805
+ createdAt: new Date().toISOString(),
806
+ };
807
+
808
+ setFindings((prev) => [...prev, newFinding]);
809
+ },
810
+ [nextId],
811
+ );
812
+
813
+ /** No-op in mock mode (environments are pre-seeded). */
814
+ const loadEnvironments: UseGrackleSocketResult["environments"]["loadEnvironments"] = useCallback(async () => {
815
+ console.log("[MockGrackle] loadEnvironments");
816
+ }, []);
817
+
818
+ /** Logs an add-environment call (mock does not persist). */
819
+ const addEnvironment: UseGrackleSocketResult["environments"]["addEnvironment"] = useCallback(
820
+ async (
821
+ displayName: string,
822
+ adapterType: string,
823
+ adapterConfig?: Record<string, unknown>,
824
+ ) => {
825
+ console.log("[MockGrackle] addEnvironment", { displayName, adapterType, adapterConfig });
826
+ },
827
+ [],
828
+ );
829
+
830
+ /** Updates an environment in mock state so edits persist in mock mode. */
831
+ const updateEnvironment: UseGrackleSocketResult["environments"]["updateEnvironment"] = useCallback(
832
+ async (
833
+ environmentId: string,
834
+ fields: { displayName?: string; adapterConfig?: Record<string, unknown> },
835
+ ) => {
836
+ console.log("[MockGrackle] updateEnvironment", { environmentId, ...fields });
837
+ setEnvironments((prev) =>
838
+ prev.map((env) => {
839
+ if (env.id !== environmentId) {
840
+ return env;
841
+ }
842
+ return {
843
+ ...env,
844
+ ...(fields.displayName !== undefined ? { displayName: fields.displayName } : {}),
845
+ ...(fields.adapterConfig !== undefined
846
+ ? { adapterConfig: JSON.stringify(fields.adapterConfig) }
847
+ : {}),
848
+ };
849
+ }),
850
+ );
851
+ },
852
+ [],
853
+ );
854
+
855
+ // ── Token methods ──────────────────────────────────
856
+
857
+ /** No-op — tokens are already in state from initial load. */
858
+ const loadTokens: UseGrackleSocketResult["tokens"]["loadTokens"] = useCallback(async () => {
859
+ console.log("[MockGrackle] loadTokens");
860
+ }, []);
861
+
862
+ /** Adds or replaces a token in state. */
863
+ const mockSetToken: UseGrackleSocketResult["tokens"]["setToken"] = useCallback(
864
+ async (name: string, _value: string, tokenType: string, envVar: string, filePath: string) => {
865
+ console.log("[MockGrackle] setToken", { name, tokenType });
866
+ setTokens((prev) => {
867
+ const without = prev.filter((t) => t.name !== name);
868
+ return [...without, { name, tokenType, envVar, filePath, expiresAt: "" }];
869
+ });
870
+ },
871
+ [],
872
+ );
873
+
874
+ /** Removes a token from state. */
875
+ const mockDeleteToken: UseGrackleSocketResult["tokens"]["deleteToken"] = useCallback(
876
+ async (name: string) => {
877
+ console.log("[MockGrackle] deleteToken", name);
878
+ setTokens((prev) => prev.filter((t) => t.name !== name));
879
+ },
880
+ [],
881
+ );
882
+
883
+ /** Updates credential provider configuration in state. */
884
+ const mockUpdateCredentialProviders: UseGrackleSocketResult["credentials"]["updateCredentialProviders"] = useCallback(
885
+ async (config: CredentialProviderConfig) => {
886
+ console.log("[MockGrackle] updateCredentialProviders", config);
887
+ setCredentialProviders(config);
888
+ },
889
+ [],
890
+ );
891
+
892
+ // ── Cleanup ───────────────────────────────────────
893
+
894
+ useEffect(() => {
895
+ return () => {
896
+ timersRef.current.forEach(clearTimeout);
897
+ timersRef.current.clear();
898
+ };
899
+ }, []);
900
+
901
+ // ── Context Value ─────────────────────────────────
902
+
903
+ const value: UseGrackleSocketResult = useMemo(
904
+ () => ({
905
+ connectionStatus: "connected",
906
+
907
+ // ── Domain groups ────────────────────────────────
908
+
909
+ environments: {
910
+ environments,
911
+ environmentsLoading: false,
912
+ provisionStatus: {},
913
+ operationError: "",
914
+ clearOperationError: () => { },
915
+ loadEnvironments,
916
+ addEnvironment,
917
+ updateEnvironment,
918
+ provisionEnvironment: async (_environmentId: string, _force?: boolean) => { },
919
+ stopEnvironment: async () => { },
920
+ removeEnvironment: async () => { },
921
+ domainHook: NOOP_DOMAIN_HOOK,
922
+ },
923
+
924
+ sessions: {
925
+ sessions,
926
+ sessionsLoading: false,
927
+ events,
928
+ eventsDropped: 0,
929
+ lastSpawnedId,
930
+ taskSessions,
931
+ spawn,
932
+ sendInput,
933
+ kill,
934
+ stopGraceful,
935
+ loadSessionEvents,
936
+ clearEvents,
937
+ loadTaskSessions: async (taskId: string) => {
938
+ console.log("[MockGrackle] loadTaskSessions", taskId);
939
+ },
940
+ domainHook: NOOP_DOMAIN_HOOK,
941
+ },
942
+
943
+ workspaces: {
944
+ workspaces,
945
+ workspacesLoading: false,
946
+ workspaceCreating: false,
947
+ loadWorkspaces: async () => { console.log("[MockGrackle] loadWorkspaces"); },
948
+ createWorkspace,
949
+ archiveWorkspace,
950
+ updateWorkspace: async (workspaceId: string, fields: { name?: string; description?: string; repoUrl?: string; environmentId?: string; workingDirectory?: string; useWorktrees?: boolean; defaultPersonaId?: string }) => {
951
+ console.log("[MockGrackle] updateWorkspace", { workspaceId, ...fields });
952
+ setWorkspaces((prev) =>
953
+ prev.map((p) => {
954
+ if (p.id !== workspaceId) {
955
+ return p;
956
+ }
957
+ return {
958
+ ...p,
959
+ ...(fields.name !== undefined ? { name: fields.name } : {}),
960
+ ...(fields.description !== undefined ? { description: fields.description } : {}),
961
+ ...(fields.repoUrl !== undefined ? { repoUrl: fields.repoUrl } : {}),
962
+ ...(fields.environmentId !== undefined ? { environmentId: fields.environmentId } : {}),
963
+ ...(fields.workingDirectory !== undefined ? { workingDirectory: fields.workingDirectory } : {}),
964
+ ...(fields.useWorktrees !== undefined ? { useWorktrees: fields.useWorktrees } : {}),
965
+ ...(fields.defaultPersonaId !== undefined ? { defaultPersonaId: fields.defaultPersonaId } : {}),
966
+ updatedAt: new Date().toISOString(),
967
+ };
968
+ }),
969
+ );
970
+ },
971
+ linkEnvironment: async (workspaceId: string, environmentId: string) => {
972
+ console.log("[MockGrackle] linkEnvironment", { workspaceId, environmentId });
973
+ if (environmentId === "error-env") {
974
+ setWorkspaceLinkError("Failed to link environment");
975
+ return;
976
+ }
977
+ setWorkspaceLinkError("");
978
+ setWorkspaces((prev) =>
979
+ prev.map((p) => {
980
+ if (p.id !== workspaceId) {
981
+ return p;
982
+ }
983
+ if (p.linkedEnvironmentIds.includes(environmentId)) {
984
+ return p;
985
+ }
986
+ return { ...p, linkedEnvironmentIds: [...p.linkedEnvironmentIds, environmentId] };
987
+ }),
988
+ );
989
+ },
990
+ unlinkEnvironment: async (workspaceId: string, environmentId: string) => {
991
+ console.log("[MockGrackle] unlinkEnvironment", { workspaceId, environmentId });
992
+ if (environmentId === "error-env") {
993
+ setWorkspaceLinkError("Failed to unlink environment");
994
+ return;
995
+ }
996
+ setWorkspaceLinkError("");
997
+ setWorkspaces((prev) =>
998
+ prev.map((p) => {
999
+ if (p.id !== workspaceId) {
1000
+ return p;
1001
+ }
1002
+ return { ...p, linkedEnvironmentIds: p.linkedEnvironmentIds.filter((id) => id !== environmentId) };
1003
+ }),
1004
+ );
1005
+ },
1006
+ linkOperationError: workspaceLinkError,
1007
+ clearLinkOperationError: () => { setWorkspaceLinkError(""); },
1008
+ domainHook: NOOP_DOMAIN_HOOK,
1009
+ },
1010
+
1011
+ tasks: {
1012
+ tasks,
1013
+ tasksLoading: false,
1014
+ taskStartingId: undefined,
1015
+ loadTasks,
1016
+ loadAllTasks: async () => {
1017
+ console.log("[MockGrackle] loadAllTasks");
1018
+ },
1019
+ createTask,
1020
+ startTask,
1021
+ stopTask: async (taskId: string) => {
1022
+ console.log("[MockGrackle] stopTask", { taskId });
1023
+ await completeTask(taskId);
1024
+ },
1025
+ completeTask,
1026
+ resumeTask,
1027
+ updateTask,
1028
+ deleteTask,
1029
+ domainHook: NOOP_DOMAIN_HOOK,
1030
+ },
1031
+
1032
+ findings: {
1033
+ findings,
1034
+ selectedFinding,
1035
+ findingLoading,
1036
+ findingsLoading: false,
1037
+ loadFindings,
1038
+ loadAllFindings,
1039
+ loadFinding,
1040
+ postFinding,
1041
+ domainHook: NOOP_DOMAIN_HOOK,
1042
+ },
1043
+
1044
+ tokens: {
1045
+ tokens,
1046
+ tokensLoading: false,
1047
+ loadTokens,
1048
+ setToken: mockSetToken,
1049
+ deleteToken: mockDeleteToken,
1050
+ domainHook: NOOP_DOMAIN_HOOK,
1051
+ },
1052
+
1053
+ credentials: {
1054
+ credentialProviders,
1055
+ credentialsLoading: false,
1056
+ updateCredentialProviders: mockUpdateCredentialProviders,
1057
+ domainHook: NOOP_DOMAIN_HOOK,
1058
+ },
1059
+
1060
+ codespaces: {
1061
+ codespaces: [],
1062
+ codespaceError: "",
1063
+ codespaceListError: "",
1064
+ codespaceCreating: false,
1065
+ listCodespaces: async () => { },
1066
+ createCodespace: async () => { },
1067
+ domainHook: NOOP_DOMAIN_HOOK,
1068
+ },
1069
+
1070
+ personas: {
1071
+ personas,
1072
+ personasLoading: false,
1073
+ createPersona: async (name: string, description: string, systemPrompt: string, runtime?: string, model?: string, maxTurns?: number, type?: string, script?: string, allowedMcpTools?: string[]) => {
1074
+ console.log("[MockGrackle] createPersona", { name });
1075
+ const newPersona: PersonaData = {
1076
+ id: `mock-persona-${Date.now()}`,
1077
+ name,
1078
+ description,
1079
+ systemPrompt,
1080
+ toolConfig: "{}",
1081
+ runtime: runtime ?? "claude-code",
1082
+ model: model || "",
1083
+ maxTurns: maxTurns || 0,
1084
+ mcpServers: "[]",
1085
+ createdAt: new Date().toISOString(),
1086
+ updatedAt: new Date().toISOString(),
1087
+ type: type || "agent",
1088
+ script: script || "",
1089
+ allowedMcpTools: allowedMcpTools || [],
1090
+ };
1091
+ setPersonas((prev) => [...prev, newPersona]);
1092
+ return newPersona;
1093
+ },
1094
+ updatePersona: async (personaId: string, name?: string, description?: string, systemPrompt?: string, runtime?: string, model?: string, maxTurns?: number, type?: string, script?: string, allowedMcpTools?: string[]) => {
1095
+ console.log("[MockGrackle] updatePersona", { personaId, name });
1096
+ const existingPersona = personas.find((persona) => persona.id === personaId);
1097
+ if (!existingPersona) {
1098
+ throw new Error(`Persona not found: ${personaId}`);
1099
+ }
1100
+
1101
+ const updatedAt = new Date().toISOString();
1102
+ const updatedPersona: PersonaData = {
1103
+ ...existingPersona,
1104
+ ...(name !== undefined ? { name } : {}),
1105
+ ...(description !== undefined ? { description } : {}),
1106
+ ...(systemPrompt !== undefined ? { systemPrompt } : {}),
1107
+ ...(runtime !== undefined ? { runtime } : {}),
1108
+ ...(model !== undefined ? { model } : {}),
1109
+ ...(maxTurns !== undefined ? { maxTurns } : {}),
1110
+ ...(type !== undefined ? { type } : {}),
1111
+ ...(script !== undefined ? { script } : {}),
1112
+ ...(allowedMcpTools !== undefined ? { allowedMcpTools } : {}),
1113
+ updatedAt,
1114
+ };
1115
+
1116
+ setPersonas((prev) =>
1117
+ prev.map((persona) => (persona.id === personaId ? updatedPersona : persona)),
1118
+ );
1119
+ return updatedPersona;
1120
+ },
1121
+ deletePersona: async (personaId: string) => {
1122
+ console.log("[MockGrackle] deletePersona", personaId);
1123
+ setPersonas((prev) => prev.filter((p) => p.id !== personaId));
1124
+ },
1125
+ domainHook: NOOP_DOMAIN_HOOK,
1126
+ },
1127
+
1128
+ schedules: {
1129
+ schedules,
1130
+ schedulesLoading: false,
1131
+ createSchedule: async (title: string, description: string, scheduleExpression: string, personaId: string, workspaceId?: string) => {
1132
+ console.log("[MockGrackle] createSchedule", { title });
1133
+ const newSchedule: ScheduleData = {
1134
+ id: `mock-schedule-${Date.now()}`,
1135
+ title,
1136
+ description,
1137
+ scheduleExpression,
1138
+ personaId,
1139
+ workspaceId: workspaceId ?? "",
1140
+ parentTaskId: "",
1141
+ enabled: true,
1142
+ lastRunAt: "",
1143
+ nextRunAt: new Date(Date.now() + 60_000).toISOString(),
1144
+ runCount: 0,
1145
+ createdAt: new Date().toISOString(),
1146
+ updatedAt: new Date().toISOString(),
1147
+ };
1148
+ setSchedules((prev) => [...prev, newSchedule]);
1149
+ return newSchedule;
1150
+ },
1151
+ updateSchedule: async (scheduleId: string, fields: Partial<ScheduleData>) => {
1152
+ console.log("[MockGrackle] updateSchedule", { scheduleId, fields });
1153
+ let updated: ScheduleData | undefined;
1154
+ setSchedules((prev) => prev.map((s) => {
1155
+ if (s.id !== scheduleId) { return s; }
1156
+ updated = { ...s, ...fields, updatedAt: new Date().toISOString() };
1157
+ return updated;
1158
+ }));
1159
+ if (!updated) {
1160
+ throw new Error(`Schedule not found: ${scheduleId}`);
1161
+ }
1162
+ return updated;
1163
+ },
1164
+ deleteSchedule: async (scheduleId: string) => {
1165
+ console.log("[MockGrackle] deleteSchedule", scheduleId);
1166
+ setSchedules((prev) => prev.filter((s) => s.id !== scheduleId));
1167
+ },
1168
+ domainHook: NOOP_DOMAIN_HOOK,
1169
+ },
1170
+
1171
+ streams: {
1172
+ streams: [],
1173
+ streamsLoading: false,
1174
+ streamsLoadedOnce: true,
1175
+ streamsLoadError: false,
1176
+ loadStreams: async () => {},
1177
+ domainHook: NOOP_DOMAIN_HOOK,
1178
+ },
1179
+
1180
+ knowledge: {
1181
+ graphData: { nodes: knowledgeNodes, links: knowledgeLinks },
1182
+ selectedNode: knowledgeSelectedNode,
1183
+ loading: false,
1184
+ selectedId: knowledgeSelectedId,
1185
+ searchQuery: knowledgeSearchQuery,
1186
+ search: async (query: string) => {
1187
+ console.log("[MockGrackle] knowledge.search", query);
1188
+ if (!query.trim()) {
1189
+ setKnowledgeSearchQuery(query);
1190
+ return;
1191
+ }
1192
+ setKnowledgeSearchQuery(query);
1193
+ // Start from workspace-scoped base set, not the full graph
1194
+ const wsId = knowledgeWorkspaceRef.current;
1195
+ const baseNodes = wsId
1196
+ ? MOCK_KNOWLEDGE_NODES.filter((n) => !n.workspaceId || n.workspaceId === wsId)
1197
+ : MOCK_KNOWLEDGE_NODES;
1198
+ const lowerQuery = query.toLowerCase();
1199
+ const filtered = baseNodes.filter((n) =>
1200
+ n.label.toLowerCase().includes(lowerQuery)
1201
+ || n.content?.toLowerCase().includes(lowerQuery)
1202
+ || n.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery))
1203
+ || n.category?.toLowerCase().includes(lowerQuery),
1204
+ );
1205
+ setKnowledgeNodes(filtered);
1206
+ const nodeIds = new Set(filtered.map((n) => n.id));
1207
+ setKnowledgeLinks(
1208
+ MOCK_KNOWLEDGE_LINKS.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target)),
1209
+ );
1210
+ setKnowledgeSelectedId(undefined);
1211
+ setKnowledgeSelectedNode(undefined);
1212
+ },
1213
+ clearSearch: () => {
1214
+ console.log("[MockGrackle] knowledge.clearSearch");
1215
+ setKnowledgeSearchQuery("");
1216
+ // Restore to workspace-scoped base set, not the full graph
1217
+ const wsId = knowledgeWorkspaceRef.current;
1218
+ const baseNodes = wsId
1219
+ ? MOCK_KNOWLEDGE_NODES.filter((n) => !n.workspaceId || n.workspaceId === wsId)
1220
+ : MOCK_KNOWLEDGE_NODES;
1221
+ setKnowledgeNodes(baseNodes);
1222
+ const nodeIds = new Set(baseNodes.map((n) => n.id));
1223
+ setKnowledgeLinks(
1224
+ MOCK_KNOWLEDGE_LINKS.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target)),
1225
+ );
1226
+ },
1227
+ selectNode: async (id: string) => {
1228
+ console.log("[MockGrackle] knowledge.selectNode", id);
1229
+ setKnowledgeSelectedId(id);
1230
+ const detail: NodeDetail | undefined = id in MOCK_KNOWLEDGE_DETAILS
1231
+ ? MOCK_KNOWLEDGE_DETAILS[id]
1232
+ : undefined;
1233
+ if (detail) {
1234
+ setKnowledgeSelectedNode(detail);
1235
+ } else {
1236
+ // Build a detail from the node and its edges
1237
+ const node = MOCK_KNOWLEDGE_NODES.find((n) => n.id === id);
1238
+ if (node) {
1239
+ const edges = MOCK_KNOWLEDGE_LINKS
1240
+ .filter((l) => l.source === id || l.target === id)
1241
+ .map((l) => ({ fromId: l.source, toId: l.target, type: l.type }));
1242
+ setKnowledgeSelectedNode({ node, edges });
1243
+ }
1244
+ }
1245
+ },
1246
+ clearSelection: () => {
1247
+ console.log("[MockGrackle] knowledge.clearSelection");
1248
+ setKnowledgeSelectedId(undefined);
1249
+ setKnowledgeSelectedNode(undefined);
1250
+ },
1251
+ expandNode: async (id: string) => {
1252
+ console.log("[MockGrackle] knowledge.expandNode", id);
1253
+ // Use functional updaters to avoid stale closure over knowledgeNodes/knowledgeLinks
1254
+ setKnowledgeNodes((prevNodes) => {
1255
+ const currentIds = new Set(prevNodes.map((n) => n.id));
1256
+ const connectedLinks = MOCK_KNOWLEDGE_LINKS.filter(
1257
+ (l) => l.source === id || l.target === id,
1258
+ );
1259
+ const newNodeIds = new Set<string>();
1260
+ for (const link of connectedLinks) {
1261
+ if (!currentIds.has(link.source)) { newNodeIds.add(link.source); }
1262
+ if (!currentIds.has(link.target)) { newNodeIds.add(link.target); }
1263
+ }
1264
+ if (newNodeIds.size === 0) {
1265
+ return prevNodes;
1266
+ }
1267
+ const newNodes = MOCK_KNOWLEDGE_NODES.filter((n) => newNodeIds.has(n.id));
1268
+ const allIds = new Set([...currentIds, ...newNodeIds]);
1269
+ // Update links inside its own functional updater using the computed allIds
1270
+ setKnowledgeLinks((prevLinks) => {
1271
+ const existingSet = new Set(prevLinks.map((l) => `${l.source}|${l.target}|${l.type}`));
1272
+ const newLinks = MOCK_KNOWLEDGE_LINKS.filter(
1273
+ (l) => allIds.has(l.source) && allIds.has(l.target)
1274
+ && !existingSet.has(`${l.source}|${l.target}|${l.type}`),
1275
+ );
1276
+ return newLinks.length > 0 ? [...prevLinks, ...newLinks] : prevLinks;
1277
+ });
1278
+ return [...prevNodes, ...newNodes];
1279
+ });
1280
+ },
1281
+ loadRecent: async (workspaceId?: string) => {
1282
+ console.log("[MockGrackle] knowledge.loadRecent", workspaceId);
1283
+ knowledgeWorkspaceRef.current = workspaceId;
1284
+ if (workspaceId) {
1285
+ const filtered = MOCK_KNOWLEDGE_NODES.filter(
1286
+ (n) => !n.workspaceId || n.workspaceId === workspaceId,
1287
+ );
1288
+ setKnowledgeNodes(filtered);
1289
+ const nodeIds = new Set(filtered.map((n) => n.id));
1290
+ setKnowledgeLinks(
1291
+ MOCK_KNOWLEDGE_LINKS.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target)),
1292
+ );
1293
+ } else {
1294
+ setKnowledgeNodes(MOCK_KNOWLEDGE_NODES);
1295
+ setKnowledgeLinks(MOCK_KNOWLEDGE_LINKS);
1296
+ }
1297
+ setKnowledgeSearchQuery("");
1298
+ },
1299
+ handleEvent: () => false,
1300
+ domainHook: NOOP_DOMAIN_HOOK,
1301
+ },
1302
+
1303
+ // ── Plugins ─────────────────────────────────────
1304
+
1305
+ plugins: {
1306
+ plugins: [
1307
+ { name: "core", description: "Core infrastructure", enabled: true, required: true, loaded: true },
1308
+ { name: "orchestration", description: "Task orchestration", enabled: true, required: false, loaded: true },
1309
+ { name: "scheduling", description: "Scheduled triggers", enabled: true, required: false, loaded: true },
1310
+ { name: "knowledge", description: "Knowledge graph", enabled: false, required: false, loaded: false },
1311
+ ],
1312
+ pluginsLoading: false,
1313
+ loadPlugins: async () => { console.log("[MockGrackle] loadPlugins"); },
1314
+ setPluginEnabled: async (name: string, enabled: boolean) => {
1315
+ console.log("[MockGrackle] setPluginEnabled", name, enabled);
1316
+ },
1317
+ },
1318
+
1319
+ // ── Top-level properties ─────────────────────────
1320
+
1321
+ appDefaultPersonaId,
1322
+ setAppDefaultPersonaId: async (personaId: string) => {
1323
+ console.log("[MockGrackle] setAppDefaultPersonaId", personaId);
1324
+ setAppDefaultPersonaIdState(personaId);
1325
+ },
1326
+ onboardingCompleted: true,
1327
+ completeOnboarding: async () => {
1328
+ console.log("[MockGrackle] completeOnboarding");
1329
+ },
1330
+ usageCache: {
1331
+ "workspace:proj-alpha": { inputTokens: 214_500, outputTokens: 44_850, costMillicents: 112_000, sessionCount: 4 },
1332
+ "workspace:proj-beta": { inputTokens: 86_700, outputTokens: 23_800, costMillicents: 48_000, sessionCount: 3 },
1333
+ "task:task-001": { inputTokens: 126_800, outputTokens: 20_850, costMillicents: 63_000, sessionCount: 2 },
1334
+ "task:task-006": { inputTokens: 18_900, outputTokens: 4_500, costMillicents: 10_000, sessionCount: 1 },
1335
+ "task_tree:task-001": { inputTokens: 126_800, outputTokens: 20_850, costMillicents: 63_000, sessionCount: 2 },
1336
+ "task_tree:task-006": { inputTokens: 18_900, outputTokens: 4_500, costMillicents: 10_000, sessionCount: 1 },
1337
+ },
1338
+ loadUsage: async (scope: string, id: string) => {
1339
+ console.log(`[MockGrackle] loadUsage(${scope}, ${id})`);
1340
+ },
1341
+ refresh,
1342
+ }),
1343
+ [
1344
+ environments,
1345
+ sessions,
1346
+ events,
1347
+ lastSpawnedId,
1348
+ taskSessions,
1349
+ workspaces,
1350
+ workspaceLinkError,
1351
+ tasks,
1352
+ findings,
1353
+ selectedFinding,
1354
+ tokens,
1355
+ credentialProviders,
1356
+ personas,
1357
+ schedules,
1358
+ appDefaultPersonaId,
1359
+ knowledgeNodes,
1360
+ knowledgeLinks,
1361
+ knowledgeSelectedNode,
1362
+ knowledgeSelectedId,
1363
+ knowledgeSearchQuery,
1364
+ spawn,
1365
+ sendInput,
1366
+ kill,
1367
+ stopGraceful,
1368
+ loadSessionEvents,
1369
+ clearEvents,
1370
+ createWorkspace,
1371
+ archiveWorkspace,
1372
+ loadTasks,
1373
+ createTask,
1374
+ startTask,
1375
+ completeTask,
1376
+ resumeTask,
1377
+ updateTask,
1378
+ deleteTask,
1379
+ loadFindings,
1380
+ loadAllFindings,
1381
+ loadFinding,
1382
+ postFinding,
1383
+ loadEnvironments,
1384
+ addEnvironment,
1385
+ updateEnvironment,
1386
+ loadTokens,
1387
+ mockSetToken,
1388
+ mockDeleteToken,
1389
+ mockUpdateCredentialProviders,
1390
+ refresh,
1391
+ ],
1392
+ );
1393
+
1394
+ return (
1395
+ <GrackleContext.Provider value={value}>{children}</GrackleContext.Provider>
1396
+ );
1397
+ }