@swarmclawai/swarmclaw 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +85 -139
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/thread/route.ts +1 -2
  4. package/src/app/api/agents/route.ts +1 -1
  5. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  6. package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
  7. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  8. package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
  9. package/src/app/api/{sessions → chats}/route.ts +5 -7
  10. package/src/app/api/plugins/route.ts +3 -0
  11. package/src/app/api/plugins/settings/route.ts +35 -0
  12. package/src/app/api/usage/route.ts +30 -0
  13. package/src/cli/index.js +35 -33
  14. package/src/cli/index.ts +40 -39
  15. package/src/cli/spec.js +29 -27
  16. package/src/components/agents/agent-card.tsx +1 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +8 -13
  19. package/src/components/agents/agent-sheet.tsx +2 -2
  20. package/src/components/agents/cron-job-form.tsx +3 -3
  21. package/src/components/agents/inspector-panel.tsx +2 -2
  22. package/src/components/auth/setup-wizard.tsx +5 -38
  23. package/src/components/chat/chat-area.tsx +10 -14
  24. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
  25. package/src/components/chat/chat-header.tsx +156 -73
  26. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
  27. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  28. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  29. package/src/components/chat/message-bubble.tsx +4 -1
  30. package/src/components/chat/message-list.tsx +2 -2
  31. package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
  32. package/src/components/chat/session-debug-panel.tsx +1 -1
  33. package/src/components/chat/tool-request-banner.tsx +3 -3
  34. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  35. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  36. package/src/components/connectors/connector-sheet.tsx +1 -1
  37. package/src/components/home/home-view.tsx +1 -1
  38. package/src/components/layout/app-layout.tsx +23 -2
  39. package/src/components/plugins/plugin-list.tsx +475 -254
  40. package/src/components/plugins/plugin-sheet.tsx +124 -10
  41. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  42. package/src/components/shared/command-palette.tsx +0 -1
  43. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  44. package/src/components/shared/settings/section-providers.tsx +1 -1
  45. package/src/components/shared/settings/settings-page.tsx +1 -12
  46. package/src/components/usage/metrics-dashboard.tsx +73 -0
  47. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  48. package/src/lib/chat.ts +1 -1
  49. package/src/lib/{sessions.ts → chats.ts} +28 -18
  50. package/src/lib/providers/claude-cli.ts +1 -1
  51. package/src/lib/server/approvals.ts +4 -4
  52. package/src/lib/server/capability-router.ts +10 -8
  53. package/src/lib/server/chat-execution.ts +36 -105
  54. package/src/lib/server/chatroom-helpers.ts +3 -3
  55. package/src/lib/server/connectors/manager.ts +4 -4
  56. package/src/lib/server/cost.ts +34 -1
  57. package/src/lib/server/daemon-state.ts +2 -2
  58. package/src/lib/server/heartbeat-service.ts +1 -1
  59. package/src/lib/server/main-agent-loop.ts +25 -160
  60. package/src/lib/server/main-session.ts +6 -13
  61. package/src/lib/server/orchestrator-lg.ts +3 -3
  62. package/src/lib/server/orchestrator.ts +5 -5
  63. package/src/lib/server/plugins.ts +112 -4
  64. package/src/lib/server/provider-health.ts +5 -3
  65. package/src/lib/server/queue.ts +12 -10
  66. package/src/lib/server/session-run-manager.test.ts +9 -6
  67. package/src/lib/server/session-run-manager.ts +1 -3
  68. package/src/lib/server/session-tools/calendar.ts +376 -0
  69. package/src/lib/server/session-tools/canvas.ts +1 -1
  70. package/src/lib/server/session-tools/chatroom.ts +4 -2
  71. package/src/lib/server/session-tools/connector.ts +5 -2
  72. package/src/lib/server/session-tools/context.ts +7 -3
  73. package/src/lib/server/session-tools/crud.ts +14 -6
  74. package/src/lib/server/session-tools/delegate.ts +95 -8
  75. package/src/lib/server/session-tools/discovery.ts +2 -2
  76. package/src/lib/server/session-tools/edit_file.ts +4 -2
  77. package/src/lib/server/session-tools/email.ts +322 -0
  78. package/src/lib/server/session-tools/file.ts +5 -2
  79. package/src/lib/server/session-tools/git.ts +1 -1
  80. package/src/lib/server/session-tools/http.ts +1 -1
  81. package/src/lib/server/session-tools/image-gen.ts +382 -0
  82. package/src/lib/server/session-tools/index.ts +74 -49
  83. package/src/lib/server/session-tools/memory.ts +139 -2
  84. package/src/lib/server/session-tools/monitor.ts +1 -1
  85. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  86. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  87. package/src/lib/server/session-tools/platform.ts +6 -3
  88. package/src/lib/server/session-tools/plugin-creator.ts +3 -3
  89. package/src/lib/server/session-tools/replicate.ts +303 -0
  90. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  91. package/src/lib/server/session-tools/sandbox.ts +4 -2
  92. package/src/lib/server/session-tools/schedule.ts +4 -2
  93. package/src/lib/server/session-tools/session-info.ts +7 -4
  94. package/src/lib/server/session-tools/shell.ts +5 -2
  95. package/src/lib/server/session-tools/subagent.ts +2 -2
  96. package/src/lib/server/session-tools/wallet.ts +29 -2
  97. package/src/lib/server/session-tools/web.ts +44 -5
  98. package/src/lib/server/storage.ts +29 -9
  99. package/src/lib/server/stream-agent-chat.ts +72 -249
  100. package/src/lib/server/tool-aliases.ts +26 -15
  101. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  102. package/src/lib/server/tool-capability-policy.ts +32 -27
  103. package/src/lib/tool-definitions.ts +4 -0
  104. package/src/lib/validation/schemas.ts +3 -1
  105. package/src/stores/use-app-store.ts +5 -5
  106. package/src/stores/use-chat-store.ts +7 -7
  107. package/src/types/index.ts +65 -3
  108. /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
  109. /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
  110. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  111. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  112. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  113. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  114. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  115. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  116. /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
  117. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  118. /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
  119. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
