agent-relay 2.0.6 → 2.0.7

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 (145) hide show
  1. package/deploy/workspace/entrypoint.sh +80 -22
  2. package/deploy/workspace/gh-credential-relay +90 -0
  3. package/dist/dashboard/out/404.html +1 -1
  4. package/{packages/dashboard/ui-dist/_next/static/chunks/677-7323947c23b35979.js → dist/dashboard/out/_next/static/chunks/320-22ebe7be58cf982a.js} +1 -1
  5. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
  6. package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-2e525b1dcc790967.js → dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js} +1 -1
  7. package/{packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js → dist/dashboard/out/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js} +1 -1
  8. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
  9. package/{packages/dashboard/ui-dist/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js → dist/dashboard/out/_next/static/chunks/app/history/page-9965d2483011b846.js} +1 -1
  10. package/dist/dashboard/out/_next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
  11. package/{packages/dashboard/ui-dist/_next/static/chunks/app/login/page-6ec54eee75877971.js → dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js} +1 -1
  12. package/{packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js → dist/dashboard/out/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js} +1 -1
  13. package/dist/dashboard/out/_next/static/chunks/app/{page-4e64923d73c35bc9.js → page-487fa38f041815c1.js} +1 -1
  14. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
  15. package/dist/dashboard/out/_next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
  16. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/{page-84161c802b020a1f.js → page-4dbe33f0f7691b7c.js} +1 -1
  17. package/dist/dashboard/out/_next/static/chunks/app/signup/{page-18a4665665f6be11.js → page-1ede2205b58649ca.js} +1 -1
  18. package/dist/dashboard/out/_next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
  19. package/dist/dashboard/out/app/onboarding.html +1 -1
  20. package/dist/dashboard/out/app/onboarding.txt +2 -2
  21. package/dist/dashboard/out/app.html +1 -1
  22. package/dist/dashboard/out/app.txt +2 -2
  23. package/dist/dashboard/out/cloud/link.html +1 -1
  24. package/dist/dashboard/out/cloud/link.txt +2 -2
  25. package/dist/dashboard/out/connect-repos.html +1 -1
  26. package/dist/dashboard/out/connect-repos.txt +2 -2
  27. package/dist/dashboard/out/history.html +1 -1
  28. package/dist/dashboard/out/history.txt +2 -2
  29. package/dist/dashboard/out/index.html +1 -1
  30. package/dist/dashboard/out/index.txt +2 -2
  31. package/dist/dashboard/out/login.html +2 -2
  32. package/dist/dashboard/out/login.txt +2 -2
  33. package/dist/dashboard/out/metrics.html +1 -1
  34. package/dist/dashboard/out/metrics.txt +2 -2
  35. package/dist/dashboard/out/pricing.html +2 -2
  36. package/dist/dashboard/out/pricing.txt +2 -2
  37. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  38. package/dist/dashboard/out/providers/setup/claude.txt +2 -2
  39. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  40. package/dist/dashboard/out/providers/setup/codex.txt +2 -2
  41. package/dist/dashboard/out/providers/setup/cursor.html +1 -1
  42. package/dist/dashboard/out/providers/setup/cursor.txt +2 -2
  43. package/dist/dashboard/out/providers.html +1 -1
  44. package/dist/dashboard/out/providers.txt +2 -2
  45. package/dist/dashboard/out/signup.html +2 -2
  46. package/dist/dashboard/out/signup.txt +2 -2
  47. package/package.json +14 -14
  48. package/packages/api-types/package.json +1 -1
  49. package/packages/bridge/dist/spawner.js +9 -0
  50. package/packages/bridge/package.json +7 -7
  51. package/packages/cloud/dist/server.js +3 -3
  52. package/packages/cloud/package.json +6 -6
  53. package/packages/config/package.json +2 -2
  54. package/packages/continuity/package.json +1 -1
  55. package/packages/daemon/package.json +12 -12
  56. package/packages/dashboard/dist/server.js +9 -9
  57. package/packages/dashboard/package.json +12 -12
  58. package/packages/dashboard/ui/react-components/App.tsx +21 -435
  59. package/packages/dashboard/ui/react-components/ChannelChat.tsx +29 -75
  60. package/packages/dashboard/ui/react-components/MessageComposer.tsx +457 -0
  61. package/packages/dashboard/ui-dist/404.html +1 -1
  62. package/packages/dashboard/ui-dist/_next/static/chunks/320-22ebe7be58cf982a.js +1 -0
  63. package/{dist/dashboard/out/_next/static/chunks/app/app/page-2e525b1dcc790967.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js} +1 -1
  64. package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-4e64923d73c35bc9.js → page-487fa38f041815c1.js} +1 -1
  65. package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
  66. package/packages/dashboard/ui-dist/app/onboarding.txt +2 -2
  67. package/packages/dashboard/ui-dist/app.html +1 -1
  68. package/packages/dashboard/ui-dist/app.txt +2 -2
  69. package/packages/dashboard/ui-dist/cloud/link.html +1 -1
  70. package/packages/dashboard/ui-dist/cloud/link.txt +2 -2
  71. package/packages/dashboard/ui-dist/connect-repos.html +1 -1
  72. package/packages/dashboard/ui-dist/connect-repos.txt +2 -2
  73. package/packages/dashboard/ui-dist/history.html +1 -1
  74. package/packages/dashboard/ui-dist/history.txt +2 -2
  75. package/packages/dashboard/ui-dist/index.html +1 -1
  76. package/packages/dashboard/ui-dist/index.txt +2 -2
  77. package/packages/dashboard/ui-dist/login.html +2 -2
  78. package/packages/dashboard/ui-dist/login.txt +2 -2
  79. package/packages/dashboard/ui-dist/metrics.html +1 -1
  80. package/packages/dashboard/ui-dist/metrics.txt +2 -2
  81. package/packages/dashboard/ui-dist/pricing.html +2 -2
  82. package/packages/dashboard/ui-dist/pricing.txt +2 -2
  83. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
  84. package/packages/dashboard/ui-dist/providers/setup/claude.txt +2 -2
  85. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
  86. package/packages/dashboard/ui-dist/providers/setup/codex.txt +2 -2
  87. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
  88. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +2 -2
  89. package/packages/dashboard/ui-dist/providers.html +1 -1
  90. package/packages/dashboard/ui-dist/providers.txt +2 -2
  91. package/packages/dashboard/ui-dist/signup.html +2 -2
  92. package/packages/dashboard/ui-dist/signup.txt +2 -2
  93. package/packages/dashboard-server/dist/server.js +5 -5
  94. package/packages/dashboard-server/package.json +12 -12
  95. package/packages/hooks/package.json +4 -4
  96. package/packages/mcp/package.json +2 -2
  97. package/packages/memory/package.json +2 -2
  98. package/packages/policy/package.json +2 -2
  99. package/packages/protocol/package.json +1 -1
  100. package/packages/resiliency/dist/cgroup-manager.d.ts +152 -0
  101. package/packages/resiliency/dist/cgroup-manager.js +394 -0
  102. package/packages/resiliency/dist/index.d.ts +1 -0
  103. package/packages/resiliency/dist/index.js +1 -0
  104. package/packages/resiliency/package.json +1 -1
  105. package/packages/sdk/package.json +2 -2
  106. package/packages/spawner/package.json +1 -1
  107. package/packages/state/package.json +1 -1
  108. package/packages/storage/package.json +2 -2
  109. package/packages/telemetry/package.json +1 -1
  110. package/packages/trajectory/package.json +2 -2
  111. package/packages/user-directory/package.json +2 -2
  112. package/packages/utils/package.json +1 -1
  113. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +8 -4
  114. package/packages/wrapper/dist/relay-pty-orchestrator.js +47 -25
  115. package/packages/wrapper/package.json +6 -6
  116. package/dist/dashboard/out/_next/static/chunks/320-900169c942e31422.js +0 -1
  117. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +0 -1
  118. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js +0 -1
  119. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +0 -1
  120. package/dist/dashboard/out/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js +0 -1
  121. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
  122. package/dist/dashboard/out/_next/static/chunks/app/login/page-6ec54eee75877971.js +0 -1
  123. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js +0 -1
  124. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +0 -1
  125. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +0 -1
  126. package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +0 -1
  127. package/packages/dashboard/ui-dist/_next/static/chunks/320-900169c942e31422.js +0 -1
  128. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +0 -1
  129. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-44813aa26ad19681.js +0 -1
  130. package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +0 -1
  131. package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
  132. package/packages/dashboard/ui-dist/_next/static/chunks/app/page-7993778218818ace.js +0 -1
  133. package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +0 -1
  134. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +0 -1
  135. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +0 -1
  136. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-18a4665665f6be11.js +0 -1
  137. package/packages/dashboard/ui-dist/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +0 -1
  138. /package/dist/dashboard/out/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_buildManifest.js +0 -0
  139. /package/dist/dashboard/out/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_ssgManifest.js +0 -0
  140. /package/packages/dashboard/ui-dist/_next/static/{KIxE0Ds_zdGuDJDQu7_sb → 1yt8VDAusp2yTBf4JFA7F}/_buildManifest.js +0 -0
  141. /package/packages/dashboard/ui-dist/_next/static/{KIxE0Ds_zdGuDJDQu7_sb → 1yt8VDAusp2yTBf4JFA7F}/_ssgManifest.js +0 -0
  142. /package/packages/dashboard/ui-dist/_next/static/{SoK46dEi3IsNBVWXD9x0L → chdCrViSD2yReZElqRfk0}/_buildManifest.js +0 -0
  143. /package/packages/dashboard/ui-dist/_next/static/{SoK46dEi3IsNBVWXD9x0L → chdCrViSD2yReZElqRfk0}/_ssgManifest.js +0 -0
  144. /package/packages/dashboard/ui-dist/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_buildManifest.js +0 -0
  145. /package/packages/dashboard/ui-dist/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/daemon",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Relay daemon server - agent coordination and message routing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,17 +22,17 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/protocol": "2.0.6",
