dsh-codinput 0.3.9 → 0.4.0

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 (37) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/NOTICE +37 -4
  3. package/README.en.md +26 -12
  4. package/README.md +26 -12
  5. package/TESTING.md +86 -6
  6. package/assets/donate-qr.jpg +0 -0
  7. package/lib/client.js +2875 -98
  8. package/package.json +5 -2
  9. package/src/client/cm.ts +19 -1
  10. package/src/client/command.ts +39 -8
  11. package/src/client/components/CodinputSurface.tsx +80 -27
  12. package/src/client/components/EditorPane.tsx +3 -1
  13. package/src/client/components/InputSlotSeat.tsx +56 -1
  14. package/src/client/components/SlashMenu.tsx +294 -41
  15. package/src/client/components/StatsRow.tsx +40 -23
  16. package/src/client/components/Toolbar.tsx +284 -48
  17. package/src/client/components/UserMessageMarkdown.tsx +158 -115
  18. package/src/client/components/VoiceActivity.tsx +30 -69
  19. package/src/client/components/slotEntry.tsx +37 -0
  20. package/src/client/host-types.ts +11 -3
  21. package/src/client/hotkey.ts +38 -0
  22. package/src/client/i18n.ts +18 -4
  23. package/src/client/index.tsx +9 -0
  24. package/src/client/markdown/code.ts +281 -0
  25. package/src/client/markdown/extensions.ts +576 -0
  26. package/src/client/markdown/heavy-assets.d.ts +11 -0
  27. package/src/client/markdown/heavy-shared.ts +50 -0
  28. package/src/client/markdown/heavy.ts +159 -0
  29. package/src/client/markdown/index.ts +289 -0
  30. package/src/client/markdown/math.ts +134 -0
  31. package/src/client/markdown/mermaid.ts +358 -0
  32. package/src/client/markdown/sanitize.ts +103 -0
  33. package/src/client/markdown/styles.ts +171 -0
  34. package/src/client/prefs.ts +206 -23
  35. package/src/client/settings.tsx +7 -0
  36. package/src/client/stats.ts +92 -20
  37. package/src/client/styles.ts +55 -30
