@agent-relay/dashboard 2.0.82 → 2.0.84

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 (228) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
  3. package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
  4. package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
  5. package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
  6. package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
  7. package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
  8. package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
  9. package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
  10. package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
  11. package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
  12. package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
  13. package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
  14. package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
  15. package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
  16. package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
  17. package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
  18. package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
  19. package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
  20. package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
  21. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
  22. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
  23. package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
  24. package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
  25. package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
  26. package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
  27. package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
  28. package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
  29. package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
  30. package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
  31. package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
  32. package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
  33. package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
  34. package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
  35. package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
  36. package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
  37. package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
  38. package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
  39. package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
  40. package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
  41. package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
  42. package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
  43. package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
  44. package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
  45. package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
  46. package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
  47. package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
  48. package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
  49. package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
  50. package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
  51. package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
  52. package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
  53. package/out/about.html +2 -2
  54. package/out/about.txt +2 -2
  55. package/out/app/onboarding.html +1 -1
  56. package/out/app/onboarding.txt +2 -2
  57. package/out/app.html +1 -1
  58. package/out/app.txt +2 -2
  59. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
  60. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  61. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  62. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  63. package/out/blog.html +2 -2
  64. package/out/blog.txt +1 -1
  65. package/out/careers.html +2 -2
  66. package/out/careers.txt +2 -2
  67. package/out/changelog.html +2 -2
  68. package/out/changelog.txt +2 -2
  69. package/out/cloud/link.html +1 -1
  70. package/out/cloud/link.txt +2 -2
  71. package/out/complete-profile.html +2 -2
  72. package/out/complete-profile.txt +2 -2
  73. package/out/connect-repos.html +1 -1
  74. package/out/connect-repos.txt +2 -2
  75. package/out/contact.html +2 -2
  76. package/out/contact.txt +2 -2
  77. package/out/dev/cli-tools.html +1 -0
  78. package/out/dev/cli-tools.txt +7 -0
  79. package/out/dev/log-viewer.html +23 -0
  80. package/out/dev/log-viewer.txt +7 -0
  81. package/out/docs.html +2 -2
  82. package/out/docs.txt +2 -2
  83. package/out/history.html +1 -1
  84. package/out/history.txt +2 -2
  85. package/out/index.html +1 -1
  86. package/out/index.txt +2 -2
  87. package/out/login.html +2 -2
  88. package/out/login.txt +2 -2
  89. package/out/metrics.html +1 -1
  90. package/out/metrics.txt +2 -2
  91. package/out/pricing.html +2 -2
  92. package/out/pricing.txt +2 -2
  93. package/out/privacy.html +2 -2
  94. package/out/privacy.txt +2 -2
  95. package/out/providers/setup/claude.html +1 -1
  96. package/out/providers/setup/claude.txt +2 -2
  97. package/out/providers/setup/codex.html +1 -1
  98. package/out/providers/setup/codex.txt +2 -2
  99. package/out/providers/setup/cursor.html +1 -1
  100. package/out/providers/setup/cursor.txt +2 -2
  101. package/out/providers.html +1 -1
  102. package/out/providers.txt +2 -2
  103. package/out/security.html +2 -2
  104. package/out/security.txt +2 -2
  105. package/out/signup.html +2 -2
  106. package/out/signup.txt +2 -2
  107. package/out/terms.html +2 -2
  108. package/out/terms.txt +2 -2
  109. package/package.json +5 -1
  110. package/src/adapters/DashboardConfigProvider.tsx +56 -0
  111. package/src/adapters/cloudFetchAdapter.ts +278 -0
  112. package/src/adapters/index.ts +3 -0
  113. package/src/adapters/types.ts +508 -0
  114. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
  115. package/src/app/app/onboarding/page.tsx +870 -170
  116. package/src/app/cloud/link/page.tsx +14 -6
  117. package/src/app/connect-repos/page.tsx +9 -3
  118. package/src/app/dev/cli-tools/page.tsx +130 -0
  119. package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
  120. package/src/app/dev/log-viewer/fixtures.ts +110 -0
  121. package/src/app/dev/log-viewer/page.tsx +288 -0
  122. package/src/app/history/page.tsx +28 -12
  123. package/src/app/page.tsx +1 -1
  124. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
  125. package/src/components/AgentCard.tsx +4 -4
  126. package/src/components/AgentLogPreview.tsx +2 -38
  127. package/src/components/App.tsx +441 -2624
  128. package/src/components/CliToolHarness.test.tsx +83 -0
  129. package/src/components/CliToolHarness.tsx +292 -0
  130. package/src/components/CoordinatorPanel.tsx +13 -6
  131. package/src/components/LogViewer.tsx +2 -42
  132. package/src/components/ProviderAuthFlow.tsx +201 -81
  133. package/src/components/ProvisioningProgress.tsx +1 -1
  134. package/src/components/ReactionChips.tsx +2 -1
  135. package/src/components/SpawnModal.test.tsx +51 -18
  136. package/src/components/SpawnModal.tsx +175 -207
  137. package/src/components/TerminalProviderSetup.tsx +1 -1
  138. package/src/components/ThreadPanel.tsx +2 -0
  139. package/src/components/WorkspaceContext.tsx +7 -19
  140. package/src/components/XTermLogViewer.tsx +190 -27
  141. package/src/components/channels/ChannelMessageList.tsx +94 -4
  142. package/src/components/channels/ChannelViewV1.tsx +35 -11
  143. package/src/components/channels/api.ts +21 -20
  144. package/src/components/channels/types.ts +16 -0
  145. package/src/components/hooks/index.ts +0 -19
  146. package/src/components/hooks/useMessages.test.ts +80 -0
  147. package/src/components/hooks/useMessages.ts +13 -4
  148. package/src/components/hooks/useOrchestrator.ts +1 -1
  149. package/src/components/hooks/usePresence.ts +45 -6
  150. package/src/components/hooks/useThread.ts +83 -46
  151. package/src/components/hooks/useTrajectory.ts +62 -5
  152. package/src/components/hooks/useWebSocket.test.ts +358 -0
  153. package/src/components/hooks/useWebSocket.ts +243 -5
  154. package/src/components/index.ts +2 -14
  155. package/src/components/layout/Header.tsx +9 -15
  156. package/src/components/layout/Sidebar.tsx +1 -8
  157. package/src/components/settings/SettingsPage.tsx +108 -47
  158. package/src/components/settings/index.ts +0 -3
  159. package/src/landing/blogData.ts +1 -1
  160. package/src/lib/agent-merge.test.ts +2 -2
  161. package/src/lib/api.ts +8 -38
  162. package/src/lib/identity.test.ts +139 -0
  163. package/src/lib/identity.ts +48 -0
  164. package/src/lib/relaycastMessageAdapters.test.ts +182 -0
  165. package/src/lib/relaycastMessageAdapters.ts +105 -0
  166. package/src/lib/sanitize-logs.test.ts +227 -0
  167. package/src/lib/sanitize-logs.ts +202 -0
  168. package/src/providers/AgentProvider.tsx +799 -0
  169. package/src/providers/ChannelProvider.tsx +528 -0
  170. package/src/providers/CloudWorkspaceProvider.tsx +402 -0
  171. package/src/providers/MessageProvider.tsx +875 -0
  172. package/src/providers/RelayConfigProvider.tsx +94 -0
  173. package/src/providers/SendProvider.tsx +497 -0
  174. package/src/providers/SettingsProvider.tsx +247 -0
  175. package/src/providers/index.ts +26 -0
  176. package/src/types/index.ts +10 -10
  177. package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
  178. package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
  179. package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
  180. package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
  181. package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
  182. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
  183. package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
  184. package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
  185. package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
  186. package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
  187. package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
  188. package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
  189. package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
  190. package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
  191. package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
  192. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
  193. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
  194. package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
  195. package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
  196. package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
  197. package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
  198. package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
  199. package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
  200. package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
  201. package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
  202. package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
  203. package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
  204. package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
  205. package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
  206. package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
  207. package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
  208. package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
  209. package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
  210. package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
  211. package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
  212. package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
  213. package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
  214. package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
  215. package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
  216. package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
  217. package/src/components/BillingResult.tsx +0 -447
  218. package/src/components/CloudSessionProvider.tsx +0 -130
  219. package/src/components/SessionExpiredModal.tsx +0 -128
  220. package/src/components/WorkspaceStatusIndicator.tsx +0 -396
  221. package/src/components/hooks/useSession.ts +0 -209
  222. package/src/components/hooks/useWorkspaceMembers.ts +0 -132
  223. package/src/components/hooks/useWorkspaceStatus.ts +0 -237
  224. package/src/components/settings/BillingSettingsPanel.tsx +0 -564
  225. package/src/components/settings/TeamSettingsPanel.tsx +0 -560
  226. package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
  227. package/src/lib/cloudApi.ts +0 -893
  228. /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_ssgManifest.js +0 -0