26
- "@agent-relay/config": "2.0.6",
27
- "@agent-relay/storage": "2.0.6",
28
- "@agent-relay/bridge": "2.0.6",
29
- "@agent-relay/utils": "2.0.6",
30
- "@agent-relay/policy": "2.0.6",
31
- "@agent-relay/memory": "2.0.6",
32
- "@agent-relay/resiliency": "2.0.6",
33
- "@agent-relay/user-directory": "2.0.6",
34
- "@agent-relay/wrapper": "2.0.6",
35
- "@agent-relay/telemetry": "2.0.6",
25
+ "@agent-relay/protocol": "2.0.7",
26
+ "@agent-relay/config": "2.0.7",
27
+ "@agent-relay/storage": "2.0.7",
28
+ "@agent-relay/bridge": "2.0.7",
29
+ "@agent-relay/utils": "2.0.7",
30
+ "@agent-relay/policy": "2.0.7",
31
+ "@agent-relay/memory": "2.0.7",
32
+ "@agent-relay/resiliency": "2.0.7",
33
+ "@agent-relay/user-directory": "2.0.7",
34
+ "@agent-relay/wrapper": "2.0.7",
35
+ "@agent-relay/telemetry": "2.0.7",
36
36
  "ws": "^8.18.3",
37
37
  "better-sqlite3": "^12.6.2",
