@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,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import { App } from 'antd'
2
3
  import { useTranslation } from 'react-i18next'
3
4
 
@@ -79,16 +80,21 @@ export const useSenderController = (props: SenderProps) => {
79
80
  interactionRequest: props.interactionRequest,
80
81
  isInlineEdit
81
82
  })
82
- const { clearInputShortcut, composerControlShortcuts, resolvedSendShortcut } = useSenderShortcuts({
83
- enabled: !hideSender && !attachments.showContextPicker && !isInlineEdit,
84
- isInlineEdit,
85
- isMac,
86
- isThinking,
87
- modelUnavailable: props.modelUnavailable,
88
- permissionModeOptions: props.permissionModeOptions ?? [],
89
- referenceActions,
90
- selectOverlays
91
- })
83
+ const isPermissionInteraction = !isInlineEdit && props.interactionRequest?.payload.kind === 'permission'
84
+ const showConfirmInteractionAction = isPermissionInteraction &&
85
+ (props.interactionOptionNavigation?.optionCount ?? 0) > 0
86
+ const sendBlockedTooltip = isPermissionInteraction ? t('chat.permissionSendBlockedTooltip') : undefined
87
+ const { clearInputShortcut, composerControlShortcuts, resolvedSendShortcut, queuedMessageShortcuts } =
88
+ useSenderShortcuts({
89
+ enabled: !hideSender && !attachments.showContextPicker && !isInlineEdit,
90
+ isInlineEdit,
91
+ isMac,
92
+ isThinking,
93
+ modelUnavailable: props.modelUnavailable,
94
+ permissionModeOptions: props.permissionModeOptions ?? [],
95
+ referenceActions,
96
+ selectOverlays
97
+ })
92
98
 