@@ -0,0 +1,182 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { Message } from '../types';
3
+ import { normalizeRelayDmMessageTargets } from './relaycastMessageAdapters.js';
4
+
5
+ function setRelayUsername(value?: string): void {
6
+ const storage = (globalThis as { localStorage?: Storage }).localStorage;
7
+ if (!storage) {
8
+ return;
9
+ }
10
+
11
+ if (value) {
12
+ storage.setItem('relay_username', value);
13
+ } else {
14
+ storage.removeItem('relay_username');
15
+ }
16
+ }
17
+
18
+ function installMockLocalStorage(): void {
19
+ if ((globalThis as { localStorage?: Storage }).localStorage) {
20
+ return;
21
+ }
22
+
23
+ const store = new Map<string, string>();
24
+ const mockStorage = {
25
+ getItem: (key: string) => store.get(key) ?? null,
26
+ setItem: (key: string, value: string) => {
27
+ store.set(key, value);
28
+ },
29
+ removeItem: (key: string) => {
30
+ store.delete(key);
31
+ },
32
+ clear: () => {
33
+ store.clear();
34
+ },
35
+ key: (index: number) => Array.from(store.keys())[index] ?? null,
36
+ get length() {
37
+ return store.size;
38
+ },
39
+ } satisfies Storage;
40
+
41
+ vi.stubGlobal('localStorage', mockStorage);
42
+ }
43
+
44
+ describe('normalizeRelayDmMessageTargets', () => {
45
+ beforeEach(() => {
46
+ installMockLocalStorage();
47
+ setRelayUsername(undefined);
48
+ });
49
+
50
+ afterEach(() => {
51
+ setRelayUsername(undefined);
52
+ });
53
+
54
+ it('maps dm_* targets to the other participant for incoming replies', () => {
55
+ const messages: Message[] = [
56
+ {
57
+ id: 'msg-1',
58
+ from: 'Natty',
59
+ to: 'dm_7b62c72644b9316e7e10a992',
60
+ content: 'hello',
61
+ timestamp: '2026-02-24T12:00:00.000Z',
62
+ },
63
+ ];
64
+
65
+ const normalized = normalizeRelayDmMessageTargets(messages, [
66
+ {
67
+ id: 'dm_7b62c72644b9316e7e10a992',
68
+ participants: ['Natty', 'test-broker-new'],
69
+ },
70
+ ]);
71
+
72
+ expect(normalized[0]?.to).toBe('test-broker-new');
73
+ });
74
+
75
+ it('maps dm_* targets to the opposite participant for sent replies', () => {
76
+ const messages: Message[] = [
77
+ {
78
+ id: 'msg-2',
79
+ from: 'test-broker-new',
80
+ to: 'dm_7b62c72644b9316e7e10a992',
81
+ content: 'ack',
82
+ timestamp: '2026-02-24T12:00:01.000Z',
83
+ },
84
+ ];
85
+
86
+ const normalized = normalizeRelayDmMessageTargets(messages, [
87
+ {
88
+ id: 'dm_7b62c72644b9316e7e10a992',
89
+ participants: ['Natty', 'test-broker-new'],
90
+ },
91
+ ]);
92
+
93
+ expect(normalized[0]?.to).toBe('Natty');
94
+ });
95
+
96
+ it('leaves non-dm and unknown dm targets unchanged', () => {
97
+ const messages: Message[] = [
98
+ {
99
+ id: 'msg-3',
100
+ from: 'Natty',
101
+ to: '#general',
102
+ content: 'channel',
103
+ timestamp: '2026-02-24T12:00:02.000Z',
104
+ },
105
+ {
106
+ id: 'msg-4',
107
+ from: 'Natty',
108
+ to: 'dm_missing',
109
+ content: 'unmapped',
110
+ timestamp: '2026-02-24T12:00:03.000Z',
111
+ },
112
+ ];
113
+
114
+ const normalized = normalizeRelayDmMessageTargets(messages, [
115
+ {
116
+ id: 'dm_7b62c72644b9316e7e10a992',
117
+ participants: ['Natty', 'test-broker-new'],
118
+ },
119
+ ]);
120
+
121
+ expect(normalized).toBe(messages);
122
+ expect(normalized[0]?.to).toBe('#general');
123
+ expect(normalized[1]?.to).toBe('dm_missing');
124
+ });
125
+
126
+ it('maps Dashboard-<suffix> targets to the project display identity when available', () => {
127
+ setRelayUsername('test-broker-new');
128
+
129
+ const messages: Message[] = [
130
+ {
131
+ id: 'msg-5',
132
+ from: 'Natty',
133
+ to: 'Dashboard-5b8c70e5',
134
+ content: 'reply',
135
+ timestamp: '2026-02-24T12:00:04.000Z',
136
+ },
137
+ ];
138
+
139
+ const normalized = normalizeRelayDmMessageTargets(messages, []);
140
+
141
+ expect(normalized[0]?.to).toBe('test-broker-new');
142
+ });
143
+
144
+ it('falls back to Dashboard when no project display identity is stored', () => {
145
+ const messages: Message[] = [
146
+ {
147
+ id: 'msg-6',
148
+ from: 'Natty',
149
+ to: 'Dashboard-5b8c70e5',
150
+ content: 'reply',
151
+ timestamp: '2026-02-24T12:00:05.000Z',
152
+ },
153
+ ];
154
+
155
+ const normalized = normalizeRelayDmMessageTargets(messages, []);
156
+
157
+ expect(normalized[0]?.to).toBe('Dashboard');
158
+ });
159
+
160
+ it('normalizes Dashboard-<suffix> participants when resolving dm_* targets', () => {
161
+ setRelayUsername('test-broker-new');
162
+
163
+ const messages: Message[] = [
164
+ {
165
+ id: 'msg-7',
166
+ from: 'Natty',
167
+ to: 'dm_7b62c72644b9316e7e10a992',
168
+ content: 'hello',
169
+ timestamp: '2026-02-24T12:00:06.000Z',
170
+ },
171
+ ];
172
+
173
+ const normalized = normalizeRelayDmMessageTargets(messages, [
174
+ {
175
+ id: 'dm_7b62c72644b9316e7e10a992',
176
+ participants: ['Natty', 'Dashboard-5b8c70e5'],
177
+ },
178
+ ]);
179
+
180
+ expect(normalized[0]?.to).toBe('test-broker-new');
181
+ });
182
+ });
@@ -0,0 +1,105 @@
1
+ import {
2
+ mapMessageWithMetaToChannelMessage,
3
+ formatReplyCountLabel,
4
+ } from '@relaycast/react';
5
+ import type { ChannelMessage as ChannelApiMessage } from '../components/channels';
6
+ import type { Message } from '../types';
7
+ import { normalizeDashboardName } from './identity';
8
+
9
+ type RelaycastMessageWithMeta = Parameters<typeof mapMessageWithMetaToChannelMessage>[1];
10
+ type RelayDmConversationLike = {
11
+ id: string;
12
+ participants: unknown[];
13
+ };
14
+
15
+ function normalizeRelayIdentity(value: string): string {
16
+ const trimmed = value.trim();
17
+ if (!trimmed) return '';
18
+ return normalizeDashboardName(trimmed);
19
+ }
20
+
21
+ export function mapRelayMessageToChannelApiMessage(
22
+ channelId: string,
23
+ message: RelaycastMessageWithMeta,
24
+ currentUserName?: string,
25
+ ): ChannelApiMessage {
26
+ return mapMessageWithMetaToChannelMessage(channelId, message, {
27
+ currentUserName,
28
+ }) as ChannelApiMessage;
29
+ }
30
+
31
+ export function formatRelayReplyCountLabel(replyCount: number): string {
32
+ return formatReplyCountLabel(replyCount);
33
+ }
34
+
35
+ function resolveDmRecipient(
36
+ participants: unknown[],
37
+ sender: string,
38
+ ): string | null {
39
+ const senderKey = normalizeRelayIdentity(sender).toLowerCase();
40
+
41
+ for (const participant of participants) {
42
+ if (typeof participant !== 'string') continue;
43
+ const normalized = normalizeRelayIdentity(participant);
44
+ if (!normalized) continue;
45
+ if (normalized.toLowerCase() !== senderKey) {
46
+ return normalized;
47
+ }
48
+ }
49
+
50
+ for (const participant of participants) {
51
+ if (typeof participant !== 'string') continue;
52
+ const normalized = normalizeRelayIdentity(participant);
53
+ if (normalized) {
54
+ return normalized;
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ export function normalizeRelayDmMessageTargets(
62
+ messages: Message[],
63
+ conversations: RelayDmConversationLike[],
64
+ ): Message[] {
65
+ if (messages.length === 0) {
66
+ return messages;
67
+ }
68
+
69
+ const participantsByConversationId = new Map<string, unknown[]>();
70
+ for (const conversation of conversations) {
71
+ participantsByConversationId.set(conversation.id, conversation.participants ?? []);
72
+ }
73
+
74
+ let changed = false;
75
+ const normalized = messages.map((message) => {
76
+ const target = message.to?.trim();
77
+ if (!target) {
78
+ return message;
79
+ }
80
+
81
+ let normalizedTarget = normalizeRelayIdentity(target);
82
+
83
+ if (target.startsWith('dm_')) {
84
+ const participants = participantsByConversationId.get(target);
85
+ if (participants) {
86
+ const resolvedRecipient = resolveDmRecipient(participants, message.from);
87
+ if (resolvedRecipient && resolvedRecipient !== target) {
88
+ normalizedTarget = resolvedRecipient;
89
+ }
90
+ }
91
+ }
92
+
93
+ if (!normalizedTarget || normalizedTarget === target) {
94
+ return message;
95
+ }
96
+
97
+ changed = true;
98
+ return {
99
+ ...message,
100
+ to: normalizedTarget,
101
+ };
102
+ });
103
+
104
+ return changed ? normalized : messages;
105
+ }
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sanitizeLogContent, isHarnessNoisyLine, isSpinnerFragment } from './sanitize-logs.js';
3
+
4
+ describe('sanitizeLogContent', () => {
5
+ it('returns empty string for falsy input', () => {
6
+ expect(sanitizeLogContent('')).toBe('');
7
+ expect(sanitizeLogContent(null as unknown as string)).toBe('');
8
+ expect(sanitizeLogContent(undefined as unknown as string)).toBe('');
9
+ });
10
+
11
+ it('passes through plain text unchanged', () => {
12
+ expect(sanitizeLogContent('Hello, world!')).toBe('Hello, world!');
13
+ });
14
+
15
+ describe('ANSI escape sequences', () => {
16
+ it('removes SGR color codes', () => {
17
+ expect(sanitizeLogContent('\x1b[31mError\x1b[0m')).toBe('Error');
18
+ expect(sanitizeLogContent('\x1b[1;32mSuccess\x1b[0m')).toBe('Success');
19
+ });
20
+
21
+ it('removes 256-color codes', () => {
22
+ expect(sanitizeLogContent('\x1b[38;5;216mOrange text\x1b[0m')).toBe('Orange text');
23
+ });
24
+
25
+ it('removes 24-bit (truecolor) codes', () => {
26
+ expect(sanitizeLogContent('\x1b[38;2;255;100;0mRGB text\x1b[0m')).toBe('RGB text');
27
+ });
28
+
29
+ it('removes cursor movement sequences', () => {
30
+ expect(sanitizeLogContent('\x1b[2KOverwritten line')).toBe('Overwritten line');
31
+ expect(sanitizeLogContent('\x1b[1AMove up')).toBe('Move up');
32
+ expect(sanitizeLogContent('\x1b[10CMove right')).toBe('Move right');
33
+ });
34
+
35
+ it('removes OSC sequences (window title)', () => {
36
+ expect(sanitizeLogContent('\x1b]0;My Title\x07Real content')).toBe('Real content');
37
+ expect(sanitizeLogContent('\x1b]0;Title\x1b\\Content')).toBe('Content');
38
+ });
39
+
40
+ it('removes DCS sequences', () => {
41
+ expect(sanitizeLogContent('\x1bPsome device string\x1b\\After')).toBe('After');
42
+ });
43
+
44
+ it('removes single-character escapes', () => {
45
+ expect(sanitizeLogContent('\x1bMReverse index')).toBe('Reverse index');
46
+ });
47
+ });
48
+
49
+ describe('orphaned/degraded sequences', () => {
50
+ it('removes orphaned CSI at start of line', () => {
51
+ expect(sanitizeLogContent('[?25h')).toBe('');
52
+ expect(sanitizeLogContent('[2K')).toBe('');
53
+ });
54
+
55
+ it('removes literal SGR without ESC byte', () => {
56
+ expect(sanitizeLogContent('[38;5;216mOrange')).toBe('Orange');
57
+ expect(sanitizeLogContent('[0mPlain')).toBe('Plain');
58
+ expect(sanitizeLogContent('[1;31mBold red[0m text')).toBe('Bold red text');
59
+ });
60
+ });
61
+
62
+ describe('control characters', () => {
63
+ it('handles carriage returns (overwrites line from start)', () => {
64
+ expect(sanitizeLogContent('line1\rline2')).toBe('line2');
65
+ expect(sanitizeLogContent('old text\rnew')).toBe('new');
66
+ expect(sanitizeLogContent('no cr here')).toBe('no cr here');
67
+ });
68
+
69
+ it('handles backspaces (overwrites previous char)', () => {
70
+ expect(sanitizeLogContent('ab\x08c')).toBe('ac');
71
+ });
72
+
73
+ it('handles consecutive backspaces correctly', () => {
74
+ expect(sanitizeLogContent('Typo\x08\x08\x08\x08Fixed text here')).toBe('Fixed text here');
75
+ });
76
+
77
+ it('removes orphaned backspaces', () => {
78
+ expect(sanitizeLogContent('\x08\x08leftover')).toBe('leftover');
79
+ });
80
+
81
+ it('preserves newlines and tabs', () => {
82
+ expect(sanitizeLogContent('line1\nline2\ttabbed')).toBe('line1\nline2\ttabbed');
83
+ });
84
+
85
+ it('removes other control characters', () => {
86
+ expect(sanitizeLogContent('text\x00\x01\x02\x03end')).toBe('textend');
87
+ });
88
+ });
89
+
90
+ describe('real-world Claude CLI output', () => {
91
+ it('handles Claude thinking indicator', () => {
92
+ const input = '\x1b[2m⠋ Thinking...\x1b[22m';
93
+ const result = sanitizeLogContent(input);
94
+ expect(result).not.toContain('\x1b');
95
+ expect(result).toContain('Thinking...');
96
+ });
97
+
98
+ it('handles Claude tool use output', () => {
99
+ const input = '\x1b[1;36m● Tool use:\x1b[0m \x1b[33mRead\x1b[0m src/index.ts';
100
+ const result = sanitizeLogContent(input);
101
+ expect(result).toBe('● Tool use: Read src/index.ts');
102
+ });
103
+
104
+ it('handles nested/compound ANSI codes', () => {
105
+ const input = '\x1b[1m\x1b[38;5;82m✓\x1b[0m \x1b[2mDone\x1b[22m';
106
+ const result = sanitizeLogContent(input);
107
+ expect(result).toBe('✓ Done');
108
+ });
109
+ });
110
+
111
+ describe('real-world Codex CLI output', () => {
112
+ it('handles Codex progress bar', () => {
113
+ const input = '\x1b[32m████████████████\x1b[0m\x1b[90m░░░░\x1b[0m 80%';
114
+ const result = sanitizeLogContent(input);
115
+ expect(result).toBe('████████████████░░░░ 80%');
116
+ });
117
+
118
+ it('handles Codex status line with carriage return (keeps last overwrite)', () => {
119
+ const input = 'Processing files... 3/10\rProcessing files... 4/10';
120
+ const result = sanitizeLogContent(input);
121
+ expect(result).toBe('Processing files... 4/10');
122
+ });
123
+ });
124
+
125
+ describe('edge cases', () => {
126
+ it('handles very long lines', () => {
127
+ const longLine = 'x'.repeat(10000) + '\x1b[31m' + 'y'.repeat(10000);
128
+ const result = sanitizeLogContent(longLine);
129
+ expect(result).toBe('x'.repeat(10000) + 'y'.repeat(10000));
130
+ });
131
+
132
+ it('handles Unicode/emoji in content', () => {
133
+ expect(sanitizeLogContent('🚀 Deploy \x1b[32msuccess\x1b[0m ✅')).toBe('🚀 Deploy success ✅');
134
+ });
135
+
136
+ it('handles malformed escape sequences gracefully', () => {
137
+ // Incomplete escape - should remove what it can
138
+ const result = sanitizeLogContent('\x1b[mtext');
139
+ expect(result).toContain('text');
140
+ });
141
+
142
+ it('handles mixed content with multiple sequence types', () => {
143
+ const input = '\x1b]0;title\x07\x1b[1;31mError:\x1b[0m Something \x1b[2Kfailed\r\n';
144
+ const result = sanitizeLogContent(input);
145
+ expect(result).toContain('Error:');
146
+ expect(result).toContain('Something');
147
+ expect(result).toContain('failed');
148
+ expect(result).not.toContain('\x1b');
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('isSpinnerFragment', () => {
154
+ it('detects braille spinner characters', () => {
155
+ expect(isSpinnerFragment('⠋')).toBe(true);
156
+ expect(isSpinnerFragment('⠙')).toBe(true);
157
+ expect(isSpinnerFragment('⣾')).toBe(true);
158
+ });
159
+
160
+ it('detects ASCII spinner characters', () => {
161
+ expect(isSpinnerFragment('|')).toBe(true);
162
+ expect(isSpinnerFragment('/')).toBe(true);
163
+ expect(isSpinnerFragment('-')).toBe(true);
164
+ expect(isSpinnerFragment('\\')).toBe(true);
165
+ });
166
+
167
+ it('detects two-char spinner sequences', () => {
168
+ expect(isSpinnerFragment('⠋⠙')).toBe(true);
169
+ });
170
+
171
+ it('rejects normal text', () => {
172
+ expect(isSpinnerFragment('hello')).toBe(false);
173
+ expect(isSpinnerFragment('Error')).toBe(false);
174
+ });
175
+
176
+ it('rejects longer strings even with spinner chars', () => {
177
+ expect(isSpinnerFragment('⠋⠙⠹')).toBe(false);
178
+ });
179
+
180
+ it('rejects empty string', () => {
181
+ expect(isSpinnerFragment('')).toBe(false);
182
+ });
183
+ });
184
+
185
+ describe('isHarnessNoisyLine', () => {
186
+ it('suppresses rust warning lines', () => {
187
+ expect(
188
+ isHarnessNoisyLine('2026-02-25T13:38:03.737927Z WARN agent_relay_broker::pty_worker: delivery echo not detected'),
189
+ ).toBe(true);
190
+ });
191
+
192
+ it('suppresses short spinner frame artifacts', () => {
193
+ expect(isHarnessNoisyLine('✶ O b')).toBe(true);
194
+ expect(isHarnessNoisyLine('✳')).toBe(true);
195
+ });
196
+
197
+ it('suppresses spinner status lines with ui instructions', () => {
198
+ expect(
199
+ isHarnessNoisyLine('⏵⏵ bypass permissions on (shift+tab to cycle) · esc to interrupt'),
200
+ ).toBe(true);
201
+ expect(isHarnessNoisyLine('Shimmying…')).toBe(true);
202
+ });
203
+
204
+ it('suppresses broker debug lines', () => {
205
+ expect(
206
+ isHarnessNoisyLine(
207
+ '2026-02-25T13:43:52.921035Z DEBUG hyper_util::client::legacy::connect::http: connected to [2600:1f18:4c12:9a01:6ea1:6e82:d8b9:9650]:443',
208
+ ),
209
+ ).toBe(true);
210
+ });
211
+
212
+ it('suppresses tool instructions regardless of spacing', () => {
213
+ expect(isHarnessNoisyLine('⏵⏵bypasspermissionson (shift+tabto[')).toBe(true);
214
+ expect(isHarnessNoisyLine('relay_send(to: \"<sender>\", message: \"...\")')).toBe(true);
215
+ });
216
+
217
+ it('suppresses broker telemetry banner fragments', () => {
218
+ expect(isHarnessNoisyLine('[broker] Run `agent-relay telemetry disable` to opt out.')).toBe(true);
219
+ expect(isHarnessNoisyLine('[broker] Run')).toBe(true);
220
+ });
221
+
222
+ it('suppresses short spinner text fragments', () => {
223
+ expect(isHarnessNoisyLine('O')).toBe(true);
224
+ expect(isHarnessNoisyLine(' b ')).toBe(true);
225
+ expect(isHarnessNoisyLine('⏢')).toBe(true);
226
+ });
227
+ });