38
38
  "pg": "^8.16.3",
@@ -679,25 +679,25 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
679
679
  const attachmentRegistry = new Map();
680
680
  // Serve dashboard static files at root (built with `next build`)
681
681
  // Search order:
682
- // 1. ui-dist/ - @agent-relay/dashboard package (new structure)
683
- // 2. dist/dashboard/out - monorepo build (legacy)
684
- // 3. src/dashboard/out - development (legacy)
682
+ // 1. ui-dist/ - @agent-relay/dashboard package (published)
683
+ // 2. ui/out - development (packages/dashboard/ui/out)
684
+ // 3. dist/dashboard/out - monorepo build
685
685
  const findDashboardDir = () => {
686
686
  let current = __dirname;
687
687
  // Try up to 10 levels up
688
688
  for (let i = 0; i < 10; i++) {
689
- // New package structure: ui-dist at package root
689
+ // Package structure: ui-dist at package root (for published package)
690
690
  const uiDistPath = path.join(current, 'ui-dist');
691
691
  if (fs.existsSync(uiDistPath))
692
692
  return uiDistPath;
693
- // Legacy: monorepo build output
693
+ // Development: ui/out at package root
694
+ const uiOutPath = path.join(current, 'ui', 'out');
695
+ if (fs.existsSync(uiOutPath))
696
+ return uiOutPath;
697
+ // Monorepo build output
694
698
  const distPath = path.join(current, 'dist', 'dashboard', 'out');
695
699
  if (fs.existsSync(distPath))
696
700
  return distPath;
697
- // Legacy: development path
698
- const srcPath = path.join(current, 'src', 'dashboard', 'out');
699
- if (fs.existsSync(srcPath))
700
- return srcPath;
701
701
  const parent = path.dirname(current);
702
702
  if (parent === current)
703
703
  break; // reached root
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/dashboard",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Web dashboard for Agent Relay - optional package for visual agent coordination",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,17 +25,17 @@
25
25
  "test:watch": "vitest"
26
26
  },
27
27
  "dependencies": {
28
- "@agent-relay/protocol": "2.0.6",
29
- "@agent-relay/config": "2.0.6",
30
- "@agent-relay/storage": "2.0.6",
31
- "@agent-relay/bridge": "2.0.6",
32
- "@agent-relay/utils": "2.0.6",
33
- "@agent-relay/resiliency": "2.0.6",
34
- "@agent-relay/trajectory": "2.0.6",
35
- "@agent-relay/cloud": "2.0.6",
36
- "@agent-relay/daemon": "2.0.6",
37
- "@agent-relay/user-directory": "2.0.6",
38
- "@agent-relay/wrapper": "2.0.6",
28
+ "@agent-relay/protocol": "2.0.7",
29
+ "@agent-relay/config": "2.0.7",
30
+ "@agent-relay/storage": "2.0.7",
31
+ "@agent-relay/bridge": "2.0.7",
32
+ "@agent-relay/utils": "2.0.7",
33
+ "@agent-relay/resiliency": "2.0.7",
34
+ "@agent-relay/trajectory": "2.0.7",
35
+ "@agent-relay/cloud": "2.0.7",
36
+ "@agent-relay/daemon": "2.0.7",
37
+ "@agent-relay/user-directory": "2.0.7",
38
+ "@agent-relay/wrapper": "2.0.7",
39
39
  "express": "^5.2.1",
40
40
  "ws": "^8.18.3"
41
41
  },
@@ -17,9 +17,8 @@ import { SpawnModal, type SpawnConfig } from './SpawnModal';
17
17
  import { NewConversationModal } from './NewConversationModal';
18
18
  import { SettingsPage, defaultSettings, type Settings } from './settings';
19
19
  import { ConversationHistory } from './ConversationHistory';
20
- import { MentionAutocomplete, getMentionQuery, completeMentionInValue, type HumanUser } from './MentionAutocomplete';
20
+ import type { HumanUser } from './MentionAutocomplete';
21
21
  import { NotificationToast, useToasts } from './NotificationToast';
22
- import { FileAutocomplete, getFileQuery, completeFileInValue } from './FileAutocomplete';
23
22
  import { WorkspaceSelector, type Workspace } from './WorkspaceSelector';
24
23
  import { AddWorkspaceModal } from './AddWorkspaceModal';
25
24
  import { LogViewerPanel } from './LogViewerPanel';
@@ -28,6 +27,7 @@ import { DecisionQueue, type Decision } from './DecisionQueue';
28
27
  import { FleetOverview } from './FleetOverview';
29
28
  import type { ServerInfo } from './ServerCard';
30
29
  import { TypingIndicator } from './TypingIndicator';
30
+ import { MessageComposer } from './MessageComposer';
31
31
  import { OnlineUsersIndicator } from './OnlineUsersIndicator';
32
32
  import { UserProfilePanel } from './UserProfilePanel';
33
33
  import { AgentProfilePanel } from './AgentProfilePanel';
@@ -1771,24 +1771,37 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
1771
1771
  });
