@elizaos/client 1.5.5-alpha.10

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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/assets/empty-module-CLMscLYw.js +1 -0
  4. package/dist/assets/main-BBZ_3lkn.css +5999 -0
  5. package/dist/assets/main-C5zNUkXH.js +7 -0
  6. package/dist/assets/main-Dz64ENQg.js +614 -0
  7. package/dist/assets/react-vendor-DM5m98rr.js +545 -0
  8. package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
  9. package/dist/elizaos-avatar.png +0 -0
  10. package/dist/elizaos-icon.png +0 -0
  11. package/dist/elizaos-logo-light.png +0 -0
  12. package/dist/elizaos.webp +0 -0
  13. package/dist/favicon.ico +0 -0
  14. package/dist/images/agents/agent1.png +0 -0
  15. package/dist/images/agents/agent2.png +0 -0
  16. package/dist/images/agents/agent3.png +0 -0
  17. package/dist/images/agents/agent4.png +0 -0
  18. package/dist/images/agents/agent5.png +0 -0
  19. package/dist/index.html +14 -0
  20. package/index.html +24 -0
  21. package/package.json +159 -0
  22. package/postcss.config.js +3 -0
  23. package/public/elizaos-avatar.png +0 -0
  24. package/public/elizaos-icon.png +0 -0
  25. package/public/elizaos-logo-light.png +0 -0
  26. package/public/elizaos.webp +0 -0
  27. package/public/favicon.ico +0 -0
  28. package/public/images/agents/agent1.png +0 -0
  29. package/public/images/agents/agent2.png +0 -0
  30. package/public/images/agents/agent3.png +0 -0
  31. package/public/images/agents/agent4.png +0 -0
  32. package/public/images/agents/agent5.png +0 -0
  33. package/src/App.tsx +222 -0
  34. package/src/components/AgentDetailsPanel.tsx +147 -0
  35. package/src/components/ChatInputArea.tsx +196 -0
  36. package/src/components/ChatMessageListComponent.tsx +139 -0
  37. package/src/components/actionTool.tsx +186 -0
  38. package/src/components/add-agent-card.tsx +77 -0
  39. package/src/components/agent-action-viewer.tsx +816 -0
  40. package/src/components/agent-avatar-stack.tsx +121 -0
  41. package/src/components/agent-card.cy.tsx +259 -0
  42. package/src/components/agent-card.tsx +177 -0
  43. package/src/components/agent-creator.tsx +142 -0
  44. package/src/components/agent-log-viewer.tsx +645 -0
  45. package/src/components/agent-memory-edit-overlay.tsx +461 -0
  46. package/src/components/agent-memory-viewer.tsx +504 -0
  47. package/src/components/agent-settings.tsx +270 -0
  48. package/src/components/agent-sidebar.tsx +178 -0
  49. package/src/components/api-key-dialog.tsx +113 -0
  50. package/src/components/app-sidebar.tsx +685 -0
  51. package/src/components/array-input.tsx +116 -0
  52. package/src/components/audio-recorder.tsx +292 -0
  53. package/src/components/avatar-panel.tsx +141 -0
  54. package/src/components/character-form.tsx +1138 -0
  55. package/src/components/chat.tsx +1813 -0
  56. package/src/components/combobox.tsx +187 -0
  57. package/src/components/confirmation-dialog.tsx +59 -0
  58. package/src/components/connection-error-banner.tsx +101 -0
  59. package/src/components/connection-status.cy.tsx +73 -0
  60. package/src/components/connection-status.tsx +155 -0
  61. package/src/components/copy-button.tsx +35 -0
  62. package/src/components/delete-button.tsx +24 -0
  63. package/src/components/env-settings.tsx +261 -0
  64. package/src/components/group-card.tsx +160 -0
  65. package/src/components/group-panel.tsx +543 -0
  66. package/src/components/input-copy.tsx +21 -0
  67. package/src/components/logs-page.tsx +41 -0
  68. package/src/components/media-content.tsx +385 -0
  69. package/src/components/memory-graph.tsx +170 -0
  70. package/src/components/missing-secrets-dialog.tsx +72 -0
  71. package/src/components/onboarding-tour.tsx +247 -0
  72. package/src/components/page-title.tsx +8 -0
  73. package/src/components/plugins-panel.tsx +383 -0
  74. package/src/components/profile-card.tsx +66 -0
  75. package/src/components/profile-overlay.tsx +283 -0
  76. package/src/components/retry-button.tsx +28 -0
  77. package/src/components/secret-panel.tsx +1505 -0
  78. package/src/components/server-management.tsx +264 -0
  79. package/src/components/split-button.tsx +148 -0
  80. package/src/components/stop-agent-button.tsx +99 -0
  81. package/src/components/ui/alert-dialog.cy.tsx +333 -0
  82. package/src/components/ui/alert-dialog.tsx +115 -0
  83. package/src/components/ui/alert.tsx +49 -0
  84. package/src/components/ui/avatar.cy.tsx +180 -0
  85. package/src/components/ui/avatar.tsx +57 -0
  86. package/src/components/ui/badge.cy.tsx +146 -0
  87. package/src/components/ui/badge.tsx +43 -0
  88. package/src/components/ui/button.cy.tsx +177 -0
  89. package/src/components/ui/button.tsx +56 -0
  90. package/src/components/ui/card.cy.tsx +160 -0
  91. package/src/components/ui/card.tsx +73 -0
  92. package/src/components/ui/chat/animated-markdown.tsx +59 -0
  93. package/src/components/ui/chat/chat-bubble.tsx +178 -0
  94. package/src/components/ui/chat/chat-container.tsx +51 -0
  95. package/src/components/ui/chat/chat-input.cy.tsx +169 -0
  96. package/src/components/ui/chat/chat-input.tsx +47 -0
  97. package/src/components/ui/chat/chat-message-list.tsx +61 -0
  98. package/src/components/ui/chat/chat-tts-button.tsx +199 -0
  99. package/src/components/ui/chat/code-block.tsx +79 -0
  100. package/src/components/ui/chat/expandable-chat.tsx +131 -0
  101. package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
  102. package/src/components/ui/chat/markdown.tsx +209 -0
  103. package/src/components/ui/chat/message-loading.tsx +48 -0
  104. package/src/components/ui/checkbox.cy.tsx +170 -0
  105. package/src/components/ui/checkbox.tsx +30 -0
  106. package/src/components/ui/collapsible.cy.tsx +283 -0
  107. package/src/components/ui/collapsible.tsx +9 -0
  108. package/src/components/ui/command.cy.tsx +313 -0
  109. package/src/components/ui/command.tsx +143 -0
  110. package/src/components/ui/dialog.cy.tsx +279 -0
  111. package/src/components/ui/dialog.tsx +104 -0
  112. package/src/components/ui/dropdown-menu.cy.tsx +273 -0
  113. package/src/components/ui/dropdown-menu.tsx +281 -0
  114. package/src/components/ui/input.cy.tsx +82 -0
  115. package/src/components/ui/input.tsx +27 -0
  116. package/src/components/ui/label.cy.tsx +157 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/resizable.tsx +42 -0
  119. package/src/components/ui/scroll-area.cy.tsx +242 -0
  120. package/src/components/ui/scroll-area.tsx +46 -0
  121. package/src/components/ui/select.cy.tsx +277 -0
  122. package/src/components/ui/select.tsx +155 -0
  123. package/src/components/ui/separator.cy.tsx +145 -0
  124. package/src/components/ui/separator.tsx +29 -0
  125. package/src/components/ui/sheet.cy.tsx +324 -0
  126. package/src/components/ui/sheet.tsx +119 -0
  127. package/src/components/ui/sidebar.tsx +734 -0
  128. package/src/components/ui/skeleton.cy.tsx +149 -0
  129. package/src/components/ui/skeleton.tsx +17 -0
  130. package/src/components/ui/split-button.cy.tsx +274 -0
  131. package/src/components/ui/split-button.tsx +112 -0
  132. package/src/components/ui/switch.tsx +28 -0
  133. package/src/components/ui/tabs.cy.tsx +271 -0
  134. package/src/components/ui/tabs.tsx +53 -0
  135. package/src/components/ui/textarea.cy.tsx +136 -0
  136. package/src/components/ui/textarea.tsx +26 -0
  137. package/src/components/ui/toast.cy.tsx +209 -0
  138. package/src/components/ui/toast.tsx +126 -0
  139. package/src/components/ui/toaster.tsx +29 -0
  140. package/src/components/ui/tooltip.cy.tsx +244 -0
  141. package/src/components/ui/tooltip.tsx +30 -0
  142. package/src/config/agent-templates.ts +349 -0
  143. package/src/config/voice-models.ts +181 -0
  144. package/src/constants.ts +23 -0
  145. package/src/context/AuthContext.tsx +44 -0
  146. package/src/context/ConnectionContext.tsx +194 -0
  147. package/src/entry.tsx +9 -0
  148. package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
  149. package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
  150. package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
  151. package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
  152. package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
  153. package/src/hooks/use-agent-management.ts +130 -0
  154. package/src/hooks/use-agent-tab-state.ts +74 -0
  155. package/src/hooks/use-agent-update.ts +469 -0
  156. package/src/hooks/use-character-convert.ts +138 -0
  157. package/src/hooks/use-confirmation.ts +55 -0
  158. package/src/hooks/use-delete-agent.ts +123 -0
  159. package/src/hooks/use-dm-channels.ts +198 -0
  160. package/src/hooks/use-elevenlabs-voices.ts +83 -0
  161. package/src/hooks/use-file-upload.ts +224 -0
  162. package/src/hooks/use-mobile.tsx +19 -0
  163. package/src/hooks/use-onboarding.tsx +49 -0
  164. package/src/hooks/use-panel-width-state.ts +147 -0
  165. package/src/hooks/use-partial-update.ts +288 -0
  166. package/src/hooks/use-plugin-details.ts +462 -0
  167. package/src/hooks/use-plugins.ts +119 -0
  168. package/src/hooks/use-query-hooks.ts +1263 -0
  169. package/src/hooks/use-server-agents.ts +62 -0
  170. package/src/hooks/use-server-version.tsx +47 -0
  171. package/src/hooks/use-sidebar-state.ts +50 -0
  172. package/src/hooks/use-socket-chat.ts +264 -0
  173. package/src/hooks/use-toast.ts +260 -0
  174. package/src/hooks/use-version.tsx +64 -0
  175. package/src/index.css +146 -0
  176. package/src/lib/api-client-config.ts +53 -0
  177. package/src/lib/api-type-mappers.ts +196 -0
  178. package/src/lib/export-utils.ts +123 -0
  179. package/src/lib/logger.ts +19 -0
  180. package/src/lib/media-utils.ts +170 -0
  181. package/src/lib/pca.test.ts +17 -0
  182. package/src/lib/pca.ts +52 -0
  183. package/src/lib/socketio-manager.ts +664 -0
  184. package/src/lib/utils.ts +168 -0
  185. package/src/main.tsx +16 -0
  186. package/src/mocks/empty-module.ts +12 -0
  187. package/src/mocks/node-module.ts +57 -0
  188. package/src/polyfills.ts +37 -0
  189. package/src/routes/agent-detail.tsx +30 -0
  190. package/src/routes/agent-list.tsx +27 -0
  191. package/src/routes/agent-settings.tsx +48 -0
  192. package/src/routes/character-detail.tsx +52 -0
  193. package/src/routes/character-form.tsx +79 -0
  194. package/src/routes/character-list.tsx +38 -0
  195. package/src/routes/chat.tsx +128 -0
  196. package/src/routes/createAgent.tsx +13 -0
  197. package/src/routes/group-new.tsx +50 -0
  198. package/src/routes/group.tsx +29 -0
  199. package/src/routes/home.tsx +218 -0
  200. package/src/routes/not-found.tsx +71 -0
  201. package/src/test/setup.ts +154 -0
  202. package/src/types/crypto-browserify.d.ts +4 -0
  203. package/src/types/index.ts +13 -0
  204. package/src/types/rooms.ts +8 -0
  205. package/src/types.ts +84 -0
  206. package/src/vite-env.d.ts +40 -0
  207. package/tailwind.config.ts +90 -0
  208. package/tsconfig.json +10 -0
  209. package/vite.config.ts +102 -0