@@ -1,18 +1,49 @@
1
1
  /**
2
- * 工具行:结构与样式逐条对齐官方 InputBar 工具行——
3
- * 左侧 .dci-tools:指令(plus)、权限(shield 触发器),其后是官方左区末位
4
- * 的 conversation.input.left 座位(第三方条目镜像,见 InputLeftSlot);
5
- * 右侧 .dci-trailing:模型与思考强度(触发器)、语音输入(官方 activity 槽位
6
- * 条目,桌面版宿主才有)、发送(.primary 圆钮);conversation.input.right 座位
7
- * 在 .dci-standard-controls 内、模型之前(见 InputRightSlot)。
8
- * 按钮沿用官方 keepFocus 语义(mousedown 不夺走编辑器焦点)。
9
- * 附件无独立按钮(官方已并入 slash「添加 · 文件」),粘贴/拖拽路径保留。
10
- * 0.1.7-rc.2 起官方上下文小圈不再在工具行(挪进卡片下方 dock 行,见 StatsRow)。
11
- * 语音展开时隐藏 tools 与模型(官方 activity 同款:capture 行接管整行,
12
- * 发送/停止保持可见)。
2
+ * 工具行:结构与样式逐条对齐官方 0.2.2 InputBar 底部 `.row`——
3
+ *
4
+ * ```
5
+ * .row (container-type:inline-size; padding:2px 8px 6px; gap:12px)
6
+ * .tools [hidden=activity] // 官方 InputBar:hidden 时 display:none
7
+ * button.add (28×28, selector 底, IconPlus size14)
8
+ * .modes // 0.2.2 新增容器
9
+ * renderSlot("conversation.input.permission", { locked })
10
+ * renderSlot("conversation.input.plan", { locked })
11
+ * renderSlot("conversation.input.left", {})
12
+ * .trailing (activity 时 .trailingActive: flex:1; margin-left:0)
13
+ * .standardControls [hidden=activity]
14
+ * renderSlot("conversation.input.right", {})
15
+ * renderSlot("conversation.input.model", { locked: modelSeatLocked })
16
+ * .activity / .activityExpanded → renderSlot("conversation.input.activity",
17
+ * { locked, onActiveChange })
18
+ * interruptible → 独立停止钮(官方 .primary 34×34 圆钮 + 16px 方块 icon)
19
+ * primary(官方 .primary 34×34、translateY(-2px)、data-sending 转圈、
20
+ * stop / sending / send.steer / send.queue / send 标签)
21
+ * ```
22
+ *
23
+ * 官方事实源(0.2.2 包源码,逐行核对):
24
+ * - InputBar JSX:`dsh-client-ui-conversation/lib/client.js` L22066-22213;
25
+ * - InputBar.module.css:同文件 CSS(.row/.tools/.modes/.trailing/.add/.primary/
26
+ * .standardControls/.activity/.trailingActive,逐值见 styles.ts);
27
+ * - 按钮语义:`primaryStops = running && subagent===null && !sending && (empty||blocked)`、
28
+ * `interruptible = running && (continuable || sending && subagent===null)`、
29
+ * `primaryLabel`、`resolveSubmitMode(busyEnter, running, "enter", steeringAvailable)`
30
+ * (同文件 L222-236、L16357-16361;插件侧 busyEnter 读不到宿主偏好,取官方
31
+ * 默认值 "queue")。
32
+ *
33
+ * permission / plan 是 0.2.2 新槽位:按座位镜像机制(InputSingleSlot)以
34
+ * ownerProps `{ locked }` 镜像——槽位已声明但无占用者 → 按官方留空;宿主
35
+ * 未声明该槽位(旧宿主,`slots.entries` 抛错)→ permission 回退自绘
36
+ * PermissionMenu(保住旧宿主功能),plan 不渲染。
37
+ *
38
+ * 模型座保持既有 issue #4 行为(ModelSlot:第三方遮蔽者让位、出厂占用者由
39
+ * 自绘芯片代表),locked 传官方 `modelSeatLocked = removed || inert || !live`
40
+ * 的插件侧近似(removed 无对应信号按 false;live ≡ sessionId/state/actions 齐备)。
41
+ *
42
+ * 按钮沿用官方 keepFocus 语义(mousedown 不夺走编辑器焦点);官方 Tooltip
43
+ * 原语不引依赖,保留等价 `title`/`aria-label`。附件行/编辑器区不动,只动本行。
13
44
  */
14
45
 
15
- import { useCallback, useState } from 'react';
46
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
16
47
  import type { DraftAttachmentId, InputActions, InputState, InputTriggerController, UseProjection } from '../host-types';
17
48
  import { openCommandMenu } from '../triggers';
18
49
  import { t, useT } from '../i18n';
@@ -23,6 +54,14 @@ import { PermissionMenu } from './PermissionMenu';
23
54
  import { VoiceActivity } from './VoiceActivity';
24
55
  import { InputLeftSlot } from './InputLeftSlot';
25
56
  import { InputRightSlot } from './InputRightSlot';
57
+ import { InputSingleSlot } from './InputSlotSeat';
58
+ import { useSlotDeclaration } from './slotEntry';
59
+
60
+ /** 0.2.2 新增子槽位(官方 InputBar 声明、在其 `.modes` 内渲染)。 */
61
+ const PERMISSION_SLOT = 'conversation.input.permission';
62
+ const PLAN_SLOT = 'conversation.input.plan';
63
+ /** 语音座位槽位(0.2.x 起 InputBar 声明;旧宿主可能没有该子槽位)。 */
64
+ const ACTIVITY_SLOT = 'conversation.input.activity';
26
65
 