93
99
  const resetComposer = () => {
94
100
  composer.resetComposerContent()
@@ -103,6 +109,7 @@ export const useSenderController = (props: SenderProps) => {
103
109
  pendingImages: composer.pendingImages,
104
110
  pendingFiles: composer.pendingFiles,
105
111
  isBusy,
112
+ allowWhileBusy: isThinking,
106
113
  isInlineEdit,
107
114
  modelUnavailable: props.modelUnavailable,
108
115
  interactionRequest: props.interactionRequest,
@@ -113,7 +120,20 @@ export const useSenderController = (props: SenderProps) => {
113
120
  t,
114
121
  resetComposer
115
122
  })
116
- const triggerSend = () => void handleSend()
123
+ const handleBlockedSendAttempt = () => {
124
+ void message.error({
125
+ content: t('chat.permissionSendBlockedError'),
126
+ key: 'chat-permission-send-blocked'
127
+ })
128
+ }
129
+ const triggerSend = (mode?: 'steer' | 'next') => {
130
+ if (isPermissionInteraction) {
131
+ handleBlockedSendAttempt()
132
+ return
133
+ }
134
+
135
+ void handleSend(mode)
136
+ }
117
137
 
118
138
  useSenderAutofocus({ autoFocus: props.autoFocus === true, editorRef })
119
139
  useSenderReferenceFocusRestore({ focusRestore, referenceActions })
@@ -123,11 +143,24 @@ export const useSenderController = (props: SenderProps) => {
123
143
  isMac,
124
144
  clearInputShortcut,
125
145
  isInlineEdit,
146
+ isThinking,
126
147
  input: composer.input,
127
148
  pendingImageCount: composer.pendingImages.length,
128
149
  pendingFileCount: composer.pendingFiles.length,
150
+ interactionOptionCount: props.interactionOptionNavigation?.optionCount ?? 0,
129
151
  onCancel: props.onCancel,
130
152
  onClear: props.onClear,
153
+ onInteractionOptionMove: props.interactionOptionNavigation?.onMove,
154
+ onInteractionOptionSubmit: props.interactionOptionNavigation?.onSubmit,
155
+ onInterrupt: props.onInterrupt,
156
+ onInterruptHint: () => {
157
+ void message.open({
158
+ type: 'info',
159
+ content: t('chat.queue.stopShortcutConfirm'),
160
+ duration: 1.6,
161
+ key: 'chat-stop-shortcut-confirm'
162
+ })
163
+ },
131
164
  onResetComposer: resetComposer,
132
165
  showReferenceActions: referenceActions.showReferenceActions,
133
166
  onCloseReferenceActions: () => referenceActions.closeReferenceActions({ restoreFocus: true }),
@@ -160,10 +193,16 @@ export const useSenderController = (props: SenderProps) => {
160
193
  isInlineEdit,
161
194
  isMac,
162
195
  isThinking,
196
+ sendBlocked: isPermissionInteraction,
197
+ sendBlockedTooltip,
198
+ showConfirmInteractionAction,
199
+ confirmInteractionLabel: showConfirmInteractionAction ? t('chat.permissionConfirmOption') : undefined,
200
+ onConfirmInteractionOption: showConfirmInteractionAction ? props.interactionOptionNavigation?.onSubmit : undefined,
163
201
  message,
164
202
  props,
165
203
  refs: { fileInputRef, modelSelectRef, effortSelectRef },
166
204
  referenceActions,
205
+ queuedMessageShortcuts,
167
206
  resolvedSendShortcut,
168
207
  selectOverlays,
169
208
  supportsEffort,
@@ -186,6 +225,12 @@ export const useSenderController = (props: SenderProps) => {
186
225
  permissionContext,
187
226
  editorRef,
188
227
  placeholder: props.placeholder ?? props.interactionRequest?.payload.question ?? t('chat.inputPlaceholder'),
228
+ secondarySendShortcut: isThinking && !isPermissionInteraction ? queuedMessageShortcuts.queueNext : undefined,
229
+ onSecondarySendShortcut: isThinking && !isPermissionInteraction
230
+ ? () => {
231
+ void handleSend('next')
232
+ }
233
+ : undefined,
189
234
  toolbar
190
235
  })
191
236
  }
@@ -1,4 +1,5 @@
1
1
  import type { RefObject } from 'react'
2
+ import { useEffect, useRef } from 'react'
2
3
 
3
4
  import type { SenderEditorHandle } from '#~/components/chat/sender/@types/sender-editor'
4
5
  import { loadChatHistory } from '#~/components/chat/sender/@utils/sender-utils'
@@ -9,11 +10,17 @@ export const useSenderKeydown = ({
9
10
  isMac,
10
11
  clearInputShortcut,
11
12
  isInlineEdit,
13
+ isThinking,
12
14
  input,
13
15
  pendingImageCount,
14
16
  pendingFileCount,
17
+ interactionOptionCount,
15
18
  onCancel,
16
19
  onClear,
20
+ onInteractionOptionMove,
21
+ onInteractionOptionSubmit,
22
+ onInterrupt,
23
+ onInterruptHint,
17
24
  onResetComposer,
18
25
  showReferenceActions,
19
26
  onCloseReferenceActions,
@@ -30,11 +37,17 @@ export const useSenderKeydown = ({
30
37
  isMac: boolean
31
38
  clearInputShortcut?: string
32
39
  isInlineEdit: boolean
40
+ isThinking: boolean
33
41
  input: string
34
42
  pendingImageCount: number
35
43
  pendingFileCount: number
44
+ interactionOptionCount: number
36
45
  onCancel?: () => void
37
46
  onClear?: () => void
47
+ onInteractionOptionMove?: (delta: number) => void
48
+ onInteractionOptionSubmit?: () => void
49
+ onInterrupt: () => void
50
+ onInterruptHint: () => void
38
51
  onResetComposer: () => void
39
52
  showReferenceActions: boolean
40
53
  onCloseReferenceActions: () => void
@@ -47,6 +60,14 @@ export const useSenderKeydown = ({
47
60
  onHistoryNavigate: (direction: 'up' | 'down') => void
48
61
  onInputClear: () => void
49
62
  }) => {
63
+ const interruptConfirmationExpiresAtRef = useRef<number | null>(null)
64
+
65
+ useEffect(() => {
66
+ if (input !== '' || pendingImageCount > 0 || pendingFileCount > 0 || !isThinking) {
67
+ interruptConfirmationExpiresAtRef.current = null
68
+ }
69
+ }, [input, isThinking, pendingFileCount, pendingImageCount])
70
+
50
71
  return (event: KeyboardEvent) => {
51
72
  if (showReferenceActions && event.key === 'Escape') {
52
73
  event.preventDefault()
@@ -71,6 +92,33 @@ export const useSenderKeydown = ({
71
92
  onInputClear()
72
93
  return
73
94
  }
95
+ const canNavigateInteractionOptions = !isInlineEdit &&
96
+ interactionOptionCount > 0 &&
97
+ input.trim() === '' &&
98
+ pendingImageCount === 0 &&
99
+ pendingFileCount === 0
100
+ if (
101
+ canNavigateInteractionOptions &&
102
+ onInteractionOptionMove != null &&
103
+ (event.key === 'ArrowUp' || event.key === 'ArrowDown')
104
+ ) {
105
+ event.preventDefault()
106
+ onInteractionOptionMove(event.key === 'ArrowUp' ? -1 : 1)
107
+ return
108
+ }
109
+ if (
110
+ canNavigateInteractionOptions &&
111
+ onInteractionOptionSubmit != null &&
112
+ event.key === 'Enter' &&
113
+ !event.metaKey &&
114
+ !event.ctrlKey &&
115
+ !event.altKey &&
116
+ !event.shiftKey
117
+ ) {
118
+ event.preventDefault()
119
+ onInteractionOptionSubmit()
120
+ return
121
+ }
74
122
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
75
123
  const selection = editorRef.current?.getSelection()
76
124
  const history = loadChatHistory()
@@ -96,7 +144,23 @@ export const useSenderKeydown = ({
96
144
  }
97
145
  if (input !== '') {
98
146
  event.preventDefault()
147
+ interruptConfirmationExpiresAtRef.current = null
99
148
  onInputClear()
149
+ return
150
+ }
151
+ if (isThinking && pendingImageCount === 0 && pendingFileCount === 0) {
152
+ event.preventDefault()
153
+ const now = Date.now()
154
+ const expiresAt = interruptConfirmationExpiresAtRef.current
155
+
156
+ if (expiresAt != null && expiresAt > now) {
157
+ interruptConfirmationExpiresAtRef.current = null
158
+ onInterrupt()
159
+ return
160
+ }
161
+
162
+ interruptConfirmationExpiresAtRef.current = now + 1800
163
+ onInterruptHint()
100
164
  }
101
165
  return
102
166
  }
@@ -8,6 +8,19 @@ import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
8
8
  import { useComposerControlShortcuts } from '#~/hooks/chat/use-composer-control-shortcuts'
9
9
  import { resolveSendShortcut } from '#~/utils/shortcutUtils'
10
10
 
11
+ const resolveQueuedMessageShortcuts = (sendShortcut: string, isMac: boolean) => {
12
+ const primary = sendShortcut
13
+ const candidates = isMac
14
+ ? ['cmd+shift+enter', 'cmd+alt+enter', 'cmd+ctrl+enter']
15
+ : ['mod+enter', 'alt+enter', 'ctrl+shift+enter']
16
+ const next = candidates.find(shortcut => shortcut !== primary) ?? candidates[0]
17
+
18
+ return {
19
+ queueSteer: primary,
20
+ queueNext: next
21
+ }
22
+ }
23
+
11
24
  export const useSenderShortcuts = ({
12
25
  enabled,
13
26
  isInlineEdit,
@@ -38,6 +51,7 @@ export const useSenderShortcuts = ({
38
51
  const { data: configRes } = useSWR<ConfigResponse>('/api/config')
39
52
  const mergedShortcuts = configRes?.sources?.merged?.shortcuts
40
53
  const resolvedSendShortcut = resolveSendShortcut(mergedShortcuts?.sendMessage, isMac)
54
+ const queuedMessageShortcuts = resolveQueuedMessageShortcuts(resolvedSendShortcut, isMac)
41
55
 
42
56
  const composerControlShortcuts = useComposerControlShortcuts({
43
57
  enabled,
@@ -73,6 +87,7 @@ export const useSenderShortcuts = ({
73
87
  return {
74
88
  clearInputShortcut: mergedShortcuts?.clearInput,
75
89
  composerControlShortcuts,
76
- resolvedSendShortcut
90
+ resolvedSendShortcut,
91
+ queuedMessageShortcuts
77
92
  }
78
93
  }
@@ -1,3 +1,4 @@
1
+ import type { SessionQueuedMessageMode } from '@vibe-forge/core'
1
2
  import type { TFunction } from 'i18next'
2
3
 
3
4
  import type { MessageInstance } from 'antd/es/message/interface'
@@ -11,6 +12,7 @@ export const useSenderSubmit = ({
11
12
  pendingImages,
12
13
  pendingFiles,
13
14
  isBusy,
15
+ allowWhileBusy,
14
16
  isInlineEdit,
15
17
  modelUnavailable,
16
18
  interactionRequest,
@@ -25,20 +27,26 @@ export const useSenderSubmit = ({
25
27
  pendingImages: Parameters<typeof buildMessageContent>[1]
26
28
  pendingFiles: Parameters<typeof buildMessageContent>[2]
27
29
  isBusy: boolean
30
+ allowWhileBusy: boolean
28
31
  isInlineEdit: boolean
29
32
  modelUnavailable?: boolean
30
33
  interactionRequest?: { id: string } | null
31
34
  onInteractionResponse?: (id: string, data: string | string[]) => void
32
- onSend: (text: string) => SenderSubmitResult | Promise<SenderSubmitResult>
33
- onSendContent: (content: ReturnType<typeof buildMessageContent>) => SenderSubmitResult | Promise<SenderSubmitResult>
35
+ onSend: (text: string, mode?: SessionQueuedMessageMode) => SenderSubmitResult | Promise<SenderSubmitResult>
36
+ onSendContent: (
37
+ content: ReturnType<typeof buildMessageContent>,
38
+ mode?: SessionQueuedMessageMode
39
+ ) => SenderSubmitResult | Promise<SenderSubmitResult>
34
40
  message: MessageInstance
35
41
  t: TFunction
36
42
  resetComposer: () => void
37
43
  }) => {
38
- return async () => {
44
+ return async (mode?: SessionQueuedMessageMode) => {
39
45
  const input = getInput()
40
46
 
41
- if (isBusy || (input.trim() === '' && pendingImages.length === 0 && pendingFiles.length === 0)) {
47
+ if (
48
+ ((isBusy && !allowWhileBusy) || (input.trim() === '' && pendingImages.length === 0 && pendingFiles.length === 0))
49
+ ) {
42
50
  return
43
51
  }
44
52
  if (!isInlineEdit && modelUnavailable) {
@@ -61,14 +69,14 @@ export const useSenderSubmit = ({
61
69
  if (pendingImages.length > 0 || pendingFiles.length > 0) {
62
70
  const content = buildMessageContent(input, pendingImages, pendingFiles)
63
71
  if (isInlineEdit) {
64
- didSubmit = (await onSendContent(content)) !== false
72
+ didSubmit = (await onSendContent(content, mode)) !== false
65
73
  } else {
66
- void onSendContent(content)
74
+ void onSendContent(content, mode)
67
75
  }
68
76
  } else if (isInlineEdit) {
69
- didSubmit = (await onSend(input)) !== false
77
+ didSubmit = (await onSend(input, mode)) !== false
70
78
  } else {
71
- void onSend(input)
79
+ void onSend(input, mode)
72
80
  }
73
81
 
74
82
  if (!didSubmit) {
@@ -1,6 +1,11 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import type { AskUserQuestionParams, ChatMessageContent, SessionStatus } from '@vibe-forge/core'
3
+ import type {
4
+ AskUserQuestionParams,
5
+ ChatMessageContent,
6
+ SessionQueuedMessageMode,
7
+ SessionStatus
8
+ } from '@vibe-forge/core'
4
9
  import type { SessionInfo } from '@vibe-forge/types'
5
10
 
6
11
  import type { ChatEffort } from '#~/hooks/chat/use-chat-effort'
@@ -10,8 +15,11 @@ import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
10
15
  import type { SenderInitialContent, SenderSubmitResult, SenderVariant } from './sender-types'
11
16
 
12
17
  export interface SenderProps {
13
- onSend: (text: string) => SenderSubmitResult | Promise<SenderSubmitResult>
14
- onSendContent: (content: ChatMessageContent[]) => SenderSubmitResult | Promise<SenderSubmitResult>
18
+ onSend: (text: string, mode?: SessionQueuedMessageMode) => SenderSubmitResult | Promise<SenderSubmitResult>
19
+ onSendContent: (
20
+ content: ChatMessageContent[],
21
+ mode?: SessionQueuedMessageMode
22
+ ) => SenderSubmitResult | Promise<SenderSubmitResult>
15
23
  variant?: SenderVariant
16
24
  adapterLocked?: boolean
17
25
  sessionStatus?: SessionStatus
@@ -20,6 +28,12 @@ export interface SenderProps {
20
28
  sessionInfo?: SessionInfo | null
21
29
  interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
22
30
  onInteractionResponse?: (id: string, data: string | string[]) => void
31
+ interactionOptionNavigation?: {
32
+ optionCount: number
33
+ activeIndex: number
34
+ onMove: (delta: number) => void
35
+ onSubmit: () => void
36
+ }
23
37
  placeholder?: string
24
38
  initialContent?: SenderInitialContent
25
39
  onCancel?: () => void
@@ -44,4 +58,6 @@ export interface SenderProps {
44
58
  adapterOptions?: Array<{ value: string; label: ReactNode }>
45
59
  onAdapterChange?: (adapter: string) => void
46
60
  modelUnavailable?: boolean
61
+ queueMode?: SessionQueuedMessageMode
62
+ onQueueModeChange?: (mode: SessionQueuedMessageMode) => void
47
63
  }
@@ -1,5 +1,6 @@
1
1
  import type { ChangeEvent, KeyboardEvent, ReactNode, RefObject } from 'react'
2
2
 
3
+ import type { SessionQueuedMessageMode } from '@vibe-forge/core'
3
4
  import type { RefSelectProps } from 'antd'
4
5
 
5
6
  import type { ChatEffort } from '#~/hooks/chat/use-chat-effort'
@@ -11,12 +12,17 @@ export interface SenderToolbarShortcuts {
11
12
  switchModel: string
12
13
  switchEffort: string
13
14
  switchPermissionMode: string
15
+ queueSteer: string
16
+ queueNext: string
14
17
  }
15
18
 
16
19
  export interface SenderToolbarState {
17
20
  isInlineEdit: boolean
18
21
  isThinking: boolean
19
22
  modelUnavailable: boolean
23
+ sendBlocked: boolean
24
+ sendBlockedTooltip?: string
25
+ showConfirmInteractionAction: boolean
20
26
  adapterLocked: boolean
21
27
  submitLoading: boolean
22
28
  supportsEffort: boolean
@@ -34,6 +40,8 @@ export interface SenderToolbarState {
34
40
  resolvedSendShortcut: string
35
41
  hasComposerContent: boolean
36
42
  hasSendText: boolean
43
+ queueMode: SessionQueuedMessageMode
44
+ showQueueModeControl: boolean
37
45
  }
38
46
 
39
47
  export interface SenderToolbarData {
@@ -47,6 +55,7 @@ export interface SenderToolbarData {
47
55
  adapterOptions?: Array<{ value: string; label: ReactNode }>
48
56
  composerControlShortcuts: SenderToolbarShortcuts
49
57
  submitLabel?: string
58
+ confirmInteractionLabel?: string
50
59
  }
51
60
 
52
61
  export interface SenderToolbarRefs {
@@ -77,7 +86,9 @@ export interface SenderToolbarHandlers {
77
86
  onToggleRecommendedModel?: (option: ModelSelectOption) => void | Promise<void>
78
87
  onEffortChange?: (effort: ChatEffort) => void
79
88
  onAdapterChange?: (adapter: string) => void
80
- onSend: () => void
89
+ onSend: (mode?: SessionQueuedMessageMode) => void
81
90
  onInterrupt: () => void
82
91
  onCancel?: () => void
92
+ onConfirmInteractionOption?: () => void
93
+ onQueueModeChange?: (mode: SessionQueuedMessageMode) => void
83
94
  }
@@ -1,4 +1,6 @@
1
1
  .chat-input-wrapper {
2
+ width: 100%;
3
+ margin: 0;
2
4
  border-top: none;
3
5
  position: relative;
4
6
  background-color: var(--bg-color);
@@ -6,6 +8,7 @@
6
8
 
7
9
  .chat-input-wrapper--inline-edit {
8
10
  width: 100%;
11
+ margin: 0;
9
12
  }
10
13
 
11
14
  .chat-input-container {
@@ -1,10 +1,8 @@
1
1
  import './Sender.scss'
2
2
 
3
- import { ThinkingStatus } from '#~/components/chat/ThinkingStatus'
4
3
  import { useSenderController } from '#~/components/chat/sender/@hooks/use-sender-controller'
5
4
 
6
5
  import { SenderBody } from './@components/sender-body/SenderBody'
7
- import { SenderInteractionPanel } from './@components/sender-interaction-panel/SenderInteractionPanel'
8
6
  import type { SenderProps } from './@types/sender-props'
9
7
 
10
8
  export function Sender(props: SenderProps) {
@@ -18,16 +16,6 @@ export function Sender(props: SenderProps) {
18
16
  controller.isInlineEdit ? 'chat-input-wrapper--inline-edit' : ''
19
17
  ].filter(Boolean).join(' ')}
20
18
  >
21
- {controller.isThinking && <ThinkingStatus />}
22
- {!controller.isInlineEdit && controller.interactionRequest != null && (
23
- <SenderInteractionPanel
24
- interactionRequest={controller.interactionRequest}
25
- permissionContext={controller.permissionContext}
26
- deniedTools={controller.deniedTools}
27
- reasons={controller.reasons}
28
- onInteractionResponse={controller.interactionResponse}
29
- />
30
- )}
31
19
  {!controller.hideSender && (
32
20
  <SenderBody
33
21
  isInlineEdit={controller.isInlineEdit}
@@ -47,6 +35,8 @@ export function Sender(props: SenderProps) {
47
35
  onCursorChange={controller.onCursorChange}
48
36
  onKeyDown={controller.handleKeyDown}
49
37
  onPaste={controller.attachments.handlePaste}
38
+ secondarySendShortcut={controller.secondarySendShortcut}
39
+ onSecondarySendShortcut={controller.onSecondarySendShortcut}
50
40
  resolveCompletionMatch={controller.completion.resolveCompletionMatch}
51
41
  resolveTokenDecorations={controller.completion.resolveTokenDecorations}
52
42
  toolbarState={controller.toolbar.toolbarState}
@@ -1,4 +1,4 @@
1
- import type { ChatMessage } from '@vibe-forge/core'
1
+ import type { ChatMessage, SessionMessageQueueState } from '@vibe-forge/core'
2
2
  import type { SessionInfo } from '@vibe-forge/types'
3
3
 
4
4
  import type { ChatErrorState, InteractionRequestState } from './interaction-state'
@@ -6,6 +6,7 @@ import type { ChatErrorState, InteractionRequestState } from './interaction-stat
6
6
  export interface ChatSessionViewSnapshot {
7
7
  messages: ChatMessage[]
8
8
  sessionInfo: SessionInfo | null
9
+ queuedMessages: SessionMessageQueueState
9
10
  errorState: ChatErrorState | null
10
11
  interactionRequest: InteractionRequestState | null
11
12
  isHydrated: boolean
@@ -18,6 +19,7 @@ export const createChatSessionViewSnapshot = (
18
19
  ): ChatSessionViewSnapshot => ({
19
20
  messages: value?.messages ?? [],
20
21
  sessionInfo: value?.sessionInfo ?? null,
22
+ queuedMessages: value?.queuedMessages ?? { steer: [], next: [] },
21
23
  errorState: value?.errorState ?? null,
22
24
  interactionRequest: value?.interactionRequest ?? null,
23
25
  isHydrated: value?.isHydrated ?? false
@@ -42,6 +44,7 @@ export const restoreChatSessionViewSnapshot = (snapshot?: ChatSessionViewSnapsho
42
44
  return {
43
45
  messages: restorable.messages,
44
46
  sessionInfo: restorable.sessionInfo,
47
+ queuedMessages: restorable.queuedMessages,
45
48
  errorState: restorable.errorState,
46
49
  interactionRequest: restorable.interactionRequest,
47
50
  isReady: restorable.isHydrated
@@ -57,7 +57,11 @@ export function useChatAdapter() {
57
57
  alt: '',
58
58
  'aria-hidden': true
59
59
  })
60
- : null,
60
+ : createElement('span', {
61
+ key: 'fallback-icon',
62
+ className: 'adapter-option__icon adapter-option__icon--fallback material-symbols-rounded',
63
+ 'aria-hidden': true
64
+ }, 'deployed_code'),
61
65
  createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
62
66
  ])
63
67
  }
@@ -410,7 +410,11 @@ export function useChatModelAdapterSelection({
410
410
  alt: '',
411
411
  'aria-hidden': true
412
412
  })
413
- : null,
413
+ : createElement('span', {
414
+ key: 'fallback-icon',
415
+ className: 'adapter-option__icon adapter-option__icon--fallback material-symbols-rounded',
416
+ 'aria-hidden': true
417
+ }, 'deployed_code'),
414
418
  createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
415
419
  ])
416
420
  }
@@ -4,9 +4,18 @@ import { useTranslation } from 'react-i18next'
4
4
  import { useLocation, useNavigate } from 'react-router-dom'
5
5
  import { useSWRConfig } from 'swr'
6
6
 
7
- import { branchSessionFromMessage, createSession, getApiErrorMessage } from '#~/api.js'
7
+ import {
8
+ branchSessionFromMessage,
9
+ createQueuedMessage,
10
+ createSession,
11
+ deleteQueuedMessage,
12
+ getApiErrorMessage,
13
+ moveQueuedMessage,
14
+ reorderQueuedMessages,
15
+ updateQueuedMessage
16
+ } from '#~/api.js'
8
17
  import { connectionManager } from '#~/connectionManager.js'
9
- import type { ChatMessageContent, Session } from '@vibe-forge/core'
18
+ import type { ChatMessageContent, Session, SessionQueuedMessageMode } from '@vibe-forge/core'
10
19
  import type { ChatEffort } from './use-chat-effort'
11
20
  import type { PermissionMode } from './use-chat-permission-mode'
12
21
 
@@ -61,7 +70,7 @@ export function useChatSessionActions({
61
70
  setIsCreating(false)
62
71
  }, [isCreating, session?.id])
63
72
 
64
- const send = useCallback(async (text: string) => {
73
+ const send = useCallback(async (text: string, _mode?: SessionQueuedMessageMode) => {
65
74
  if (text.trim() === '' || isThinking) return false
66
75
  if (!hasAvailableModels) {
67
76
  void message.warning(t('chat.modelConfigRequired'))
@@ -108,7 +117,7 @@ export function useChatSessionActions({
108
117
  t
109
118
  ])
110
119
 
111
- const sendContent = useCallback(async (content: ChatMessageContent[]) => {
120
+ const sendContent = useCallback(async (content: ChatMessageContent[], _mode?: SessionQueuedMessageMode) => {
112
121
  if (content.length === 0 || isThinking) return false
113
122
  if (!hasAvailableModels) {
114
123
  void message.warning(t('chat.modelConfigRequired'))
@@ -200,11 +209,97 @@ export function useChatSessionActions({
200
209
  return runMessageAction(messageId, 'edit', { content })
201
210
  }, [runMessageAction])
202
211
 
212
+ const enqueueContent = useCallback(async (mode: SessionQueuedMessageMode, content: ChatMessageContent[]) => {
213
+ if (session?.id == null || session.id === '') {
214
+ return false
215
+ }
216
+ if (content.length === 0) {
217
+ return false
218
+ }
219
+
220
+ try {
221
+ await createQueuedMessage(session.id, mode, content)
222
+ return true
223
+ } catch (err) {
224
+ console.error(err)
225
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
226
+ return false
227
+ }
228
+ }, [message, session?.id, t])
229
+
230
+ const updateQueuedContent = useCallback(async (queueId: string, content: ChatMessageContent[]) => {
231
+ if (session?.id == null || session.id === '') {
232
+ return false
233
+ }
234
+ if (content.length === 0) {
235
+ return false
236
+ }
237
+
238
+ try {
239
+ await updateQueuedMessage(session.id, queueId, content)
240
+ return true
241
+ } catch (err) {
242
+ console.error(err)
243
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
244
+ return false
245
+ }
246
+ }, [message, session?.id, t])
247
+
248
+ const removeQueuedContent = useCallback(async (queueId: string) => {
249
+ if (session?.id == null || session.id === '') {
250
+ return false
251
+ }
252
+
253
+ try {
254
+ await deleteQueuedMessage(session.id, queueId)
255
+ return true
256
+ } catch (err) {
257
+ console.error(err)
258
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
259
+ return false
260
+ }
261
+ }, [message, session?.id, t])
262
+
263
+ const moveQueuedContent = useCallback(async (queueId: string, mode: SessionQueuedMessageMode) => {
264
+ if (session?.id == null || session.id === '') {
265
+ return false
266
+ }
267
+
268
+ try {
269
+ await moveQueuedMessage(session.id, queueId, mode)
270
+ return true
271
+ } catch (err) {
272
+ console.error(err)
273
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
274
+ return false
275
+ }
276
+ }, [message, session?.id, t])
277
+
278
+ const reorderQueuedContent = useCallback(async (mode: SessionQueuedMessageMode, ids: string[]) => {
279
+ if (session?.id == null || session.id === '') {
280
+ return false
281
+ }
282
+
283
+ try {
284
+ await reorderQueuedMessages(session.id, mode, ids)
285
+ return true
286
+ } catch (err) {
287
+ console.error(err)
288
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
289
+ return false
290
+ }
291
+ }, [message, session?.id, t])
292
+
203
293
  return {
204
294
  isCreating,
205
295
  isThinking,
206
296
  send,
207
297
  sendContent,
298
+ enqueueContent,
299
+ updateQueuedContent,
300
+ removeQueuedContent,
301
+ moveQueuedContent,
302
+ reorderQueuedContent,
208
303
  editMessage,
209
304
  forkMessage,
210
305
  interrupt,