3
+ import { useEffect, useState, useMemo, useRef, useCallback, type ReactNode } from 'react'
4
4
  import type { Session } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
@@ -14,11 +14,24 @@ import {
14
14
  } from '@/components/shared/connector-platform-icon'
15
15
  import { AgentAvatar } from '@/components/agents/agent-avatar'
16
16
  import { ModelCombobox } from '@/components/shared/model-combobox'
17
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
17
18
  import { toast } from 'sonner'
18
19
  import type { ProviderType } from '@/types'
19
20
  import { copyTextToClipboard } from '@/lib/clipboard'
20
21
  import { useWs } from '@/hooks/use-ws'
21
22
 
23
+ function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
24
+ return (
25
+ <Tooltip>
26
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
27
+ <TooltipContent side={side} sideOffset={6}
28
+ className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[8px] px-2.5 py-1.5 text-[11px] z-[100]">
29
+ {label}
30
+ </TooltipContent>
31
+ </Tooltip>
32
+ )
33
+ }
34
+
22
35
  function shortPath(p: string): string {
23
36
  return (p || '').replace(/^\/Users\/\w+/, '~')
24
37
  }
@@ -109,6 +122,9 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
109
122
  const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
110
123
  const [walletBalance, setWalletBalance] = useState<number | null>(null)
111
124
  const [headerWidgets, setHeaderWidgets] = useState<Array<{ id: string; label: string; icon?: string }>>([])
125
+ const [goalModalOpen, setGoalModalOpen] = useState(false)
126
+ const [goalDraft, setGoalDraft] = useState('')
127
+ const goalInputRef = useRef<HTMLTextAreaElement>(null)
112
128
 
113
129
  useEffect(() => {
114
130
  api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
@@ -197,17 +213,17 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
197
213
  const handleDismissResumeHandle = async (e: React.MouseEvent) => {
198
214
  e.stopPropagation()
199
215
  try {
200
- await api('PUT', `/sessions/${session.id}`, {
216
+ await api('PUT', `/chats/${session.id}`, {
201
217
  claudeSessionId: null,
202
218
  codexThreadId: null,
203
219
  opencodeSessionId: null,
204
- delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
220
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
205
221
  })
206
222
  await loadSessions()
207
223
  } catch { /* best-effort */ }
208
224
  }
209
225
 
210
- const heartbeatSupported = (session.tools?.length ?? 0) > 0
226
+ const heartbeatSupported = (session.plugins?.length ?? 0) > 0
211
227
  const loopIsOngoing = appSettings.loopMode === 'ongoing'
212
228
  const { heartbeatEnabled, heartbeatIntervalSec, heartbeatExplicitOptIn } = useMemo(() => {
213
229
  // Resolve through the same cascade as the backend: settings → agent → session
@@ -254,7 +270,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
254
270
  }
255
271
  }, [appSettings, agent, session])
256
272
  const heartbeatWillRun = heartbeatEnabled && (loopIsOngoing || heartbeatExplicitOptIn)
257
- const isMainSession = session.name === '__main__'
273
+ const hasMainLoop = session.id.startsWith('agent-thread-') || session.sessionType === 'orchestrated'
258
274
  const missionState = session.mainLoopState || {}
259
275
  const missionPaused = missionState.paused === true
260
276
  const missionMode = missionState.autonomyMode === 'assist' ? 'assist' : 'autonomous'
@@ -270,10 +286,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
270
286
  if (session.agentId) {
271
287
  await api('PUT', `/agents/${session.agentId}`, { heartbeatEnabled: next })
272
288
  // Clear any stale session-level override so the agent value wins
273
- await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: null })
289
+ await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: null })
274
290
  await Promise.all([loadAgents(), loadSessions()])