27
66
  export interface ToolbarProps {
28
67
  sessionId: string | undefined;
@@ -34,18 +73,23 @@ export interface ToolbarProps {
34
73
  useProjection: UseProjection | undefined;
35
74
  /**
36
75
  * conversation.input.left / .right 条目的 standard props 原样面(宿主下发
37
- * 给本插件 bar/body 条目的 kit 减去 owner props),供两座第三方条目转发
38
- * 用。它是条目 props 的一部分而非门控:缺席时条目仍按槽位条目渲染,只是
76
+ * 给本插件 bar/body 条目的 kit 减去 owner props),供各座位转发渲染用。
77
+ * 它是条目 props 的一部分而非门控:缺席时条目仍按槽位条目渲染,只是
39
78
  * 拿不到这组标准面(见 InputSlotSeat)。
40
79
  */
41
80
  inputStandard?: Record<string, unknown> | undefined;
42
- /** owner disabled(权限触发器随之锁住,对齐官方 locked)。 */
81
+ /** owner disabled(官方 bar owner `inert`:模型运行器主动禁用整个输入面)。 */
43
82
  disabled?: boolean;
83
+ /** 会话级阻塞(官方 bar owner `blocked`:消息输入惰性但模型选择保持可用)。 */
84
+ blocked?: boolean;
85
+ /** 会话运行中(官方 useSession s.running):双按钮语义与标签用。 */
86
+ running?: boolean;
44
87
  onNotify?(level: 'info' | 'error', text: string): void;
45
88
  /** 主发送手势(与编辑器发送键同一策略)。 */
46
89
  onSend(): void;
90
+ /** 官方 primaryDisabled 的非停止分支(empty || disabled || machineBusy)。 */
47
91
  sendDisabled?: boolean;
48
- /** 会话运行中且草稿空(官方 primaryStops):主按钮变停止。 */
92
+ /** 官方 primaryStops:主按钮变停止(running && !sending && (empty || blocked))。 */
49
93
  primaryStops?: boolean;
50
94
  /** 停止生成(官方 stop:conversation.cancel)。 */
51
95
  onStop?(): void;
@@ -56,51 +100,213 @@ function keepFocus(event: React.MouseEvent): void {
56
100
  event.preventDefault();
57
101
  }
58
102
 
103
+ /**
104
+ * 官方 `resolveSubmitMode(busyEnter, running, gesture, steeringAvailable)` 的
105
+ * 插件侧等价(该函数在「running && steeringAvailable」时对 enter 手势原样返回
106
+ * busyEnter 偏好)。宿主偏好藏在官方 bar 条目的 inject 面里,本插件拿不到,
107
+ * 取官方默认值 `DEFAULT_BUSY_ENTER_BEHAVIOR = "queue"`(0.2.2
108
+ * dsh-client-ui-conversation/lib/client.js L16334)。返回联合类型是为了让
109
+ * 「steer/queue」两条标签分支都成为可达代码,后续宿主透传偏好时只改这里。
110
+ */
111
+ function resolvePrimarySubmitMode(): 'steer' | 'queue' {
112
+ return 'queue';
113
+ }
114
+
115
+ /**
116
+ * 官方 `useMenuLauncher((source) => source === "command")` 的插件侧等价:
117
+ * 指令候选菜单是否打开(add 按钮 aria-expanded)。launcher 是可选服务面,
118
+ * 缺席时恒 false,不抛错。
119
+ */
120
+ function useCommandMenuOpen(triggers: InputTriggerController | undefined): boolean {
121
+ const launcher = triggers?.launcher;
122
+ const subscribe = useCallback(
123
+ (fn: () => void) => {
124
+ if (!launcher || typeof launcher.subscribe !== 'function') return () => {};
125
+ try {
126
+ return launcher.subscribe(fn) ?? (() => {});
127
+ } catch {
128
+ return () => {};
129
+ }
130
+ },
131
+ [launcher],
132
+ );
133
+ const getSnapshot = useCallback((): string | null => {
134
+ if (!launcher || typeof launcher.getSnapshot !== 'function') return null;
135
+ try {
136
+ return launcher.getSnapshot() ?? null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }, [launcher]);
141
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) === 'command';
142
+ }
143
+
144
+ /**
145
+ * 官方 `observeControlRow(row)`(0.2.2 `lib/types/client/skeleton/control-row-layout.js`
146
+ * 逐行复刻):行内放不下时给 row 加 `data-model-compact`(CSS 隐藏模型文字、
147
+ * 显示模型图标);换行可用所以只有连图标都放不下才 compact。同步测量:
148
+ * 每次通知先清属性再量两段 children + columnGap。
149
+ */
150
+ function observeControlRow(row: HTMLDivElement | null): (() => void) | undefined {
151
+ if (row === null) return undefined;
152
+ const measure = (): void => {
153
+ row.removeAttribute('data-model-compact');
154
+ const style = getComputedStyle(row);
155
+ const available = row.getBoundingClientRect().width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
156
+ const widths = Array.from(row.children, (child) => child.getBoundingClientRect().width).filter((width) => width > 0);
157
+ const needed = widths.reduce((sum, width) => sum + width, 0) + Math.max(0, widths.length - 1) * parseFloat(style.columnGap);
158
+ row.toggleAttribute('data-model-compact', needed > available);
159
+ };
160
+ const resize = new ResizeObserver(measure);
161
+ resize.observe(row);
162
+ for (const child of row.children) resize.observe(child);
163
+ const mutation = new MutationObserver(measure);
164
+ mutation.observe(row, {
165
+ subtree: true,
166
+ childList: true,
167
+ characterData: true,
168
+ attributes: true,
169
+ attributeFilter: ['hidden'],
170
+ });
171
+ const fonts = document.fonts;
172
+ fonts?.addEventListener('loadingdone', measure);
173
+ measure();
174
+ return () => {
175
+ resize.disconnect();
176
+ mutation.disconnect();
177
+ fonts?.removeEventListener('loadingdone', measure);
178
+ };
179
+ }
180
+
59
181
  export function Toolbar(props: ToolbarProps): JSX.Element {
60
- const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify, inputStandard } = props;
182
+ const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, blocked, running, onNotify, inputStandard } = props;
61
183
  useT();
62
184
 
185
+ const rowRef = useRef<HTMLDivElement | null>(null);
186
+ useLayoutEffect(() => observeControlRow(rowRef.current), []);
187
+
63
188
  const attachments: readonly DraftAttachmentId[] = state?.attachmentIds ?? [];
64
- // 官方 activity 语义:语音展开时本行左侧工具与模型控件隐藏、capture 行伸展。
189
+ const draft = state?.draft ?? '';
190
+ const phase = state?.phase;
191
+ const sending = phase === 'sending';
192
+ const empty = draft.trim() === '' && attachments.length === 0;
193
+ /**
194
+ * 官方 `locked` = removed || inert || !live || blocked || parentOffline
195
+ * (插件侧 removed/parentOffline 无信号按 false):消息动作全锁。
196
+ */
197
+ const locked = disabled === true || blocked === true;
198
+ /** 「input 面活」近似官方 live = input/keyboard/inputActions 齐备。 */
199
+ const live = sessionId !== undefined && state !== undefined && actions !== undefined;
200
+ /**
201
+ * 官方 `modelSeatLocked` = removed || inert || !live —— **不含 blocked**
202
+ * (blocked 只惰性化消息输入,模型选择保持可用)。
203
+ */
204
+ const modelSeatLocked = disabled === true || !live;
205
+ /** 官方 plainMessageDraft = !empty && phase === "plain" && 非 `/` 命令行。 */
206
+ const plainMessageDraft = !empty && phase === 'plain' && !draft.trimStart().startsWith('/');
207
+ /** 官方 interruptible(普通会话:subagent 不适用、continuable=false)= running && sending。 */
208
+ const interruptible = running === true && sending;
209
+ /**
210
+ * 官方 resolveSubmitMode(busyEnter, running, "enter", steeringAvailable):
211
+ * gesture 是 "enter" → 返回 busyEnter 偏好。插件侧读不到宿主偏好(该值在
212
+ * 官方 bar 条目自己的 inject 面里,不归本插件所有),取官方默认
213
+ * DEFAULT_BUSY_ENTER_BEHAVIOR = "queue"(conversation client.js L16334)。
214
+ * 留成函数返回联合类型:宿主未来透传偏好时只改这一处。
215
+ */
216
+ const primarySubmitMode = resolvePrimarySubmitMode();
217
+ const primaryLabel = props.primaryStops
218
+ ? t('tool.stop')
219
+ : sending
220
+ ? t('tool.sending')
221
+ : running === true && !locked && plainMessageDraft
222
+ ? t(primarySubmitMode === 'steer' ? 'tool.sendSteer' : 'tool.sendQueue')
223
+ : t('tool.send');
224
+
225
+ // 官方 activity 语义:语音展开时本行左侧工具与模型控件隐藏、capture 行伸展
226
+ // (隐藏用官方 `hidden` 属性 + CSS display:none,而不是重排 DOM)。
65
227
  const [voiceActive, setVoiceActive] = useState(false);
66
228
  const onVoiceActiveChange = useCallback((active: boolean): void => setVoiceActive(active), []);
229
+ // 官方同款:切换会话收起 activity。
230
+ useEffect(() => {
231
+ setVoiceActive(false);
232
+ }, [sessionId]);
233
+
234
+ const commandMenuOpen = useCommandMenuOpen(triggers);
235
+ // 0.2.2 新槽位声明探测:「已声明无占用者」与「旧宿主未声明」必须分开处理。
236
+ const permissionDeclaration = useSlotDeclaration(PERMISSION_SLOT);
237
+ const planDeclaration = useSlotDeclaration(PLAN_SLOT);
238
+ /**
239
+ * activity 座位声明态:官方 0.2.x 的 renderSlot 即使槽位无占用者也会输出
240
+ * `<div data-slot style="display:contents">` 包装,所以官方 `.activity` 永远
241
+ * 不是 `:empty`——实测(隔离 0.2.2 宿主)无 voice 时 `.activity` 仍是 0×0 的
242
+ * flex item,占一个 row gap(官方 trailing=306.6;隐藏则 294.6,少 12px)。
243
+ * 因此「已声明」时座位必须保留(哪怕空);「未声明」(旧宿主没有这个子槽位)
244
+ * 时官方输入框本就没有该元素,我们也不渲染,避免多出一个 gap。
245
+ */
246
+ const activityDeclared = useSlotDeclaration(ACTIVITY_SLOT) === 'declared';
67
247
 
68
248
  const onCommandClick = (): void => {
69
249
  if (!triggers) return;
70
250
  openCommandMenu(triggers, undefined, caretRef.current ?? 0);
71
251
  };
72
252
 
253
+ // 旧宿主(permission 槽位未声明)回退自绘菜单;0.2.2 声明后镜像官方条目。
254
+ const permissionFallback = (
255
+ <PermissionMenu
256
+ sessionId={sessionId}
257
+ actions={actions}
258
+ currentDraft={draft}
259
+ useProjection={useProjection}
260
+ disabled={locked}
261
+ onNotify={onNotify}
262
+ />
263
+ );
264
+
73
265
  return (
74
- <div className="dci-row" data-voice-active={voiceActive || undefined}>
75
- <div className="dci-tools">
266
+ <div className="dci-row" ref={rowRef}>
267
+ <div className="dci-tools" hidden={voiceActive}>
76
268
  <button
77
269
  type="button"
78
270
  className="dci-add"
79
- aria-label={t("tool.command")}
271
+ aria-label={t('tool.command')}
80
272
  aria-haspopup="listbox"
81
- title={t("tool.command")}
82
- disabled={!triggers}
273
+ aria-expanded={commandMenuOpen}
274
+ title={t('tool.command')}
275
+ disabled={locked || !triggers}
83
276
  onMouseDown={keepFocus}
84
277
  onClick={onCommandClick}
85
278
  >
86
279
  <IconPlusOfficial size={14} />
87
280
  </button>
88
- <PermissionMenu
89
- sessionId={sessionId}
90
- actions={actions}
91
- currentDraft={state?.draft ?? ''}
92
- useProjection={useProjection}
93
- disabled={disabled}
94
- onNotify={onNotify}
95
- />
96
- {/* 官方左区同构:`.tools` 内 modes(permission→plan)之后是
97
- renderSlot("conversation.input.left") 的末位。 */}
281
+ {/* 官方 `.modes`:permission → plan 两枚 0.2.2 新单值座。 */}
282
+ <div className="dci-modes">
283
+ {permissionDeclaration === 'declared' ? (
284
+ <InputSingleSlot
285
+ slot={PERMISSION_SLOT}
286
+ sessionId={sessionId}
287
+ standard={inputStandard}
288
+ ownerProps={{ locked }}
289
+ fallback={permissionFallback}
290
+ />
291
+ ) : (
292
+ permissionFallback
293
+ )}
294
+ {planDeclaration === 'declared' ? (
295
+ <InputSingleSlot
296
+ slot={PLAN_SLOT}
297
+ sessionId={sessionId}
298
+ standard={inputStandard}
299
+ ownerProps={{ locked }}
300
+ />
301
+ ) : null}
302
+ </div>
303
+ {/* 官方左区同构:modes 之后是 renderSlot("conversation.input.left")。 */}
98
304
  {state !== undefined && sessionId !== undefined ? (
99
305
  <InputLeftSlot sessionId={sessionId} standard={inputStandard} />
100
306
  ) : null}
101
307
  </div>
102
- <div className="dci-trailing">
103
- <div className="dci-standard-controls">
308
+ <div className={voiceActive ? 'dci-trailing dci-trailing-active' : 'dci-trailing'}>
309
+ <div className="dci-standard-controls" hidden={voiceActive}>
104
310
  {/* 官方同款门槛:input === undefined || sessionId === undefined 时
105
311
  整个 input.right 座位不渲染(renderSlot 调用点的三元条件)。 */}
106
312
  {state !== undefined && sessionId !== undefined ? (
@@ -112,28 +318,58 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
112
318
  <ModelSlot
113
319
  sessionId={sessionId}
114
320
  standard={inputStandard}
115
- locked={disabled === true}
321
+ locked={modelSeatLocked}
116
322
  fallback={<ModelMenu sessionId={sessionId} onNotify={onNotify} />}
117
323
  />
118
324
  </div>
119
- <div className="dci-voice-seat">
120
- <VoiceActivity
121
- sessionId={sessionId}
122
- inputActions={actions}
123
- locked={disabled === true}
124
- onActiveChange={onVoiceActiveChange}
125
- />
126
- </div>
325
+ {/* 官方 activity 座位:input/session 存在且槽位已声明才渲染(官方
326
+ renderSlot 空槽也输出 display:contents 包装 → 空座位仍占一个 gap)。
327
+ 无占用者时 div 为空但保留为 0 宽 flex item,与官方几何一致。 */}
328
+ {state !== undefined && sessionId !== undefined && activityDeclared ? (
329
+ <div className={voiceActive ? 'dci-voice-seat dci-voice-seat-expanded' : 'dci-voice-seat'}>
330
+ <VoiceActivity
331
+ sessionId={sessionId}
332
+ inputActions={actions}
333
+ locked={locked}
334
+ onActiveChange={onVoiceActiveChange}
335
+ />
336
+ </div>
337
+ ) : null}
338
+ {/* 官方 interruptible:运行中且正在 sending 时,主按钮之外单出停止钮。 */}
339
+ {interruptible ? (
340
+ <button
341
+ type="button"
342
+ className="dci-send"
343
+ aria-label={t('tool.stop')}
344
+ title={t('tool.stop')}
345
+ disabled={!props.onStop}
346
+ onMouseDown={keepFocus}
347
+ onClick={props.onStop}
348
+ >
349
+ <IconStopOfficial size={16} />
350
+ </button>
351
+ ) : null}
127
352
  <button
128
353
  type="button"
129
354
  className="dci-send"
130
- aria-label={props.primaryStops ? t('tool.stop') : t('tool.send')}
131
- title={props.primaryStops ? t('tool.stop') : t('tool.send')}
132
- disabled={props.primaryStops ? !props.onStop : props.sendDisabled}
355
+ aria-label={primaryLabel}
356
+ title={primaryLabel}
357
+ aria-busy={sending}
358
+ data-sending={sending ? '' : undefined}
359
+ disabled={props.sendDisabled}
133
360
  onMouseDown={keepFocus}
134
361
  onClick={() => (props.primaryStops ? props.onStop?.() : props.onSend())}
135
362
  >
136
- {props.primaryStops ? <IconStopOfficial size={16} /> : <IconSendOfficial size={16} />}
363
+ {props.primaryStops ? (
364
+ <IconStopOfficial size={16} />
365
+ ) : sending ? (
366
+ <svg className="dci-sending-spinner" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
367
+ <circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" strokeOpacity="0.35" strokeWidth="2" />
368
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
369
+ </svg>
370
+ ) : (
371
+ <IconSendOfficial size={16} />
372
+ )}
137
373
  </button>
138
374
  </div>
139
375
  </div>