@vibe-forge/client 0.11.2 → 0.11.3

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 (111) hide show
  1. package/dist/assets/{arc-De_WjPJ3.js → arc-h39NrT24.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-C4aR2zTE.js → blockDiagram-c4efeb88-CaDg46I6.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-BZH3rq_m.js → c4Diagram-c83219d4-CDqjcF9U.js} +1 -1
  4. package/dist/assets/channel-CBULQbJz.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-BzJgBrIK.js → classDiagram-beda092f-BDnZm8nO.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-5ZtXcnT3.js → classDiagram-v2-2358418a-BUi85KJW.js} +1 -1
  7. package/dist/assets/clone-dkS7LczW.js +1 -0
  8. package/dist/assets/{createText-1719965b-DUVvEtmR.js → createText-1719965b-Ca5dEwfo.js} +1 -1
  9. package/dist/assets/{cssMode-GoTNjuXX.js → cssMode-Ysz7NfYo.js} +1 -1
  10. package/dist/assets/{edges-96097737-Dd7m4Cvs.js → edges-96097737-CdSKqxZt.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-DxqFlG_f.js → erDiagram-0228fc6a-B-veAUv_.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-DU0C5kCI.js → flowDb-c6c81e3f-DD8Cx7_9.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-Di1uDa_X.js → flowDiagram-50d868cf-9f-_x1ET.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-1miffU4x.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-CwG8aty5.js → flowchart-elk-definition-6af322e1-5RhpQM4M.js} +1 -1
  16. package/dist/assets/{freemarker2-j39cqTlI.js → freemarker2-SgMdIXw4.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-baO_lzL-.js → ganttDiagram-a2739b55-DnxNghZA.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-COoHjYMf.js → gitGraphDiagram-82fe8481-CBvS3Tf9.js} +1 -1
  19. package/dist/assets/{graph-KxESr4M5.js → graph-CkHF299-.js} +1 -1
  20. package/dist/assets/{handlebars-BgjdZO8G.js → handlebars-C57IyLUe.js} +1 -1
  21. package/dist/assets/{html-Ba7tYObe.js → html-YsDy5wvW.js} +1 -1
  22. package/dist/assets/{htmlMode-Bztvbig1.js → htmlMode-7o_VDODD.js} +1 -1
  23. package/dist/assets/{index-5325376f-BMTAx2mL.js → index-5325376f-BzOVQPu-.js} +1 -1
  24. package/dist/assets/{index-Pm_kLJvG.js → index-BHFpctk6.js} +320 -319
  25. package/dist/assets/index-D_XqxIvp.css +32 -0
  26. package/dist/assets/{infoDiagram-8eee0895-CC74qbHY.js → infoDiagram-8eee0895-DJ-UI1h4.js} +1 -1
  27. package/dist/assets/{javascript-C1e1cllX.js → javascript-BHQ9NEZr.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-C4MyOdE6.js → journeyDiagram-c64418c1-DwfykcG9.js} +1 -1
  29. package/dist/assets/{jsonMode-BC98AlvF.js → jsonMode-3QjftkMM.js} +1 -1
  30. package/dist/assets/{layout-CxAyTlr7.js → layout-CbViRb_b.js} +1 -1
  31. package/dist/assets/{line-DhaUfI71.js → line-DBdBvv9D.js} +1 -1
  32. package/dist/assets/{linear-MYukzldK.js → linear-BDAfhcjn.js} +1 -1
  33. package/dist/assets/{liquid-DahfJEYl.js → liquid-B0cPPzIR.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-BWDJcswW.js → lspLanguageFeatures-IOxbobOz.js} +1 -1
  35. package/dist/assets/{mdx-BELlF_FD.js → mdx-Dma_RA8P.js} +1 -1
  36. package/dist/assets/{mermaid.core-BrQnSGSY.js → mermaid.core-Cvn8Go4x.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-B0FoxTiy.js → mindmap-definition-8da855dc-DEnYq0Lc.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-Ddr2cjSL.js → pieDiagram-a8764435-ZC4j8sHU.js} +1 -1
  39. package/dist/assets/{python--C9if_AD.js → python-Be0WX4q5.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-BlEs7Mrl.js → quadrantDiagram-1e28029f-DUaqHlIB.js} +1 -1
  41. package/dist/assets/{razor-B9U9JxKn.js → razor-Tjhny-uT.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-kEFOAu2v.js → requirementDiagram-08caed73-DjSal3es.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-BBghez8I.js → sankeyDiagram-a04cb91d-BMDXMrMz.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-CJqgzdUE.js → sequenceDiagram-c5b8d532-CQl9YUlH.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-BER4XEI6.js → stateDiagram-1ecb1508-DG7mU9jD.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-EBV2vSks.js → stateDiagram-v2-c2b004d7-DTbR_azy.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-k0eswZsE.js → styles-b4e223ce-C9aS3zb8.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-Ckr7GA-0.js → styles-ca3715f6-Bh3keVTZ.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-C1bpSwV3.js → styles-d45a18b0-BDcLLa65.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-CDtKpGvy.js → svgDrawCommon-b86b1483-B9H5ZS_9.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080-BeGR-vua.js → timeline-definition-faaaa080-DCMYCBhK.js} +1 -1
  52. package/dist/assets/{tsMode-D_gJXIy3.js → tsMode-DVqLsn98.js} +1 -1
  53. package/dist/assets/{typescript-BoKcNXkN.js → typescript-wMVyXw7G.js} +1 -1
  54. package/dist/assets/{xml-DZvURlJ-.js → xml-w0gzmn0c.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-DxfeLuYV.js → xychartDiagram-f5964ef8-CdxyD3K5.js} +1 -1
  56. package/dist/assets/{yaml-CTC8PAGY.js → yaml-C29TL1ed.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +4 -4
  59. package/src/api/sessions.ts +70 -1
  60. package/src/api/types.ts +2 -1
  61. package/src/api.ts +5 -0
  62. package/src/components/chat/AGENTS.md +14 -2
  63. package/src/components/chat/ChatComposerCard.scss +73 -0
  64. package/src/components/chat/ChatComposerCard.tsx +59 -0
  65. package/src/components/chat/ChatHeader.tsx +3 -1
  66. package/src/components/chat/ChatHistoryView.tsx +215 -49
  67. package/src/components/chat/CurrentTodoList.scss +210 -200
  68. package/src/components/chat/CurrentTodoList.tsx +116 -48
  69. package/src/components/chat/QueuedMessagesCard.scss +195 -0
  70. package/src/components/chat/QueuedMessagesCard.tsx +170 -0
  71. package/src/components/chat/sender/@components/adapter-select/AdapterSelectControl.scss +8 -0
  72. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.scss +152 -28
  73. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.tsx +95 -23
  74. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +7 -0
  75. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.scss +161 -45
  76. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.tsx +310 -71
  77. package/src/components/chat/sender/@components/sender-monaco-editor/SenderMonacoEditor.tsx +18 -0
  78. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-monaco-editor.ts +86 -9
  79. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.scss +98 -1
  80. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.tsx +137 -17
  81. package/src/components/chat/sender/@components/sender-toolbar/SenderToolbar.tsx +12 -6
  82. package/src/components/chat/sender/@core/build-sender-controller-result.ts +6 -0
  83. package/src/components/chat/sender/@core/build-sender-toolbar.ts +25 -2
  84. package/src/components/chat/sender/@core/create-sender-toolbar-handlers.ts +9 -2
  85. package/src/components/chat/sender/@core/interaction-request.ts +2 -2
  86. package/src/components/chat/sender/@core/sender-toolbar-bindings.ts +28 -4
  87. package/src/components/chat/sender/@hooks/use-sender-controller.ts +56 -11
  88. package/src/components/chat/sender/@hooks/use-sender-keydown.ts +64 -0
  89. package/src/components/chat/sender/@hooks/use-sender-shortcuts.ts +16 -1
  90. package/src/components/chat/sender/@hooks/use-sender-submit.ts +16 -8
  91. package/src/components/chat/sender/@types/sender-props.ts +19 -3
  92. package/src/components/chat/sender/@types/sender-toolbar-types.ts +12 -1
  93. package/src/components/chat/sender/Sender.scss +3 -0
  94. package/src/components/chat/sender/Sender.tsx +2 -12
  95. package/src/hooks/chat/session-view-cache.ts +4 -1
  96. package/src/hooks/chat/use-chat-adapter.ts +5 -1
  97. package/src/hooks/chat/use-chat-model-adapter-selection.tsx +5 -1
  98. package/src/hooks/chat/use-chat-session-actions.ts +99 -4
  99. package/src/hooks/chat/use-chat-session-messages.ts +20 -1
  100. package/src/hooks/chat/use-chat-session.ts +2 -0
  101. package/src/main.tsx +8 -0
  102. package/src/resources/locales/en.json +32 -1
  103. package/src/resources/locales/zh.json +32 -1
  104. package/src/routes/ChatRoute.scss +45 -1
  105. package/src/routes/ChatRoute.tsx +3 -0
  106. package/dist/assets/channel-BvERb8WU.js +0 -1
  107. package/dist/assets/clone-B9_0v-6Y.js +0 -1
  108. package/dist/assets/flowDiagram-v2-4f6560a1-LpS8Kb00.js +0 -1
  109. package/dist/assets/index-C1oh0w9H.css +0 -32
  110. package/src/components/chat/ThinkingStatus.scss +0 -70
  111. package/src/components/chat/ThinkingStatus.tsx +0 -13
@@ -1,12 +1,18 @@
1
+ /* eslint-disable max-lines */
1
2
  import './SenderInteractionPanel.scss'
2
3
 
3
- import { Button } from 'antd'
4
- import { useEffect, useState } from 'react'
4
+ import { useEffect, useMemo, useRef, useState } from 'react'
5
+ import type { KeyboardEvent as ReactKeyboardEvent } from 'react'
6
+
7
+ import { Button, Tooltip } from 'antd'
5
8
  import { useTranslation } from 'react-i18next'
6
9
 
7
10
  import type { AskUserQuestionParams } from '@vibe-forge/core'
8
11
  import type { PermissionInteractionContext } from '@vibe-forge/types'
9
12
 
13
+ import { ChatComposerCard } from '#~/components/chat/ChatComposerCard'
14
+ import { getLoopedIndex } from '#~/hooks/use-roving-focus-list'
15
+
10
16
  const primaryOptionValues = new Set(['allow_once', 'allow_session', 'deny_once'])
11
17
 
12
18
  const getOptionMeta = (value?: string) => {
@@ -28,118 +34,351 @@ const getOptionMeta = (value?: string) => {
28
34
  }
29
35
  }
30
36
 
37
+ const renderInfoButton = (title: string) => (
38
+ <Tooltip title={title} placement='top' destroyOnHidden>
39
+ <span
40
+ className='material-symbols-rounded interaction-panel__info-trigger'
41
+ aria-label={title}
42
+ role='img'
43
+ onMouseDown={(event) => {
44
+ event.preventDefault()
45
+ event.stopPropagation()
46
+ }}
47
+ onClick={(event) => {
48
+ event.preventDefault()
49
+ event.stopPropagation()
50
+ }}
51
+ >
52
+ info
53
+ </span>
54
+ </Tooltip>
55
+ )
56
+
57
+ const getInteractionOptionKey = (
58
+ option: { label: string; value?: string },
59
+ idx: number
60
+ ) => option.value ?? `${idx}:${option.label}`
61
+
31
62
  export function SenderInteractionPanel({
32
63
  interactionRequest,
64
+ activeOptionIndex,
33
65
  permissionContext,
34
66
  deniedTools,
35
67
  reasons: _reasons,
68
+ onActiveOptionIndexChange,
69
+ onMoveActiveOption,
36
70
  onInteractionResponse
37
71
  }: {
38
72
  interactionRequest: { id: string; payload: AskUserQuestionParams }
73
+ activeOptionIndex: number
39
74
  permissionContext?: PermissionInteractionContext
40
75
  deniedTools: string[]
41
76
  reasons: string[]
77
+ onActiveOptionIndexChange: (index: number) => void
78
+ onMoveActiveOption: (delta: number) => void
42
79
  onInteractionResponse?: (id: string, data: string | string[]) => void
43
80
  }) {
44
81
  const { t } = useTranslation()
45
- const [showAllOptions, setShowAllOptions] = useState(false)
82
+ const [showAllPermissionOptions, setShowAllPermissionOptions] = useState(false)
83
+ const isPermissionInteraction = permissionContext != null
84
+ const options = interactionRequest.payload.options ?? []
85
+ const optionsContainerRef = useRef<HTMLDivElement | null>(null)
86
+ const normalizedActiveOptionIndex = options.length === 0
87
+ ? -1
88
+ : Math.min(Math.max(activeOptionIndex, 0), options.length - 1)
89
+
90
+ const optionItems = useMemo(() =>
91
+ options.map((option, index) => ({
92
+ option,
93
+ index,
94
+ meta: getOptionMeta(option.value)
95
+ })), [options])
96
+ const primaryPermissionOptionItems = useMemo(
97
+ () => optionItems.filter(({ option }) => primaryOptionValues.has(option.value ?? '')),
98
+ [optionItems]
99
+ )
100
+ const secondaryPermissionOptionItems = useMemo(
101
+ () => optionItems.filter(({ option }) => !primaryOptionValues.has(option.value ?? '')),
102
+ [optionItems]
103
+ )
104
+ const activePermissionOptionIsSecondary = isPermissionInteraction &&
105
+ secondaryPermissionOptionItems.some(({ index }) => index === normalizedActiveOptionIndex)
106
+ const visibleOptionItems = isPermissionInteraction
107
+ ? ((showAllPermissionOptions || activePermissionOptionIsSecondary) ? optionItems : primaryPermissionOptionItems)
108
+ : optionItems
46
109
 
47
110
  const toolNames = [
48
111
  permissionContext?.subjectLabel?.trim() ?? '',
49
112
  ...deniedTools.map(tool => tool.trim())
50
113
  ].filter((value, index, values) => value !== '' && values.indexOf(value) === index)
51
114
  const toolSummary = toolNames.join('、')
52
- const title = toolSummary === ''
53
- ? interactionRequest.payload.question
54
- : t('chat.permissionRequestTitleWithTool', { tool: toolSummary })
55
- const primaryOptions =
56
- interactionRequest.payload.options?.filter(option => primaryOptionValues.has(option.value ?? '')) ?? []
57
- const secondaryOptions =
58
- interactionRequest.payload.options?.filter(option => !primaryOptionValues.has(option.value ?? '')) ?? []
115
+ const title = isPermissionInteraction && toolSummary !== ''
116
+ ? t('chat.permissionRequestTitleWithTool', { tool: toolSummary })
117
+ : interactionRequest.payload.question
118
+
119
+ const focusOptionAtIndex = (index: number, attempt = 0) => {
120
+ const option = optionsContainerRef.current?.querySelector<HTMLButtonElement>(
121
+ `.interaction-panel__option[data-option-index="${index}"]`
122
+ )
123
+
124
+ if (option != null) {
125
+ option.focus()
126
+ return
127
+ }
128
+
129
+ if (attempt >= 5) {
130
+ return
131
+ }
132
+
133
+ window.setTimeout(() => {
134
+ focusOptionAtIndex(index, attempt + 1)
135
+ }, 40)
136
+ }
59
137
 
60
138
  useEffect(() => {
61
- setShowAllOptions(false)
139
+ setShowAllPermissionOptions(false)
62
140
  }, [interactionRequest.id])
63
141
 
142
+ useEffect(() => {
143
+ if (activePermissionOptionIsSecondary) {
144
+ setShowAllPermissionOptions(true)
145
+ }
146
+ }, [activePermissionOptionIsSecondary])
147
+
148
+ useEffect(() => {
149
+ if (!isPermissionInteraction || options.length === 0) {
150
+ return
151
+ }
152
+ let cancelled = false
153
+ const targetIndex = normalizedActiveOptionIndex >= 0 ? normalizedActiveOptionIndex : 0
154
+
155
+ const focusWhenReady = (attempt = 0) => {
156
+ if (cancelled) {
157
+ return
158
+ }
159
+
160
+ const option = optionsContainerRef.current?.querySelector<HTMLButtonElement>(
161
+ `.interaction-panel__option[data-option-index="${targetIndex}"]`
162
+ )
163
+
164
+ if (option != null) {
165
+ option.focus()
166
+ return
167
+ }
168
+
169
+ if (attempt >= 5) {
170
+ return
171
+ }
172
+
173
+ window.setTimeout(() => {
174
+ focusWhenReady(attempt + 1)
175
+ }, 40)
176
+ }
177
+
178
+ focusWhenReady()
179
+
180
+ return () => {
181
+ cancelled = true
182
+ }
183
+ }, [
184
+ interactionRequest.id,
185
+ isPermissionInteraction,
186
+ normalizedActiveOptionIndex,
187
+ options.length,
188
+ showAllPermissionOptions
189
+ ])
190
+
191
+ const handleSubmitOption = (option: { label: string; value?: string }) => {
192
+ onInteractionResponse?.(interactionRequest.id, option.value ?? option.label)
193
+ }
194
+
195
+ const moveOptionFocus = (delta: number, focus = isPermissionInteraction) => {
196
+ if (options.length === 0) {
197
+ return
198
+ }
199
+
200
+ const sourceIndex = normalizedActiveOptionIndex >= 0 ? normalizedActiveOptionIndex : 0
201
+ const nextIndex = getLoopedIndex(sourceIndex, delta, options.length)
202
+ onMoveActiveOption(delta)
203
+ if (focus) {
204
+ focusOptionAtIndex(nextIndex)
205
+ }
206
+ }
207
+
208
+ const handleNavButtonKeyDown = (
209
+ event: ReactKeyboardEvent<HTMLElement>,
210
+ delta: number
211
+ ) => {
212
+ if ((event.key === 'ArrowUp' && delta < 0) || (event.key === 'ArrowDown' && delta > 0)) {
213
+ event.preventDefault()
214
+ moveOptionFocus(delta)
215
+ }
216
+ }
217
+
218
+ const handleOptionKeyDown = (
219
+ event: ReactKeyboardEvent<HTMLElement>,
220
+ optionIndex: number
221
+ ) => {
222
+ if (event.key === 'ArrowDown') {
223
+ event.preventDefault()
224
+ onActiveOptionIndexChange(getLoopedIndex(optionIndex, 1, options.length))
225
+ focusOptionAtIndex(getLoopedIndex(optionIndex, 1, options.length))
226
+ return
227
+ }
228
+
229
+ if (event.key === 'ArrowUp') {
230
+ event.preventDefault()
231
+ onActiveOptionIndexChange(getLoopedIndex(optionIndex, -1, options.length))
232
+ focusOptionAtIndex(getLoopedIndex(optionIndex, -1, options.length))
233
+ }
234
+ }
235
+
236
+ const handleTogglePermissionOptions = () => {
237
+ setShowAllPermissionOptions((current) => {
238
+ const next = !current
239
+
240
+ if (!next && activePermissionOptionIsSecondary) {
241
+ const fallbackIndex = primaryPermissionOptionItems.at(-1)?.index ?? 0
242
+ onActiveOptionIndexChange(fallbackIndex)
243
+ }
244
+
245
+ return next
246
+ })
247
+ }
248
+
249
+ const showOptionControls = options.length > 1
250
+
64
251
  return (
65
- <div className='interaction-panel'>
66
- <div className='interaction-panel__header'>
67
- <div className='interaction-question'>
68
- {title}
252
+ <ChatComposerCard
253
+ className={[
254
+ 'interaction-panel',
255
+ isPermissionInteraction ? 'interaction-panel--permission' : 'interaction-panel--question'
256
+ ].filter(Boolean).join(' ')}
257
+ summaryClassName='interaction-panel__summary'
258
+ bodyClassName='interaction-panel__body'
259
+ narrow
260
+ summary={
261
+ <div className='interaction-panel__header'>
262
+ <div className='interaction-panel__title-wrap'>
263
+ {!isPermissionInteraction && (
264
+ <span className='material-symbols-rounded interaction-panel__title-icon'>
265
+ help
266
+ </span>
267
+ )}
268
+ <div className='interaction-question'>{title}</div>
269
+ </div>
270
+ {showOptionControls && (
271
+ <div className='interaction-panel__nav' aria-label={t('chat.interactionOptionNavigation')}>
272
+ <Tooltip title={t('chat.interactionOptionPrevious')} placement='top' destroyOnHidden>
273
+ <button
274
+ type='button'
275
+ className='interaction-panel__nav-button'
276
+ aria-label={t('chat.interactionOptionPrevious')}
277
+ onMouseDown={(event) => {
278
+ event.preventDefault()
279
+ }}
280
+ onKeyDown={(event) => handleNavButtonKeyDown(event, -1)}
281
+ onClick={() => moveOptionFocus(-1)}
282
+ >
283
+ <span className='material-symbols-rounded'>keyboard_arrow_up</span>
284
+ </button>
285
+ </Tooltip>
286
+ <Tooltip title={t('chat.interactionOptionNext')} placement='top' destroyOnHidden>
287
+ <button
288
+ type='button'
289
+ className='interaction-panel__nav-button'
290
+ aria-label={t('chat.interactionOptionNext')}
291
+ onMouseDown={(event) => {
292
+ event.preventDefault()
293
+ }}
294
+ onKeyDown={(event) => handleNavButtonKeyDown(event, 1)}
295
+ onClick={() => moveOptionFocus(1)}
296
+ >
297
+ <span className='material-symbols-rounded'>keyboard_arrow_down</span>
298
+ </button>
299
+ </Tooltip>
300
+ </div>
301
+ )}
69
302
  </div>
70
- </div>
71
- <div className='interaction-panel__options'>
72
- {primaryOptions.map((option: { label: string; value?: string; description?: string }) => {
73
- const meta = getOptionMeta(option.value)
303
+ }
304
+ >
305
+ <div ref={optionsContainerRef} className='interaction-panel__options'>
306
+ {visibleOptionItems.map(({ option, index, meta }) => {
307
+ const optionKey = getInteractionOptionKey(option, index)
308
+ const isActive = index === normalizedActiveOptionIndex
309
+
310
+ if (isPermissionInteraction) {
311
+ return (
312
+ <Button
313
+ key={optionKey}
314
+ block
315
+ data-option-index={index}
316
+ tabIndex={isActive ? 0 : -1}
317
+ className={[
318
+ 'interaction-panel__option',
319
+ `interaction-panel__option--${meta.tone}`,
320
+ isActive ? 'is-active' : ''
321
+ ].filter(Boolean).join(' ')}
322
+ onFocus={() => onActiveOptionIndexChange(index)}
323
+ onKeyDown={(event) => handleOptionKeyDown(event, index)}
324
+ onClick={() => handleSubmitOption(option)}
325
+ >
326
+ <span className='interaction-panel__option-icon material-symbols-rounded'>{meta.icon}</span>
327
+ <span className='interaction-panel__option-copy'>
328
+ <span className='interaction-panel__option-text'>
329
+ <span className='interaction-panel__option-label'>{option.label}</span>
330
+ {option.description && (
331
+ <span className='interaction-panel__option-description'>
332
+ {option.description}
333
+ </span>
334
+ )}
335
+ </span>
336
+ </span>
337
+ </Button>
338
+ )
339
+ }
74
340
 
75
341
  return (
76
342
  <Button
77
- key={option.value ?? option.label}
78
- className={[
79
- 'interaction-panel__option',
80
- `interaction-panel__option--${meta.tone}`
81
- ].join(' ')}
82
- onClick={() => onInteractionResponse?.(interactionRequest.id, option.value ?? option.label)}
343
+ key={optionKey}
344
+ block
345
+ data-option-index={index}
346
+ tabIndex={isActive ? 0 : -1}
347
+ className={`interaction-panel__option interaction-panel__option--question ${isActive ? 'is-active' : ''}`
348
+ .trim()}
349
+ onFocus={() => onActiveOptionIndexChange(index)}
350
+ onKeyDown={(event) => handleOptionKeyDown(event, index)}
351
+ onClick={() => handleSubmitOption(option)}
83
352
  >
84
- <span className='interaction-panel__option-icon material-symbols-rounded'>{meta.icon}</span>
85
- <div className='interaction-panel__option-copy'>
86
- <div className='interaction-panel__option-text'>
87
- <div className='interaction-panel__option-label'>{option.label}</div>
88
- {option.description && (
89
- <div className='interaction-panel__option-description'>
90
- {option.description}
91
- </div>
92
- )}
93
- </div>
94
- </div>
353
+ <span className='interaction-panel__option-main'>
354
+ <span className='interaction-panel__option-index' aria-hidden='true'>
355
+ {index + 1}.
356
+ </span>
357
+ <span className='interaction-panel__option-label'>{option.label}</span>
358
+ {option.description && (
359
+ <span className='interaction-panel__option-side'>
360
+ {renderInfoButton(option.description)}
361
+ </span>
362
+ )}
363
+ </span>
95
364
  </Button>
96
365
  )
97
366
  })}
98
- {showAllOptions && secondaryOptions.length > 0 && (
99
- <div className='interaction-panel__secondary'>
100
- {secondaryOptions.map((option: { label: string; value?: string; description?: string }) => {
101
- const meta = getOptionMeta(option.value)
102
-
103
- return (
104
- <Button
105
- key={option.value ?? option.label}
106
- className={[
107
- 'interaction-panel__option',
108
- `interaction-panel__option--${meta.tone}`
109
- ].join(' ')}
110
- onClick={() => onInteractionResponse?.(interactionRequest.id, option.value ?? option.label)}
111
- >
112
- <span className='interaction-panel__option-icon material-symbols-rounded'>{meta.icon}</span>
113
- <div className='interaction-panel__option-copy'>
114
- <div className='interaction-panel__option-text'>
115
- <div className='interaction-panel__option-label'>{option.label}</div>
116
- {option.description && (
117
- <div className='interaction-panel__option-description'>
118
- {option.description}
119
- </div>
120
- )}
121
- </div>
122
- </div>
123
- </Button>
124
- )
125
- })}
126
- </div>
127
- )}
128
- {secondaryOptions.length > 0 && (
367
+ {isPermissionInteraction && secondaryPermissionOptionItems.length > 0 && (
129
368
  <Button
130
369
  type='text'
131
370
  className='interaction-panel__toggle'
132
- onClick={() => setShowAllOptions(current => !current)}
371
+ onClick={handleTogglePermissionOptions}
133
372
  >
134
373
  <span className='interaction-panel__toggle-label'>
135
- {showAllOptions ? t('chat.permissionCollapseOptions') : t('chat.permissionExpandOptions')}
374
+ {showAllPermissionOptions ? t('chat.permissionCollapseOptions') : t('chat.permissionExpandOptions')}
136
375
  </span>
137
376
  <span className='interaction-panel__toggle-icon material-symbols-rounded'>
138
- {showAllOptions ? 'expand_less' : 'expand_more'}
377
+ {showAllPermissionOptions ? 'expand_less' : 'expand_more'}
139
378
  </span>
140
379
  </Button>
141
380
  )}
142
381
  </div>
143
- </div>
382
+ </ChatComposerCard>
144
383
  )
145
384
  }
@@ -18,7 +18,10 @@ export function SenderMonacoEditor({
18
18
  placeholder,
19
19
  disabled,
20
20
  sendShortcut,
21
+ sendShortcutDisabled,
21
22
  onSendShortcut,
23
+ secondarySendShortcut,
24
+ onSecondarySendShortcut,
22
25
  onInputChange,
23
26
  onCursorChange,
24
27
  onKeyDown,
@@ -32,7 +35,10 @@ export function SenderMonacoEditor({
32
35
  placeholder: string
33
36
  disabled: boolean
34
37
  sendShortcut: string
38
+ sendShortcutDisabled?: boolean
35
39
  onSendShortcut: () => void
40
+ secondarySendShortcut?: string
41
+ onSecondarySendShortcut?: () => void
36
42
  onInputChange: (value: string, cursorOffset: number | null) => void
37
43
  onCursorChange: (cursorOffset: number | null) => void
38
44
  onKeyDown: (event: KeyboardEvent) => void
@@ -56,7 +62,10 @@ export function SenderMonacoEditor({
56
62
  value,
57
63
  disabled,
58
64
  sendShortcut,
65
+ sendShortcutDisabled,
59
66
  onSendShortcut,
67
+ secondarySendShortcut,
68
+ onSecondarySendShortcut,
60
69
  onInputChange,
61
70
  onCursorChange,
62
71
  onKeyDown,
@@ -79,17 +88,25 @@ export function SenderMonacoEditor({
79
88
  options={{
80
89
  ariaLabel: placeholder,
81
90
  automaticLayout: true,
91
+ bracketPairColorization: { enabled: false },
82
92
  domReadOnly: disabled,
83
93
  folding: false,
84
94
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
85
95
  fontSize: FONT_SIZE,
86
96
  glyphMargin: false,
97
+ guides: {
98
+ bracketPairs: false,
99
+ highlightActiveBracketPair: false,
100
+ indentation: false
101
+ },
87
102
  hideCursorInOverviewRuler: true,
88
103
  lineDecorationsWidth: 0,
89
104
  lineHeight: LINE_HEIGHT,
90
105
  lineNumbers: 'off',
91
106
  lineNumbersMinChars: 0,
107
+ matchBrackets: 'never',
92
108
  minimap: { enabled: false },
109
+ occurrencesHighlight: 'off',
93
110
  overviewRulerBorder: false,
94
111
  overviewRulerLanes: 0,
95
112
  padding: { top: 0, bottom: 0 },
@@ -99,6 +116,7 @@ export function SenderMonacoEditor({
99
116
  renderLineHighlight: 'none',
100
117
  roundedSelection: true,
101
118
  scrollBeyondLastLine: false,
119
+ selectionHighlight: false,
102
120
  scrollbar: {
103
121
  alwaysConsumeMouseWheel: false,
104
122
  horizontal: 'hidden',
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable max-lines */
2
-
3
2
  import type { SenderEditorHandle } from '#~/components/chat/sender/@types/sender-editor'
4
3
  import type { SenderCompletionMatch, SenderTokenDecoration } from '#~/components/chat/sender/@utils/sender-completion'
5
4
  import { isShortcutMatch } from '#~/utils/shortcutUtils'
@@ -17,13 +16,21 @@ import {
17
16
  import { useSenderEditorHandle } from './use-sender-editor-handle'
18
17
  import { useSenderMonacoTheme } from './use-sender-monaco-theme'
19
18
 
19
+ const hasPastedImageFile = (clipboardData?: DataTransfer | null) => {
20
+ return Array.from(clipboardData?.items ?? [])
21
+ .some(item => item.kind === 'file' && item.type.startsWith('image/'))
22
+ }
23
+
20
24
  export const useSenderMonacoEditor = ({
21
25
  editorRef,
22
26
  modelPath,
23
27
  value,
24
28
  disabled,
25
29
  sendShortcut,
30
+ sendShortcutDisabled,
26
31
  onSendShortcut,
32
+ secondarySendShortcut,
33
+ onSecondarySendShortcut,
27
34
  onInputChange,
28
35
  onCursorChange,
29
36
  onKeyDown,
@@ -37,7 +44,10 @@ export const useSenderMonacoEditor = ({
37
44
  value: string
38
45
  disabled: boolean
39
46
  sendShortcut: string
47
+ sendShortcutDisabled?: boolean
40
48
  onSendShortcut: () => void
49
+ secondarySendShortcut?: string
50
+ onSecondarySendShortcut?: () => void
41
51
  onInputChange: (value: string, cursorOffset: number | null) => void
42
52
  onCursorChange: (cursorOffset: number | null) => void
43
53
  onKeyDown: (event: KeyboardEvent) => void
@@ -56,7 +66,10 @@ export const useSenderMonacoEditor = ({
56
66
  const decorationsRef = useRef<MonacoEditorNamespace.IEditorDecorationsCollection | null>(null)
57
67
  const disabledRef = useRef(disabled)
58
68
  const sendShortcutRef = useRef(sendShortcut)
69
+ const sendShortcutDisabledRef = useRef(Boolean(sendShortcutDisabled))
59
70
  const onSendShortcutRef = useRef(onSendShortcut)
71
+ const secondarySendShortcutRef = useRef(secondarySendShortcut)
72
+ const onSecondarySendShortcutRef = useRef(onSecondarySendShortcut)
60
73
  const onInputChangeRef = useRef(onInputChange)
61
74
  const onCursorChangeRef = useRef(onCursorChange)
62
75
  const onKeyDownRef = useRef(onKeyDown)
@@ -67,7 +80,10 @@ export const useSenderMonacoEditor = ({
67
80
 
68
81
  disabledRef.current = disabled
69
82
  sendShortcutRef.current = sendShortcut
83
+ sendShortcutDisabledRef.current = Boolean(sendShortcutDisabled)
70
84
  onSendShortcutRef.current = onSendShortcut
85
+ secondarySendShortcutRef.current = secondarySendShortcut
86
+ onSecondarySendShortcutRef.current = onSecondarySendShortcut
71
87
  onInputChangeRef.current = onInputChange
72
88
  onCursorChangeRef.current = onCursorChange
73
89
  onKeyDownRef.current = onKeyDown
@@ -141,30 +157,91 @@ export const useSenderMonacoEditor = ({
141
157
  const domNode = editor.getDomNode()
142
158
 
143
159
  if (domNode != null) {
144
- const handleDomPaste: EventListener = (event) => {
145
- if (!(event instanceof ClipboardEvent)) {
160
+ const inputTargets = Array.from(
161
+ domNode.querySelectorAll<HTMLElement>('.native-edit-context, textarea.inputarea')
162
+ )
163
+ const shouldHandleImagePaste = (event: ClipboardEvent, requireFocus: boolean) => {
164
+ if (!hasPastedImageFile(event.clipboardData)) {
165
+ return false
166
+ }
167
+
168
+ if (!requireFocus) {
169
+ return true
170
+ }
171
+
172
+ const activeElement = document.activeElement
173
+
174
+ return editor.hasTextFocus() ||
175
+ (activeElement != null && domNode.contains(activeElement)) ||
176
+ (event.target instanceof Node && domNode.contains(event.target))
177
+ }
178
+ const handleImagePaste = (
179
+ event: ClipboardEvent,
180
+ { requireFocus, stopImmediately }: { requireFocus: boolean; stopImmediately: boolean }
181
+ ) => {
182
+ if (!shouldHandleImagePaste(event, requireFocus)) {
146
183
  return
147
184
  }
185
+
186
+ event.preventDefault()
187
+ event.stopPropagation()
188
+
189
+ if (stopImmediately) {
190
+ event.stopImmediatePropagation()
191
+ }
192
+
148
193
  void onPasteRef.current(event)
149
194
  }
150
- const nativeEditContext = domNode.querySelector('.native-edit-context')
195
+ const handleDocumentPaste = (event: ClipboardEvent) => {
196
+ if (!shouldHandleImagePaste(event, true)) {
197
+ return
198
+ }
199
+
200
+ handleImagePaste(event, { requireFocus: true, stopImmediately: true })
201
+ }
202
+ const handleNativePaste: EventListener = (event) => {
203
+ if (!(event instanceof ClipboardEvent)) {
204
+ return
205
+ }
206
+
207
+ handleImagePaste(event, { requireFocus: false, stopImmediately: true })
208
+ }
151
209
  const handleNativeKeyDown: EventListener = (event) => {
152
210
  if (!(event instanceof KeyboardEvent)) {
153
211
  return
154
212
  }
155
- if (isShortcutMatch(event, sendShortcutRef.current, navigator.platform.includes('Mac'))) {
213
+ if (
214
+ secondarySendShortcutRef.current != null &&
215
+ onSecondarySendShortcutRef.current != null &&
216
+ isShortcutMatch(event, secondarySendShortcutRef.current, navigator.platform.includes('Mac'))
217
+ ) {
218
+ event.preventDefault()
219
+ event.stopPropagation()
220
+ onSecondarySendShortcutRef.current()
221
+ return
222
+ }
223
+ if (
224
+ !sendShortcutDisabledRef.current &&
225
+ isShortcutMatch(event, sendShortcutRef.current, navigator.platform.includes('Mac'))
226
+ ) {
156
227
  event.preventDefault()
157
228
  event.stopPropagation()
158
229
  onSendShortcutRef.current()
159
230
  }
160
231
  }
161
232
 
162
- domNode.addEventListener('paste', handleDomPaste)
163
- nativeEditContext?.addEventListener('keydown', handleNativeKeyDown, true)
233
+ document.addEventListener('paste', handleDocumentPaste, true)
234
+ for (const inputTarget of inputTargets) {
235
+ inputTarget.addEventListener('paste', handleNativePaste, true)
236
+ inputTarget.addEventListener('keydown', handleNativeKeyDown, true)
237
+ }
164
238
  disposables.push({
165
239
  dispose: () => {
166
- domNode.removeEventListener('paste', handleDomPaste)
167
- nativeEditContext?.removeEventListener('keydown', handleNativeKeyDown, true)
240
+ document.removeEventListener('paste', handleDocumentPaste, true)
241
+ for (const inputTarget of inputTargets) {
242
+ inputTarget.removeEventListener('paste', handleNativePaste, true)
243
+ inputTarget.removeEventListener('keydown', handleNativeKeyDown, true)
244
+ }
168
245
  }
169
246
  })
170
247
  }