1772
1772
  }, [currentHuman, dmSelectedAgentsByHuman]);
1773
1773
 
1774
- const handleDmSend = useCallback(async (_to: string, content: string): Promise<boolean> => {
1774
+ const handleDmSend = useCallback(async (content: string, attachmentIds?: string[]): Promise<boolean> => {
1775
1775
  if (!currentHuman) return false;
1776
1776
  const humanName = currentHuman.name;
1777
1777
 
1778
1778
  // Always send to the human
1779
- await sendMessage(humanName, content);
1779
+ await sendMessage(humanName, content, undefined, attachmentIds);
1780
1780
 
1781
1781
  // Only send to agents if they were explicitly selected for this conversation
1782
1782
  // Don't send to agents in pure 1:1 human conversations
1783
1783
  if (selectedDmAgents.length > 0) {
1784
1784
  for (const agent of selectedDmAgents) {
1785
- await sendMessage(agent, content);
1785
+ await sendMessage(agent, content, undefined, attachmentIds);
1786
1786
  }
1787
1787
  }
1788
1788
 
1789
1789
  return true;
1790
1790
  }, [currentHuman, selectedDmAgents, sendMessage]);
1791
1791
 
1792
+ const handleMainComposerSend = useCallback(
1793
+ async (content: string, attachmentIds?: string[]) => {
1794
+ const recipient = currentChannel === 'general' ? '*' : currentChannel;
1795
+
1796
+ if (currentHuman) {
1797
+ return handleDmSend(content, attachmentIds);
1798
+ }
1799
+
1800
+ return sendMessage(recipient, content, undefined, attachmentIds);
1801
+ },
1802
+ [currentChannel, currentHuman, handleDmSend, sendMessage]
1803
+ );
1804
+
1792
1805
  const dmInviteCommands = useMemo(() => {
1793
1806
  if (!currentHuman) return [];
1794
1807
  return agents
@@ -2690,15 +2703,16 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
2690
2703
  {viewMode !== 'channels' && (
2691
2704
  <div className="p-2 sm:p-4 bg-bg-tertiary border-t border-border-subtle">
2692
2705
  <MessageComposer
2693
- recipient={currentChannel === 'general' ? '*' : currentChannel}
2694
2706
  agents={agents}
2695
2707
  humanUsers={humanUsers}
2696
- onSend={currentHuman ? handleDmSend : sendMessage}
2708
+ onSend={handleMainComposerSend}
2697
2709
  onTyping={sendTyping}
2698
2710
  isSending={isSending}
2699
2711
  error={sendError}
2700
2712
  insertMention={pendingMention}
2701
2713
  onMentionInserted={() => setPendingMention(undefined)}
2714
+ enableFileAutocomplete
2715
+ placeholder={`Message ${currentChannel === 'general' ? 'everyone' : '@' + currentChannel}...`}
2702
2716
  />
2703
2717
  </div>
2704
2718
  )}
@@ -2974,434 +2988,6 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
2974
2988
  );
2975
2989
  }
2976
2990
 
2977
- /**
2978
- * Pending attachment interface for UI state
2979
- */
2980
- interface PendingAttachment {
2981
- id: string;
2982
- file: File;
2983
- preview: string;
2984
- isUploading: boolean;
2985
- uploadedId?: string;
2986
- error?: string;
2987
- }
2988
-
2989
- /**
2990
- * Message Composer Component with @-mention autocomplete and image attachments
2991
- */
2992
- interface MessageComposerProps {
2993
- recipient: string;
2994
- agents: Agent[];
2995
- humanUsers: HumanUser[];
2996
- onSend: (to: string, content: string, thread?: string, attachmentIds?: string[]) => Promise<boolean>;
2997
- onTyping?: (isTyping: boolean) => void;
2998
- isSending: boolean;
2999
- error: string | null;
3000
- insertMention?: string;
3001
- onMentionInserted?: () => void;
3002
- }
3003
-
3004
- function MessageComposer({ recipient, agents, humanUsers, onSend, onTyping, isSending, error, insertMention, onMentionInserted }: MessageComposerProps) {
3005
- const [message, setMessage] = useState('');
3006
- const [cursorPosition, setCursorPosition] = useState(0);
3007
- const [showMentions, setShowMentions] = useState(false);
3008
- const [showFiles, setShowFiles] = useState(false);
3009
- const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
3010
- const textareaRef = useRef<HTMLTextAreaElement>(null);
3011
- const fileInputRef = useRef<HTMLInputElement>(null);
3012
-
3013
- // Handle insertMention prop - insert @username when triggered from outside
3014
- useEffect(() => {
3015
- if (insertMention && onMentionInserted) {
3016
- const mentionText = `@${insertMention} `;
3017
- // Insert at current cursor position or append to end
3018
- const textarea = textareaRef.current;
3019
- if (textarea) {
3020
- const start = textarea.selectionStart || message.length;
3021
- const newMessage = message.slice(0, start) + mentionText + message.slice(start);
3022
- setMessage(newMessage);
3023
- // Focus and set cursor position after the mention
3024
- setTimeout(() => {
3025
- textarea.focus();
3026
- const newPos = start + mentionText.length;
3027
- textarea.setSelectionRange(newPos, newPos);
3028
- }, 0);
3029
- } else {
3030
- // Fallback: just append to message
3031
- setMessage(prev => prev + mentionText);
3032
- }
3033
- onMentionInserted();
3034
- }
3035
- }, [insertMention, onMentionInserted, message]);
3036
-
3037
- // Process image files (used by both paste and file input)
3038
- const processImageFiles = useCallback(async (imageFiles: File[]) => {
3039
- for (const file of imageFiles) {
3040
- const id = crypto.randomUUID();
3041
- const preview = URL.createObjectURL(file);
3042
-
3043
- // Add to pending attachments
3044
- setAttachments(prev => [...prev, {
3045
- id,
3046
- file,
3047
- preview,
3048
- isUploading: true,
3049
- }]);
3050
-
3051
- // Upload the file
3052
- try {
3053
- const result = await api.uploadAttachment(file);
3054
- if (result.success && result.data) {
3055
- setAttachments(prev => prev.map(a =>
3056
- a.id === id
3057
- ? { ...a, isUploading: false, uploadedId: result.data!.attachment.id }
3058
- : a
3059
- ));
3060
- } else {
3061
- setAttachments(prev => prev.map(a =>
3062
- a.id === id
3063
- ? { ...a, isUploading: false, error: result.error || 'Upload failed' }
3064
- : a
3065
- ));
3066
- }
3067
- } catch (err) {
3068
- setAttachments(prev => prev.map(a =>
3069
- a.id === id
3070
- ? { ...a, isUploading: false, error: 'Upload failed' }
3071
- : a
3072
- ));
3073
- }
3074
- }
3075
- }, []);
3076
-
3077
- // Handle file selection from file input
3078
- const handleFileSelect = useCallback((files: FileList | null) => {
3079
- if (!files || files.length === 0) return;
3080
-
3081
- const imageFiles = Array.from(files).filter(file =>
3082
- file.type.startsWith('image/')
3083
- );
3084
-
3085
- if (imageFiles.length > 0) {
3086
- processImageFiles(imageFiles);
3087
- }
3088
- }, [processImageFiles]);
3089
-
3090
- // Handle paste for clipboard images
3091
- const handlePaste = useCallback((e: React.ClipboardEvent) => {
3092
- const clipboardData = e.clipboardData;
3093
- if (!clipboardData) return;
3094
-
3095
- // Collect image files from both sources
3096
- let imageFiles: File[] = [];
3097
-
3098
- // Method 1: Check clipboardData.files (works for file pastes)
3099
- if (clipboardData.files && clipboardData.files.length > 0) {
3100
- imageFiles = Array.from(clipboardData.files).filter(file =>
3101
- file.type.startsWith('image/')
3102
- );
3103
- }
3104
-
3105
- // Method 2: Check clipboardData.items (works for screenshots/copied images)
3106
- // This is the primary method for pasted images from clipboard
3107
- if (imageFiles.length === 0 && clipboardData.items) {
3108
- const items = Array.from(clipboardData.items);
3109
- for (const item of items) {
3110
- // Check if this item is an image
3111
- if (item.kind === 'file' && item.type.startsWith('image/')) {
3112
- const file = item.getAsFile();
3113
- if (file) {
3114
- imageFiles.push(file);
3115
- }
3116
- }
3117
- }
3118
- }
3119
-
3120
- // Process any found images
3121
- if (imageFiles.length > 0) {
3122
- e.preventDefault();
3123
- processImageFiles(imageFiles);
3124
- }
3125
- }, [processImageFiles]);
3126
-
3127
- // Remove an attachment
3128
- const removeAttachment = useCallback((id: string) => {
3129
- setAttachments(prev => {
3130
- const attachment = prev.find(a => a.id === id);
3131
- if (attachment) {
3132
- URL.revokeObjectURL(attachment.preview);
3133
- }
3134
- return prev.filter(a => a.id !== id);
3135
- });
3136
- }, []);
3137
-
3138
- const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
3139
- const value = e.target.value;
3140
- const cursorPos = e.target.selectionStart || 0;
3141
- setMessage(value);
3142
- setCursorPosition(cursorPos);
3143
-
3144
- // Send typing indicator when user has content
3145
- onTyping?.(value.trim().length > 0);
3146
-
3147
- // Check for file autocomplete first (@ followed by path-like pattern)
3148
- const fileQuery = getFileQuery(value, cursorPos);
3149
- if (fileQuery !== null) {
3150
- setShowFiles(true);
3151
- setShowMentions(false);
3152
- return;
3153
- }
3154
-
3155
- // Check for mention autocomplete (@ at start without path patterns)
3156
- const mentionQuery = getMentionQuery(value, cursorPos);
3157
- if (mentionQuery !== null) {
3158
- setShowMentions(true);
3159
- setShowFiles(false);
3160
- return;
3161
- }
3162
-
3163
- // Neither - hide both
3164
- setShowMentions(false);
3165
- setShowFiles(false);
3166
- };
3167
-
3168
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
3169
- // Don't handle Enter/Tab when autocomplete is visible (let autocomplete handle it)
3170
- if ((showMentions || showFiles) && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Tab')) {
3171
- return; // Let the autocomplete component handle these keys
3172
- }
3173
-
3174
- if (e.key === 'Enter' && !e.shiftKey && !showMentions && !showFiles) {
3175
- e.preventDefault();
3176
- if ((message.trim() || attachments.length > 0) && !isSending) {
3177
- handleSubmit(e as unknown as React.FormEvent);
3178
- }
3179
- }
3180
- };
3181
-
3182
- const handleMentionSelect = (mention: string, newValue: string) => {
3183
- setMessage(newValue);
3184
- setShowMentions(false);
3185
- setShowFiles(false);
3186
- setTimeout(() => {
3187
- if (textareaRef.current) {
3188
- textareaRef.current.focus();
3189
- const pos = newValue.indexOf(' ') + 1;
3190
- textareaRef.current.setSelectionRange(pos, pos);
3191
- }
3192
- }, 0);
3193
- };
3194
-
3195
- const handleFilePathSelect = (filePath: string, newValue: string) => {
3196
- setMessage(newValue);
3197
- setShowFiles(false);
3198
- setShowMentions(false);
3199
- setTimeout(() => {
3200
- if (textareaRef.current) {
3201
- textareaRef.current.focus();
3202
- const pos = newValue.indexOf(' ', 1) + 1; // After @path/to/file<space>
3203
- textareaRef.current.setSelectionRange(pos, pos);
3204
- }
3205
- }, 0);
3206
- };
3207
-
3208
- const handleSubmit = async (e: React.FormEvent) => {
3209
- e.preventDefault();
3210
-
3211
- // Need either message or attachments
3212
- const hasMessage = message.trim().length > 0;
3213
- const hasAttachments = attachments.length > 0;
3214
- if ((!hasMessage && !hasAttachments) || isSending) return;
3215
-
3216
- // Check if any attachments are still uploading
3217
- const stillUploading = attachments.some(a => a.isUploading);
3218
- if (stillUploading) return;
3219
-
3220
- // Get uploaded attachment IDs
3221
- const attachmentIds = attachments
3222
- .filter(a => a.uploadedId)
3223
- .map(a => a.uploadedId!);
3224
-
3225
- const mentionMatch = message.match(/^@(\S+)\s*([\s\S]*)/);
3226
- let target: string;
3227
- let content: string;
3228
-
3229
- if (mentionMatch) {
3230
- const mentionedName = mentionMatch[1];
3231
- content = mentionMatch[2] || '';
3232
-
3233
- if (mentionedName === '*' || mentionedName.toLowerCase() === 'everyone' || mentionedName.toLowerCase() === 'all') {
3234
- target = '*';
3235
- } else if (mentionedName.toLowerCase().startsWith('team:')) {
3236
- // Team mention - pass through to backend (e.g., team:frontend)
3237
- target = mentionedName;
3238
- } else {
3239
- // Check if this is a file path mention (contains / or ends with common file extensions)
3240
- // If so, keep the full message as content and use default recipient
3241
- if (mentionedName.includes('/') || /\.(ts|tsx|js|jsx|json|md|py|go|rs|java|c|cpp|h|css|html|yaml|yml|toml)$/i.test(mentionedName)) {
3242
- target = recipient;
3243
- content = message; // Keep the @path/to/file in the message
3244
- } else {
3245
- target = mentionedName;
3246
- }
3247
- }
3248
- } else {
3249
- target = recipient;
3250
- content = message;
3251
- }
3252
-
3253
- // If no message but has attachments, send with default text
3254
- if (!content.trim() && attachmentIds.length > 0) {
3255
- content = '[Screenshot attached]';
3256
- }
3257
-
3258
- const success = await onSend(
3259
- target,
3260
- content || message,
3261
- undefined,
3262
- attachmentIds.length > 0 ? attachmentIds : undefined
3263
- );
3264
-
3265
- if (success) {
3266
- // Clean up previews
3267
- attachments.forEach(a => URL.revokeObjectURL(a.preview));
3268
- setMessage('');
3269
- setAttachments([]);
3270
- setShowMentions(false);
3271
- setShowFiles(false);
3272
- }
3273
- };
3274
-
3275
- // Check if we can send (have content or attachments, not uploading)
3276
- const canSend = (message.trim() || attachments.length > 0) &&
3277
- !isSending &&
3278
- !attachments.some(a => a.isUploading);
3279
-
3280
- return (
3281
- <form className="flex flex-col gap-1.5 sm:gap-2" onSubmit={handleSubmit}>
3282
- {/* Attachment previews */}
3283
- {attachments.length > 0 && (
3284
- <div className="flex flex-wrap gap-1.5 sm:gap-2 p-1.5 sm:p-2 bg-bg-card rounded-lg border border-border-subtle">
3285
- {attachments.map(attachment => (
3286
- <div
3287
- key={attachment.id}
3288
- className="relative group"
3289
- >
3290
- <img
3291
- src={attachment.preview}
3292
- alt={attachment.file.name}
3293
- className={`h-16 w-auto rounded-lg object-cover ${attachment.isUploading ? 'opacity-50' : ''} ${attachment.error ? 'border-2 border-error' : ''}`}
3294
- />
3295
- {attachment.isUploading && (
3296
- <div className="absolute inset-0 flex items-center justify-center">
3297
- <svg className="animate-spin h-5 w-5 text-accent-cyan" viewBox="0 0 24 24">
3298
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" strokeDasharray="32" strokeLinecap="round" />
3299
- </svg>
3300
- </div>
3301
- )}
3302
- {attachment.error && (
3303
- <div className="absolute bottom-0 left-0 right-0 bg-error/90 text-white text-[10px] px-1 py-0.5 truncate">
3304
- {attachment.error}
3305
- </div>
3306
- )}
3307
- <button
3308
- type="button"
3309
- onClick={() => removeAttachment(attachment.id)}
3310
- className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-bg-tertiary border border-border-subtle rounded-full flex items-center justify-center text-text-muted hover:text-error hover:border-error transition-colors opacity-0 group-hover:opacity-100"
3311
- title="Remove"
3312
- >
3313
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
3314
- <line x1="18" y1="6" x2="6" y2="18" />
3315
- <line x1="6" y1="6" x2="18" y2="18" />
3316
- </svg>
3317
- </button>
3318
- </div>
3319
- ))}
3320
- </div>
3321
- )}
3322
-
3323
- {/* Input row */}
3324
- <div className="flex items-center gap-1.5 sm:gap-3">
3325
- {/* Image upload button */}
3326
- <input
3327
- ref={fileInputRef}
3328
- type="file"
3329
- accept="image/*"
3330
- multiple
3331
- className="hidden"
3332
- onChange={(e) => handleFileSelect(e.target.files)}
3333
- />
3334
- <button
3335
- type="button"
3336
- onClick={() => fileInputRef.current?.click()}
3337
- className="p-2 sm:p-2.5 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-text-muted hover:text-accent-cyan hover:border-accent-cyan/50 transition-colors flex-shrink-0"
3338
- title="Attach screenshot (or paste from clipboard)"
3339
- >
3340
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="sm:w-[18px] sm:h-[18px]">
3341
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
3342
- <circle cx="8.5" cy="8.5" r="1.5" />
3343
- <polyline points="21 15 16 10 5 21" />
3344
- </svg>
3345
- </button>
3346
-
3347
- <div className="flex-1 relative min-w-0">
3348
- {/* Agent mention autocomplete */}
3349
- <MentionAutocomplete
3350
- agents={agents}
3351
- humanUsers={humanUsers}
3352
- inputValue={message}
3353
- cursorPosition={cursorPosition}
3354
- onSelect={handleMentionSelect}
3355
- onClose={() => setShowMentions(false)}
3356
- isVisible={showMentions}
3357
- />
3358
- {/* File path autocomplete */}
3359
- <FileAutocomplete
3360
- inputValue={message}
3361
- cursorPosition={cursorPosition}
3362
- onSelect={handleFilePathSelect}
3363
- onClose={() => setShowFiles(false)}
3364
- isVisible={showFiles}
3365
- />
3366
- <textarea
3367
- ref={textareaRef}
3368
- className="w-full py-2 sm:py-3 px-3 sm:px-4 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-sm font-sans text-text-primary outline-none transition-all duration-200 resize-none min-h-[40px] sm:min-h-[44px] max-h-[100px] sm:max-h-[120px] overflow-y-auto focus:border-accent-cyan/50 focus:shadow-[0_0_0_3px_rgba(0,217,255,0.1)] placeholder:text-text-muted"
3369
- placeholder={`Message ${recipient === '*' ? 'everyone' : '@' + recipient}...`}
3370
- value={message}
3371
- onChange={handleInputChange}
3372
- onKeyDown={handleKeyDown}
3373
- onPaste={handlePaste}
3374
- onSelect={(e) => setCursorPosition((e.target as HTMLTextAreaElement).selectionStart || 0)}
3375
- disabled={isSending}
3376
- rows={1}
3377
- />
3378
- </div>
3379
- <button
3380
- type="submit"
3381
- className="py-2 sm:py-3 px-3 sm:px-5 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-lg sm:rounded-xl text-xs sm:text-sm cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none flex-shrink-0"
3382
- disabled={!canSend}
3383
- title={isSending ? 'Sending...' : attachments.some(a => a.isUploading) ? 'Uploading...' : 'Send message'}
3384
- >
3385
- {isSending ? (
3386
- <span className="hidden sm:inline">Sending...</span>
3387
- ) : attachments.some(a => a.isUploading) ? (
3388
- <span className="hidden sm:inline">Uploading...</span>
3389
- ) : (
3390
- <span className="flex items-center gap-1 sm:gap-2">
3391
- <span className="hidden sm:inline">Send</span>
3392
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
3393
- <line x1="22" y1="2" x2="11" y2="13"></line>
3394
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
3395
- </svg>
3396
- </span>
3397
- )}
3398
- </button>
3399
- {error && <span className="text-error text-xs ml-2">{error}</span>}
3400
- </div>
3401
- </form>
3402
- );
3403
- }
3404
-
3405
2991
  function LoadingSpinner() {
3406
2992
  return (
3407
2993
  <svg className="animate-spin mb-4 text-accent-cyan" width="28" height="28" viewBox="0 0 24 24">