275
291
  } else {
276
- await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: next })
292
+ await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: next })
277
293
  await loadSessions()
278
294
  }
279
295
  toast.success(`Heartbeat ${next ? 'enabled' : 'disabled'}`)
@@ -295,10 +311,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
295
311
  heartbeatEnabled: true,
296
312
  })
297
313
  // Clear stale session-level overrides
298
- await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
314
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
299
315
  await Promise.all([loadAgents(), loadSessions()])
300
316
  } else {
301
- await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
317
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
302
318
  await loadSessions()
303
319
  }
304
320
  } finally {
@@ -307,10 +323,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
307
323
  }
308
324
 
309
325
  const postMainLoopAction = async (action: string, extra?: Record<string, unknown>) => {
310
- if (!isMainSession || mainLoopSaving) return
326
+ if (!hasMainLoop || mainLoopSaving) return
311
327
  setMainLoopSaving(true)
312
328
  try {
313
- const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/sessions/${session.id}/main-loop`, {
329
+ const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/chats/${session.id}/main-loop`, {
314
330
  action,
315
331
  ...(extra || {}),
316
332
  })
@@ -344,17 +360,22 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
344
360
  void postMainLoopAction('nudge')
345
361
  }
346
362
 
347
- const handleSetMissionGoal = () => {
348
- if (!isMainSession) return
349
- const seededGoal = typeof missionState.goal === 'string' ? missionState.goal : ''
350
- const raw = window.prompt('Set mission goal', seededGoal)
351
- const goal = (raw || '').trim()
363
+ const handleOpenGoalModal = () => {
364
+ if (!hasMainLoop) return
365
+ setGoalDraft(typeof missionState.goal === 'string' ? missionState.goal : '')
366
+ setGoalModalOpen(true)
367
+ requestAnimationFrame(() => goalInputRef.current?.focus())
368
+ }
369
+
370
+ const handleSubmitGoal = () => {
371
+ const goal = goalDraft.trim()
372
+ setGoalModalOpen(false)
352
373
  if (!goal) return
353
374
  void postMainLoopAction('set_goal', { goal })
354
375
  }
355
376
 
356
377
  const handleClearMissionEvents = () => {
357
- if (!isMainSession || missionEventsCount <= 0) return
378
+ if (!hasMainLoop || missionEventsCount <= 0) return
358
379
  void postMainLoopAction('clear_events')
359
380
  }
360
381
 
@@ -473,7 +494,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
473
494
  const handleModelSwitch = async (nextProvider: ProviderType, nextModel: string) => {
474
495
  setModelSwitcherOpen(false)
475
496
  try {
476
- await api('PUT', `/sessions/${session.id}`, { provider: nextProvider, model: nextModel })
497
+ await api('PUT', `/chats/${session.id}`, { provider: nextProvider, model: nextModel })
477
498
  await loadSessions()
478
499
  } catch (err: unknown) {
479
500
  toast.error(err instanceof Error ? err.message : 'Failed to switch model')
@@ -502,12 +523,13 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
502
523
  }, [mainLoopNotice])
503
524
 
504
525
  // Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
505
- const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
506
- const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
526
+ const hasToolToggles = ((agent?.plugins?.length ?? 0) > 0) || ((session.plugins?.length ?? 0) > 0)
527
+ const hasMemoryLink = !!(agent && session.plugins?.includes('memory'))
507
528
  const hasSourceFilter = !!hasMultipleSources
508
- const hasContextBar = !!(isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
529
+ const hasContextBar = !!(hasMainLoop || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
509
530
 
510
531
  return (
532
+ <>
511
533
  <header
512
534
  className="relative z-20 border-b border-white/[0.06] shrink-0"
513
535
  style={{
@@ -586,8 +608,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
586
608
  onClick={agent ? startRename : undefined}
587
609
  title={agent ? 'Click to rename' : undefined}
588
610
  >{
589
- session.name === '__main__' ? 'Main Chat'
590
- : session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
611
+ session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
591
612
  : session.name
592
613
  }</span>
593
614
  )}
@@ -673,6 +694,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
673
694
  )}
674
695
  {modelName && (
675
696
  <div className="relative shrink-0" ref={modelSwitcherRef}>
697
+ <Tip label="Switch LLM model">
676
698
  <button
677
699
  type="button"
678
700
  onClick={() => {
@@ -681,13 +703,13 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
681
703
  }}
682
704
  disabled={streaming}
683
705
  className="inline-flex items-center gap-1 text-[11px] text-text-3/45 font-mono shrink-0 cursor-pointer bg-transparent border-none px-1 py-0.5 rounded-[5px] hover:bg-white/[0.04] hover:text-text-3/70 transition-colors disabled:cursor-default disabled:hover:text-text-3/45"
684
- title="Switch model"
685
706
  >
686
707
  {modelName}
687
708
  <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-30">
688
709
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
689
710
  </svg>
690
711
  </button>
712
+ </Tip>
691
713
  {modelSwitcherOpen && (
692
714
  <div className="absolute z-50 top-full left-0 mt-2 w-[280px] rounded-[12px] border border-white/[0.08] bg-surface backdrop-blur-md shadow-xl p-3">
693
715
  <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
@@ -717,16 +739,17 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
717
739
  )}
718
740
  </div>
719
741
  )}
742
+ <Tip label={`Open working directory: ${shortPath(session.cwd)}`}>
720
743
  <button
721
744
  type="button"
722
745
  onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
723
746
  className="inline-flex items-center shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-text-3/20 hover:text-text-3/50 hover:bg-white/[0.04] transition-colors"
724
- title={shortPath(session.cwd)}
725
747
  >
726
748
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
727
749
  <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
728
750
  </svg>
729
751
  </button>
752
+ </Tip>
730
753
  {/* Live agent status */}
731
754
  {(() => {
732
755
  const liveStatus = agentStatus || (missionState.status ? {
@@ -776,12 +799,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
776
799
  {/* Heartbeat compound control */}
777
800
  {heartbeatSupported && (
778
801
  <div className="flex items-center rounded-[8px] shrink-0" style={{ background: 'rgba(255,255,255,0.025)' }}>
802
+ <Tip label={heartbeatWillRun ? 'Disable heartbeat — periodic check-ins' : 'Enable heartbeat — periodic check-ins'}>
779
803
  <button
780
804
  onClick={handleToggleHeartbeat}
781
805
  disabled={heartbeatSaving}
782
806
  className={`flex items-center gap-1.5 pl-2.5 pr-1.5 py-1 transition-colors cursor-pointer border-none text-[11px] font-600
783
807
  ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/60 hover:bg-white/[0.04]'}`}
784
- title={heartbeatWillRun ? 'Disable heartbeat' : 'Enable heartbeat'}
785
808
  >
786
809
  <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/30'}`} />
787
810
  HB
@@ -789,18 +812,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
789
812
  <span className="text-[9px] text-text-3/40">(bounded)</span>
790
813
  )}
791
814
  </button>
815
+ </Tip>
792
816
  <div className="relative" ref={hbDropdownRef}>
817
+ <Tip label="Set heartbeat interval">
793
818
  <button
794
819
  onClick={() => setHbDropdownOpen((o) => !o)}
795
820
  disabled={heartbeatSaving}
796
821
  className="flex items-center gap-0.5 pl-1 pr-2 py-1 text-text-3/50 hover:text-text-3/70 hover:bg-white/[0.04] transition-colors cursor-pointer border-none"
797
- title="Set heartbeat interval"
798
822
  >
799
823
  <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
800
824
  <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
801
825
  <polyline points="6 9 12 15 18 9" />
802
826
  </svg>
803
827
  </button>
828
+ </Tip>
804
829
  {hbDropdownOpen && (
805
830
  <div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
806
831
  {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
@@ -886,53 +911,58 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
886
911
  {/* Context bar: tools, mission controls, links */}
887
912
  {hasContextBar && (
888
913
  <div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
889
- {isMainSession && (
914
+ {hasMainLoop && (
890
915
  <>
891
- <button
892
- onClick={handleToggleMissionPause}
893
- disabled={mainLoopSaving}
894
- className={`flex items-center gap-1.5 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
895
- ${missionPaused ? 'bg-amber-500/10 hover:bg-amber-500/18 text-amber-300' : 'bg-emerald-500/8 hover:bg-emerald-500/12 text-emerald-400'}`}
896
- title={missionPaused ? 'Resume mission' : 'Pause mission'}
897
- >
898
- <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
899
- {missionPaused ? 'Paused' : 'Live'}
900
- </button>
916
+ <Tip label={missionPaused ? 'Resume mission loop' : 'Pause mission loop'}>
917
+ <button
918
+ onClick={handleToggleMissionPause}
919
+ disabled={mainLoopSaving}
920
+ className={`flex items-center gap-1.5 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
921
+ ${missionPaused ? 'bg-amber-500/10 hover:bg-amber-500/18 text-amber-300' : 'bg-emerald-500/8 hover:bg-emerald-500/12 text-emerald-400'}`}
922
+ >
923
+ <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
924
+ {missionPaused ? 'Paused' : 'Live'}
925
+ </button>
926
+ </Tip>
901
927
 
902
- <button
903
- onClick={handleToggleMissionMode}
904
- disabled={mainLoopSaving}
905
- className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
906
- ${missionMode === 'autonomous' ? 'bg-indigo-500/12 hover:bg-indigo-500/20 text-indigo-300' : 'bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60'}`}
907
- title="Toggle autonomy mode"
908
- >
909
- {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
910
- </button>
911
- <button
912
- onClick={handleNudgeMission}
913
- disabled={mainLoopSaving || missionPaused}
914
- className="px-2 py-1 rounded-[7px] bg-blue-500/8 hover:bg-blue-500/15 text-blue-400 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600"
915
- title="Run one tick"
916
- >
917
- Nudge
918
- </button>
919
- <button
920
- onClick={handleSetMissionGoal}
921
- disabled={mainLoopSaving}
922
- className="px-2 py-1 rounded-[7px] bg-fuchsia-500/8 hover:bg-fuchsia-500/15 text-fuchsia-300 transition-colors cursor-pointer border-none text-[10px] font-600"
923
- title="Set mission goal"
924
- >
925
- Goal
926
- </button>
927
- {missionEventsCount > 0 && (
928
+ <Tip label={missionMode === 'autonomous' ? 'Switch to assisted mode' : 'Switch to autonomous mode'}>
928
929
  <button
929
- onClick={handleClearMissionEvents}
930
+ onClick={handleToggleMissionMode}
930
931
  disabled={mainLoopSaving}
931
- className="px-2 py-1 rounded-[7px] bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60 transition-colors cursor-pointer border-none text-[10px] font-600"
932
- title="Clear pending events"
932
+ className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
933
+ ${missionMode === 'autonomous' ? 'bg-indigo-500/12 hover:bg-indigo-500/20 text-indigo-300' : 'bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60'}`}
933
934
  >
934
- Events {missionEventsCount}
935
+ {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
935
936
  </button>
937
+ </Tip>
938
+ <Tip label="Run one iteration of the mission loop">
939
+ <button
940
+ onClick={handleNudgeMission}
941
+ disabled={mainLoopSaving || missionPaused}
942
+ className="px-2 py-1 rounded-[7px] bg-blue-500/8 hover:bg-blue-500/15 text-blue-400 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600"
943
+ >
944
+ Nudge
945
+ </button>
946
+ </Tip>
947
+ <Tip label="Set or edit the mission goal">
948
+ <button
949
+ onClick={handleOpenGoalModal}
950
+ disabled={mainLoopSaving}
951
+ className="px-2 py-1 rounded-[7px] bg-fuchsia-500/8 hover:bg-fuchsia-500/15 text-fuchsia-300 transition-colors cursor-pointer border-none text-[10px] font-600"
952
+ >
953
+ Goal
954
+ </button>
955
+ </Tip>
956
+ {missionEventsCount > 0 && (
957
+ <Tip label="Clear queued mission events">
958
+ <button
959
+ onClick={handleClearMissionEvents}
960
+ disabled={mainLoopSaving}
961
+ className="px-2 py-1 rounded-[7px] bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60 transition-colors cursor-pointer border-none text-[10px] font-600"
962
+ >
963
+ Events {missionEventsCount}
964
+ </button>
965
+ </Tip>
936
966
  )}
937
967
  <span className="text-[9px] text-text-3/40 uppercase tracking-wider shrink-0">
938
968
  {missionStatus}{missionMomentum !== null ? ` · ${missionMomentum}` : ''}
@@ -942,6 +972,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
942
972
  </>
943
973
  )}
944
974
  {hasMemoryLink && (
975
+ <Tip label="View agent memories">
945
976
  <button
946
977
  onClick={() => { setMemoryAgentFilter(session.agentId!); setActiveView('memory'); setSidebarOpen(true) }}
947
978
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-accent-soft/40 hover:bg-accent-soft/70 transition-colors cursor-pointer text-[10px] font-600 text-accent-bright/55 hover:text-accent-bright/80 shrink-0"
@@ -951,9 +982,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
951
982
  </svg>
952
983
  Memories
953
984
  </button>
985
+ </Tip>
954
986
  )}
955
987
  {hasSourceFilter && onConnectorFilterChange && connectorSources && (
956
988
  <div className="relative shrink-0" ref={sourceDropdownRef}>
989
+ <Tip label="Filter messages by source connector">
957
990
  <button
958
991
  onClick={() => setSourceDropdownOpen((o) => !o)}
959
992
  className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600 shrink-0 ${
@@ -961,7 +994,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
961
994
  ? 'bg-accent-soft/60 text-accent-bright/80 hover:bg-accent-soft'
962
995
  : 'bg-white/[0.03] text-text-3/50 hover:bg-white/[0.06] hover:text-text-3/70'
963
996
  }`}
964
- title="Filter by message source"
965
997
  >
966
998
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
967
999
  <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
@@ -973,6 +1005,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
973
1005
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
974
1006
  </svg>
975
1007
  </button>
1008
+ </Tip>
976
1009
  {sourceDropdownOpen && (
977
1010
  <div className="absolute top-full left-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[140px]">
978
1011
  <button
@@ -1005,11 +1038,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1005
1038
  )}
1006
1039
  {isOpenClawAgent && openclawSessionKey && (
1007
1040
  <>
1041
+ <Tip label="Sync chat history from OpenClaw gateway">
1008
1042
  <button
1009
1043
  onClick={handleSyncHistory}
1010
1044
  disabled={syncingHistory}
1011
1045
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-indigo-500/8 hover:bg-indigo-500/12 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600 text-indigo-400 shrink-0"
1012
- title="Sync from gateway"
1013
1046
  >
1014
1047
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
1015
1048
  <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" />
@@ -1017,10 +1050,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1017
1050
  </svg>
1018
1051
  {syncingHistory ? 'Syncing...' : 'Sync'}
1019
1052
  </button>
1053
+ </Tip>
1020
1054
  {syncResult && <span className="text-[9px] text-emerald-300/80 shrink-0">{syncResult}</span>}
1021
1055
  </>
1022
1056
  )}
1023
1057
  {linkedTask && (
1058
+ <Tip label="View linked task">
1024
1059
  <button
1025
1060
  onClick={() => setActiveView('tasks')}
1026
1061
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-amber-500/8 hover:bg-amber-500/12 transition-colors cursor-pointer text-[10px] font-600 text-amber-500 shrink-0"
@@ -1030,13 +1065,14 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1030
1065
  </svg>
1031
1066
  <span className="truncate max-w-[160px]">{linkedTask.title}</span>
1032
1067
  </button>
1068
+ </Tip>
1033
1069
  )}
1034
1070
  {resumeHandle && (
1035
1071
  <div className="flex items-center rounded-[7px] bg-white/[0.03] group/resume shrink-0">
1072
+ <Tip label="Copy CLI resume command">
1036
1073
  <button
1037
1074
  onClick={handleCopySessionId}
1038
1075
  className="flex items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
1039
- title="Copy resume command"
1040
1076
  >
1041
1077
  <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/40 shrink-0">
1042
1078
  <path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
@@ -1045,22 +1081,24 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1045
1081
  {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
1046
1082
  </span>
1047
1083
  </button>
1084
+ </Tip>
1085
+ <Tip label="Dismiss resume handle">
1048
1086
  <button
1049
1087
  onClick={handleDismissResumeHandle}
1050
1088
  className="px-1 py-1 rounded-r-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer opacity-0 group-hover/resume:opacity-100"
1051
- title="Dismiss"
1052
1089
  >
1053
1090
  <svg width="8" height="8" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40 hover:text-text-3">
1054
1091
  <path d="M4 4l8 8M12 4l-8 8" />
1055
1092
  </svg>
1056
1093
  </button>
1094
+ </Tip>
1057
1095
  </div>
1058
1096
  )}
1059
1097
  {browserActive && (
1098
+ <Tip label="Close the browser session">
1060
1099
  <button
1061
1100
  onClick={onStopBrowser}
1062
1101
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-accent-bright/8 hover:bg-red-500/12 transition-colors cursor-pointer group text-[10px] font-600 shrink-0"
1063
- title="Stop browser"
1064
1102
  >
1065
1103
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright group-hover:text-red-400">
1066
1104
  <rect x="3" y="3" width="18" height="14" rx="2" /><path d="M3 9h18" />
@@ -1070,9 +1108,54 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1070
1108
  <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
1071
1109
  </svg>
1072
1110
  </button>
1111
+ </Tip>
1073
1112
  )}
1074
1113
  </div>
1075
1114
  )}
1115
+
1076
1116
  </header>
1117
+
1118
+ {/* Goal modal — fixed to viewport, not constrained by header */}
1119
+ {goalModalOpen && (
1120
+ <div className="fixed inset-0 z-[200] flex items-center justify-center" onClick={() => setGoalModalOpen(false)}>
1121
+ <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
1122
+ <div
1123
+ className="relative w-[420px] max-w-[90vw] rounded-[16px] border border-white/[0.08] bg-surface shadow-2xl p-6"
1124
+ onClick={(e) => e.stopPropagation()}
1125
+ >
1126
+ <h4 className="font-display text-[15px] font-700 text-text mb-1">Set Mission Goal</h4>
1127
+ <p className="text-[11px] text-text-3/60 mb-4">Define what this agent should work towards.</p>
1128
+ <textarea
1129
+ ref={goalInputRef}
1130
+ value={goalDraft}
1131
+ onChange={(e) => setGoalDraft(e.target.value)}
1132
+ onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleSubmitGoal(); if (e.key === 'Escape') setGoalModalOpen(false) }}
1133
+ placeholder="Describe the agent's mission..."
1134
+ rows={4}
1135
+ className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30 mb-1 resize-y min-h-[80px]"
1136
+ style={{ fontFamily: 'inherit' }}
1137
+ />
1138
+ <p className="text-[10px] text-text-3/40 mb-3">Cmd+Enter to submit</p>
1139
+ <div className="flex items-center justify-end gap-2">
1140
+ <button
1141
+ onClick={() => setGoalModalOpen(false)}
1142
+ className="px-4 py-2 rounded-[8px] text-[12px] font-600 text-text-3 bg-white/[0.04] hover:bg-white/[0.08] transition-colors cursor-pointer border-none"
1143
+ style={{ fontFamily: 'inherit' }}
1144
+ >
1145
+ Cancel
1146
+ </button>
1147
+ <button
1148
+ onClick={handleSubmitGoal}
1149
+ disabled={!goalDraft.trim()}
1150
+ className="px-4 py-2 rounded-[8px] text-[12px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-soft/80 transition-colors cursor-pointer border border-accent-bright/20 disabled:opacity-40 disabled:cursor-default"
1151
+ style={{ fontFamily: 'inherit' }}
1152
+ >
1153
+ Set Goal
1154
+ </button>
1155
+ </div>
1156
+ </div>
1157
+ </div>
1158
+ )}
1159
+ </>
1077
1160
  )
1078
1161
  }
@@ -3,8 +3,8 @@
3
3
  import { useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
- import { SessionCard } from './session-card'
7
- import { fetchMessages } from '@/lib/sessions'
6
+ import { ChatCard } from './chat-card'
7
+ import { fetchMessages } from '@/lib/chats'
8
8
  import { toast } from 'sonner'
9
9
  import { Skeleton } from '@/components/shared/skeleton'
10
10
  import { EmptyState } from '@/components/shared/empty-state'
@@ -17,7 +17,7 @@ interface Props {
17
17
  type SessionFilter = 'all' | 'active' | 'human' | 'orchestrated'
18
18
  type SortMode = 'lastActive' | 'name' | 'messages'
19
19
 
20
- export function SessionList({ inSidebar, onSelect }: Props) {
20
+ export function ChatList({ inSidebar, onSelect }: Props) {
21
21
  const sessions = useAppStore((s) => s.sessions)
22
22
  const currentUser = useAppStore((s) => s.currentUser)
23
23
  const currentSessionId = useAppStore((s) => s.currentSessionId)
@@ -44,7 +44,6 @@ export function SessionList({ inSidebar, onSelect }: Props) {
44
44
 
45
45
  const allUserSessions = useMemo(() => {
46
46
  return Object.values(sessions).filter((s) => {
47
- if (s.name === '__main__') return false
48
47
  const owner = (s.user || '').toLowerCase()
49
48
  const isPlatformOwned = owner === 'system' || owner === 'connector' || owner === 'swarm'
50
49
  const isCurrentUserOwned = !!currentUser && owner === currentUser.toLowerCase()
@@ -186,7 +185,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
186
185
  <div className="flex flex-col gap-1 px-2 pb-4">
187
186
  {filtered.map((s) => (
188
187
  <div key={s.id} className="group/pin relative">
189
- <SessionCard
188
+ <ChatCard
190
189
  session={s}
191
190
  active={s.id === currentSessionId}
192
191
  onClick={() => handleSelect(s.id)}
@@ -3,9 +3,10 @@
3
3
  import { useState, useRef, useEffect } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
- import { AVAILABLE_TOOLS, PLATFORM_TOOLS, TOOL_LABELS } from '@/lib/tool-definitions'
6
+ import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
7
7
  import type { ToolDefinition } from '@/lib/tool-definitions'
8
8
  import type { Session } from '@/types'
9
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
9
10
 
10
11
  const TOOL_GROUPS: { label: string; tools: ToolDefinition[] }[] = [
11
12
  { label: 'Plugins', tools: AVAILABLE_TOOLS },
@@ -26,7 +27,7 @@ export function ChatToolToggles({ session }: Props) {
26
27
  const skills = useAppStore((s) => s.skills)
27
28
 
28
29
  const agent = session.agentId ? agents[session.agentId] : null
29
- const sessionTools: string[] = session.tools || []
30
+ const sessionTools: string[] = session.plugins || []
30
31
 
31
32
  // Agent's skill IDs
32
33
  const agentSkillIds: string[] = agent?.skillIds || []
@@ -44,7 +45,7 @@ export function ChatToolToggles({ session }: Props) {
44
45
  const updated = sessionTools.includes(toolId)
45
46
  ? sessionTools.filter((t) => t !== toolId)
46
47
  : [...sessionTools, toolId]
47
- await api('PUT', `/sessions/${session.id}`, { tools: updated })
48
+ await api('PUT', `/chats/${session.id}`, { plugins: updated })
48
49
  loadSessions()
49
50
  }
50
51
 
@@ -69,26 +70,33 @@ export function ChatToolToggles({ session }: Props) {
69
70
  {open && (
70
71
  <div className="absolute top-full left-0 mt-1.5 w-[260px] max-h-[420px] overflow-y-auto rounded-[12px] border border-white/[0.08] shadow-xl z-[120] overflow-hidden"
71
72
  style={{ animation: 'fade-in 0.15s ease', backgroundColor: '#171a2b' }}>
72
-
73
+ <TooltipProvider delayDuration={300}>
73
74
  {TOOL_GROUPS.map((group, gi) => (
74
75
  <div key={group.label} className={`px-3 pb-1 ${gi === 0 ? 'pt-3' : 'pt-1 border-t border-white/[0.04]'}`}>
75
76
  <p className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider mb-2">{group.label}</p>
76
77
  {group.tools.map((tool) => {
77
78
  const enabled = sessionTools.includes(tool.id)
78
79
  return (
79
- <label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer" title={tool.description}>
80
- <div
81
- onClick={() => toggleTool(tool.id)}
82
- className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
83
- ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
84
- >
85
- <div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all duration-200
86
- ${enabled ? 'left-[16px]' : 'left-[2px]'}`} />
87
- </div>
88
- <span className={`text-[12px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
89
- {tool.label}
90
- </span>
91
- </label>
80
+ <Tooltip key={tool.id}>
81
+ <TooltipTrigger asChild>
82
+ <label className="flex items-center gap-2.5 py-1.5 cursor-pointer">
83
+ <div
84
+ onClick={() => toggleTool(tool.id)}
85
+ className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
86
+ ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
87
+ >
88
+ <div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all duration-200
89
+ ${enabled ? 'left-[16px]' : 'left-[2px]'}`} />
90
+ </div>
91
+ <span className={`text-[12px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
92
+ {tool.label}
93
+ </span>
94
+ </label>
95
+ </TooltipTrigger>
96
+ <TooltipContent side="right" sideOffset={8} className="max-w-[200px] bg-[#1e2140] text-text-2 border border-white/[0.08] text-[11px] leading-snug px-2.5 py-1.5">
97
+ {tool.description}
98
+ </TooltipContent>
99
+ </Tooltip>
92
100
  )
93
101
  })}
94
102
  </div>
@@ -110,6 +118,7 @@ export function ChatToolToggles({ session }: Props) {
110
118
  </div>
111
119
  )}
112
120
 
121
+ </TooltipProvider>
113
122
  <div className="px-3 py-2 border-t border-white/[0.04] bg-white/[0.02]">
114
123
  <p className="text-[10px] text-text-3/70">Changes apply to the next message</p>
115
124
  </div>