@@ -0,0 +1,243 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, beforeEach, mock } from 'bun:test';
3
+ import { usePanelWidthState } from '../use-panel-width-state';
4
+
5
+ // Mock clientLogger
6
+ mock.module('../../lib/logger', () => ({
7
+ default: {
8
+ error: mock(),
9
+ },
10
+ }));
11
+
12
+ describe('usePanelWidthState', () => {
13
+ beforeEach(() => {
14
+ // Clear localStorage before each test
15
+ localStorage.clear();
16
+
17
+ // Reset window dimensions (above 1400 threshold)
18
+ Object.defineProperty(window, 'innerWidth', {
19
+ writable: true,
20
+ configurable: true,
21
+ value: 1500,
22
+ });
23
+ });
24
+
25
+ it('should initialize with default values when localStorage is empty', () => {
26
+ const { result } = renderHook(() => usePanelWidthState());
27
+
28
+ expect(result.current.mainPanelSize).toBe(70);
29
+ expect(result.current.sidebarPanelSize).toBe(30);
30
+ expect(result.current.isFloatingMode).toBe(false);
31
+ expect(result.current.floatingThreshold).toBe(1400);
32
+ });
33
+
34
+ it('should load persisted values from localStorage on mount', async () => {
35
+ // Set up localStorage with saved values (use correct keys)
36
+ localStorage.setItem('eliza-main-panel-width', '65');
37
+ localStorage.setItem('eliza-sidebar-panel-width', '35');
38
+
39
+ const { result } = renderHook(() => usePanelWidthState());
40
+
41
+ await waitFor(() => {
42
+ expect(result.current.mainPanelSize).toBe(65);
43
+ expect(result.current.sidebarPanelSize).toBe(35);
44
+ });
45
+ });
46
+
47
+ it('should persist main panel size to localStorage when updated', async () => {
48
+ const { result } = renderHook(() => usePanelWidthState());
49
+
50
+ act(() => {
51
+ result.current.setMainPanelSize(75);
52
+ });
53
+
54
+ await waitFor(() => {
55
+ expect(result.current.mainPanelSize).toBe(75);
56
+ });
57
+
58
+ expect(localStorage.getItem('eliza-main-panel-width')).toBe('75');
59
+ });
60
+
61
+ it('should persist sidebar panel size to localStorage when updated', async () => {
62
+ const { result } = renderHook(() => usePanelWidthState());
63
+
64
+ act(() => {
65
+ result.current.setSidebarPanelSize(25);
66
+ });
67
+
68
+ await waitFor(() => {
69
+ expect(result.current.sidebarPanelSize).toBe(25);
70
+ });
71
+
72
+ expect(localStorage.getItem('eliza-sidebar-panel-width')).toBe('25');
73
+ });
74
+
75
+ it('should validate and clamp panel sizes within bounds', async () => {
76
+ const { result } = renderHook(() => usePanelWidthState());
77
+
78
+ // Test main panel size clamping
79
+ act(() => {
80
+ result.current.setMainPanelSize(5); // Below minimum (20)
81
+ });
82
+
83
+ await waitFor(() => {
84
+ expect(result.current.mainPanelSize).toBe(20);
85
+ });
86
+
87
+ act(() => {
88
+ result.current.setMainPanelSize(95); // Above maximum (80)
89
+ });
90
+
91
+ await waitFor(() => {
92
+ expect(result.current.mainPanelSize).toBe(80);
93
+ });
94
+
95
+ // Test sidebar panel size clamping
96
+ act(() => {
97
+ result.current.setSidebarPanelSize(15); // Below minimum (20)
98
+ });
99
+
100
+ await waitFor(() => {
101
+ expect(result.current.sidebarPanelSize).toBe(20);
102
+ });
103
+
104
+ act(() => {
105
+ result.current.setSidebarPanelSize(85); // Above maximum (80)
106
+ });
107
+
108
+ await waitFor(() => {
109
+ expect(result.current.sidebarPanelSize).toBe(80);
110
+ });
111
+ });
112
+
113
+ it('should reset panel sizes to defaults and clear localStorage', () => {
114
+ // Set up some custom values first
115
+ localStorage.setItem('eliza-main-panel-width', '60');
116
+ localStorage.setItem('eliza-sidebar-panel-width', '40');
117
+
118
+ const { result } = renderHook(() => usePanelWidthState());
119
+
120
+ // Verify initial persisted values
121
+ expect(result.current.mainPanelSize).toBe(60);
122
+ expect(result.current.sidebarPanelSize).toBe(40);
123
+
124
+ // Reset to defaults
125
+ act(() => {
126
+ result.current.resetPanelSizes();
127
+ });
128
+
129
+ expect(result.current.mainPanelSize).toBe(70);
130
+ expect(result.current.sidebarPanelSize).toBe(30);
131
+ expect(localStorage.getItem('eliza-main-panel-width')).toBe(null);
132
+ expect(localStorage.getItem('eliza-sidebar-panel-width')).toBe(null);
133
+ });
134
+
135
+ it('should handle localStorage errors gracefully when reading', () => {
136
+ // Set up invalid values
137
+ localStorage.setItem('eliza-main-panel-width', 'invalid');
138
+ localStorage.setItem('eliza-sidebar-panel-width', 'invalid');
139
+
140
+ const { result } = renderHook(() => usePanelWidthState());
141
+
142
+ // Should fall back to defaults
143
+ expect(result.current.mainPanelSize).toBe(70);
144
+ expect(result.current.sidebarPanelSize).toBe(30);
145
+ });
146
+
147
+ it('should handle localStorage errors gracefully when writing', () => {
148
+ const { result } = renderHook(() => usePanelWidthState());
149
+
150
+ // Mock localStorage to throw an error on setItem
151
+ const originalSetItem = localStorage.setItem;
152
+ localStorage.setItem = () => {
153
+ throw new Error('localStorage save error');
154
+ };
155
+
156
+ // Should still update state even if localStorage fails
157
+ act(() => {
158
+ result.current.setMainPanelSize(65);
159
+ });
160
+
161
+ expect(result.current.mainPanelSize).toBe(65);
162
+
163
+ // Restore original setItem immediately
164
+ localStorage.setItem = originalSetItem;
165
+ });
166
+
167
+ it('should handle invalid JSON values from localStorage', () => {
168
+ localStorage.setItem('eliza-main-panel-width', 'not-a-number');
169
+ localStorage.setItem('eliza-sidebar-panel-width', 'also-not-a-number');
170
+
171
+ const { result } = renderHook(() => usePanelWidthState());
172
+
173
+ // Should use defaults when values can't be parsed
174
+ expect(result.current.mainPanelSize).toBe(70);
175
+ expect(result.current.sidebarPanelSize).toBe(30);
176
+ });
177
+
178
+ it('should handle out-of-range values from localStorage', () => {
179
+ localStorage.setItem('eliza-main-panel-width', '150'); // Way too high (> 100)
180
+ localStorage.setItem('eliza-sidebar-panel-width', '0'); // Way too low (<= 0)
181
+
182
+ const { result } = renderHook(() => usePanelWidthState());
183
+
184
+ // Should fall back to defaults when values are out of valid loading range
185
+ expect(result.current.mainPanelSize).toBe(70); // Default value
186
+ expect(result.current.sidebarPanelSize).toBe(30); // Default value
187
+ });
188
+
189
+ it('should detect floating mode based on window width', () => {
190
+ // Set window width below threshold
191
+ Object.defineProperty(window, 'innerWidth', {
192
+ writable: true,
193
+ configurable: true,
194
+ value: 1300,
195
+ });
196
+
197
+ const { result } = renderHook(() => usePanelWidthState());
198
+
199
+ expect(result.current.isFloatingMode).toBe(true);
200
+ });
201
+
202
+ it('should toggle floating mode manually', () => {
203
+ const { result } = renderHook(() => usePanelWidthState());
204
+
205
+ expect(result.current.isFloatingMode).toBe(false);
206
+
207
+ act(() => {
208
+ result.current.toggleFloatingMode();
209
+ });
210
+
211
+ expect(result.current.isFloatingMode).toBe(true);
212
+ });
213
+
214
+ it('should update floating threshold and persist to localStorage', () => {
215
+ const { result } = renderHook(() => usePanelWidthState());
216
+
217
+ act(() => {
218
+ result.current.setFloatingThreshold(900);
219
+ });
220
+
221
+ expect(result.current.floatingThreshold).toBe(900);
222
+ expect(localStorage.getItem('eliza-floating-threshold')).toBe('900');
223
+ });
224
+
225
+ it('should respond to window resize events', () => {
226
+ const { result } = renderHook(() => usePanelWidthState());
227
+
228
+ // Initially not in floating mode (width 1500 > 1400)
229
+ expect(result.current.isFloatingMode).toBe(false);
230
+
231
+ act(() => {
232
+ // Simulate window resize to trigger floating mode
233
+ Object.defineProperty(window, 'innerWidth', {
234
+ writable: true,
235
+ configurable: true,
236
+ value: 1300,
237
+ });
238
+ window.dispatchEvent(new Event('resize'));
239
+ });
240
+
241
+ expect(result.current.isFloatingMode).toBe(true);
242
+ });
243
+ });
@@ -0,0 +1,117 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, beforeEach, mock } from 'bun:test';
3
+ import { useSidebarState } from '../use-sidebar-state';
4
+
5
+ // Mock clientLogger
6
+ mock.module('../../lib/logger', () => ({
7
+ default: {
8
+ error: mock(),
9
+ },
10
+ }));
11
+
12
+ describe('useSidebarState', () => {
13
+ beforeEach(() => {
14
+ // Clear localStorage before each test
15
+ localStorage.clear();
16
+ });
17
+
18
+ it('should initialize with false when no stored state exists', () => {
19
+ const { result } = renderHook(() => useSidebarState());
20
+
21
+ expect(result.current.isVisible).toBe(false);
22
+ });
23
+
24
+ it('should load stored state when it exists', async () => {
25
+ localStorage.setItem('eliza-agent-sidebar-visible', 'true');
26
+
27
+ const { result } = renderHook(() => useSidebarState());
28
+
29
+ await waitFor(() => {
30
+ expect(result.current.isVisible).toBe(true);
31
+ });
32
+ });
33
+
34
+ it('should save state when setSidebarVisible is called', async () => {
35
+ const { result } = renderHook(() => useSidebarState());
36
+
37
+ act(() => {
38
+ result.current.setSidebarVisible(true);
39
+ });
40
+
41
+ await waitFor(() => {
42
+ expect(result.current.isVisible).toBe(true);
43
+ });
44
+
45
+ expect(localStorage.getItem('eliza-agent-sidebar-visible')).toBe('true');
46
+ });
47
+
48
+ it('should toggle state when toggleSidebar is called', async () => {
49
+ const { result } = renderHook(() => useSidebarState());
50
+
51
+ // Initially false
52
+ expect(result.current.isVisible).toBe(false);
53
+
54
+ // Toggle to true
55
+ act(() => {
56
+ result.current.toggleSidebar();
57
+ });
58
+
59
+ await waitFor(() => {
60
+ expect(result.current.isVisible).toBe(true);
61
+ });
62
+
63
+ expect(localStorage.getItem('eliza-agent-sidebar-visible')).toBe('true');
64
+
65
+ // Toggle back to false
66
+ act(() => {
67
+ result.current.toggleSidebar();
68
+ });
69
+
70
+ await waitFor(() => {
71
+ expect(result.current.isVisible).toBe(false);
72
+ });
73
+
74
+ expect(localStorage.getItem('eliza-agent-sidebar-visible')).toBe('false');
75
+ });
76
+
77
+ it('should handle localStorage errors gracefully', () => {
78
+ // Set up invalid JSON
79
+ localStorage.setItem('eliza-agent-sidebar-visible', 'invalid-json');
80
+
81
+ const { result } = renderHook(() => useSidebarState());
82
+
83
+ // Should default to false when JSON parsing fails
84
+ expect(result.current.isVisible).toBe(false);
85
+ });
86
+
87
+ it('should handle localStorage save errors gracefully', async () => {
88
+ // Mock localStorage to throw an error on setItem
89
+ const originalSetItem = localStorage.setItem;
90
+ localStorage.setItem = () => {
91
+ throw new Error('localStorage save error');
92
+ };
93
+
94
+ const { result } = renderHook(() => useSidebarState());
95
+
96
+ // Should still update state even if localStorage fails
97
+ act(() => {
98
+ result.current.setSidebarVisible(true);
99
+ });
100
+
101
+ await waitFor(() => {
102
+ expect(result.current.isVisible).toBe(true);
103
+ });
104
+
105
+ // Restore original setItem
106
+ localStorage.setItem = originalSetItem;
107
+ });
108
+
109
+ it('should handle invalid JSON in localStorage', () => {
110
+ localStorage.setItem('eliza-agent-sidebar-visible', 'invalid-json');
111
+
112
+ const { result } = renderHook(() => useSidebarState());
113
+
114
+ // Should default to false when JSON parsing fails
115
+ expect(result.current.isVisible).toBe(false);
116
+ });
117
+ });
@@ -0,0 +1,130 @@
1
+ import type { Agent, UUID } from '@elizaos/core';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useState } from 'react';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { useStartAgent, useStopAgent } from './use-query-hooks';
6
+ import { useToast } from './use-toast';
7
+ // Direct error handling
8
+
9
+ /**
10
+ * Custom hook for managing agents (starting, stopping, and tracking status)
11
+ */
12
+ /**
13
+ * Custom hook for agent management.
14
+ * Allows starting and stopping agents with mutation operations.
15
+ * Provides functions to check if an agent is currently starting or stopping.
16
+ * @returns Object with functions for starting and stopping agents, checking agent status, and lists of agents in starting and stopping processes.
17
+ */
18
+ export function useAgentManagement() {
19
+ const navigate = useNavigate();
20
+ const queryClient = useQueryClient();
21
+ const { toast } = useToast();
22
+
23
+ // Mutations for starting and stopping agents
24
+ const startAgentMutation = useStartAgent();
25
+ const stopAgentMutation = useStopAgent();
26
+
27
+ // Track agents that are currently in the process of starting or stopping
28
+ const [startingAgents, setStartingAgents] = useState<UUID[]>([]);
29
+ const [stoppingAgents, setStoppingAgents] = useState<UUID[]>([]);
30
+
31
+ /**
32
+ * Start an agent and navigate to its chat
33
+ */
34
+ const startAgent = async (agent: Agent) => {
35
+ if (!agent.id) {
36
+ toast({
37
+ title: 'Error',
38
+ description: 'Agent ID is missing',
39
+ variant: 'destructive',
40
+ });
41
+ return;
42
+ }
43
+
44
+ const agentId = agent.id as UUID;
45
+
46
+ // Prevent starting if already in progress
47
+ if (startingAgents.includes(agentId)) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ // Add agent to starting list
53
+ setStartingAgents((prev) => [...prev, agentId]);
54
+
55
+ // Start the agent
56
+ await startAgentMutation.mutateAsync(agentId);
57
+ } catch (error) {
58
+ console.error('Failed to start agent:', error);
59
+ // Let the mutation's onError handler show the appropriate toast
60
+ } finally {
61
+ // Remove agent from starting list regardless of success/failure
62
+ setStartingAgents((prev) => prev.filter((id) => id !== agentId));
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Stop an agent
68
+ */
69
+ const stopAgent = async (agent: Agent) => {
70
+ if (!agent.id) {
71
+ toast({
72
+ title: 'Error',
73
+ description: 'Agent ID is missing',
74
+ variant: 'destructive',
75
+ });
76
+ return;
77
+ }
78
+
79
+ const agentId = agent.id as UUID;
80
+
81
+ // Prevent stopping if already in progress
82
+ if (stoppingAgents.includes(agentId)) {
83
+ return;
84
+ }
85
+
86
+ try {
87
+ // Add agent to stopping list
88
+ setStoppingAgents((prev) => [...prev, agentId]);
89
+
90
+ // Stop the agent
91
+ await stopAgentMutation.mutateAsync(agentId);
92
+
93
+ toast({
94
+ title: 'Agent Stopped',
95
+ description: `${agent.name} has been stopped`,
96
+ });
97
+ } catch (error) {
98
+ console.error('Failed to stop agent:', error);
99
+ // Let the mutation's onError handler show the appropriate toast
100
+ } finally {
101
+ // Remove agent from stopping list regardless of success/failure
102
+ setStoppingAgents((prev) => prev.filter((id) => id !== agentId));
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Check if an agent is currently starting
108
+ */
109
+ const isAgentStarting = (agentId: UUID | undefined | null) => {
110
+ if (!agentId) return false;
111
+ return startingAgents.includes(agentId);
112
+ };
113
+
114
+ /**
115
+ * Check if an agent is currently stopping
116
+ */
117
+ const isAgentStopping = (agentId: UUID | undefined | null) => {
118
+ if (!agentId) return false;
119
+ return stoppingAgents.includes(agentId);
120
+ };
121
+
122
+ return {
123
+ startAgent,
124
+ stopAgent,
125
+ isAgentStarting,
126
+ isAgentStopping,
127
+ startingAgents,
128
+ stoppingAgents,
129
+ };
130
+ }
@@ -0,0 +1,74 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import type { UUID } from '@elizaos/core';
3
+ import clientLogger from '@/lib/logger';
4
+
5
+ // Key for storing agent tab states in localStorage
6
+ const AGENT_TAB_STATE_KEY = 'eliza-agent-tab-states';
7
+
8
+ type TabValue = 'details' | 'actions' | 'logs' | 'memories' | string;
9
+
10
+ interface AgentTabStates {
11
+ [agentId: string]: TabValue;
12
+ }
13
+
14
+ /**
15
+ * Custom hook to manage agent sidebar tab state with localStorage persistence
16
+ * Each agent remembers its last selected tab when switching between agents
17
+ */
18
+ export function useAgentTabState(agentId: UUID | undefined) {
19
+ const [currentTab, setCurrentTab] = useState<TabValue>('details');
20
+
21
+ // Load tab states from localStorage
22
+ const getStoredTabStates = useCallback((): AgentTabStates => {
23
+ try {
24
+ const stored = localStorage.getItem(AGENT_TAB_STATE_KEY);
25
+ return stored ? JSON.parse(stored) : {};
26
+ } catch (error) {
27
+ clientLogger.error('Error reading agent tab states from localStorage:', error);
28
+ return {};
29
+ }
30
+ }, []);
31
+
32
+ // Save tab states to localStorage
33
+ const saveTabStates = useCallback((states: AgentTabStates) => {
34
+ try {
35
+ localStorage.setItem(AGENT_TAB_STATE_KEY, JSON.stringify(states));
36
+ } catch (error) {
37
+ clientLogger.error('Error saving agent tab states to localStorage:', error);
38
+ }
39
+ }, []);
40
+
41
+ // Load the tab state for the current agent when agentId changes
42
+ useEffect(() => {
43
+ if (!agentId) {
44
+ setCurrentTab('details');
45
+ return;
46
+ }
47
+
48
+ const storedStates = getStoredTabStates();
49
+ const agentTabState = storedStates[agentId] || 'details';
50
+ setCurrentTab(agentTabState);
51
+ }, [agentId, getStoredTabStates]);
52
+
53
+ // Update tab state and persist to localStorage
54
+ const updateTab = useCallback(
55
+ (newTab: TabValue) => {
56
+ setCurrentTab(newTab);
57
+
58
+ if (agentId) {
59
+ const storedStates = getStoredTabStates();
60
+ const updatedStates = {
61
+ ...storedStates,
62
+ [agentId]: newTab,
63
+ };
64
+ saveTabStates(updatedStates);
65
+ }
66
+ },
67
+ [agentId, getStoredTabStates, saveTabStates]
68
+ );
69
+
70
+ return {
71
+ currentTab,
72
+ setTab: updateTab,
73
+ };
74
+ }